commonlibsse_ng\rel\module/
mod.rs

1// C++ Original code
2// - https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/REL/Module.h
3// - load_segments, clear: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/src/REL/Module.cpp
4// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
5// SPDX-License-Identifier: MIT
6//
7// SPDX-FileCopyrightText: (C) 2025 SARDONYX
8// SPDX-License-Identifier: Apache-2.0 OR MIT
9
10//! Module handling library for Skyrim SE/AE/VR .
11
12mod module_core;
13mod module_handle;
14mod runtime;
15mod segment;
16
17pub use self::module_core::{Module, ModuleInitError};
18pub use self::module_handle::{ModuleHandle, ModuleHandleError};
19pub use self::runtime::{Runtime, get_skyrim_dir, get_skyrim_exe_path};
20pub use self::segment::{Segment, SegmentName};
21
22use std::sync::{LazyLock, RwLock};
23
24static MODULE: LazyLock<RwLock<ModuleState>> = LazyLock::new(|| RwLock::new(ModuleState::new()));
25
26/// Returns `true` if the current execution environment is `SkyrimVR.exe`.
27///
28/// ### When to use
29/// - Use this to simply distinguish VR from SE/AE, e.g., for branching based on class layout.
30///
31/// ### When *not* to use
32/// - If you also need other runtime info from [`ModuleState`].
33///   Calling this separately can cause redundant locking; prefer accessing [`ModuleState`] directly instead.
34///
35/// ### Note
36/// Internally uses [`ModuleState::map_or_init`]; returns `false` on init failure or if not VR.
37#[inline]
38pub fn is_vr() -> bool {
39    ModuleState::map_or_init(|m| m.runtime.is_vr()).ok().is_some_and(|is_vr| is_vr)
40}
41
42/// Represents the state of the module.
43///
44/// This enum implements an API to manage a single global variable of internally managed module (e.g. `SkyrimSE.exe`) information.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum ModuleState {
47    /// The module is successfully initialized and active.
48    Active(Module),
49
50    /// The module instance has been explicitly cleared and memory has been freed.
51    Cleared,
52
53    /// The module failed to initialize.
54    FailedInit(ModuleInitError),
55}
56
57impl ModuleState {
58    /// Initialize the module.
59    fn new() -> Self {
60        let module = Module::new();
61        #[cfg(feature = "test_on_ci")]
62        let module = module.or_else(|_| Module::new_with_msvcrt());
63        #[cfg(feature = "test_on_local")]
64        let module = module.or_else(|_| Module::new_from_skyrim_exe());
65        match module {
66            Ok(module) => Self::Active(module),
67            Err(err) => Self::FailedInit(err),
68        }
69    }
70
71    /// Attempts to apply a function to the active module state.
72    ///
73    /// This function tries to acquire a read lock on the module state and applies
74    /// the provided function `f` if the module state is [`ModuleState::Active`].
75    ///
76    /// If you do not know when you did the [`Self::reset`], or if you want it to be reinitialized automatically if necessary,
77    /// [`Self::map_or_init`] is useful.
78    ///
79    /// # Example
80    /// ```
81    /// use commonlibsse_ng::rel::module::ModuleState;
82    ///
83    /// let result = ModuleState::map_active(|module| module.version.clone());
84    /// match result {
85    ///     Ok(version) => println!("Module version: {}", version),
86    ///     Err(err) => eprintln!("Error: {:?}", err),
87    /// }
88    /// ```
89    ///
90    /// # Errors
91    /// Returns an error if:
92    /// - The module state is [`ModuleState::Cleared`].
93    /// - The module state is [`ModuleState::FailedInit`], in which case the initialization error is propagated.
94    /// - The internal lock is poisoned.
95    #[inline]
96    pub fn map_active<F, T>(f: F) -> Result<T, ModuleStateError>
97    where
98        F: FnOnce(&Module) -> T,
99    {
100        let guard = MODULE.read().map_err(|_| ModuleStateError::ModuleLockIsPoisoned)?;
101
102        match &*guard {
103            Self::Active(module) => Ok(f(module)),
104            Self::Cleared => Err(ModuleStateError::ModuleHasBeenCleared),
105            Self::FailedInit(module_init_error) => {
106                Err(ModuleStateError::FailedInit { source: module_init_error.clone() })
107            }
108        }
109    }
110
111    /// Attempts to apply a function to the active module state, initializing it if necessary.
112    ///
113    /// If the module state is `Cleared`/`FailedInit`, it will be initialized before applying the function `f`.
114    /// This function also attempts a read lock first and falls back to initialization only if needed.
115    ///
116    /// # Example
117    /// ```
118    /// use commonlibsse_ng::rel::module::ModuleState;
119    ///
120    /// let result = ModuleState::map_or_init(|module| module.version.clone());
121    /// match result {
122    ///     Ok(version) => println!("Module version: {}", version),
123    ///     Err(err) => eprintln!("Error: {:?}", err),
124    /// }
125    /// ```
126    ///
127    /// # Errors
128    /// Returns an error if:
129    /// - The module state is [`ModuleState::FailedInit`], in which case the initialization error is propagated.
130    /// - The internal lock is poisoned.
131    pub fn map_or_init<F, T>(f: F) -> Result<T, ModuleStateError>
132    where
133        F: FnOnce(&Module) -> T,
134    {
135        if let Ok(guard) = MODULE.read() {
136            if let Self::Active(module) = &*guard {
137                return Ok(f(module));
138            }
139        }
140
141        // The fact that it was not `Active` means that it absolutely needs to be initialized.
142        let (ret, module_state) = match Module::new() {
143            Ok(module) => (Ok(f(&module)), Self::Active(module)),
144            Err(err) => {
145                let ret_err = ModuleStateError::FailedInit { source: err.clone() };
146                (Err(ret_err), Self::FailedInit(err))
147            }
148        };
149
150        // Delaying lock acquisition to avoid prolonged lock acquisition.
151        MODULE
152            .write()
153            .map(|mut guard| *guard = module_state)
154            .map_err(|_| ModuleStateError::ModuleLockIsPoisoned)?;
155
156        ret
157    }
158
159    /// Clears the module, transitioning it to the `Cleared` state.
160    ///
161    /// # Example
162    /// ```
163    /// use commonlibsse_ng::rel::module::ModuleState;
164    ///
165    /// assert!(ModuleState::reset().is_ok());
166    /// ```
167    ///
168    /// # Errors
169    /// If the thread that had previously acquired a lock on the singleton instance panics, an error is returned.
170    #[inline]
171    pub fn reset() -> Result<(), ModuleStateError> {
172        MODULE.write().map_or(Err(ModuleStateError::ModuleLockIsPoisoned), |mut guard| {
173            *guard = Self::Cleared;
174            Ok(())
175        })
176    }
177}
178
179/// Type definition for treating an instance of information management as an error when it is in
180/// a state where information cannot be obtained.
181#[derive(Debug, Clone, snafu::Snafu)]
182pub enum ModuleStateError {
183    /// The thread that was getting Module's lock panicked.
184    ModuleLockIsPoisoned,
185
186    /// Module has been cleared
187    ModuleHasBeenCleared,
188
189    /// Module initialization error
190    #[snafu(display("Module initialization error: {source}"))]
191    FailedInit { source: crate::rel::module::ModuleInitError },
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_module_reset() {
200        assert!(ModuleState::reset().is_ok());
201    }
202}