From 03a127f24bc5b3327d5fe75853f320e404222f1d Mon Sep 17 00:00:00 2001 From: Niklas Kapelle Date: Thu, 19 Mar 2026 13:07:06 +0100 Subject: [PATCH] added game type --- src/actions/download.rs | 3 +- src/nexus/api.rs | 10 +- src/types.rs | 4 + src/types/game.rs | 7 +- src/types/game_type.rs | 186 ++++++++++++++++++++++++++++ src/types/mod_config.rs | 28 ++++- src/types/nexus_id.rs | 13 ++ tests/data/root_config_complex.toml | 2 + 8 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 src/types/game_type.rs create mode 100644 src/types/nexus_id.rs diff --git a/src/actions/download.rs b/src/actions/download.rs index 27266a7..3ab2939 100644 --- a/src/actions/download.rs +++ b/src/actions/download.rs @@ -34,7 +34,8 @@ pub fn handle_nxm(root_config: &mut RootConfig, raw_url: &str) -> anyhow::Result unpack(root_config, &mod_id, dl_file)?; - let new_mod = ModConfig::from_mod_info(&mod_id, &mod_id, &mod_info); + 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); diff --git a/src/nexus/api.rs b/src/nexus/api.rs index 84fae05..13d3a73 100644 --- a/src/nexus/api.rs +++ b/src/nexus/api.rs @@ -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) } } diff --git a/src/types.rs b/src/types.rs index ffed997..eb0fd95 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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 { diff --git a/src/types/game.rs b/src/types/game.rs index 2a92def..68ab620 100644 --- a/src/types/game.rs +++ b/src/types/game.rs @@ -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) -> Self { Self { path: path.as_ref().to_owned(), + kind: GameType::default(), } } diff --git a/src/types/game_type.rs b/src/types/game_type.rs new file mode 100644 index 0000000..dbaeb60 --- /dev/null +++ b/src/types/game_type.rs @@ -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 { + 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 { + 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 { + 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>(deserializer: D) -> Result { + 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(&self, serializer: S) -> Result { + 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); + } + } +} diff --git a/src/types/mod_config.rs b/src/types/mod_config.rs index 056b164..fcd50ef 100644 --- a/src/types/mod_config.rs +++ b/src/types/mod_config.rs @@ -2,7 +2,10 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; -use crate::nexus::ModInfo; +use crate::{ + nexus::ModInfo, + types::{GameType, NexusID}, +}; /// Config for an available mod #[derive(Debug, Clone, Deserialize, Serialize)] @@ -26,7 +29,10 @@ pub struct ModConfig { ignore: Vec, name: Option, - nexus_id: Option, + + nexus_id: Option, + + game: GameType, } impl ModConfig { @@ -38,12 +44,20 @@ impl ModConfig { ignore: Vec::new(), name: None, nexus_id: None, + game: GameType::Unknown, } } - pub fn from_mod_info(id: &str, source: impl AsRef, mod_info: &ModInfo) -> Self { + pub fn from_mod_info( + id: &str, + source: impl AsRef, + 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 } @@ -69,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 { diff --git a/src/types/nexus_id.rs b/src/types/nexus_id.rs new file mode 100644 index 0000000..ecd3fd0 --- /dev/null +++ b/src/types/nexus_id.rs @@ -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 } + } +} diff --git a/tests/data/root_config_complex.toml b/tests/data/root_config_complex.toml index 3dc2818..d565b58 100644 --- a/tests/data/root_config_complex.toml +++ b/tests/data/root_config_complex.toml @@ -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"