commonlibsse_ng\skse/
interfaces.rs

1// SPDX-FileCopyrightText: (C) 2024 metricexpansion
2// SPDX-License-Identifier: MIT OR CC-BY-NC-SA-4.0
3//
4// See: https://gitlab.com/metricexpansion/SkyrimOutfitSystemSE/-/issues/2#note_2332635556
5
6pub mod load;
7pub mod messaging;
8pub mod object;
9pub mod papyrus;
10pub mod query;
11pub mod scaleform;
12pub mod serialization;
13pub mod task;
14pub mod trampoline;
15
16use crate::{rel::version::Version, rex::kernel32::get_current_module};
17
18#[repr(C)]
19#[derive(Debug)]
20pub struct PluginVersionData {
21    pub data_version: u32,
22    pub plugin_version: u32,
23    pub plugin_name: [u8; 256],
24    pub author: [u8; 256],
25    pub support_email: [u8; 252],
26    pub version_independence_ex: u32,
27    pub version_independence: u32,
28    pub compatible_versions: [u32; 16],
29    /// Insert the packed value of the minimum SKSE version required for operation.
30    ///
31    /// 0 if you are not sure.
32    pub xse_minimum: u32,
33}
34
35const _: () = {
36    use core::mem::offset_of;
37
38    assert!(offset_of!(PluginVersionData, data_version) == 0x000);
39    assert!(offset_of!(PluginVersionData, plugin_version) == 0x004);
40    assert!(offset_of!(PluginVersionData, plugin_name) == 0x008);
41    assert!(offset_of!(PluginVersionData, author) == 0x108);
42    assert!(offset_of!(PluginVersionData, support_email) == 0x208);
43    assert!(offset_of!(PluginVersionData, version_independence_ex) == 0x304);
44    assert!(offset_of!(PluginVersionData, version_independence) == 0x308);
45    assert!(offset_of!(PluginVersionData, compatible_versions) == 0x30C);
46    assert!(offset_of!(PluginVersionData, xse_minimum) == 0x34C);
47    assert!(size_of::<PluginVersionData>() == 0x350);
48};
49
50impl PluginVersionData {
51    pub const VERSION: u32 = 1;
52
53    pub const VERSION_INDEPENDENT_ADDRESS_LIBRARY_POST_AE: u32 = 1;
54    pub const VERSION_INDEPENDENT_SIGNATURES: u32 = 1 << 1;
55    pub const VERSION_INDEPENDENT_STRUCTS_POST_629: u32 = 1 << 2;
56
57    pub const VERSION_INDEPENDENT_EX_NO_STRUCT_USE: u32 = 1;
58
59    pub const fn set_plugin_version(&mut self, version: u32) {
60        self.plugin_version = version;
61    }
62
63    pub const fn get_plugin_version(&self) -> u32 {
64        self.plugin_version
65    }
66
67    pub fn set_plugin_name(&mut self, name: &str) {
68        Self::set_char_buffer(name, &mut self.plugin_name);
69    }
70
71    pub fn get_plugin_name(&self) -> &str {
72        Self::get_char_buffer(&self.plugin_name)
73    }
74
75    pub fn set_author_name(&mut self, name: &str) {
76        Self::set_char_buffer(name, &mut self.author);
77    }
78
79    pub fn get_author_name(&self) -> &str {
80        Self::get_char_buffer(&self.author)
81    }
82
83    pub fn set_author_email(&mut self, email: &str) {
84        Self::set_char_buffer(email, &mut self.support_email);
85    }
86
87    pub fn get_author_email(&self) -> &str {
88        Self::get_char_buffer(&self.support_email)
89    }
90
91    fn set_char_buffer(input: &str, buffer: &mut [u8]) {
92        let bytes = input.as_bytes();
93        let len = bytes.len().min(buffer.len() - 1);
94        buffer[..len].copy_from_slice(&bytes[..len]);
95        buffer[len] = 0;
96    }
97
98    fn get_char_buffer(buffer: &[u8]) -> &str {
99        let end = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
100        core::str::from_utf8(&buffer[..end]).unwrap_or("")
101    }
102
103    pub fn get_singleton() -> Option<&'static Self> {
104        use windows::Win32::System::LibraryLoader::GetProcAddress;
105        use windows::core::s;
106
107        let f = unsafe { GetProcAddress(get_current_module(), s!("SKSEPlugin_Version")) };
108        #[allow(clippy::fn_to_numeric_cast_any)]
109        f.map(|f| unsafe { &*(f as *const Self) })
110    }
111}
112
113#[repr(u32)]
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum StructCompatibility {
116    Dependent = 0,
117    Independent = 1,
118}
119
120#[repr(transparent)]
121#[derive(Debug, Clone, Copy)]
122pub struct VersionNumber {
123    packed: u32,
124}
125
126impl VersionNumber {
127    #[inline]
128    pub const fn new(major: u16, minor: u16, patch: u16, build: u16) -> Self {
129        Self {
130            packed: ((major as u32) << 24)
131                | ((minor as u32) << 16)
132                | ((patch as u32) << 8)
133                | (build as u32),
134        }
135    }
136
137    #[inline]
138    pub const fn default_const() -> Self {
139        Self::new(0, 0, 0, 0)
140    }
141
142    #[inline]
143    pub const fn from_version(version: Version) -> Self {
144        Self { packed: version.pack() }
145    }
146
147    #[inline]
148    pub const fn from_packed(packed: u32) -> Self {
149        Self { packed }
150    }
151
152    #[inline]
153    pub const fn to_packed(self) -> u32 {
154        self.packed
155    }
156}
157
158pub const fn to_fixed_str<const N: usize>(s: &str) -> [u8; N] {
159    let bytes = s.as_bytes();
160    let bytes_len = bytes.len();
161
162    assert!(bytes_len < N, "The length of the input string is too large for the specified size.");
163
164    let mut buf = [0_u8; N];
165
166    let mut i = 0;
167    while i < bytes_len {
168        let b = bytes[i];
169        assert!(b != 0, "The input string contains a null byte.");
170        assert!(b.is_ascii(), "The input string contains non-ASCII characters.");
171        buf[i] = b;
172        i += 1;
173    }
174
175    buf
176}
177
178// SPDX-FileCopyrightText: (C) 2023 peelz
179// SPDX-License-Identifier: MIT
180// https://github.com/notpeelz/cstr-literal/blob/master/src/lib.rs#L11
181//
182/// Create str to Cstr at compile time.
183///
184/// # Example
185///
186/// ```rust
187/// const HELLO: &std::ffi::CStr = commonlibsse_ng::skse::interfaces::new_cstr(concat!("hello", "\0"));
188/// ```
189///
190/// # Panics
191///
192/// This function panics if:
193/// - The input string contains null bytes before the null terminator.
194/// - The input string is not null-terminated.
195pub const fn new_cstr(s: &'static str) -> &'static std::ffi::CStr {
196    let mut bytes = s.as_bytes();
197    loop {
198        match bytes {
199            [0, _, ..] => panic!("C strings cannot contain null bytes"),
200            [] => panic!("C strings must be null-terminated"),
201            [0] => break,
202            [_, remaining @ ..] => bytes = remaining,
203        }
204    }
205
206    // SAFETY: The input string is validated to be null-terminated and without interior null bytes.
207    unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(s.as_bytes()) }
208}
209
210#[repr(transparent)]
211#[derive(Debug, Clone)]
212pub struct String256([u8; 256]);
213
214impl String256 {
215    /// Creates a new String256
216    /// # Panics
217    /// - Non ascii characters
218    /// - Null byte
219    pub const fn new(s: &str) -> Self {
220        Self(to_fixed_str(s))
221    }
222
223    /// Creates a new String256 with a default value of 0
224    pub const fn default_const() -> Self {
225        Self([0; 256])
226    }
227}
228
229#[repr(transparent)]
230#[derive(Debug, Clone)]
231pub struct String252([u8; 252]);
232
233impl String252 {
234    /// Creates a new String252
235    /// # Panics
236    /// - Non ascii characters
237    /// - Null byte
238    pub const fn new(s: &str) -> Self {
239        Self(to_fixed_str(s))
240    }
241
242    /// Creates a new String252 with a default value of 0
243    pub const fn default_const() -> Self {
244        Self([0; 252])
245    }
246}
247
248#[repr(C)]
249#[derive(Debug, Clone)]
250pub struct RuntimeCompatibility {
251    // The bool order of this bit flag is valid as long as ByteOrder is little-endian.
252    pub address_library: bool,
253    pub signature_scanning: bool,
254    pub structs_post_629: bool,
255
256    /// Initialization of pad is necessary because an error will occur if the memory is uninitialized.
257    /// (Otherwise, UB will occur when [`core::mem::transmute`] is done on `PluginVersionData`.
258    pub _pad0: u8,
259    // _pad1: u8,
260    // _pad2: u16,
261    pub compatible_versions: [VersionNumber; 16],
262}
263
264const _: () = {
265    assert!(core::mem::size_of::<RuntimeCompatibility>() == 0x44);
266};
267
268impl RuntimeCompatibility {
269    pub const fn new() -> Self {
270        Self {
271            address_library: true,
272            signature_scanning: false,
273            structs_post_629: false,
274            _pad0: 0,
275            // _pad1: 0,
276            // _pad2: 0,
277            compatible_versions: [VersionNumber::new(0, 0, 0, 0); 16],
278        }
279    }
280}
281
282impl Default for RuntimeCompatibility {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288#[repr(C)]
289#[derive(Debug, Clone)]
290pub struct PluginDeclarationInfo {
291    /// The version number of the plugin.
292    pub version: VersionNumber,
293    /// The plugin's name (maximum of 256 characters).
294    pub name: String256,
295    /// The name of the plugin's author (maximum of 256 characters).
296    pub author: String256,
297    /// A support email address for the plugin (maximum of 256 characters).
298    pub support_email: String252,
299    /// Defines the compatibility with structure layout of the plugin.
300    ///
301    /// For most of modern CommonLibSSE-era plugin development structs in Skyrim have remained
302    /// unchanged. In AE 1.6.629, however, the layout of some structs changed. If this is flagged
303    /// as independent, then SKSE will let your plugin work with runtimes before and after this
304    /// change. CommonLibSSE NG defaults to flagging a plugin independent because it supports
305    /// both struct layouts in a single plugin. If your plugin has any RE'd structs that have
306    /// changed you should override this.
307    pub struct_compatibility: StructCompatibility,
308    /// A definition of the runtime compatibility for the plugin.
309    ///
310    /// This can be either an indicator of how version-independence is achieved (either through using Address Library
311    /// or signature scanning, indicated with a value from `skse::VersionIndependence`, or a list of up to
312    /// 16 version numbers of Skyrim runtimes that are supported by this plugin.
313    pub runtime_compatibility: RuntimeCompatibility,
314    /// The minimum SKSE version required for the plugin; this should almost always be left 0.
315    pub minimum_skse_version: VersionNumber,
316}
317
318const _: () = {
319    use std::mem::offset_of;
320
321    assert!(0x000 == offset_of!(PluginDeclarationInfo, version));
322    assert!(0x004 == offset_of!(PluginDeclarationInfo, name));
323    assert!(0x104 == offset_of!(PluginDeclarationInfo, author));
324    assert!(0x204 == offset_of!(PluginDeclarationInfo, support_email));
325    assert!(0x300 == offset_of!(PluginDeclarationInfo, struct_compatibility));
326    assert!(0x304 == offset_of!(PluginDeclarationInfo, runtime_compatibility));
327    assert!(0x348 == offset_of!(PluginDeclarationInfo, minimum_skse_version));
328};
329
330/// The same memory layout as `PluginVersionData`.
331#[repr(C)]
332#[derive(Debug, Clone)]
333pub struct PluginDeclaration {
334    pub data_version: u32,
335    pub data: PluginDeclarationInfo,
336}
337const _: () = assert!(0x350 == core::mem::size_of::<PluginDeclaration>());
338
339impl PluginDeclaration {
340    pub fn get_singleton() -> Option<&'static mut Self> {
341        use windows::Win32::System::LibraryLoader::GetProcAddress;
342        use windows::core::s;
343
344        let f = unsafe { GetProcAddress(get_current_module(), s!("SKSEPlugin_Version")) };
345
346        #[allow(clippy::fn_to_numeric_cast_any)]
347        f.map(|f| unsafe { &mut *(f as *mut Self) })
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use core::mem;
355
356    #[test]
357    fn test_plugin_version_data_matches_plugin_declaration_info() {
358        const PLUGIN_VERSION_DATA: PluginVersionData = PluginVersionData {
359            data_version: 1,
360            plugin_version: 2,
361            plugin_name: [0; 256],
362            author: [0; 256],
363            support_email: [0; 252],
364            version_independence_ex: 1,
365            version_independence: 1,
366            compatible_versions: [0; 16],
367            xse_minimum: 0,
368        };
369
370        const PLUGIN_DECLARATION: PluginDeclaration = PluginDeclaration {
371            data_version: 1,
372            data: PluginDeclarationInfo {
373                version: VersionNumber::new(0, 0, 0, 2),
374                name: String256::default_const(),
375                author: String256::default_const(),
376                support_email: String252::default_const(),
377                struct_compatibility: StructCompatibility::Independent,
378                runtime_compatibility: RuntimeCompatibility {
379                    address_library: true,
380                    signature_scanning: false,
381                    structs_post_629: false,
382                    _pad0: 0,
383                    compatible_versions: [VersionNumber::default_const(); 16],
384                },
385                minimum_skse_version: VersionNumber::default_const(),
386            },
387        };
388
389        // NOTE: By doing this at compile time, it also serves as a UB check when type conversion is performed.
390        const PLUGIN_VERSION_BYTES: [u8; 848] = unsafe { mem::transmute(PLUGIN_VERSION_DATA) };
391        const PLUGIN_DECLARATION_BYTES: [u8; 848] = unsafe { mem::transmute(PLUGIN_DECLARATION) };
392
393        pretty_assertions::assert_eq!(PLUGIN_VERSION_BYTES, PLUGIN_DECLARATION_BYTES); // Array can't evaluate at compile time
394    }
395}