Implement dialog for critical errors #57

Merged
lucas merged 3 commits from issue/37 into master 2023-03-08 20:41:35 +01:00
27 changed files with 388 additions and 180 deletions

View file

@ -9,6 +9,7 @@
- dtmm: indicate when a deployment is necessary - dtmm: indicate when a deployment is necessary
- dtmm: check for Steam game update before deployment - dtmm: check for Steam game update before deployment
- dtmm: remove unused bundles from previous deployment - dtmm: remove unused bundles from previous deployment
- dtmm: show dialog for critical errors
=== Fixed === Fixed

46
Cargo.lock generated
View file

@ -44,6 +44,12 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.2" version = "0.7.2"
@ -699,11 +705,13 @@ dependencies = [
"druid", "druid",
"dtmt-shared", "dtmt-shared",
"futures", "futures",
"lazy_static",
"oodle-sys", "oodle-sys",
"path-slash", "path-slash",
"sdk", "sdk",
"serde", "serde",
"serde_sjson", "serde_sjson",
"strip-ansi-escapes",
"time", "time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@ -1425,7 +1433,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8c31eaef73f18e0d938785e01ab471ec73e3f90c3389e84335ade689ba953b" checksum = "db8c31eaef73f18e0d938785e01ab471ec73e3f90c3389e84335ade689ba953b"
dependencies = [ dependencies = [
"arrayvec", "arrayvec 0.7.2",
"serde", "serde",
] ]
@ -2401,6 +2409,15 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "strip-ansi-escapes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
dependencies = [
"vte",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -2692,6 +2709,12 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]] [[package]]
name = "unic-bidi" name = "unic-bidi"
version = "0.9.0" version = "0.9.0"
@ -2818,6 +2841,27 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vte"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
dependencies = [
"arrayvec 0.5.2",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View file

@ -25,3 +25,5 @@ zip = "0.6.4"
tokio-stream = { version = "0.1.12", features = ["fs"] } tokio-stream = { version = "0.1.12", features = ["fs"] }
path-slash = "0.2.1" path-slash = "0.2.1"
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] } time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] }
strip-ansi-escapes = "0.1.1"
lazy_static = "1.4.0"

View file

@ -14,19 +14,19 @@ use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use zip::ZipArchive; use zip::ZipArchive;
use crate::state::{ModInfo, PackageInfo, State}; use crate::state::{ActionState, ModInfo, PackageInfo};
use crate::util::config::{ConfigSerialize, LoadOrderEntry}; use crate::util::config::{ConfigSerialize, LoadOrderEntry};
use super::read_sjson_file; use super::read_sjson_file;
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> { pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<ModInfo> {
let data = fs::read(&info.path) let data = fs::read(&info.path)
.await .await
.wrap_err_with(|| format!("failed to read file {}", info.path.display()))?; .wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
let data = Cursor::new(data); let data = Cursor::new(data);
let mut archive = ZipArchive::new(data).wrap_err("failed to open ZIP archive")?; let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
if tracing::enabled!(tracing::Level::DEBUG) { if tracing::enabled!(tracing::Level::DEBUG) {
let names = archive.file_names().fold(String::new(), |mut s, name| { let names = archive.file_names().fold(String::new(), |mut s, name| {
@ -38,7 +38,7 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
} }
let dir_name = { let dir_name = {
let f = archive.by_index(0).wrap_err("archive is empty")?; let f = archive.by_index(0).wrap_err("Archive is empty")?;
if !f.is_dir() { if !f.is_dir() {
let err = eyre::eyre!("archive does not have a top-level directory"); let err = eyre::eyre!("archive does not have a top-level directory");
@ -62,15 +62,15 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
let mut f = archive let mut f = archive
.by_name(name) .by_name(name)
.wrap_err("failed to read mod config from archive")?; .wrap_err("Failed to read mod config from archive")?;
let mut buf = Vec::with_capacity(f.size() as usize); let mut buf = Vec::with_capacity(f.size() as usize);
f.read_to_end(&mut buf) f.read_to_end(&mut buf)
.wrap_err("failed to read mod config from archive")?; .wrap_err("Failed to read mod config from archive")?;
let data = String::from_utf8(buf).wrap_err("mod config is not valid UTF-8")?; 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")? serde_sjson::from_str(&data).wrap_err("Failed to deserialize mod config")?
}; };
tracing::debug!(?mod_cfg); tracing::debug!(?mod_cfg);
@ -83,29 +83,29 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
let mut f = archive let mut f = archive
.by_name(name) .by_name(name)
.wrap_err("failed to read file index from archive")?; .wrap_err("Failed to read file index from archive")?;
let mut buf = Vec::with_capacity(f.size() as usize); let mut buf = Vec::with_capacity(f.size() as usize);
f.read_to_end(&mut buf) f.read_to_end(&mut buf)
.wrap_err("failed to read file index from archive")?; .wrap_err("Failed to read file index from archive")?;
let data = String::from_utf8(buf).wrap_err("file index is not valid UTF-8")?; 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")? serde_sjson::from_str(&data).wrap_err("Failed to deserialize file index")?
}; };
tracing::trace!(?files); tracing::trace!(?files);
let mod_dir = state.get_mod_dir(); let mod_dir = state.mod_dir;
tracing::trace!("Creating mods directory {}", mod_dir.display()); tracing::trace!("Creating mods directory {}", mod_dir.display());
fs::create_dir_all(&mod_dir) fs::create_dir_all(Arc::as_ref(&mod_dir))
.await .await
.wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?; .wrap_err_with(|| format!("Failed to create data directory {}", mod_dir.display()))?;
tracing::trace!("Extracting mod archive to {}", mod_dir.display()); tracing::trace!("Extracting mod archive to {}", mod_dir.display());
archive archive
.extract(&mod_dir) .extract(Arc::as_ref(&mod_dir))
.wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?; .wrap_err_with(|| format!("Failed to extract archive to {}", mod_dir.display()))?;
let packages = files let packages = files
.into_iter() .into_iter()
@ -117,23 +117,23 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
} }
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { pub(crate) async fn delete_mod(state: ActionState, info: &ModInfo) -> Result<()> {
let mod_dir = state.get_mod_dir().join(&info.id); let mod_dir = state.mod_dir.join(&info.id);
fs::remove_dir_all(&mod_dir) fs::remove_dir_all(&mod_dir)
.await .await
.wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; .wrap_err_with(|| format!("Failed to remove directory {}", mod_dir.display()))?;
Ok(()) Ok(())
} }
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn save_settings(state: State) -> Result<()> { pub(crate) async fn save_settings(state: ActionState) -> Result<()> {
let cfg = ConfigSerialize::from(&state); let cfg = ConfigSerialize::from(&state);
tracing::info!("Saving settings to '{}'", state.config_path.display()); tracing::info!("Saving settings to '{}'", state.config_path.display());
tracing::debug!(?cfg); tracing::debug!(?cfg);
let data = serde_sjson::to_string(&cfg).wrap_err("failed to serialize config")?; let data = serde_sjson::to_string(&cfg).wrap_err("Failed to serialize config")?;
fs::write(state.config_path.as_ref(), &data) fs::write(state.config_path.as_ref(), &data)
.await .await
@ -155,11 +155,11 @@ async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
let cfg: ModConfig = read_sjson_file(&config_path) let cfg: ModConfig = read_sjson_file(&config_path)
.await .await
.wrap_err_with(|| format!("failed to read mod config '{}'", config_path.display()))?; .wrap_err_with(|| format!("Failed to read mod config '{}'", config_path.display()))?;
let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path) let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path)
.await .await
.wrap_err_with(|| format!("failed to read file index '{}'", index_path.display()))?; .wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
let packages = files let packages = files
.into_iter() .into_iter()
@ -186,12 +186,12 @@ where
} }
Err(err) => { Err(err) => {
return Err(err) return Err(err)
.wrap_err_with(|| format!("failed to open directory '{}'", mod_dir.display())); .wrap_err_with(|| format!("Failed to open directory '{}'", mod_dir.display()));
} }
}; };
let stream = ReadDirStream::new(read_dir) let stream = ReadDirStream::new(read_dir)
.map(|res| res.wrap_err("failed to read dir entry")) .map(|res| res.wrap_err("Failed to read dir entry"))
.then(read_mod_dir_entry); .then(read_mod_dir_entry);
tokio::pin!(stream); tokio::pin!(stream);

