modde_games/oblivion_remastered/
mod.rs1pub mod saves;
5pub mod scanner;
6
7use std::path::{Path, PathBuf};
8
9use modde_core::installer::InstallMethod;
10
11use crate::policies::{BareLayoutPolicy, ContentPolicy, DllOverridePolicy, StagingDllSearch};
12use crate::traits::{ContentCategory, GamePlugin, ModSafety};
13
14pub struct OblivionRemasteredGame;
16
17pub static OBLIVION_REMASTERED: OblivionRemasteredGame = OblivionRemasteredGame;
18
19const STEAM_APP_ID: &str = "2623190";
20const PROJECT_NAME: &str = "OblivionRemastered";
21
22const OR_SAVE_BREAKING_EXT: &[&str] = &["esp", "esm", "pak", "ucas", "utoc", "dll", "lua"];
23const OR_COSMETIC_EXT: &[&str] = &["dds", "png", "jpg", "tga", "nif"];
24const OR_CONTENT_CATEGORIES: &[(&str, ContentCategory)] = &[
25 ("esp", ContentCategory::Plugin),
26 ("esm", ContentCategory::Plugin),
27 ("pak", ContentCategory::Archive),
28 ("ucas", ContentCategory::Archive),
29 ("utoc", ContentCategory::Archive),
30 ("nif", ContentCategory::Mesh),
31 ("dds", ContentCategory::Texture),
32 ("png", ContentCategory::Texture),
33 ("jpg", ContentCategory::Texture),
34 ("tga", ContentCategory::Texture),
35 ("lua", ContentCategory::Script),
36 ("dll", ContentCategory::Binary),
37];
38
39const OR_CONTENT_POLICY: ContentPolicy = ContentPolicy {
40 save_breaking_ext: OR_SAVE_BREAKING_EXT,
41 cosmetic_ext: OR_COSMETIC_EXT,
42 save_breaking_dirs: &["plugins", "logicmods"],
43 categories: OR_CONTENT_CATEGORIES,
44};
45
46const OR_BARE_LAYOUT_POLICY: BareLayoutPolicy = BareLayoutPolicy {
47 root_dirs: &["data", "mods", "content", "paks", "~mods"],
48 root_file_exts: &["esp", "esm", "pak", "ucas", "utoc"],
49 case_insensitive_dirs: true,
50};
51
52const OR_PROXY_DLLS: &[&str] = &["dwmapi", "xinput1_3", "d3d11", "dxgi", "version", "winmm"];
53const OR_DLL_POLICY: DllOverridePolicy = DllOverridePolicy {
54 proxy_dlls: OR_PROXY_DLLS,
55 staging_search: StagingDllSearch::DirectChildDirs,
56};
57
58#[must_use]
60pub fn paks_root(install: &Path) -> PathBuf {
61 install.join(PROJECT_NAME).join("Content").join("Paks")
62}
63
64impl GamePlugin for OblivionRemasteredGame {
65 fn game_id(&self) -> &'static str {
66 "oblivion-remastered"
67 }
68
69 fn display_name(&self) -> &'static str {
70 "The Elder Scrolls IV: Oblivion Remastered"
71 }
72
73 fn mod_directory(&self, install: &Path) -> PathBuf {
74 paks_root(install).join("~mods")
75 }
76
77 fn save_directory(&self) -> Option<PathBuf> {
78 Some(
79 modde_core::paths::steam_common()
80 .parent()?
81 .join("compatdata")
82 .join(STEAM_APP_ID)
83 .join("pfx/drive_c/users/steamuser/Documents/My Games/Oblivion Remastered/Saves"),
84 )
85 }
86
87 fn supports_save_profiles(&self) -> bool {
88 true
89 }
90
91 fn classify_mod(&self, mod_dir: &Path) -> ModSafety {
92 OR_CONTENT_POLICY.classify_mod(mod_dir)
93 }
94
95 fn classify_extension(&self, ext: &str) -> ContentCategory {
96 OR_CONTENT_POLICY.classify_extension(ext)
97 }
98
99 fn executable_dir(&self, install: &Path) -> PathBuf {
100 install.join(PROJECT_NAME).join("Binaries/Win64")
101 }
102
103 fn wine_dll_overrides(&self, game_dir: &Path) -> smallvec::SmallVec<[String; 4]> {
104 OR_DLL_POLICY.from_executable_dir(&self.executable_dir(game_dir))
105 }
106
107 fn wine_dll_overrides_from_staging(&self, staging: &Path) -> smallvec::SmallVec<[String; 4]> {
108 OR_DLL_POLICY.from_staging(staging)
109 }
110
111 fn archive_extensions(&self) -> &[&str] {
112 &["pak", "ucas", "utoc"]
113 }
114
115 fn has_plugin_system(&self) -> bool {
116 true
117 }
118
119 fn steam_app_id_u32(&self) -> Option<u32> {
120 Some(2623190)
121 }
122
123 fn nexus_game_domain(&self) -> Option<&str> {
124 Some("oblivionremastered")
125 }
126
127 fn analyze_mod_archive(&self, extracted_dir: &Path) -> Option<InstallMethod> {
128 if has_root_file_with_ext(extracted_dir, &["pak", "ucas", "utoc"]) {
129 return Some(InstallMethod::SingleFileSet);
130 }
131 if extracted_dir.join("Data").is_dir() {
132 return Some(InstallMethod::StripContentRoot {
133 root: "Data".to_string(),
134 });
135 }
136 None
137 }
138
139 fn recognizes_bare_layout(&self, extracted_dir: &Path) -> bool {
140 OR_BARE_LAYOUT_POLICY.recognizes(extracted_dir)
141 }
142}
143
144fn has_root_file_with_ext(dir: &Path, extensions: &[&str]) -> bool {
145 std::fs::read_dir(dir).is_ok_and(|entries| {
146 entries.flatten().any(|entry| {
147 let path = entry.path();
148 path.is_file()
149 && path
150 .extension()
151 .and_then(|ext| ext.to_str())
152 .is_some_and(|ext| {
153 extensions
154 .iter()
155 .any(|candidate| ext.eq_ignore_ascii_case(candidate))
156 })
157 })
158 })
159}