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}