Compare commits

...

3 Commits

Author SHA1 Message Date
03a127f24b added game type 2026-03-19 13:07:06 +01:00
ed9e23ed3b renamed add command to include 2026-03-18 21:36:55 +01:00
3949723303 add name and nexusid to mod 2026-03-18 21:11:36 +01:00
12 changed files with 274 additions and 23 deletions

View File

@@ -3,7 +3,7 @@ use log::error;
use crate::{
nexus::{NXMUrl, download_nxm},
types::RootConfig,
types::{ModConfig, RootConfig},
unpacker::unpack,
};
@@ -32,11 +32,12 @@ pub fn handle_nxm(root_config: &mut RootConfig, raw_url: &str) -> anyhow::Result
return Err(anyhow!("Mod with generated id already exists"));
}
let new_mod = unpack(root_config, &mod_id, dl_file)?;
unpack(root_config, &mod_id, dl_file)?;
let file_id: u64 = nxm_url.file.parse()?;
let new_mod = ModConfig::from_mod_info(&mod_id, &mod_id, &mod_info, file_id);
root_config.add_mod(&new_mod);
root_config.save_to_file()?;
Ok(())
}

View File

@@ -15,7 +15,7 @@ pub struct Args {
#[derive(Subcommand, Debug)]
pub enum Commands {
Activate { instance: String, target: PathBuf },
Add { instance: String, mod_id: String },
Include { instance: String, mod_id: String },
LoadOrder { instance: String },
ApiCheck,
Download { url: String },

View File

@@ -10,7 +10,7 @@ use fomod_manager::{
},
cli::{self, Args},
nexus::NexusAPI,
types::RootConfig,
types::{ModConfig, RootConfig},
unpacker::unpack,
};
@@ -63,7 +63,11 @@ fn command_order(root_config: &RootConfig, instance_id: &str) -> anyhow::Result<
}
fn command_download(root_config: &mut RootConfig, raw_url: &str) -> anyhow::Result<()> {
handle_nxm(root_config, raw_url)
handle_nxm(root_config, raw_url)?;
root_config.save_to_file()?;
Ok(())
}
fn command_unpack(
@@ -76,7 +80,9 @@ fn command_unpack(
return Err(anyhow!("Mod already exists"));
}
let new_mod = unpack(root_config, id, file)?;
unpack(root_config, id, file)?;
let new_mod = ModConfig::new(id, id);
root_config.add_mod(&new_mod);
@@ -105,7 +111,7 @@ fn main() -> Result<(), Box<dyn Error>> {
cli::Commands::Activate { instance, target } => {
command_activate(&root_config, &instance, &target)?;
}
cli::Commands::Add { instance, mod_id } => {
cli::Commands::Include { instance, mod_id } => {
command_add(&root_config, &instance, &mod_id)?;
}
cli::Commands::LoadOrder { instance } => {

View File

@@ -2,6 +2,6 @@ mod api;
mod downloader;
mod url;
pub use api::NexusAPI;
pub use api::{ModInfo, NexusAPI};
pub use downloader::download_nxm;
pub use url::NXMUrl;

View File

@@ -1,7 +1,7 @@
use serde::Deserialize;
use url::Url;
use crate::nexus::NXMUrl;
use crate::{nexus::NXMUrl, types::GameType};
const NEXUS_ENDPOINT: &str = "https://api.nexusmods.com";
@@ -95,7 +95,7 @@ pub struct ModInfo {
pub mod_id: u64,
// pub game_id: u64,
// pub allow_rating: bool,
// pub domain_name: String,
pub domain_name: String,
// pub category_id: u64,
pub version: String,
// pub endorsement_count: u64,
@@ -145,6 +145,10 @@ impl ModInfo {
if short_name.len() > MAX_CHARS {
short_name.truncate(MAX_CHARS);
}
format!("{}-{}", short_name, self.mod_id)
format!("{}-{}", short_name.to_lowercase(), self.mod_id)
}
pub fn get_game_type(&self) -> GameType {
GameType::from_nexus_domain(&self.domain_name).unwrap_or(GameType::Unknown)
}
}

View File

@@ -1,20 +1,24 @@
use thiserror::Error;
mod game;
mod game_type;
mod installed_mod;
mod link;
mod mod_config;
mod mod_file;
mod modded_instance;
mod root_config;
mod nexus_id;
pub use game::*;
pub use game_type::GameType;
pub use installed_mod::*;
pub use link::*;
pub use mod_config::*;
pub use mod_file::*;
pub use modded_instance::*;
pub use root_config::*;
pub use nexus_id::*;
#[derive(Error, Debug)]
pub enum ConfigReadWriteError {

View File

@@ -6,18 +6,23 @@ use std::{
use serde::{Deserialize, Serialize};
use crate::{types::link::Link, utils::walk_all_files};
use crate::{
types::{GameType, link::Link},
utils::walk_all_files,
};
/// Available game
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Game {
path: PathBuf,
kind: GameType,
}
impl Game {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_owned(),
kind: GameType::default(),
}
}

186
src/types/game_type.rs Normal file
View File

@@ -0,0 +1,186 @@
use std::fmt::Display;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum GameType {
Oblivion,
Skyrim,
Fallout3,
FalloutNV,
Fallout4,
SkyrimSE,
Fallout4VR,
SkyrimVR,
Morrowind,
Starfield,
OpenMW,
OblivionRemastered,
Custom(String),
#[default]
Unknown,
}
impl GameType {
pub fn to_libloot_type(self) -> Option<libloot::GameType> {
match self {
GameType::Oblivion => Some(libloot::GameType::Oblivion),
GameType::Skyrim => Some(libloot::GameType::Skyrim),
GameType::Fallout3 => Some(libloot::GameType::Fallout3),
GameType::FalloutNV => Some(libloot::GameType::FalloutNV),
GameType::Fallout4 => Some(libloot::GameType::Fallout4),
GameType::SkyrimSE => Some(libloot::GameType::SkyrimSE),
GameType::Fallout4VR => Some(libloot::GameType::Fallout4VR),
GameType::SkyrimVR => Some(libloot::GameType::SkyrimVR),
GameType::Morrowind => Some(libloot::GameType::Morrowind),
GameType::Starfield => Some(libloot::GameType::Starfield),
GameType::OpenMW => Some(libloot::GameType::OpenMW),
GameType::OblivionRemastered => Some(libloot::GameType::OblivionRemastered),
GameType::Custom(_) => None,
GameType::Unknown => None,
}
}
pub fn to_nexus_domain(self) -> Option<String> {
match self {
GameType::Oblivion => Some("oblivion".to_owned()),
GameType::Skyrim => Some("skyrim".to_owned()),
GameType::Fallout3 => Some("fallout3".to_owned()),
GameType::FalloutNV => Some("newvegas".to_owned()),
GameType::Fallout4 => Some("fallout4".to_owned()),
GameType::SkyrimSE => Some("skyrimspecialedition".to_owned()),
GameType::Fallout4VR => Some("fallout4".to_owned()),
GameType::SkyrimVR => Some("skyrimspecialedition".to_owned()),
GameType::Morrowind => Some("morrowind".to_owned()),
GameType::Starfield => Some("starfield".to_owned()),
GameType::OpenMW => Some("morrowind".to_owned()),
GameType::OblivionRemastered => Some("oblivionremastered".to_owned()),
GameType::Custom(_) => None,
GameType::Unknown => None,
}
}
pub fn from_nexus_domain(domain: &str) -> Option<Self> {
match domain {
"oblivion" => Some(GameType::Oblivion),
"skyrim" => Some(GameType::Skyrim),
"fallout3" => Some(GameType::Fallout3),
"newvegas" => Some(GameType::FalloutNV),
"fallout4" => Some(GameType::Fallout4),
"skyrimspecialedition" => Some(GameType::SkyrimSE),
"morrowind" => Some(GameType::Morrowind),
"starfield" => Some(GameType::Starfield),
"oblivionremastered" => Some(GameType::OblivionRemastered),
_ => None,
}
}
}
impl Display for GameType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
GameType::Oblivion => "Oblivion",
GameType::Skyrim => "Skyrim",
GameType::Fallout3 => "Fallout 3",
GameType::FalloutNV => "Fallout New Vegas",
GameType::Fallout4 => "Fallout 4",
GameType::SkyrimSE => "Skyrim Special Edition",
GameType::Fallout4VR => "Fallout 4 VR",
GameType::SkyrimVR => "Skyrim VR",
GameType::Morrowind => "Morrowind",
GameType::Starfield => "Starfield",
GameType::OpenMW => "OpenMW",
GameType::OblivionRemastered => "Oblivion Remastered",
GameType::Custom(name) => name,
GameType::Unknown => "Unknown",
};
write!(f, "{}", s)
}
}
impl<'de> Deserialize<'de> for GameType {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(match s.as_str() {
"oblivion" => Self::Oblivion,
"skyrim" => Self::Skyrim,
"fo3" => Self::Fallout3,
"fonv" => Self::FalloutNV,
"fo4" => Self::Fallout4,
"sse" => Self::SkyrimSE,
"fo4vr" => Self::Fallout4VR,
"skyrimvr" => Self::SkyrimVR,
"morrowind" => Self::Morrowind,
"starfield" => Self::Starfield,
"openmw" => Self::OpenMW,
"oblivionrm" => Self::OblivionRemastered,
"unknown" => Self::Unknown,
_ => Self::Custom(s),
})
}
}
impl Serialize for GameType {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let s = match self {
Self::Custom(s) => s,
Self::Oblivion => "oblivion",
Self::Skyrim => "skyrim",
Self::Fallout3 => "fo3",
Self::FalloutNV => "fonv",
Self::Fallout4 => "fo4",
Self::SkyrimSE => "sse",
Self::Fallout4VR => "fo4vr",
Self::SkyrimVR => "skyrimvr",
Self::Morrowind => "morrowind",
Self::Starfield => "starfield",
Self::OpenMW => "openmw",
Self::OblivionRemastered => "oblivionrm",
Self::Unknown => "unknown",
};
serializer.serialize_str(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Deserialize, Serialize, PartialEq, Debug)]
struct Wrapper {
value: GameType,
}
fn roundtrip(game_type: GameType) {
let val = Wrapper { value: game_type };
let serialized = toml::to_string(&val).unwrap();
let deserialized: Wrapper = toml::from_str(&serialized).unwrap();
assert_eq!(val, deserialized);
}
#[test]
fn parse_back_and_forth_all() {
for e in [
GameType::Oblivion,
GameType::Skyrim,
GameType::Fallout3,
GameType::FalloutNV,
GameType::Fallout4,
GameType::SkyrimSE,
GameType::Fallout4VR,
GameType::SkyrimVR,
GameType::Morrowind,
GameType::Starfield,
GameType::OpenMW,
GameType::OblivionRemastered,
GameType::Custom("custom".to_owned()),
GameType::Unknown,
] {
roundtrip(e);
}
}
}