View file

@ -22,7 +22,7 @@ use tokio::io::AsyncWriteExt;
use tracing::Instrument; use tracing::Instrument;
use super::read_sjson_file; use super::read_sjson_file;
use crate::state::{PackageInfo, State}; use crate::state::{ActionState, PackageInfo};
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";
@ -74,7 +74,7 @@ where
); );
fs::copy(path, &backup_path).await.wrap_err_with(|| { fs::copy(path, &backup_path).await.wrap_err_with(|| {
format!( format!(
"failed to back up {} '{}' to '{}'", "Failed to back up {} '{}' to '{}'",
file_name, file_name,
path.display(), path.display(),
backup_path.display() backup_path.display()
@ -83,13 +83,13 @@ where
tracing::debug!("Reading {} from original '{}'", file_name, path.display()); tracing::debug!("Reading {} from original '{}'", file_name, path.display());
fs::read(path).await.wrap_err_with(|| { fs::read(path).await.wrap_err_with(|| {
format!("failed to read {} file: {}", file_name, path.display()) format!("Failed to read {} file: {}", file_name, path.display())
})? })?
} }
Err(err) => { Err(err) => {
return Err(err).wrap_err_with(|| { return Err(err).wrap_err_with(|| {
format!( format!(
"failed to read {} from backup '{}'", "Failed to read {} from backup '{}'",
file_name, file_name,
backup_path.display() backup_path.display()
) )
@ -100,17 +100,17 @@ where
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn patch_game_settings(state: Arc<State>) -> Result<()> { async fn patch_game_settings(state: Arc<ActionState>) -> Result<()> {
let settings_path = state.game_dir.join("bundle").join(SETTINGS_FILE_PATH); let settings_path = state.game_dir.join("bundle").join(SETTINGS_FILE_PATH);
let settings = read_file_with_backup(&settings_path) let settings = read_file_with_backup(&settings_path)
.await .await
.wrap_err("failed to read settings.ini")?; .wrap_err("Failed to read settings.ini")?;
let settings = String::from_utf8(settings).wrap_err("settings.ini is not valid UTF-8")?; let settings = String::from_utf8(settings).wrap_err("Settings.ini is not valid UTF-8")?;
let mut f = fs::File::create(&settings_path) let mut f = fs::File::create(&settings_path)
.await .await
.wrap_err_with(|| format!("failed to open {}", settings_path.display()))?; .wrap_err_with(|| format!("Failed to open {}", settings_path.display()))?;
let Some(i) = settings.find("boot_script =") else { let Some(i) = settings.find("boot_script =") else {
eyre::bail!("couldn't find 'boot_script' field"); eyre::bail!("couldn't find 'boot_script' field");
@ -138,7 +138,7 @@ fn make_package(info: &PackageInfo) -> Result<Package> {
.next() .next()
.ok_or_else(|| eyre::eyre!("missing file extension")) .ok_or_else(|| eyre::eyre!("missing file extension"))
.and_then(BundleFileType::from_str) .and_then(BundleFileType::from_str)
.wrap_err("invalid file name in package info")?; .wrap_err("Invalid file name in package info")?;
let name: String = it.collect(); let name: String = it.collect();
pkg.add_file(file_type, name); pkg.add_file(file_type, name);
} }
@ -146,7 +146,7 @@ fn make_package(info: &PackageInfo) -> Result<Package> {
Ok(pkg) Ok(pkg)
} }
fn build_mod_data_lua(state: Arc<State>) -> String { fn build_mod_data_lua(state: Arc<ActionState>) -> String {
let mut lua = String::from("return {\n"); let mut lua = String::from("return {\n");
// DMF is handled explicitely by the loading procedures, as it actually drives most of that // DMF is handled explicitely by the loading procedures, as it actually drives most of that
@ -203,7 +203,7 @@ fn build_mod_data_lua(state: Arc<State>) -> String {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> { async fn build_bundles(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string()); let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string());
let mut tasks = Vec::new(); let mut tasks = Vec::new();
@ -216,9 +216,9 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
let _enter = span.enter(); let _enter = span.enter();
let lua = build_mod_data_lua(state.clone()); let lua = build_mod_data_lua(state.clone());
let lua = CString::new(lua).wrap_err("failed to build CString from mod data Lua string")?; let lua = CString::new(lua).wrap_err("Failed to build CString from mod data Lua string")?;
let file = let file =
lua::compile(MOD_DATA_SCRIPT, &lua).wrap_err("failed to compile mod data Lua file")?; lua::compile(MOD_DATA_SCRIPT, &lua).wrap_err("Failed to compile mod data Lua file")?;
mod_bundle.add_file(file); mod_bundle.add_file(file);
} }
@ -227,16 +227,16 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
let span = tracing::trace_span!("building mod packages", name = mod_info.name); let span = tracing::trace_span!("building mod packages", name = mod_info.name);
let _enter = span.enter(); let _enter = span.enter();
let mod_dir = state.get_mod_dir().join(&mod_info.id); let mod_dir = state.mod_dir.join(&mod_info.id);
for pkg_info in &mod_info.packages { for pkg_info in &mod_info.packages {
let span = tracing::trace_span!("building package", name = pkg_info.name); let span = tracing::trace_span!("building package", name = pkg_info.name);
let _enter = span.enter(); let _enter = span.enter();
let pkg = make_package(pkg_info).wrap_err("failed to make package")?; let pkg = make_package(pkg_info).wrap_err("Failed to make package")?;
let mut variant = BundleFileVariant::new(); let mut variant = BundleFileVariant::new();
let bin = pkg let bin = pkg
.to_binary() .to_binary()
.wrap_err("failed to serialize package to binary")?; .wrap_err("Failed to serialize package to binary")?;
variant.set_data(bin); variant.set_data(bin);
let mut file = BundleFile::new(pkg_info.name.clone(), BundleFileType::Package); let mut file = BundleFile::new(pkg_info.name.clone(), BundleFileType::Package);
file.add_variant(variant); file.add_variant(variant);
@ -260,11 +260,11 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
let task = async move { let task = async move {
let bundle = { let bundle = {
let bin = fs::read(&src).await.wrap_err_with(|| { let bin = fs::read(&src).await.wrap_err_with(|| {
format!("failed to read bundle file '{}'", src.display()) format!("Failed to read bundle file '{}'", src.display())
})?; })?;
let name = Bundle::get_name_from_path(&ctx, &src); let name = Bundle::get_name_from_path(&ctx, &src);
Bundle::from_binary(&ctx, name, bin) Bundle::from_binary(&ctx, name, bin)
.wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))? .wrap_err_with(|| format!("Failed to parse bundle '{}'", src.display()))?
}; };
tracing::debug!( tracing::debug!(
@ -283,7 +283,7 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
let _ = fs::remove_file(&dest).await; let _ = fs::remove_file(&dest).await;
fs::copy(&src, &dest).await.wrap_err_with(|| { fs::copy(&src, &dest).await.wrap_err_with(|| {
format!( format!(
"failed to copy bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", "Failed to copy bundle {pkg_name} for mod {mod_name}. Src: {}, dest: {}",
src.display(), src.display(),
dest.display() dest.display()
) )
@ -311,7 +311,7 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
tracing::trace!("Writing mod bundle to '{}'", path.display()); tracing::trace!("Writing mod bundle to '{}'", path.display());
fs::write(&path, mod_bundle.to_binary()?) fs::write(&path, mod_bundle.to_binary()?)
.await .await
.wrap_err_with(|| format!("failed to write bundle to '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to write bundle to '{}'", path.display()))?;
} }
bundles.push(mod_bundle); bundles.push(mod_bundle);
@ -320,7 +320,7 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> { async fn patch_boot_bundle(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
let bundle_dir = Arc::new(state.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 bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())));
@ -329,14 +329,14 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
let mut boot_bundle = async { let mut boot_bundle = async {
let bin = read_file_with_backup(&bundle_path) let bin = read_file_with_backup(&bundle_path)
.await .await
.wrap_err("failed to read boot bundle")?; .wrap_err("Failed to read boot bundle")?;
Bundle::from_binary(&state.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") .wrap_err("Failed to parse boot bundle")
} }
.instrument(tracing::trace_span!("read boot bundle")) .instrument(tracing::trace_span!("read boot bundle"))
.await .await
.wrap_err_with(|| format!("failed to read bundle '{}'", BOOT_BUNDLE_NAME))?; .wrap_err_with(|| format!("Failed to read bundle '{}'", BOOT_BUNDLE_NAME))?;
{ {
tracing::trace!("Adding mod package file to boot bundle"); tracing::trace!("Adding mod package file to boot bundle");
@ -381,16 +381,16 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
let bundle_name = Murmur64::hash(&pkg_info.name) let bundle_name = Murmur64::hash(&pkg_info.name)
.to_string() .to_string()
.to_ascii_lowercase(); .to_ascii_lowercase();
let src = state.get_mod_dir().join(&mod_info.id).join(&bundle_name); let src = state.mod_dir.join(&mod_info.id).join(&bundle_name);
{ {
let bin = fs::read(&src) let bin = fs::read(&src)
.await .await
.wrap_err_with(|| format!("failed to read bundle file '{}'", src.display()))?; .wrap_err_with(|| format!("Failed to read bundle file '{}'", src.display()))?;
let name = Bundle::get_name_from_path(&state.ctx, &src); let name = Bundle::get_name_from_path(&state.ctx, &src);
let dml_bundle = Bundle::from_binary(&state.ctx, name, bin) let dml_bundle = Bundle::from_binary(&state.ctx, name, bin)
.wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))?; .wrap_err_with(|| format!("Failed to parse bundle '{}'", src.display()))?;
bundles.push(dml_bundle); bundles.push(dml_bundle);
}; };
@ -416,14 +416,14 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
let _ = fs::remove_file(&dest).await; let _ = fs::remove_file(&dest).await;
fs::copy(&src, &dest).await.wrap_err_with(|| { fs::copy(&src, &dest).await.wrap_err_with(|| {
format!( format!(
"failed to copy bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", "Failed to copy bundle {pkg_name} for mod {mod_name}. Src: {}, dest: {}",
src.display(), src.display(),
dest.display() dest.display()
) )
})?; })?;
} }
let pkg = make_package(pkg_info).wrap_err("failed to create package file for dml")?; let pkg = make_package(pkg_info).wrap_err("Failed to create package file for dml")?;
variant.set_data(pkg.to_binary()?); variant.set_data(pkg.to_binary()?);
let mut f = BundleFile::new(DML_BUNDLE_NAME.to_string(), BundleFileType::Package); let mut f = BundleFile::new(DML_BUNDLE_NAME.to_string(), BundleFileType::Package);
@ -437,9 +437,9 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
let _enter = span.enter(); 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 lua = CString::new(lua).wrap_err("Failed to build CString from mod main Lua string")?;
let file = let file =
lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?; lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("Failed to compile mod main Lua file")?;
boot_bundle.add_file(file); boot_bundle.add_file(file);
} }
@ -447,10 +447,10 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
async { async {
let bin = boot_bundle let bin = boot_bundle
.to_binary() .to_binary()
.wrap_err("failed to serialize boot bundle")?; .wrap_err("Failed to serialize boot bundle")?;
fs::write(&bundle_path, bin) fs::write(&bundle_path, bin)
.await .await
.wrap_err_with(|| format!("failed to write main bundle: {}", bundle_path.display())) .wrap_err_with(|| format!("Failed to write main bundle: {}", bundle_path.display()))
} }
.instrument(tracing::trace_span!("write boot bundle")) .instrument(tracing::trace_span!("write boot bundle"))
.await?; .await?;
@ -461,7 +461,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
} }
#[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))] #[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))]
async fn patch_bundle_database<B>(state: Arc<State>, bundles: B) -> Result<()> async fn patch_bundle_database<B>(state: Arc<ActionState>, bundles: B) -> Result<()>
where where
B: AsRef<[Bundle]>, B: AsRef<[Bundle]>,
{ {
@ -471,9 +471,9 @@ where
let mut db = { let mut db = {
let bin = read_file_with_backup(&database_path) let bin = read_file_with_backup(&database_path)
.await .await
.wrap_err("failed to read bundle database")?; .wrap_err("Failed to read bundle database")?;
let mut r = Cursor::new(bin); let mut r = Cursor::new(bin);
let db = BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database")?; let db = BundleDatabase::from_binary(&mut r).wrap_err("Failed to parse bundle database")?;
tracing::trace!("Finished parsing bundle database"); tracing::trace!("Finished parsing bundle database");
db db
}; };
@ -486,7 +486,7 @@ where
{ {
let bin = db let bin = db
.to_binary() .to_binary()
.wrap_err("failed to serialize bundle database")?; .wrap_err("Failed to serialize bundle database")?;
fs::write(&database_path, bin).await.wrap_err_with(|| { fs::write(&database_path, bin).await.wrap_err_with(|| {
format!( format!(
"failed to write bundle database to '{}'", "failed to write bundle database to '{}'",
@ -499,7 +499,7 @@ where
} }
#[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))] #[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))]
async fn write_deployment_data<B>(state: Arc<State>, bundles: B) -> Result<()> async fn write_deployment_data<B>(state: Arc<ActionState>, bundles: B) -> Result<()>
where where
B: AsRef<[Bundle]>, B: AsRef<[Bundle]>,
{ {
@ -512,11 +512,11 @@ where
.collect(), .collect(),
}; };
let path = state.game_dir.join(DEPLOYMENT_DATA_PATH); let path = state.game_dir.join(DEPLOYMENT_DATA_PATH);
let data = serde_sjson::to_string(&info).wrap_err("failed to serizalie deployment data")?; let data = serde_sjson::to_string(&info).wrap_err("Failed to serizalie deployment data")?;
fs::write(&path, &data) fs::write(&path, &data)
.await .await
.wrap_err_with(|| format!("failed to write deployment data to '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to write deployment data to '{}'", path.display()))?;
Ok(()) Ok(())
} }
@ -525,7 +525,7 @@ where
game_dir = %state.game_dir.display(), game_dir = %state.game_dir.display(),
mods = state.mods.len() mods = state.mods.len()
))] ))]
pub(crate) async fn deploy_mods(state: State) -> Result<()> { pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> {
let state = Arc::new(state); let state = Arc::new(state);
{ {
@ -552,15 +552,15 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
if let Some(err) = err.downcast_ref::<std::io::Error>() && err.kind() == ErrorKind::NotFound { if let Some(err) = err.downcast_ref::<std::io::Error>() && err.kind() == ErrorKind::NotFound {
Ok(None) Ok(None)
} else { } else {
Err(err).wrap_err("failed to read deployment data") Err(err).wrap_err("Failed to read deployment data")
} }
} }
} }
} }
) )
.wrap_err("failed to gather deployment information")?; .wrap_err("Failed to gather deployment information")?;
let game_info = game_info.wrap_err("failed to collect Steam info")?; let game_info = game_info.wrap_err("Failed to collect Steam info")?;
tracing::debug!(?game_info, ?deployment_info); tracing::debug!(?game_info, ?deployment_info);
@ -581,12 +581,12 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
tracing::info!("Build mod bundles"); tracing::info!("Build mod bundles");
let mut bundles = build_bundles(state.clone()) let mut bundles = build_bundles(state.clone())
.await .await
.wrap_err("failed to build mod bundles")?; .wrap_err("Failed to build mod bundles")?;
tracing::info!("Patch boot bundle"); tracing::info!("Patch boot bundle");
let mut more_bundles = patch_boot_bundle(state.clone()) let mut more_bundles = patch_boot_bundle(state.clone())
.await .await
.wrap_err("failed to patch boot bundle")?; .wrap_err("Failed to patch boot bundle")?;
bundles.append(&mut more_bundles); bundles.append(&mut more_bundles);
if let Some(info) = &deployment_info { if let Some(info) = &deployment_info {
@ -609,7 +609,7 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
tracing::debug!("Removing unused bundle '{}'", file_name); tracing::debug!("Removing unused bundle '{}'", file_name);
if let Err(err) = fs::remove_file(&path).await.wrap_err_with(|| { if let Err(err) = fs::remove_file(&path).await.wrap_err_with(|| {
format!("failed to remove unused bundle '{}'", path.display()) format!("Failed to remove unused bundle '{}'", path.display())
}) { }) {
tracing::error!("{:?}", err); tracing::error!("{:?}", err);
} }
@ -626,24 +626,24 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
tracing::info!("Patch game settings"); tracing::info!("Patch game settings");
patch_game_settings(state.clone()) patch_game_settings(state.clone())
.await .await
.wrap_err("failed to patch game settings")?; .wrap_err("Failed to patch game settings")?;
tracing::info!("Patching bundle database"); tracing::info!("Patching bundle database");
patch_bundle_database(state.clone(), &bundles) patch_bundle_database(state.clone(), &bundles)
.await .await
.wrap_err("failed to patch bundle database")?; .wrap_err("Failed to patch bundle database")?;
tracing::info!("Writing deployment data"); tracing::info!("Writing deployment data");
write_deployment_data(state.clone(), &bundles) write_deployment_data(state.clone(), &bundles)
.await .await
.wrap_err("failed to write deployment data")?; .wrap_err("Failed to write deployment data")?;
tracing::info!("Finished deploying mods"); tracing::info!("Finished deploying mods");
Ok(()) Ok(())
} }
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { pub(crate) async fn reset_mod_deployment(state: ActionState) -> Result<()> {
let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())); let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()));
let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path, SETTINGS_FILE_PATH]; let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path, SETTINGS_FILE_PATH];
let bundle_dir = state.game_dir.join("bundle"); let bundle_dir = state.game_dir.join("bundle");
@ -662,14 +662,14 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
} }
Err(err) => { Err(err) => {
return Err(err).wrap_err_with(|| { return Err(err).wrap_err_with(|| {
format!("failed to read deployment info at '{}'", path.display()) format!("Failed to read deployment info at '{}'", path.display())
}); });
} }
}; };
let data = String::from_utf8(data).wrap_err("invalid UTF8 in deployment data")?; let data = String::from_utf8(data).wrap_err("Invalid UTF8 in deployment data")?;
serde_sjson::from_str(&data).wrap_err("invalid SJSON in deployment data")? serde_sjson::from_str(&data).wrap_err("Invalid SJSON in deployment data")?
}; };
for name in info.bundles { for name in info.bundles {
@ -697,7 +697,7 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
fs::copy(&backup, &path) fs::copy(&backup, &path)
.await .await
.wrap_err_with(|| format!("failed to copy from '{}'", backup.display()))?; .wrap_err_with(|| format!("Failed to copy from '{}'", backup.display()))?;
tracing::debug!("Deleting backup: {}", backup.display()); tracing::debug!("Deleting backup: {}", backup.display());
@ -705,7 +705,7 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) => { Err(err) => {
Err(err).wrap_err_with(|| format!("failed to remove '{}'", backup.display())) Err(err).wrap_err_with(|| format!("Failed to remove '{}'", backup.display()))
} }
} }
} }

