Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion crates/cli/src/commands/profile.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod modengine2;

use std::{fs, path::PathBuf};

use clap::{ArgAction, Args, Subcommand};
Expand All @@ -11,14 +13,25 @@ use me3_mod_protocol::{
};
use tracing::error;

use crate::{config::Config, db::DbContext, output::OutputBuilder, Game};
use crate::{
commands::profile::modengine2::ModEngine2Config, config::Config, db::DbContext,
output::OutputBuilder, Game,
};

#[derive(Copy, Clone, Debug, clap::ValueEnum)]
pub enum ProfileConvertFormat {
ModEngine2,
}

#[derive(Subcommand, Debug)]
#[command(flatten_help = true)]
pub enum ProfileCommands {
/// Create a new ModProfile.
Create(ProfileCreateArgs),

/// Convert an alternative mod loader configuration file to a me3 ModProfile.
Convert(ProfileConvertArgs),

/// List profiles in the profile dir.
#[clap(disable_help_flag = true)]
List,
Expand All @@ -27,6 +40,28 @@ pub enum ProfileCommands {
Show(#[clap(flatten)] ProfileNameArgs),
}

#[derive(Args, Debug)]
pub struct ProfileConvertArgs {
/// A format identifier of the mod loader, currently only "modengine2" is supported.
format: ProfileConvertFormat,

/// Game to associate with this profile for one-click launches, if it cannot be inferred from the configuration file.
#[clap(
short('g'),
long,
hide_possible_values = false,
help_heading = "Game selection"
)]
#[arg(value_enum)]
game: Option<Game>,

/// Path to the configuration file to read.
input: PathBuf,

/// Output path to the me3 profile to be written.
output: PathBuf,
}

#[derive(Args, Debug)]
pub struct ProfileCreateArgs {
#[clap(flatten)]
Expand Down Expand Up @@ -219,6 +254,16 @@ pub fn create(config: Config, args: ProfileCreateArgs) -> color_eyre::Result<()>
Ok(())
}

#[tracing::instrument(err, skip_all)]
pub fn convert(args: ProfileConvertArgs) -> color_eyre::Result<()> {
let config = std::fs::read_to_string(args.input)?;
let me2: ModEngine2Config = toml::from_str(&config)?;
let me3_toml = toml::to_string_pretty(&me2.into_mod_profile())?;
std::fs::write(args.output, me3_toml)?;

Ok(())
}

