Skip to main content

modde_games/oblivion_remastered/
mod.rs

1//! The Oblivion Remastered game plugin: a UE5 wrapper around the Gamebryo
2//! engine, mixing `~mods` `.pak` layout with Bethesda-style plugins.
3
4pub 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
14/// [`GamePlugin`] for The Elder Scrolls IV: Oblivion Remastered.
15pub 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/// The UE5 `Content/Paks` directory for Oblivion Remastered, relative to `install`.
59#[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}