View file

@ -17,7 +17,7 @@ where
let path = path.as_ref(); let path = path.as_ref();
let buf = fs::read(path) let buf = fs::read(path)
.await .await
.wrap_err_with(|| format!("failed to read file '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to read file '{}'", path.display()))?;
let data = String::from_utf8(buf).wrap_err("invalid UTF8")?; let data = String::from_utf8(buf).wrap_err("Invalid UTF8")?;
serde_sjson::from_str(&data).wrap_err("failed to deserialize SJSON") serde_sjson::from_str(&data).wrap_err("Failed to deserialize SJSON")
} }

View file

@ -1,5 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use color_eyre::eyre::Context;
use color_eyre::Help;
use color_eyre::Report;
use color_eyre::Result; use color_eyre::Result;
use druid::{ExtEventSink, SingleUse, Target}; use druid::{ExtEventSink, SingleUse, Target};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
@ -10,11 +13,19 @@ use crate::controller::app::*;
use crate::controller::game::*; use crate::controller::game::*;
use crate::state::AsyncAction; use crate::state::AsyncAction;
use crate::state::ACTION_FINISH_SAVE_SETTINGS; use crate::state::ACTION_FINISH_SAVE_SETTINGS;
use crate::state::ACTION_SHOW_ERROR_DIALOG;
use crate::state::{ use crate::state::{
ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY, ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY,
ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG, ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG,
}; };
async fn send_error(sink: Arc<RwLock<ExtEventSink>>, err: Report) {
sink.write()
.await
.submit_command(ACTION_SHOW_ERROR_DIALOG, SingleUse::new(err), Target::Auto)
.expect("failed to send command");
}
async fn handle_action( async fn handle_action(
event_sink: Arc<RwLock<ExtEventSink>>, event_sink: Arc<RwLock<ExtEventSink>>,
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>, action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
@ -23,8 +34,9 @@ async fn handle_action(
let event_sink = event_sink.clone(); let event_sink = event_sink.clone();
match action { match action {
AsyncAction::DeployMods(state) => tokio::spawn(async move { AsyncAction::DeployMods(state) => tokio::spawn(async move {
if let Err(err) = deploy_mods(state).await { if let Err(err) = deploy_mods(state).await.wrap_err("Failed to deploy mods") {
tracing::error!("Failed to deploy mods: {:?}", err); tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink
@ -33,8 +45,11 @@ async fn handle_action(
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
.expect("failed to send command"); .expect("failed to send command");
}), }),
AsyncAction::AddMod((state, info)) => tokio::spawn(async move { AsyncAction::AddMod(state, info) => tokio::spawn(async move {
match import_mod(state, info).await { match import_mod(state, info)
.await
.wrap_err("Failed to import mod")
{
Ok(mod_info) => { Ok(mod_info) => {
event_sink event_sink
.write() .write()
@ -47,18 +62,22 @@ async fn handle_action(
.expect("failed to send command"); .expect("failed to send command");
} }
Err(err) => { Err(err) => {
tracing::error!("Failed to import mod: {:?}", err); tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
} }
}), }),
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { AsyncAction::DeleteMod(state, info) => tokio::spawn(async move {
if let Err(err) = delete_mod(state, &info).await { let mod_dir = state.mod_dir.join(&info.id);
tracing::error!( if let Err(err) = delete_mod(state, &info)
"Failed to delete mod files. \ .await
You might want to clean up the data directory manually. \ .wrap_err("Failed to delete mod files")
Reason: {:?}", .with_suggestion(|| {
err format!("Clean the folder '{}' manually", mod_dir.display())
); })
{
tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink
@ -72,8 +91,12 @@ async fn handle_action(
.expect("failed to send command"); .expect("failed to send command");
}), }),
AsyncAction::ResetDeployment(state) => tokio::spawn(async move { AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
if let Err(err) = reset_mod_deployment(state).await { if let Err(err) = reset_mod_deployment(state)
tracing::error!("Failed to reset mod deployment: {:?}", err); .await
.wrap_err("Failed to reset mod deployment")
{
tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink
@ -83,8 +106,12 @@ async fn handle_action(
.expect("failed to send command"); .expect("failed to send command");
}), }),
AsyncAction::SaveSettings(state) => tokio::spawn(async move { AsyncAction::SaveSettings(state) => tokio::spawn(async move {
if let Err(err) = save_settings(state).await { if let Err(err) = save_settings(state)
tracing::error!("Failed to save settings: {:?}", err); .await
.wrap_err("Failed to save settings")
{
tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink

View file

@ -58,7 +58,7 @@ fn main() -> Result<()> {
} }
let config = util::config::read_config(&default_config_path, &matches) let config = util::config::read_config(&default_config_path, &matches)
.wrap_err("failed to read config file")?; .wrap_err("Failed to read config file")?;
let game_info = dtmt_shared::collect_game_info()?; let game_info = dtmt_shared::collect_game_info()?;
@ -71,7 +71,7 @@ fn main() -> Result<()> {
config.data_dir.unwrap_or_default(), config.data_dir.unwrap_or_default(),
); );
state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter()) state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter())
.wrap_err("failed to load mods")?; .wrap_err("Failed to load mods")?;
state state
}; };

View file

@ -1,6 +1,9 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use druid::{im::Vector, Data, Lens}; use druid::{
im::{HashMap, Vector},
Data, Lens, WindowHandle, WindowId,
};
use dtmt_shared::ModConfig; use dtmt_shared::ModConfig;
use super::SelectedModLens; use super::SelectedModLens;
@ -86,6 +89,9 @@ pub(crate) struct State {
pub config_path: Arc<PathBuf>, pub config_path: Arc<PathBuf>,
#[lens(ignore)] #[lens(ignore)]
#[data(ignore)] #[data(ignore)]
pub windows: HashMap<WindowId, WindowHandle>,
#[lens(ignore)]
#[data(ignore)]
pub ctx: Arc<sdk::Context>, pub ctx: Arc<sdk::Context>,
} }
@ -110,6 +116,7 @@ impl State {
game_dir: Arc::new(game_dir), game_dir: Arc::new(game_dir),
data_dir: Arc::new(data_dir), data_dir: Arc::new(data_dir),
log: Arc::new(String::new()), log: Arc::new(String::new()),
windows: HashMap::new(),
} }
} }

View file

@ -1,10 +1,14 @@
use std::sync::Arc; use std::{path::PathBuf, sync::Arc};
use color_eyre::Report;
use druid::{ use druid::{
AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target, im::Vector, AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse,
Target, WindowHandle, WindowId,
}; };
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::ui::window;
use super::{ModInfo, State}; use super::{ModInfo, State};
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod"); pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod");
@ -37,12 +41,42 @@ pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector =
pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty"); pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty");
pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector<SingleUse<Report>> =
Selector::new("dtmm.action.show-error-dialog");
pub(crate) const ACTION_SET_WINDOW_HANDLE: Selector<SingleUse<(WindowId, WindowHandle)>> =
Selector::new("dtmm.action.set-window-handle");
// A sub-selection of `State`'s fields that are required in `AsyncAction`s and that are
// `Send + Sync`
pub(crate) struct ActionState {
pub mods: Vector<Arc<ModInfo>>,
pub game_dir: Arc<PathBuf>,
pub data_dir: Arc<PathBuf>,
pub mod_dir: Arc<PathBuf>,
pub config_path: Arc<PathBuf>,
pub ctx: Arc<sdk::Context>,
}
impl From<State> for ActionState {
fn from(state: State) -> Self {
Self {
mods: state.mods,
game_dir: state.game_dir,
mod_dir: Arc::new(state.data_dir.join("mods")),
data_dir: state.data_dir,
config_path: state.config_path,
ctx: state.ctx,
}
}
}
pub(crate) enum AsyncAction { pub(crate) enum AsyncAction {
DeployMods(State), DeployMods(ActionState),
ResetDeployment(State), ResetDeployment(ActionState),
AddMod((State, FileInfo)), AddMod(ActionState, FileInfo),
DeleteMod((State, Arc<ModInfo>)), DeleteMod(ActionState, Arc<ModInfo>),
SaveSettings(State), SaveSettings(ActionState),
} }
pub(crate) struct Delegate { pub(crate) struct Delegate {
@ -73,7 +107,7 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_START_DEPLOY) => { cmd if cmd.is(ACTION_START_DEPLOY) => {
if self if self
.sender .sender
.send(AsyncAction::DeployMods(state.clone())) .send(AsyncAction::DeployMods(state.clone().into()))
.is_ok() .is_ok()
{ {
state.is_deployment_in_progress = true; state.is_deployment_in_progress = true;
@ -91,7 +125,7 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => {
if self if self
.sender .sender
.send(AsyncAction::ResetDeployment(state.clone())) .send(AsyncAction::ResetDeployment(state.clone().into()))
.is_ok() .is_ok()
{ {
state.is_reset_in_progress = true; state.is_reset_in_progress = true;
@ -147,11 +181,12 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
let info = cmd let info = cmd
.get(ACTION_START_DELETE_SELECTED_MOD) .get(ACTION_START_DELETE_SELECTED_MOD)
.and_then(|info| info.take()) .and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
if self if self
.sender .sender
.send(AsyncAction::DeleteMod((state.clone(), info))) .send(AsyncAction::DeleteMod(state.clone().into(), info))
.is_err() .is_err()
{ {
tracing::error!("Failed to queue action to deploy mods"); tracing::error!("Failed to queue action to deploy mods");
@ -162,8 +197,9 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => {
let info = cmd let info = cmd
.get(ACTION_FINISH_DELETE_SELECTED_MOD) .get(ACTION_FINISH_DELETE_SELECTED_MOD)
.and_then(|info| info.take()) .and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id); let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id);
let Some((index, _)) = found else { let Some((index, _)) = found else {
return Handled::No; return Handled::No;
@ -177,9 +213,10 @@ impl AppDelegate<State> for Delegate {
let info = cmd let info = cmd
.get(ACTION_ADD_MOD) .get(ACTION_ADD_MOD)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
if self if self
.sender .sender
.send(AsyncAction::AddMod((state.clone(), info.clone()))) .send(AsyncAction::AddMod(state.clone().into(), info.clone()))
.is_err() .is_err()
{ {
tracing::error!("Failed to queue action to add mod"); tracing::error!("Failed to queue action to add mod");
@ -190,9 +227,11 @@ impl AppDelegate<State> for Delegate {
let info = cmd let info = cmd
.get(ACTION_FINISH_ADD_MOD) .get(ACTION_FINISH_ADD_MOD)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
if let Some(info) = info.take() { if let Some(info) = info.take() {
state.add_mod(info); state.add_mod(info);
} }
Handled::Yes Handled::Yes
} }
cmd if cmd.is(ACTION_LOG) => { cmd if cmd.is(ACTION_LOG) => {
@ -209,7 +248,7 @@ impl AppDelegate<State> for Delegate {
state.is_next_save_pending = true; state.is_next_save_pending = true;
} else if self } else if self
.sender .sender
.send(AsyncAction::SaveSettings(state.clone())) .send(AsyncAction::SaveSettings(state.clone().into()))
.is_ok() .is_ok()
{ {
state.is_save_in_progress = true; state.is_save_in_progress = true;
@ -233,6 +272,31 @@ impl AppDelegate<State> for Delegate {
state.dirty = true; state.dirty = true;
Handled::Yes Handled::Yes
} }
cmd if cmd.is(ACTION_SHOW_ERROR_DIALOG) => {
let err = cmd
.get(ACTION_SHOW_ERROR_DIALOG)
.and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value");
let window = state
.windows
.get(&window::main::WINDOW_ID)
.expect("root window does not exist");
let dialog = window::dialog::error::<State>(err, window.clone());
ctx.new_window(dialog);
Handled::Yes
}
cmd if cmd.is(ACTION_SET_WINDOW_HANDLE) => {
let (id, handle) = cmd
.get(ACTION_SET_WINDOW_HANDLE)
.and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value");
state.windows.insert(id, handle);
Handled::Yes
}
cmd => { cmd => {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
tracing::warn!("Unknown command: {:?}", cmd); tracing::warn!("Unknown command: {:?}", cmd);
@ -241,4 +305,19 @@ impl AppDelegate<State> for Delegate {
} }
} }
} }
fn window_added(
&mut self,
id: WindowId,
handle: WindowHandle,
data: &mut State,
_: &Env,
_: &mut DelegateCtx,
) {
data.windows.insert(id, handle);
}
fn window_removed(&mut self, id: WindowId, data: &mut State, _: &Env, _: &mut DelegateCtx) {
data.windows.remove(&id);
}
} }

View file

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

View file

@ -0,0 +1,36 @@
use color_eyre::Report;
use druid::widget::{Button, CrossAxisAlignment, Flex, Label, LineBreaking, MainAxisAlignment};
use druid::{Data, WidgetExt, WindowDesc, WindowHandle, WindowLevel, WindowSizePolicy};
const ERROR_DIALOG_SIZE: (f64, f64) = (750., 400.);
pub fn error<T: Data>(err: Report, parent: WindowHandle) -> WindowDesc<T> {
let msg = format!("A critical error ocurred: {:?}", err);
let stripped =
strip_ansi_escapes::strip(msg.as_bytes()).expect("failed to strip ANSI in error");
let msg = String::from_utf8_lossy(&stripped);
let text = Label::new(msg.to_string()).with_line_break_mode(LineBreaking::WordWrap);
let button = Button::new("Ok")
.on_click(|ctx, _, _| {
ctx.window().close();
})
.align_right();
let widget = Flex::column()
.main_axis_alignment(MainAxisAlignment::SpaceBetween)
.cross_axis_alignment(CrossAxisAlignment::End)
.with_child(text)
.with_spacer(20.)
.with_child(button)
.padding(10.);
WindowDesc::new(widget)
.title("Error")
.with_min_size(ERROR_DIALOG_SIZE)
.resizable(false)
.window_size_policy(WindowSizePolicy::Content)
.set_always_on_top(true)
.set_level(WindowLevel::Modal(parent))
}

View file

@ -1,25 +1,30 @@
use std::sync::Arc; use std::sync::Arc;
use druid::im::Vector; use druid::im::Vector;
use druid::lens;
use druid::widget::{ use druid::widget::{
Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment,
Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher,
}; };
use druid::{lens, LifeCycleCtx};
use druid::{ use druid::{
Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse,
TextAlignment, Widget, WidgetExt, WindowDesc, TextAlignment, Widget, WidgetExt, WindowDesc, WindowId,
}; };
use lazy_static::lazy_static;
use crate::state::{ use crate::state::{
ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP,
ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_DELETE_SELECTED_MOD,
ACTION_START_RESET_DEPLOYMENT, ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
}; };
use crate::ui::theme; use crate::ui::theme;
use crate::ui::widget::controller::{AutoScrollController, DirtyStateController}; use crate::ui::widget::controller::{AutoScrollController, DirtyStateController};
use crate::ui::widget::PathBufFormatter; use crate::ui::widget::PathBufFormatter;
lazy_static! {
pub static ref WINDOW_ID: WindowId = WindowId::next();
}
const TITLE: &str = "Darktide Mod Manager"; const TITLE: &str = "Darktide Mod Manager";
const WINDOW_SIZE: (f64, f64) = (1080., 720.); const WINDOW_SIZE: (f64, f64) = (1080., 720.);
const MOD_DETAILS_MIN_WIDTH: f64 = 325.; const MOD_DETAILS_MIN_WIDTH: f64 = 325.;
@ -324,4 +329,9 @@ fn build_window() -> impl Widget<State> {
.with_flex_child(build_main(), 1.0) .with_flex_child(build_main(), 1.0)
.with_child(build_log_view()) .with_child(build_log_view())
.controller(DirtyStateController) .controller(DirtyStateController)
.on_added(|_, ctx: &mut LifeCycleCtx, _, _| {
ctx.submit_command(
ACTION_SET_WINDOW_HANDLE.with(SingleUse::new((*WINDOW_ID, ctx.window().clone()))),
);
})
} }

View file

@ -7,7 +7,7 @@ use clap::{parser::ValueSource, ArgMatches};
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::state::{ModInfo, State}; use crate::state::{ActionState, ModInfo};
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub(crate) struct LoadOrderEntrySerialize<'a> { pub(crate) struct LoadOrderEntrySerialize<'a> {
@ -31,8 +31,8 @@ pub(crate) struct ConfigSerialize<'a> {
mod_order: Vec<LoadOrderEntrySerialize<'a>>, mod_order: Vec<LoadOrderEntrySerialize<'a>>,
} }
impl<'a> From<&'a State> for ConfigSerialize<'a> { impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
fn from(state: &'a State) -> Self { fn from(state: &'a ActionState) -> Self {
Self { Self {
game_dir: &state.game_dir, game_dir: &state.game_dir,
data_dir: &state.data_dir, data_dir: &state.data_dir,
@ -113,10 +113,10 @@ where
match fs::read(path) { match fs::read(path) {
Ok(data) => { Ok(data) => {
let data = String::from_utf8(data).wrap_err_with(|| { let data = String::from_utf8(data).wrap_err_with(|| {
format!("config file {} contains invalid UTF-8", path.display()) format!("Config file '{}' contains invalid UTF-8", path.display())
})?; })?;
let mut cfg: Config = serde_sjson::from_str(&data) let mut cfg: Config = serde_sjson::from_str(&data)
.wrap_err_with(|| format!("invalid config file {}", path.display()))?; .wrap_err_with(|| format!("Invalid config file {}", path.display()))?;
cfg.path = path.clone(); cfg.path = path.clone();
Ok(cfg) Ok(cfg)
@ -124,7 +124,7 @@ where
Err(err) if err.kind() == ErrorKind::NotFound => { Err(err) if err.kind() == ErrorKind::NotFound => {
if matches.value_source("config") != Some(ValueSource::DefaultValue) { if matches.value_source("config") != Some(ValueSource::DefaultValue) {
return Err(err) return Err(err)
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?; .wrap_err_with(|| format!("Failed to read config file {}", path.display()))?;
} }
{ {
@ -132,7 +132,7 @@ where
.parent() .parent()
.expect("a file path always has a parent directory"); .expect("a file path always has a parent directory");
fs::create_dir_all(parent).wrap_err_with(|| { fs::create_dir_all(parent).wrap_err_with(|| {
format!("failed to create directories {}", parent.display()) format!("Failed to create directories {}", parent.display())
})?; })?;
} }
@ -145,7 +145,7 @@ where
{ {
let data = serde_sjson::to_string(&config) let data = serde_sjson::to_string(&config)
.wrap_err("failed to serialize default config value")?; .wrap_err("Failed to serialize default config value")?;
fs::write(&config.path, data).wrap_err_with(|| { fs::write(&config.path, data).wrap_err_with(|| {
format!( format!(
"failed to write default config to {}", "failed to write default config to {}",
@ -157,7 +157,7 @@ where
Ok(config) Ok(config)
} }
Err(err) => { Err(err) => {
Err(err).wrap_err_with(|| format!("failed to read config file {}", path.display())) Err(err).wrap_err_with(|| format!("Failed to read config file {}", path.display()))
} }
} }
} }

View file

@ -20,7 +20,8 @@ impl ChannelWriter {
impl std::io::Write for ChannelWriter { impl std::io::Write for ChannelWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let tx = self.tx.clone(); let tx = self.tx.clone();
let string = String::from_utf8_lossy(buf).to_string(); let stripped = strip_ansi_escapes::strip(buf)?;
let string = String::from_utf8_lossy(&stripped).to_string();
// The `send` errors when the receiving end has closed. // The `send` errors when the receiving end has closed.
// But there's not much we can do at that point, so we just ignore it. // But there's not much we can do at that point, so we just ignore it.

View file

@ -66,7 +66,7 @@ async fn find_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
let (path, mut file) = if let Some(path) = dir { let (path, mut file) = if let Some(path) = dir {
let file = File::open(&path.join(PROJECT_CONFIG_NAME)) let file = File::open(&path.join(PROJECT_CONFIG_NAME))
.await .await
.wrap_err_with(|| format!("failed to open file: {}", path.display())) .wrap_err_with(|| format!("Failed to open file: {}", path.display()))
.with_suggestion(|| { .with_suggestion(|| {
format!( format!(
"Make sure the file at '{}' exists and is readable", "Make sure the file at '{}' exists and is readable",
@ -90,7 +90,7 @@ async fn find_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
} }
Err(err) => { Err(err) => {
let err = Report::new(err) let err = Report::new(err)
.wrap_err(format!("failed to open file: {}", path.display())); .wrap_err(format!("Failed to open file: {}", path.display()));
return Err(err); return Err(err);
} }
} }
@ -100,10 +100,10 @@ async fn find_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
let mut buf = String::new(); let mut buf = String::new();
file.read_to_string(&mut buf) file.read_to_string(&mut buf)
.await .await
.wrap_err("invalid UTF-8")?; .wrap_err("Invalid UTF-8")?;
let mut cfg: ModConfig = let mut cfg: ModConfig =
serde_sjson::from_str(&buf).wrap_err("failed to deserialize mod config")?; serde_sjson::from_str(&buf).wrap_err("Failed to deserialize mod config")?;
cfg.dir = path; cfg.dir = path;
Ok(cfg) Ok(cfg)
} }
@ -175,18 +175,18 @@ where
path.set_extension("package"); path.set_extension("package");
let sjson = fs::read_to_string(&path) let sjson = fs::read_to_string(&path)
.await .await
.wrap_err_with(|| format!("failed to read file {}", path.display()))?; .wrap_err_with(|| format!("Failed to read file {}", path.display()))?;
let pkg_name = package.to_slash_lossy().to_string(); let pkg_name = package.to_slash_lossy().to_string();
let pkg = Package::from_sjson(sjson, pkg_name.clone(), root) let pkg = Package::from_sjson(sjson, pkg_name.clone(), root)
.await .await
.wrap_err_with(|| format!("invalid package file {}", &pkg_name))?; .wrap_err_with(|| format!("Invalid package file {}", &pkg_name))?;
compile_package_files(&pkg, root) compile_package_files(&pkg, root)
.await .await
.wrap_err("failed to compile package") .wrap_err("Failed to compile package")
.and_then(|files| compile_bundle(pkg_name, files)) .and_then(|files| compile_bundle(pkg_name, files))
.wrap_err("failed to build bundle") .wrap_err("Failed to build bundle")
} }
fn normalize_file_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> { fn normalize_file_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
@ -211,7 +211,7 @@ pub(crate) async fn read_project_config(dir: Option<PathBuf>) -> Result<ModConfi
let mut cfg = find_project_config(dir).await?; let mut cfg = find_project_config(dir).await?;
cfg.resources.init = normalize_file_path(cfg.resources.init) cfg.resources.init = normalize_file_path(cfg.resources.init)
.wrap_err("invalid config field 'resources.init'") .wrap_err("Invalid config field 'resources.init'")
.with_suggestion(|| { .with_suggestion(|| {
"Specify a file path relative to and child path of the \ "Specify a file path relative to and child path of the \
directory where 'dtmt.cfg' is." directory where 'dtmt.cfg' is."
@ -225,7 +225,7 @@ pub(crate) async fn read_project_config(dir: Option<PathBuf>) -> Result<ModConfi
if let Some(path) = cfg.resources.data { if let Some(path) = cfg.resources.data {
let path = normalize_file_path(path) let path = normalize_file_path(path)
.wrap_err("invalid config field 'resources.data'") .wrap_err("Invalid config field 'resources.data'")
.with_suggestion(|| { .with_suggestion(|| {
"Specify a file path relative to and child path of the \ "Specify a file path relative to and child path of the \
directory where 'dtmt.cfg' is." directory where 'dtmt.cfg' is."
@ -241,7 +241,7 @@ pub(crate) async fn read_project_config(dir: Option<PathBuf>) -> Result<ModConfi
if let Some(path) = cfg.resources.localization { if let Some(path) = cfg.resources.localization {
let path = normalize_file_path(path) let path = normalize_file_path(path)
.wrap_err("invalid config field 'resources.localization'") .wrap_err("Invalid config field 'resources.localization'")
.with_suggestion(|| { .with_suggestion(|| {
"Specify a file path relative to and child path of the \ "Specify a file path relative to and child path of the \
directory where 'dtmt.cfg' is." directory where 'dtmt.cfg' is."
@ -281,7 +281,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
fs::create_dir_all(out_path) fs::create_dir_all(out_path)
.await .await
.wrap_err_with(|| format!("failed to create output directory '{}'", out_path.display()))?; .wrap_err_with(|| format!("Failed to create output directory '{}'", out_path.display()))?;
let file_map = Arc::new(Mutex::new(FileIndexMap::new())); let file_map = Arc::new(Mutex::new(FileIndexMap::new()));
@ -335,7 +335,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
); );
fs::write(&path, &data) fs::write(&path, &data)
.await .await
.wrap_err_with(|| format!("failed to write bundle to '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to write bundle to '{}'", path.display()))?;
if let Some(game_dir) = game_dir.as_ref() { if let Some(game_dir) = game_dir.as_ref() {
let path = game_dir.join(&name); let path = game_dir.join(&name);
@ -347,7 +347,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
); );
fs::write(&path, &data) fs::write(&path, &data)
.await .await
.wrap_err_with(|| format!("failed to write bundle to '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to write bundle to '{}'", path.display()))?;
} }
Ok(()) Ok(())
@ -355,7 +355,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
try_join_all(tasks) try_join_all(tasks)
.await .await
.wrap_err("failed to build mod bundles")?; .wrap_err("Failed to build mod bundles")?;
{ {
let file_map = file_map.lock().await; let file_map = file_map.lock().await;
@ -363,7 +363,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
let path = out_path.join("files.sjson"); let path = out_path.join("files.sjson");
fs::write(&path, data) fs::write(&path, data)
.await .await
.wrap_err_with(|| format!("failed to write file index to '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to write file index to '{}'", path.display()))?;
} }
tracing::info!("Compiled bundles written to '{}'", out_path.display()); tracing::info!("Compiled bundles written to '{}'", out_path.display());

View file

@ -61,7 +61,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
if let Some(name) = matches.get_one::<String>("replace") { if let Some(name) = matches.get_one::<String>("replace") {
let mut file = File::open(&file_path) let mut file = File::open(&file_path)
.await .await
.wrap_err_with(|| format!("failed to open '{}'", file_path.display()))?; .wrap_err_with(|| format!("Failed to open '{}'", file_path.display()))?;
if let Some(variant) = bundle if let Some(variant) = bundle
.files_mut() .files_mut()
@ -72,7 +72,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
let mut data = Vec::new(); let mut data = Vec::new();
file.read_to_end(&mut data) file.read_to_end(&mut data)
.await .await
.wrap_err("failed to read input file")?; .wrap_err("Failed to read input file")?;
variant.set_data(data); variant.set_data(data);
} else { } else {
let err = eyre::eyre!("No file '{}' in this bundle.", name) let err = eyre::eyre!("No file '{}' in this bundle.", name)
@ -99,11 +99,11 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
let out_path = matches.get_one::<PathBuf>("output").unwrap_or(bundle_path); let out_path = matches.get_one::<PathBuf>("output").unwrap_or(bundle_path);
let data = bundle let data = bundle
.to_binary() .to_binary()
.wrap_err("failed to write changed bundle to output")?; .wrap_err("Failed to write changed bundle to output")?;
fs::write(out_path, &data) fs::write(out_path, &data)
.await .await
.wrap_err("failed to write data to output file")?; .wrap_err("Failed to write data to output file")?;
Ok(()) Ok(())
} else { } else {

View file

@ -98,7 +98,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
async move { async move {
if let Err(err) = print_bundle_contents(&ctx, &p, fmt) if let Err(err) = print_bundle_contents(&ctx, &p, fmt)
.await .await
.wrap_err_with(|| format!("failed to list contents of bundle {}", p.display())) .wrap_err_with(|| format!("Failed to list contents of bundle {}", p.display()))
{ {
tracing::error!("{err:?}"); tracing::error!("{err:?}");
} }

View file

@ -122,7 +122,7 @@ pub(crate) async fn run(mut ctx: sdk::Context, matches: &ArgMatches) -> Result<(
.expect("required argument not found"); .expect("required argument not found");
u64::from_str_radix(s, 16) u64::from_str_radix(s, 16)
.wrap_err("failed to parse argument as hexadecimal string")? .wrap_err("Failed to parse argument as hexadecimal string")?
}; };
let groups = sub_matches let groups = sub_matches

View file

@ -86,7 +86,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
let root = if let Some(dir) = matches.get_one::<String>("root") { let root = if let Some(dir) = matches.get_one::<String>("root") {
if dir == "." { if dir == "." {
std::env::current_dir() std::env::current_dir()
.wrap_err("the current working dir is invalid") .wrap_err("The current working dir is invalid")
.with_suggestion(|| "Change to a different directory.")? .with_suggestion(|| "Change to a different directory.")?
} else { } else {
PathBuf::from(dir) PathBuf::from(dir)
@ -142,13 +142,13 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
.recursive(true) .recursive(true)
.create(&dir) .create(&dir)
.await .await
.wrap_err_with(|| format!("failed to create directory {}", dir.display()))?; .wrap_err_with(|| format!("Failed to create directory {}", dir.display()))?;
tracing::trace!("Writing file {}", path.display()); tracing::trace!("Writing file {}", path.display());
fs::write(&path, content.as_bytes()) fs::write(&path, content.as_bytes())
.await .await
.wrap_err_with(|| format!("failed to write content to path {}", path.display())) .wrap_err_with(|| format!("Failed to write content to path {}", path.display()))
}); });
futures::stream::iter(templates) futures::stream::iter(templates)

View file

@ -52,7 +52,7 @@ async fn process_dir_entry(res: Result<DirEntry>) -> Result<(OsString, Vec<u8>)>
let data = fs::read(&path) let data = fs::read(&path)
.await .await
.wrap_err_with(|| format!("failed to read '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to read '{}'", path.display()))?;
Ok((entry.file_name(), data)) Ok((entry.file_name(), data))
} }
@ -87,7 +87,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
let data = fs::read(&path) let data = fs::read(&path)
.await .await
.wrap_err_with(|| format!("failed to read mod config at {}", path.display()))?; .wrap_err_with(|| format!("Failed to read mod config at {}", path.display()))?;
zip.start_file(name.to_slash_lossy(), Default::default())?; zip.start_file(name.to_slash_lossy(), Default::default())?;
zip.write_all(&data)?; zip.write_all(&data)?;
@ -101,10 +101,10 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
); );
let read_dir = fs::read_dir(&path) let read_dir = fs::read_dir(&path)
.await .await
.wrap_err_with(|| format!("failed to read directory '{}'", path.display()))?; .wrap_err_with(|| format!("Failed to read directory '{}'", path.display()))?;
let stream = ReadDirStream::new(read_dir) let stream = ReadDirStream::new(read_dir)
.map(|res| res.wrap_err("failed to read dir entry")) .map(|res| res.wrap_err("Failed to read dir entry"))
.then(process_dir_entry); .then(process_dir_entry);
tokio::pin!(stream); tokio::pin!(stream);
@ -121,7 +121,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
fs::write(&dest, data.into_inner()) fs::write(&dest, data.into_inner())
.await .await
.wrap_err_with(|| format!("failed to write mod archive to '{}'", dest.display())) .wrap_err_with(|| format!("Failed to write mod archive to '{}'", dest.display()))
.with_suggestion(|| "Make sure that parent directories exist.".to_string())?; .with_suggestion(|| "Make sure that parent directories exist.".to_string())?;
tracing::info!("Mod archive written to {}", dest.display()); tracing::info!("Mod archive written to {}", dest.display());

View file

@ -75,7 +75,7 @@ async fn main() -> Result<()> {
tokio::spawn(async move { tokio::spawn(async move {
let res = File::open(&path) let res = File::open(&path)
.await .await
.wrap_err_with(|| format!("failed to open dictionary file: {}", path.display())); .wrap_err_with(|| format!("Failed to open dictionary file: {}", path.display()));
let f = match res { let f = match res {
Ok(f) => f, Ok(f) => f,
@ -102,7 +102,7 @@ async fn main() -> Result<()> {
tokio::spawn(async move { tokio::spawn(async move {
let conf = tokio::task::spawn_blocking(|| { let conf = tokio::task::spawn_blocking(|| {
confy::load::<GlobalConfig>(clap::crate_name!(), None) confy::load::<GlobalConfig>(clap::crate_name!(), None)
.wrap_err("failed to load global configuration") .wrap_err("Failed to load global configuration")
}) })
.await; .await;

View file

@ -548,7 +548,7 @@ impl BundleFile {
let _enter = span.enter(); let _enter = span.enter();
let header = BundleFileVariant::read_header(r) let header = BundleFileVariant::read_header(r)
.wrap_err_with(|| format!("failed to read header {i}"))?; .wrap_err_with(|| format!("Failed to read header {i}"))?;
// TODO: Figure out how `header.unknown_1` correlates to `properties::DATA` // TODO: Figure out how `header.unknown_1` correlates to `properties::DATA`
// if props.contains(Properties::DATA) { // if props.contains(Properties::DATA) {
@ -572,18 +572,18 @@ impl BundleFile {
let data = vec![]; let data = vec![];
let s = r let s = r
.read_string_len(header.size) .read_string_len(header.size)
.wrap_err("failed to read data file name")?; .wrap_err("Failed to read data file name")?;
(data, Some(s)) (data, Some(s))
} else { } else {
let mut data = vec![0; header.size]; let mut data = vec![0; header.size];
r.read_exact(&mut data) r.read_exact(&mut data)
.wrap_err_with(|| format!("failed to read file {i}"))?; .wrap_err_with(|| format!("Failed to read file {i}"))?;
let data_file_name = if header.len_data_file_name > 0 { let data_file_name = if header.len_data_file_name > 0 {
let s = r let s = r
.read_string_len(header.len_data_file_name) .read_string_len(header.len_data_file_name)
.wrap_err("failed to read data file name")?; .wrap_err("Failed to read data file name")?;
Some(s) Some(s)
} else { } else {
None None
@ -662,7 +662,7 @@ impl BundleFile {
match file_type { match file_type {
BundleFileType::Lua => { BundleFileType::Lua => {
let sjson = let sjson =
CString::new(sjson.as_ref()).wrap_err("failed to build CString from SJSON")?; CString::new(sjson.as_ref()).wrap_err("Failed to build CString from SJSON")?;
lua::compile(name, sjson) lua::compile(name, sjson)
} }
BundleFileType::Unknown(_) => { BundleFileType::Unknown(_) => {
@ -784,7 +784,7 @@ impl BundleFile {
} }
}; };
let res = res.wrap_err_with(|| format!("failed to decompile file {name}")); let res = res.wrap_err_with(|| format!("Failed to decompile file {name}"));
match res { match res {
Ok(files) => files, Ok(files) => files,
Err(err) => { Err(err) => {

View file

@ -164,7 +164,7 @@ impl Bundle {
OodleLZ_FuzzSafe::No, OodleLZ_FuzzSafe::No,
OodleLZ_CheckCRC::No, OodleLZ_CheckCRC::No,
) )
.wrap_err_with(|| format!("failed to decompress chunk {chunk_index}"))?; .wrap_err_with(|| format!("Failed to decompress chunk {chunk_index}"))?;
if unpacked_size_tracked < CHUNK_SIZE { if unpacked_size_tracked < CHUNK_SIZE {
raw_buffer.resize(unpacked_size_tracked, 0); raw_buffer.resize(unpacked_size_tracked, 0);
@ -192,7 +192,7 @@ impl Bundle {
let _enter = span.enter(); let _enter = span.enter();
let file = BundleFile::from_reader(ctx, &mut r, *props) let file = BundleFile::from_reader(ctx, &mut r, *props)
.wrap_err_with(|| format!("failed to read file {i}"))?; .wrap_err_with(|| format!("Failed to read file {i}"))?;
files.push(file); files.push(file);
} }

View file

@ -38,7 +38,7 @@ where
lua::lua_setglobal(state, b"code\0".as_ptr() as _); lua::lua_setglobal(state, b"code\0".as_ptr() as _);
let name = CString::new(name.as_bytes()) let name = CString::new(name.as_bytes())
.wrap_err_with(|| format!("cannot convert name into CString: {}", name))?; .wrap_err_with(|| format!("Cannot convert name into CString: {}", name))?;
lua::lua_pushstring(state, name.as_ptr() as _); lua::lua_pushstring(state, name.as_ptr() as _);
lua::lua_setglobal(state, b"name\0".as_ptr() as _); lua::lua_setglobal(state, b"name\0".as_ptr() as _);

View file

@ -148,7 +148,7 @@ impl Package {
None None
} else { } else {
let t = BundleFileType::from_str(ty) let t = BundleFileType::from_str(ty)
.wrap_err("invalid file type in package definition")?; .wrap_err("Invalid file type in package definition")?;
Some(t) Some(t)
}; };
@ -200,7 +200,7 @@ impl Package {
} }
} }
serde_sjson::to_string(&map).wrap_err("failed to serialize Package to SJSON") serde_sjson::to_string(&map).wrap_err("Failed to serialize Package to SJSON")
} }
#[tracing::instrument("Package::from_binary", skip(binary, ctx), fields(binary_len = binary.as_ref().len()))] #[tracing::instrument("Package::from_binary", skip(binary, ctx), fields(binary_len = binary.as_ref().len()))]

View file

@ -56,7 +56,7 @@ impl TryFrom<&str> for Murmur64 {
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(value: &str) -> Result<Self, Self::Error> {
u64::from_str_radix(value, 16) u64::from_str_radix(value, 16)
.map(Self) .map(Self)
.wrap_err_with(|| format!("failed to convert value to Murmur64: {value}")) .wrap_err_with(|| format!("Failed to convert value to Murmur64: {value}"))
} }
} }
@ -164,7 +164,7 @@ impl TryFrom<&str> for Murmur32 {
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(value: &str) -> Result<Self, Self::Error> {
u32::from_str_radix(value, 16) u32::from_str_radix(value, 16)
.map(Self) .map(Self)
.wrap_err_with(|| format!("failed to convert value to Murmur32: {value}")) .wrap_err_with(|| format!("Failed to convert value to Murmur32: {value}"))
} }
} }