arcdps\exports/
mod.rs

1//! ArcDPS exports.
2//!
3//! Calling an export before ArcDPS calls `init` will cause a panic.
4
5mod has;
6pub mod raw;
7
8pub use self::has::*;
9
10use crate::{
11    evtc::{Event, Profession},
12    globals::ArcGlobals,
13    imgui::sys::ImVec4,
14};
15use num_enum::{IntoPrimitive, TryFromPrimitive};
16use std::{
17    ffi::{CString, NulError, OsString},
18    mem::MaybeUninit,
19    ops::RangeInclusive,
20    os::windows::prelude::*,
21    path::PathBuf,
22    slice,
23};
24use windows::Win32::Foundation::HMODULE;
25
26#[cfg(feature = "serde")]
27use serde::{Deserialize, Serialize};
28
29#[cfg(feature = "strum")]
30use strum::{Display, EnumCount, EnumIter, IntoStaticStr, VariantNames};
31
32/// Retrieves the ArcDPS version as string.
33#[inline]
34pub fn version() -> Option<&'static str> {
35    ArcGlobals::get().version
36}
37
38/// Retrieves the config path from ArcDPS.
39///
40/// # Examples
41/// ```no_run
42/// use std::fs;
43/// use arcdps::exports;
44///
45/// # fn foo() -> Option<()> {
46/// let config_path = exports::config_path()?;
47/// let config_dir = config_path.parent()?;
48/// fs::write(config_dir.join("foo.txt"), "lorem ipsum");
49/// # None }
50/// ```
51#[inline]
52pub fn config_path() -> Option<PathBuf> {
53    let ptr = unsafe { raw::e0_config_path() };
54    if !ptr.is_null() {
55        // calculate length
56        let mut len = 0;
57        while unsafe { *ptr.offset(len) } != 0 {
58            len += 1;
59        }
60
61        // transform data
62        let slice = unsafe { slice::from_raw_parts(ptr, len as usize) };
63        let string = OsString::from_wide(slice);
64        Some(PathBuf::from(string))
65    } else {
66        None
67    }
68}
69
70/// Logs a message to ArcDPS' log file `arcdps.log`.
71///
72/// Returns an error if the passed message could not be converted to a C-compatible string.
73///
74/// # Examples
75/// ```no_run
76/// use arcdps::exports;
77///
78/// exports::log_to_file("message from my plugin");
79/// ```
80#[inline]
81pub fn log_to_file(message: impl Into<String>) -> Result<(), NulError> {
82    let string = CString::new(message.into())?;
83    unsafe { raw::e3_log_file(string.as_ptr()) };
84    Ok(())
85}
86
87/// ArcDPS core UI color.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
89#[repr(C)]
90pub enum CoreColor {
91    Transparent,
92    White,
93    LightWhite,
94    LightGrey,
95    LightYellow,
96    LightGreen,
97    LightRed,
98    LightTeal,
99    MediumGrey,
100    DarkGrey,
101    Num,
102}
103
104/// ArcDPS color type.
105pub type Color = [f32; 4];
106
107/// Current ArcDPS color settings.
108///
109/// Use the associated functions to access individual colors.
110///
111/// This holds pointers to color information in memory until dropped.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct Colors {
114    raw: [*mut ImVec4; 5],
115}
116
117impl Colors {
118    /// Range of valid subgroups.
119    const SUB_RANGE: RangeInclusive<usize> = 0..=15;
120
121    /// Reads a color from the raw color array.
122    ///
123    /// The first index is the index of the subarray.
124    /// The second index is the index of the color within the subarray.
125    ///
126    /// This will return [`None`] if the pointer retrieved from ArcDPS is null or was not initialized.
127    ///
128    /// This is unsafe since indexing the raw color array is only valid with specific indices.
129    /// Incorrect indices may cause undefined behavior.
130    unsafe fn read_color(&self, first_index: usize, second_index: usize) -> Option<Color> {
131        let ptr = self.raw[first_index];
132        if !ptr.is_null() {
133            let color = *ptr.add(second_index);
134            Some(color.into())
135        } else {
136            None
137        }
138    }
139
140    /// Returns the base color for a specific [`CoreColor`].
141    ///
142    /// This will return [`None`] if ArcDPS did not yield the requested color when the [`Colors`] struct was retrieved.
143    #[inline]
144    pub fn core(&self, color: CoreColor) -> Option<Color> {
145        unsafe { self.read_color(0, color as usize) }
146    }
147
148    /// Returns the base color for a specific [`Profession`].
149    ///
150    /// This will return [`None`] if ArcDPS did not yield the requested color when the [`Colors`] struct was retrieved.
151    #[inline]
152    pub fn prof_base(&self, prof: Profession) -> Option<Color> {
153        unsafe { self.read_color(1, prof as usize) }
154    }
155
156    /// Returns the highlight color for a specific [`Profession`].
157    ///
158    /// This will return [`None`] if ArcDPS did not yield the requested color when the [`Colors`] struct was retrieved.
159    #[inline]
160    pub fn prof_highlight(&self, prof: Profession) -> Option<Color> {
161        unsafe { self.read_color(2, prof as usize) }
162    }
163
164    /// Returns the base color for a specific subgroup.
165    ///
166    /// This will return [`None`] if ArcDPS did not yield the requested color when the [`Colors`] struct was retrieved.
167    /// Also returns [`None`] if the subgroup is out of the game subgroup range.
168    #[inline]
169    pub fn sub_base(&self, sub: usize) -> Option<Color> {
170        // range check
171        if Self::SUB_RANGE.contains(&sub) {
172            unsafe { self.read_color(3, sub) }
173        } else {
174            None
175        }
176    }
177
178    /// Returns the highlight color for a specific subgroup.
179    ///
180    /// This will return [`None`] if ArcDPS did not yield the requested color when the [`Colors`] struct was retrieved.
181    /// Also returns [`None`] if the subgroup is out of the game subgroup range.
182    #[inline]
183    pub fn sub_highlight(&self, sub: usize) -> Option<Color> {
184        // range check
185        if Self::SUB_RANGE.contains(&sub) {
186            unsafe { self.read_color(4, sub) }
187        } else {
188            None
189        }
190    }
191}
192
193/// Retrieves the color settings from ArcDPS.
194///
195/// # Examples
196/// ```no_run
197/// use arcdps::{Profession, exports};
198///
199/// let colors = exports::colors();
200/// let guard = colors.prof_base(Profession::Guardian);
201/// ```
202#[inline]
203pub fn colors() -> Colors {
204    // zeroing this is important for our null pointer checks later
205    let mut colors = MaybeUninit::zeroed();
206    unsafe { raw::e5_colors(colors.as_mut_ptr()) };
207    Colors {
208        raw: unsafe { colors.assume_init() },
209    }
210}
211
212/// Current ArcDPS UI settings.
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct UISettings {
215    /// Whether the UI is hidden.
216    pub hidden: bool,
217
218    /// Whether the UI is always drawn.
219    ///
220    /// When `false`, the UI is hidden during loading screens & character select.
221    pub draw_always: bool,
222
223    /// Whether pressing the modifiers are required to move windows.
224    pub modifiers_move_lock: bool,
225
226    /// Whether pressing the modifiers are required to click windows.
227    pub modifiers_click_lock: bool,
228
229    /// Whether windows should close with the `ESC` key.
230    pub close_with_esc: bool,
231}
232
233/// Retrieves the UI settings from ArcDPS.
234///
235/// # Examples
236/// ```no_run
237/// use arcdps::exports;
238///
239/// let ui_settings = exports::ui_settings();
240/// if !ui_settings.hidden {
241///     # let ui: arcdps::imgui::Ui = todo!();
242///     ui.text("hello world");
243/// }
244/// ```
245pub fn ui_settings() -> UISettings {
246    let raw = unsafe { raw::e6_ui_settings() };
247    UISettings {
248        hidden: raw & 1 == 1,
249        draw_always: (raw >> 1) & 1 == 1,
250        modifiers_move_lock: (raw >> 2) & 1 == 1,
251        modifiers_click_lock: (raw >> 3) & 1 == 1,
252        close_with_esc: (raw >> 4) & 1 == 1,
253    }
254}
255
256/// Current ArcDPS modifier keybinds as virtual key ids.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub struct Modifiers {
259    /// Virtual key id of the first modifier used by ArcDPS.
260    pub modifier1: u16,
261
262    /// Virtual key id of the second modifier used by ArcDPS.
263    pub modifier2: u16,
264
265    /// Virtual key id of the "multi" modifier used by ArcDPS.
266    pub modifier_multi: u16,
267}
268
269/// Retrieves the modifier keybinds from ArcDPS.
270///
271/// # Examples
272/// ```no_run
273/// use arcdps::exports;
274///
275/// let modifiers = exports::modifiers();
276/// let multi = modifiers.modifier_multi;
277/// ```
278#[inline]
279pub fn modifiers() -> Modifiers {
280    let raw = unsafe { raw::e7_modifiers() };
281    Modifiers {
282        modifier1: raw as u16,
283        modifier2: (raw >> 16) as u16,
284        modifier_multi: (raw >> 32) as u16,
285    }
286}
287
288/// Logs a message to ArcDPS' logger window.
289///
290/// Text can be colored in a HTML-like way: `<c=#aaaaaa>colored text</c>`.
291///
292/// Returns an error if the passed message could not be converted to a C-compatible string.
293///
294/// # Examples
295/// ```no_run
296/// use arcdps::exports;
297///
298/// exports::log_to_window("message from my plugin");
299/// ```
300#[inline]
301pub fn log_to_window(message: impl Into<String>) -> Result<(), NulError> {
302    let string = CString::new(message.into())?;
303    unsafe { raw::e8_log_window(string.as_ptr()) };
304    Ok(())
305}
306
307/// Adds an [`Event`] to ArcDPS' event processing.
308///
309/// `is_statechange` will be set to [`StateChange::Extension`](crate::StateChange::Extension), padding will be set to contain `sig`.
310/// Event will end up processed like ArcDPS events and logged to EVTC.
311#[inline]
312pub fn add_event(event: &Event, sig: u32) {
313    unsafe { raw::e9_add_event(event, sig) }
314}
315
316/// Adds an [`Event`] to ArcDPS' event processing.
317///
318/// `is_statechange` will be set to [`StateChange::ExtensionCombat`](crate::StateChange::ExtensionCombat), padding will be set to contain `sig`.
319/// Event will end up processed like ArcDPS events and logged to EVTC.
320///
321/// Contrary to [`add_event`], the `skill_id` is treated as skill id and will be added to the EVTC skill table.
322#[inline]
323pub fn add_event_combat(event: &Event, sig: u32) {
324    unsafe { raw::e10_add_event_combat(event, sig) }
325}
326
327/// Requests to load an extension (plugin/addon).
328///
329/// ArcDPS will `LoadLibrary` the `handle` to increment the reference count, call `get_init_addr` and call its returned function.
330/// Returns [`AddExtensionResult`] indicating success or failure.
331///
332/// This uses version 2 (`addextension2`) of the extension API.
333#[inline]
334pub fn add_extension(handle: HMODULE) -> AddExtensionResult {
335    unsafe { raw::add_extension(handle) }
336        .try_into()
337        .expect("unexpected add extension result")
338}
339
340/// Result of an [`add_extension`] operation.
341#[derive(
342    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, IntoPrimitive, TryFromPrimitive,
343)]
344#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
345#[cfg_attr(
346    feature = "strum",
347    derive(Display, EnumCount, EnumIter, IntoStaticStr, VariantNames)
348)]
349#[repr(u32)]
350pub enum AddExtensionResult {
351    /// Extension was loaded successfully.
352    Ok,
353
354    /// Extension-specific error.
355    ExtensionError,
356
357    /// ImGui version did not match.
358    ImGuiError,
359
360    /// Obsolete ArcDPS module.
361    Obsolete,
362
363    /// An extension with the same `sig` already exists.
364    SigExists,
365
366    /// Extension did not provide callback function table.
367    NoExport,
368
369    /// Extension did not provide an `init` function.
370    NoInit,
371
372    /// Failed to load extension module with `LoadLibrary`.
373    ///
374    /// Safe to call `GetLastError`.
375    LoadError,
376}
377
378/// Requests to free a loaded extension (plugin/addon).
379///
380/// ArcDPS will call `get_release_addr` and its returned function.
381/// Upon returning from [`free_extension`] there will be no more pending callbacks.
382/// However, the caller must ensure to callbacks are executing before freeing.
383/// Returns the [`HMODULE`] handle of the module if the extension was found.
384///
385/// This uses version 2 (`freeextension2`) of the extension API.
386#[inline]
387pub fn free_extension(sig: u32) -> Option<HMODULE> {
388    let handle = unsafe { raw::free_extension(sig) };
389    (!handle.is_invalid()).then_some(handle)
390}