View File

@@ -2,6 +2,11 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::{
nexus::ModInfo,
types::{GameType, NexusID},
};
/// Config for an available mod
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
@@ -22,6 +27,12 @@ pub struct ModConfig {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
ignore: Vec<String>,
name: Option<String>,
nexus_id: Option<NexusID>,
game: GameType,
}
impl ModConfig {
@@ -31,9 +42,26 @@ impl ModConfig {
path: source.as_ref().to_owned(),
root_mod: false,
ignore: Vec::new(),
name: None,
nexus_id: None,
game: GameType::Unknown,
}
}
pub fn from_mod_info(
id: &str,
source: impl AsRef<Path>,
mod_info: &ModInfo,
file_id: u64,
) -> Self {
let mut normal = Self::new(id, source);
normal.name = Some(mod_info.name.clone());
normal.game = mod_info.get_game_type();
normal.nexus_id = Some(NexusID::new(mod_info.mod_id, file_id));
normal
}
pub fn add_id(mut self, id: &str) -> Self {
self.id = id.to_owned();
self
@@ -55,6 +83,14 @@ impl ModConfig {
pub fn ignore(&self) -> &[String] {
&self.ignore
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn nexus_id(&self) -> Option<&NexusID> {
self.nexus_id.as_ref()
}
}
fn is_false(b: &bool) -> bool {

13
src/types/nexus_id.rs Normal file
View File

@@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
pub struct NexusID {
mod_id: u64,
file_id: u64,
}
impl NexusID {
pub fn new(mod_id: u64, file_id: u64) -> Self {
Self { mod_id, file_id }
}
}

View File

@@ -4,13 +4,9 @@ use anyhow::anyhow;
use log::error;
use zip::ZipArchive;
use crate::types::{ModConfig, RootConfig};
use crate::types::RootConfig;
pub fn unpack(
root_config: &RootConfig,
id: &str,
path: impl AsRef<Path>,
) -> anyhow::Result<ModConfig> {
pub fn unpack(root_config: &RootConfig, id: &str, path: impl AsRef<Path>) -> anyhow::Result<()> {
let extract_to = root_config.mod_location().join(id);
if fs::exists(&extract_to)? {
@@ -37,9 +33,7 @@ pub fn unpack(
}
}?;
let new_mod = ModConfig::new(id, id);
Ok(new_mod)
Ok(())
}
fn unpack_7z_file(path: impl AsRef<Path>, to: impl AsRef<Path>) -> anyhow::Result<()> {

View File

@@ -4,9 +4,11 @@ nexus_api_key = "1234"
[games.example_game]
path = "/home/user/games/sse"
kind = "sse"
[games.sse]
path = "games/sse"
kind = "unkown"
[instances.example1]
path = "example1.toml"