Skip to main content

modde_games/gamebryo/
mod.rs

1//! The Gamebryo-engine game plugin (Oblivion, Fallout 3/New Vegas): a
2//! data-driven [`GamePlugin`] shared across the supported Gamebryo titles,
3//! plus `plugins.txt`-style load-order file helpers.
4
5pub mod saves;
6pub mod scanner;
7
8use std::path::{Path, PathBuf};
9
10use modde_core::installer::InstallMethod;
11
12use crate::policies::{BareLayoutPolicy, ContentPolicy};
13use crate::traits::{ContentCategory, GamePlugin, ModSafety};
14
15/// A configurable [`GamePlugin`] instance for a specific Gamebryo-engine game.
16pub struct GamebryoGame {
17    game_id: &'static str,
18    display_name: &'static str,
19    steam_app_id: &'static str,
20    my_games_dir: &'static str,
21    ini_files: &'static [&'static str],
22    archive_ext: &'static [&'static str],
23    nexus_domain: &'static str,
24}
25
26impl GamebryoGame {
27    #[must_use]
28    pub const fn new(
29        game_id: &'static str,
30        display_name: &'static str,
31        steam_app_id: &'static str,
32        my_games_dir: &'static str,
33        ini_files: &'static [&'static str],
34        archive_ext: &'static [&'static str],
35        nexus_domain: &'static str,
36    ) -> Self {
37        Self {
38            game_id,
39            display_name,
40            steam_app_id,
41            my_games_dir,
42            ini_files,
43            archive_ext,
44            nexus_domain,
45        }
46    }
47}
48
49const GAMEBRYO_SAVE_BREAKING_EXT: &[&str] = &["esp", "esm", "pex", "dll", "obse", "nvse"];
50const GAMEBRYO_COSMETIC_EXT: &[&str] = &[
51    "nif", "bsa", "dds", "png", "tga", "jpg", "kf", "wav", "mp3", "ogg", "ini", "xml",
52];
53
54const GAMEBRYO_CONTENT_CATEGORIES: &[(&str, ContentCategory)] = &[
55    ("esp", ContentCategory::Plugin),
56    ("esm", ContentCategory::Plugin),
57    ("dds", ContentCategory::Texture),
58    ("png", ContentCategory::Texture),
59    ("tga", ContentCategory::Texture),
60    ("jpg", ContentCategory::Texture),
61    ("nif", ContentCategory::Mesh),
62    ("kf", ContentCategory::Mesh),
63    ("wav", ContentCategory::Sound),
64    ("mp3", ContentCategory::Sound),
65    ("ogg", ContentCategory::Sound),
66    ("pex", ContentCategory::Script),
67    ("obse", ContentCategory::Script),
68    ("nvse", ContentCategory::Script),
69    ("bsa", ContentCategory::Archive),
70    ("ini", ContentCategory::Config),
71    ("xml", ContentCategory::Config),
72    ("dll", ContentCategory::Binary),
73];
74
75const GAMEBRYO_CONTENT_POLICY: ContentPolicy = ContentPolicy {
76    save_breaking_ext: GAMEBRYO_SAVE_BREAKING_EXT,
77    cosmetic_ext: GAMEBRYO_COSMETIC_EXT,
78    save_breaking_dirs: &["scripts"],
79    categories: GAMEBRYO_CONTENT_CATEGORIES,
80};
81
82const GAMEBRYO_BARE_LAYOUT_POLICY: BareLayoutPolicy = BareLayoutPolicy {
83    root_dirs: &[
84        "data", "meshes", "textures", "sound", "music", "menus", "scripts", "shaders",
85    ],
86    root_file_exts: &["esp", "esm", "bsa"],
87    case_insensitive_dirs: true,
88};
89
90pub const FALLOUT_NEW_VEGAS: GamebryoGame = GamebryoGame::new(
91    "fallout-new-vegas",
92    "Fallout: New Vegas",
93    "22380",
94    "FalloutNV",
95    &["Fallout.ini", "FalloutPrefs.ini", "FalloutCustom.ini"],
96    &["bsa"],
97    "newvegas",
98);
99
100pub const OBLIVION: GamebryoGame = GamebryoGame::new(
101    "oblivion",
102    "The Elder Scrolls IV: Oblivion",
103    "22330",
104    "Oblivion",
105    &["Oblivion.ini"],
106    &["bsa"],
107    "oblivion",
108);
109
110/// Read a `plugins.txt`-style load-order file, stripping comments and the
111/// leading `*` enabled-marker from each plugin name.
112pub fn read_plugin_order_file(path: &Path) -> std::io::Result<Vec<String>> {
113    let content = std::fs::read_to_string(path)?;
114    Ok(content
115        .lines()
116        .map(str::trim)
117        .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with(';'))
118        .map(|line| line.trim_start_matches('*').to_string())
119        .collect())
120}
121
122/// Write a `plugins.txt`-style load-order file, marking every plugin enabled
123/// with a leading `*`.
124pub fn write_plugin_order_file(path: &Path, plugins: &[String]) -> std::io::Result<()> {
125    if let Some(parent) = path.parent() {
126        std::fs::create_dir_all(parent)?;
127    }
128    let mut content = String::new();
129    for plugin in plugins {
130        content.push('*');
131        content.push_str(plugin);
132        content.push('\n');
133    }
134    std::fs::write(path, content)
135}
136
137impl GamePlugin for GamebryoGame {
138    fn game_id(&self) -> &str {
139        self.game_id
140    }
141
142    fn display_name(&self) -> &str {
143        self.display_name
144    }
145
146    fn mod_directory(&self, install: &Path) -> PathBuf {
147        install.join("Data")
148    }
149
150    fn save_directory(&self) -> Option<PathBuf> {
151        let compat = modde_core::paths::steam_common()
152            .parent()?
153            .join("compatdata")
154            .join(self.steam_app_id)
155            .join("pfx/drive_c/users/steamuser/Documents/My Games")
156            .join(self.my_games_dir)
157            .join("Saves");
158        Some(compat)
159    }
160
161    fn supports_save_profiles(&self) -> bool {
162        true
163    }
164
165    fn classify_mod(&self, mod_dir: &Path) -> ModSafety {
166        GAMEBRYO_CONTENT_POLICY.classify_mod(mod_dir)
167    }
168
169    fn classify_extension(&self, ext: &str) -> ContentCategory {
170        GAMEBRYO_CONTENT_POLICY.classify_extension(ext)
171    }
172
173    fn ini_file_names(&self) -> &[&str] {
174        self.ini_files
175    }
176
177    fn archive_extensions(&self) -> &[&str] {
178        self.archive_ext
179    }
180
181    fn has_plugin_system(&self) -> bool {
182        true
183    }
184
185    fn steam_app_id_u32(&self) -> Option<u32> {
186        self.steam_app_id.parse().ok()
187    }
188
189    fn nexus_game_domain(&self) -> Option<&str> {
190        Some(self.nexus_domain)
191    }
192
193    fn analyze_mod_archive(&self, extracted_dir: &Path) -> Option<InstallMethod> {
194        extracted_dir
195            .join("Data")
196            .is_dir()
197            .then(|| InstallMethod::StripContentRoot {
198                root: "Data".to_string(),
199            })
200    }
201
202    fn recognizes_bare_layout(&self, extracted_dir: &Path) -> bool {
203        GAMEBRYO_BARE_LAYOUT_POLICY.recognizes(extracted_dir)
204    }
205}