1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
//! [Unofficial Extras](https://github.com/Krappa322/arcdps_unofficial_extras_releases) support.
//!
//! *Requires the `"extras"` feature.*

pub mod callbacks;
pub mod exports;
pub mod keybinds;
pub mod message;
pub mod user;

mod globals;

pub use keybinds::{Control, Key, KeyCode, Keybind, KeybindChange, MouseCode};
pub use message::{ChannelType, ChatMessageInfo, ChatMessageInfoOwned};
pub use user::{UserInfo, UserInfoIter, UserInfoOwned, UserRole};

use crate::util::str_from_cstr;
use callbacks::{
    RawExtrasChatMessageCallback, RawExtrasKeybindChangedCallback,
    RawExtrasLanguageChangedCallback, RawExtrasSquadUpdateCallback,
};
use globals::EXTRAS_GLOBALS;
use std::{ops::RangeInclusive, os::raw::c_char};
use windows::Win32::Foundation::HMODULE;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Supported Unofficial Extras API version.
const API_VERSION: u32 = 2;

/// Supported [`ExtrasSubscriberInfo`] version range.
const SUB_INFO_RANGE: RangeInclusive<u32> = 1..=2;

/// Version with message callback addition.
const MESSAGE_CALLBACK: u32 = 2;

/// Helper to check compatibility.
#[inline]
fn check_compat(api_version: u32, sub_info_version: u32) -> bool {
    api_version == API_VERSION && SUB_INFO_RANGE.contains(&sub_info_version)
}

/// Information about the [Unofficial Extras](https://github.com/Krappa322/arcdps_unofficial_extras_releases) addon.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ExtrasAddonInfo {
    /// Version of the API.
    ///
    /// Gets incremented whenever a function signature or behavior changes in a breaking way.
    ///
    /// Current version is `2`.
    pub api_version: u32,

    /// Highest known version of the [`ExtrasSubscriberInfo`] struct.
    ///
    /// Also determines the size of the subscriber info buffer in the init call.
    /// The buffer is only guaranteed to have enough space for known [`ExtrasSubscriberInfo`] versions.
    ///
    /// Current version is `2`.
    pub max_info_version: u32,

    /// String version of the Unofficial Extras addon.
    ///
    /// Gets changed on every release.
    pub string_version: Option<&'static str>,
}

impl ExtrasAddonInfo {
    /// Checks compatibility with the Unofficial Extras addon.
    pub fn is_compatible(&self) -> bool {
        check_compat(self.api_version, self.max_info_version)
    }

    /// Whether the Unofficial Extras addon supports the chat message callback.
    pub fn supports_chat_message_callback(&self) -> bool {
        self.max_info_version >= MESSAGE_CALLBACK
    }
}

impl From<RawExtrasAddonInfo> for ExtrasAddonInfo {
    fn from(raw: RawExtrasAddonInfo) -> Self {
        Self {
            api_version: raw.api_version,
            max_info_version: raw.max_info_version,
            string_version: unsafe { str_from_cstr(raw.string_version) },
        }
    }
}

#[derive(Debug, Clone)]
#[repr(C)]
pub struct RawExtrasAddonInfo {
    /// Version of the API.
    ///
    /// Gets incremented whenever a function signature or behavior changes in a breaking way.
    ///
    /// Current version is `2`.
    pub api_version: u32,

    /// Highest known version of the [`ExtrasSubscriberInfo`] struct.
    ///
    /// Also determines the size of the subscriber info buffer in the init call.
    /// The buffer is only guaranteed to have enough space for known [`ExtrasSubscriberInfo`] versions.
    ///
    /// Current version is `2`.
    pub max_info_version: u32,

    /// String version of the Unofficial Extras addon.
    ///
    /// Gets changed on every release.
    /// The string is valid for the entire lifetime of the Unofficial Extras DLL.
    pub string_version: *const c_char,

