1pub 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
15pub 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
110pub 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
122pub 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}