oliver 0.5.1

Lightweight CLI mod development tool for Baldur's Gate 3 on Linux
Documentation
use anyhow::Result;
use clap::{Parser, Subcommand};
use oliver::{DEFAULT_APP_DATA_DIR, Settings};
use std::{path::PathBuf, process::ExitCode};

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true), author, version, about)]
struct Cli {
    #[clap(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Pack loose mod files into an archive loadable by the game.
    Pack(Pack),

    /// Print a summary of the contents in a given file.
    Parse(Parse),

    /// Unpacks all of the files contained in a mod.
    Unpack(Unpack),
}

#[derive(Debug, Parser)]
struct Dump {
    /// Path to the file to dump.
    ///
    /// The file must be either a .pak format in the LSPK format.
    #[clap()]
    path: PathBuf,
}

#[derive(Debug, Parser)]
struct Export {
    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// The file where the exported mod data should be written.
    #[clap()]
    destination: PathBuf,
}

#[derive(Debug, Parser)]
struct Import {
    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// The file where the exported mod data should be read.
    #[clap()]
    source: PathBuf,
}

#[derive(Debug, Parser)]
struct List {
    #[clap(flatten)]
    app_data_path: AppDataPath,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Parse {
    /// Paths to the files to summarize.
    ///
    /// Each file must be either a .pak format in the LSPK format or a .lsf file.
    #[clap()]
    paths: Vec<PathBuf>,

    /// Print extra information.
    #[clap(long, short)]
    verbose: bool,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Install {
    /// Print extra information.
    #[clap(long, short)]
    verbose: bool,

    /// Don't perform any actions, but validate whether command would succeed or not.
    ///
    /// If `--verbose` is also specified, invoking with `--dry-run` will indicate the version of
    /// any successfully updated addon.
    #[clap(long, short = 'n')]
    dry_run: bool,

    /// Reinstall even if the same version is already installed.
    #[clap(long, short)]
    refresh: bool,

    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// Paths to the mod files to install. By default, this will only install mods if the version
    /// is newer than the currently installed version (if any exists).
    ///
    /// The mod file can be either a `.pak` file or a zip file containing a `.pak`.
    #[clap()]
    mod_file_paths: Vec<String>,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Uninstall {
    /// Don't perform any actions, but validate whether command would succeed or not.
    #[clap(long, short = 'n')]
    dry_run: bool,

    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// Indexes of the mods in the order to uninstall.
    #[clap()]
    mod_indexes: Vec<usize>,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Pack {
    /// Path to the directory containing loose mod files to pack.
    #[clap()]
    mod_files_root: PathBuf,

    /// The output file to pack the mod into.
    #[clap(long, short)]
    destination: Option<PathBuf>,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Unpack {
    /// The mod file to unpack.
    #[clap()]
    mod_file_path: PathBuf,

    /// The directory to unpack the mod into.
    #[clap(long, short)]
    destination: Option<PathBuf>,
}

#[derive(Debug, Parser)]
struct AppDataPath {
    /// Path to the `AppData` folder that contains the installation.
    ///
    /// If you're not using Steam, this will likely be in your Wine prefix under
    /// `drive_c/users/<username>/AppData`, where `<username>` is the name of your user.
    ///
    /// This directory is expected to contain the file `modsettings.lsx` in the relative path
    /// `Local/Larian Studios/Baldur's Gate 3/PlayerProfiles/Public/`.
    #[clap(
        long,
        default_value_t = DEFAULT_APP_DATA_DIR.into()
    )]
    app_data_path: String,
}

fn run() -> Result<()> {
    static_assertions_next::assert_cfg!(
        target_os = "linux",
        format!(
            "This tool currently only supports Linux. If you're interested in using this on \
             another platform, feel free to let me know by filing an issue at {}!",
            env!("CARGO_PKG_REPOSTIORY")
        )
    );

    match Cli::parse().command {
        Command::Parse(Parse { paths, verbose }) => Settings::parse(paths, verbose),
        Command::Pack(Pack {
            mod_files_root,
            destination,
        }) => Settings::pack(mod_files_root, destination),
        Command::Unpack(Unpack {
            mod_file_path,
            destination,
        }) => Settings::unpack(&mod_file_path, destination),
    }
}

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("{e}");
            ExitCode::FAILURE
        }
    }
}