    /// Account name of the logged-in player, including leading `':'`.
    ///
    /// The string is only valid for the duration of the init call.
    pub self_account_name: *const c_char,

    /// The handle to the Unofficial Extras module.
    ///
    /// Use this to call the exports of the DLL.
    pub extras_handle: HMODULE,
}

impl RawExtrasAddonInfo {
    /// Checks compatibility with the Unofficial Extras addon.
    pub fn is_compatible(&self) -> bool {
        check_compat(self.api_version, self.max_info_version)
    }

    /// Whether the Unofficial Extras addon supports the message callback.
    pub fn supports_chat_message_callback(&self) -> bool {
        self.max_info_version >= MESSAGE_CALLBACK
    }
}

/// Subscriber header shared across different versions.
#[derive(Debug)]
#[repr(C)]
pub struct ExtrasSubscriberInfoHeader {
    /// The version of the following info struct
    /// This has to be set to the version you want to use.
    pub info_version: u32,

    /// Unused padding.
    pub unused1: u32,
}

/// Information about a subscriber to updates from Unofficial Extras.
#[derive(Debug)]
#[repr(C)]
pub struct ExtrasSubscriberInfo {
    /// Header shared across different versions.
    pub header: ExtrasSubscriberInfoHeader,

    /// Name of the addon subscribing to the changes.
    ///
    /// Must be valid for the lifetime of the subscribing addon.
    /// Set to `nullptr` if initialization fails.
    pub subscriber_name: *const c_char,

    /// Called whenever anything in the squad changes.
    ///
    /// Only the users that changed are sent.
    /// If a user is removed from the squad, it will be sent with `role` set to [`UserRole::None`]
    pub squad_update_callback: Option<RawExtrasSquadUpdateCallback>,

    /// Called whenever the language is changed.
    ///
    /// Either by Changing it in the UI or by pressing the Right Ctrl (default) key.
    /// Will also be called directly after initialization, with the current language, to get the startup language.
    pub language_changed_callback: Option<RawExtrasLanguageChangedCallback>,

    /// Called whenever a keybind is changed.
    ///
    /// By changing it in the ingame UI, by pressing the translation shortcut or with the Presets feature of this plugin.
    /// It is called for every keybind separately.
    ///
    /// After initialization this is called for every current keybind that exists.
    /// If you want to get a single keybind, at any time you want, call the exported function.
    pub keybind_changed_callback: Option<RawExtrasKeybindChangedCallback>,

    /// Called whenever a chat message is sent in your party/squad.
    pub chat_message_callback: Option<RawExtrasChatMessageCallback>,
}

impl ExtrasSubscriberInfo {
    /// Subscribes to unofficial extras callbacks after checking for compatibility.
    ///
    /// Unsupported callbacks will be skipped.
    ///
    /// Name needs to be null-terminated.
    pub unsafe fn subscribe(
        &mut self,
        extras_addon: &RawExtrasAddonInfo,
        name: &'static str,
        squad_update: Option<RawExtrasSquadUpdateCallback>,
        language_changed: Option<RawExtrasLanguageChangedCallback>,
        keybind_changed: Option<RawExtrasKeybindChangedCallback>,
        chat_message: Option<RawExtrasChatMessageCallback>,
    ) {
        if extras_addon.is_compatible() {
            // initialize globals
            EXTRAS_GLOBALS.init(
                extras_addon.extras_handle,
                str_from_cstr(extras_addon.string_version),
            );

            // we simply use the max version
            self.header.info_version = extras_addon.max_info_version;

            self.subscriber_name = name.as_ptr() as *const c_char;
            self.squad_update_callback = squad_update;
            self.language_changed_callback = language_changed;
            self.keybind_changed_callback = keybind_changed;

            // only attempt to write message callback if supported
            if extras_addon.supports_chat_message_callback() {
                self.chat_message_callback = chat_message;
            }
        }
    }
}