Implement mod dependencies #63
5 changed files with 117 additions and 12 deletions
|
@ -10,6 +10,8 @@
|
||||||
- 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
|
- dtmm: show dialog for critical errors
|
||||||
|
- dtmm: check mod order before deployment
|
||||||
|
- dtmt: add mod dependencies to config
|
||||||
|
|
||||||
=== Fixed
|
=== Fixed
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use tokio_stream::wrappers::ReadDirStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
use crate::state::{ActionState, ModInfo, PackageInfo};
|
use crate::state::{ActionState, ModInfo, ModOrder, PackageInfo};
|
||||||
use crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
use crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
||||||
|
|
||||||
use super::read_sjson_file;
|
use super::read_sjson_file;
|
||||||
|
@ -216,3 +216,64 @@ where
|
||||||
Ok::<_, color_eyre::Report>(mods)
|
Ok::<_, color_eyre::Report>(mods)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_mod_order(state: &ActionState) -> Result<()> {
|
||||||
|
{
|
||||||
|
let first = state.mods.get(0);
|
||||||
|
if first.is_none() || !(first.unwrap().id == "dml" && first.unwrap().enabled) {
|
||||||
|
// TODO: Add a suggestion where to get it, once that's published
|
||||||
|
eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
.mods
|
||||||
|
.iter()
|
||||||
|
.filter(|i| i.enabled)
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(i, info)| tracing::debug!(i, ?info));
|
||||||
|
|
||||||
|
for (i, mod_info) in state.mods.iter().filter(|i| i.enabled).enumerate() {
|
||||||
|
for dep in &mod_info.depends {
|
||||||
|
let dep_info = state.mods.iter().enumerate().find(|(_, m)| m.id == dep.id);
|
||||||
|
|
||||||
|
match dep_info {
|
||||||
|
Some((_, dep_info)) if !dep_info.enabled => {
|
||||||
|
eyre::bail!(
|
||||||
|
"Dependency '{}' ({}) must be enabled.",
|
||||||
|
dep_info.name,
|
||||||
|
dep.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some((j, dep_info)) if dep.order == ModOrder::Before && j >= i => {
|
||||||
|
eyre::bail!(
|
||||||
|
"Dependency '{}' ({}) must be loaded before '{}'",
|
||||||
|
dep_info.name,
|
||||||
|
dep.id,
|
||||||
|
mod_info.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some((j, dep_info)) if dep.order == ModOrder::After && j <= i => {
|
||||||
|
eyre::bail!(
|
||||||
|
"Dependency '{}' ({}) must be loaded after '{}'",
|
||||||
|
dep_info.name,
|
||||||
|
dep.id,
|
||||||
|
mod_info.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
eyre::bail!(
|
||||||
|
"Missing dependency '{}' for mod '{}'",
|
||||||
|
dep.id,
|
||||||
|
mod_info.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
// All good
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ use tokio::io::AsyncWriteExt;
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
|
||||||
use super::read_sjson_file;
|
use super::read_sjson_file;
|
||||||
|
use crate::controller::app::check_mod_order;
|
||||||
use crate::state::{ActionState, PackageInfo};
|
use crate::state::{ActionState, PackageInfo};
|
||||||
|
|
||||||
const MOD_BUNDLE_NAME: &str = "packages/mods";
|
const MOD_BUNDLE_NAME: &str = "packages/mods";
|
||||||
|
@ -525,14 +526,6 @@ where
|
||||||
pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> {
|
pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> {
|
||||||
let state = Arc::new(state);
|
let state = Arc::new(state);
|
||||||
|
|
||||||
{
|
|
||||||
let first = state.mods.get(0);
|
|
||||||
if first.is_none() || !(first.unwrap().id == "dml" && first.unwrap().enabled) {
|
|
||||||
// TODO: Add a suggestion where to get it, once that's published
|
|
||||||
eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, game_info, deployment_info) = tokio::try_join!(
|
let (_, game_info, deployment_info) = tokio::try_join!(
|
||||||
async {
|
async {
|
||||||
let path = state.game_dir.join("bundle");
|
let path = state.game_dir.join("bundle");
|
||||||
|
@ -576,6 +569,8 @@ pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_mod_order(&state)?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Deploying {} mods to {}",
|
"Deploying {} mods to {}",
|
||||||
state.mods.iter().filter(|i| i.enabled).count(),
|
state.mods.iter().filter(|i| i.enabled).count(),
|
||||||
|
|
|
@ -39,6 +39,36 @@ pub(crate) struct ModResourceInfo {
|
||||||
pub localization: Option<PathBuf>,
|
pub localization: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Data, Debug, PartialEq)]
|
||||||
|
pub(crate) enum ModOrder {
|
||||||
|
Before,
|
||||||
|
After,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Data, Debug, PartialEq)]
|
||||||
|
pub(crate) struct ModDependency {
|
||||||
|
pub id: String,
|
||||||
|
pub order: ModOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dtmt_shared::ModDependency> for ModDependency {
|
||||||
|
fn from(value: dtmt_shared::ModDependency) -> Self {
|
||||||
|
match value {
|
||||||
|
dtmt_shared::ModDependency::ID(id) => ModDependency {
|
||||||
|
id,
|
||||||
|
order: ModOrder::Before,
|
||||||
|
},
|
||||||
|
dtmt_shared::ModDependency::Config { id, order } => ModDependency {
|
||||||
|
id,
|
||||||
|
order: match order {
|
||||||
|
dtmt_shared::ModOrder::Before => ModOrder::Before,
|
||||||
|
dtmt_shared::ModOrder::After => ModOrder::After,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Data, Debug, Lens, PartialEq)]
|
#[derive(Clone, Data, Debug, Lens, PartialEq)]
|
||||||
pub(crate) struct ModInfo {
|
pub(crate) struct ModInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -51,6 +81,7 @@ pub(crate) struct ModInfo {
|
||||||
#[lens(ignore)]
|
#[lens(ignore)]
|
||||||
#[data(ignore)]
|
#[data(ignore)]
|
||||||
pub resources: ModResourceInfo,
|
pub resources: ModResourceInfo,
|
||||||
|
pub depends: Vector<ModDependency>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModInfo {
|
impl ModInfo {
|
||||||
|
@ -66,6 +97,7 @@ impl ModInfo {
|
||||||
data: cfg.resources.data,
|
data: cfg.resources.data,
|
||||||
localization: cfg.resources.localization,
|
localization: cfg.resources.localization,
|
||||||
},
|
},
|
||||||
|
depends: cfg.depends.into_iter().map(ModDependency::from).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,11 @@ use std::path::PathBuf;
|
||||||
mod log;
|
mod log;
|
||||||
|
|
||||||
pub use log::*;
|
pub use log::*;
|
||||||
|
use serde::Deserialize;
|
||||||
use steamlocate::SteamDir;
|
use steamlocate::SteamDir;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct ModConfigResources {
|
pub struct ModConfigResources {
|
||||||
pub init: PathBuf,
|
pub init: PathBuf,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -15,7 +16,21 @@ pub struct ModConfigResources {
|
||||||
pub localization: Option<PathBuf>,
|
pub localization: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ModOrder {
|
||||||
|
Before,
|
||||||
|
After,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ModDependency {
|
||||||
|
ID(String),
|
||||||
|
Config { id: String, order: ModOrder },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct ModConfig {
|
pub struct ModConfig {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub dir: std::path::PathBuf,
|
pub dir: std::path::PathBuf,
|
||||||
|
@ -26,7 +41,7 @@ pub struct ModConfig {
|
||||||
pub packages: Vec<std::path::PathBuf>,
|
pub packages: Vec<std::path::PathBuf>,
|
||||||
pub resources: ModConfigResources,
|
pub resources: ModConfigResources,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub depends: Vec<String>,
|
pub depends: Vec<ModDependency>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const STEAMAPP_ID: u32 = 1361210;
|
pub const STEAMAPP_ID: u32 = 1361210;
|
||||||
|
|
Loading…
Add table
Reference in a new issue