#[tracing::instrument(err, skip_all)]
pub fn show(db: DbContext, config: Config, name: ProfileNameArgs) -> color_eyre::Result<()> {
let profile_path = name.into_profile_path(&config)?;
Expand Down
168 changes: 168 additions & 0 deletions crates/cli/src/commands/profile/modengine2.rs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stylistic nit: I don't like the ModEngine2 prefix on all structs, think it's fine if only ModEngine2Config keeps it

Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::{ffi::OsStr, path::PathBuf};

use me3_mod_protocol::{native::Native, package::Package, ModProfile, ModProfileV1};
use serde::Deserialize;

#[derive(Deserialize)]
pub struct ModEngine2Mod {
pub name: String,
pub path: PathBuf,
pub enabled: Option<bool>,
}

#[derive(Default, Deserialize)]
pub struct ModEngine2Global {
#[serde(default)]
pub external_dlls: Vec<PathBuf>,
}

#[derive(Default, Deserialize)]
pub struct ModEngine2ModLoader {
#[serde(default)]
pub mods: Vec<ModEngine2Mod>,
}

#[derive(Default, Deserialize)]
pub struct ModEngine2ScyllaHide {
#[serde(default)]
pub enabled: bool,
}

#[derive(Default, Deserialize)]
pub struct ModEngine2Extensions {
#[serde(default)]
pub mod_loader: ModEngine2ModLoader,

#[serde(default)]
pub scylla_hide: ModEngine2ScyllaHide,
}

#[derive(Default, Deserialize)]
pub struct ModEngine2Config {
#[serde(default)]
pub modengine: ModEngine2Global,
#[serde(default)]
pub extension: ModEngine2Extensions,
}

impl ModEngine2Config {
pub fn into_mod_profile(self) -> ModProfile {
let mut profile = ModProfileV1::default();

// Loose heuristic to make sure SC gets load_early = true
fn is_seamless_coop(file_name: &OsStr) -> bool {
file_name == "ersc.dll" || file_name == "nrsc.dll"
}

for external_dll in self.modengine.external_dlls {
let native_name = external_dll.file_name().unwrap();
let load_early = is_seamless_coop(native_name);
let mut native = Native::new(external_dll);
native.load_early = load_early;

profile.natives.push(native);
}

for me2_mod in self.extension.mod_loader.mods {
let mut package = Package::new(me2_mod.path.clone()).with_id(me2_mod.name);
package.enabled = me2_mod.enabled.unwrap_or(true);

profile.packages.push(package);
}

if self.extension.scylla_hide.enabled {
profile.disable_arxan = Some(true);
}

profile.into()
}
}

#[cfg(test)]
mod tests {
use std::path::Path;

use me3_mod_protocol::{dependency::Dependency, package::WithPackageSource};

use super::*;

#[test]
fn default_config() {
const DEFAULT_CONFIG: &str = r#"
[modengine]
debug = false
external_dlls = []

[extension.mod_loader]
enabled = true
loose_params = false
mods = [
{ enabled = true, name = "default", path = "mod" }
]

[extension.scylla_hide]
enabled = false
"#;

let me2: ModEngine2Config = toml::from_str(DEFAULT_CONFIG).unwrap();
let profile = me2.into_mod_profile();
assert_eq!(profile.disable_arxan(), None);

let packages = profile.packages();
assert_eq!(packages.len(), 1);
assert_eq!(packages[0].id(), "default");
assert_eq!(packages[0].source().as_path(), Path::new("mod"));
assert!(packages[0].enabled);
}

#[test]
fn example_config() {
const EXAMPLE_CONFIG: &str = r#"
[modengine]
debug = false
external_dlls = [
"mods\\SeamlessCoop\\ersc.dll",
"elden_ring_practice_tool.dll",
]

[extension.mod_loader]
enabled = true
loose_params = false
mods = [
{ enabled = true, name = "convergence", path = "mods\\convergence" },
{ enabled = false, name = "clever", path = "mods\\clever" },
]

[extension.scylla_hide]
enabled = true
"#;

let me2: ModEngine2Config = toml::from_str(EXAMPLE_CONFIG).unwrap();
let profile = me2.into_mod_profile();
assert_eq!(profile.disable_arxan(), Some(true));

let packages = profile.packages();
assert_eq!(packages.len(), 2);
assert_eq!(packages[0].id(), "convergence");
assert_eq!(
packages[0].source().as_path(),
Path::new("mods\\convergence")
);
assert!(packages[0].enabled);
assert_eq!(packages[1].id(), "clever");
assert!(!packages[1].enabled);

let natives = profile.natives();
assert_eq!(natives.len(), 2);
assert_eq!(
natives[0].path.as_path(),
Path::new("mods\\SeamlessCoop\\ersc.dll")
);
assert!(natives[0].load_early);
assert_eq!(
natives[1].path.as_path(),
Path::new("elden_ring_practice_tool.dll")
);
assert!(natives.iter().all(|native| native.enabled));
}
}
1 change: 1 addition & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ fn main() {
commands::launch::launch(db, config, args, tmp_log_file_path.clone())
}
Commands::Profile(ProfileCommands::Create(args)) => commands::profile::create(config, args),
Commands::Profile(ProfileCommands::Convert(args)) => commands::profile::convert(args),
Commands::Profile(ProfileCommands::List) => commands::profile::list(db),
Commands::Profile(ProfileCommands::Show(name)) => commands::profile::show(db, config, name),
#[cfg(target_os = "windows")]
Expand Down
24 changes: 15 additions & 9 deletions crates/mod-protocol/src/lib.rs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making this specific profile version have public fields, imo it would be better to have a generalized builder that creates the newest profile version (with v2 support in mind), like the one on my profile v2 PR https://github.com/Dasaav-dsv/me3/edit/feat/profile-v2/crates/mod-protocol/src/profile/builder.rs?pr=/garyttierney/me3/pull/530

Original file line number Diff line number Diff line change
Expand Up @@ -136,48 +136,54 @@ impl ModProfile {
}
}

impl Into<ModProfile> for ModProfileV1 {
fn into(self) -> ModProfile {
ModProfile::V1(self)
}
}

#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
pub struct ModProfileV1 {
/// The games that this profile supports.
#[serde(default)]
supports: Vec<Supports>,
pub supports: Vec<Supports>,

/// Native modules (DLLs) that will be loaded.
#[serde(default)]
#[serde(alias = "native")]
natives: Vec<Native>,
pub natives: Vec<Native>,

/// A collection of packages containing assets that should be considered for loading
/// before the DVDBND.
#[serde(default)]
#[serde(alias = "package")]
packages: Vec<Package>,
pub packages: Vec<Package>,

/// Name of an alternative savefile to use (in the default savefile directory).
#[serde(default)]
savefile: Option<String>,
pub savefile: Option<String>,

/// Starts the game with multiplayer server connectivity enabled.
#[serde(default)]
start_online: Option<bool>,
pub start_online: Option<bool>,

/// Try to neutralize Arxan GuardIT code protection to improve mod stability.
#[serde(default)]
disable_arxan: Option<bool>,
pub disable_arxan: Option<bool>,

/// Patch memory limits for supported games to improve mod stability.
#[serde(default)]
#[serde(alias = "patch_mem")]
mem_patch: Option<bool>,
pub mem_patch: Option<bool>,

/// Override how many megabytes of memory the supported game should allocate
/// (with `mem_patch = true`).
#[serde(default)]
mem_patch_heap_size: Option<u32>,
pub mem_patch_heap_size: Option<u32>,

/// Debug game property overrides.
#[serde(default)]
debug_properties: DebugProperties,
pub debug_properties: DebugProperties,
}

#[cfg(test)]
Expand Down
Loading