jokolink/
mlink.rs

1#![allow(clippy::not_unsafe_ptr_arg_deref)]
2
3use std::net::Ipv4Addr;
4
5use bitflags::bitflags;
6use color_eyre::eyre::bail;
7use num_derive::FromPrimitive;
8use num_derive::ToPrimitive;
9use serde::{Deserialize, Serialize};
10use tracing::*;
11
12/// As the CMumbleLink has all the fields multiple
13#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14pub struct MumbleLink {
15    pub ui_tick: u32,
16    pub f_avatar_position: [f32; 3],
17    pub f_avatar_front: [f32; 3],
18    pub f_camera_position: [f32; 3],
19    pub f_camera_front: [f32; 3],
20    pub identity: CIdentity,
21    pub context: CMumbleContext,
22}
23
24impl MumbleLink {
25    /// The most used function probably. will check if the `link_ptr->ui_tick > &self.ui_tick`
26    /// and update self's fields based on that.
27    pub fn update(&mut self, link_ptr: *const CMumbleLink) -> Result<(), MumbleUpdateError> {
28        let cmlink = match unsafe { link_ptr.as_ref() } {
29            Some(cmlink) => cmlink,
30            None => return Err(MumbleUpdateError::CMLinkPtrAsRefError),
31        };
32
33        if self.ui_tick != cmlink.ui_tick {
34            self.ui_tick = cmlink.ui_tick;
35
36            self.f_avatar_position = cmlink.f_avatar_position;
37
38            self.f_avatar_front = cmlink.f_avatar_front;
39
40            self.f_camera_position = cmlink.f_camera_position;
41
42            self.f_camera_front = cmlink.f_camera_front;
43            self.identity.update(link_ptr)?;
44            self.context.update(link_ptr);
45        }
46
47        Ok(())
48    }
49    pub fn update_from_slice(&mut self, buffer: &[u8]) -> Result<(), MumbleUpdateError> {
50        assert!(buffer.len() >= 1093);
51        self.update(buffer.as_ptr() as *const CMumbleLink)
52    }
53}
54
55#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
56#[repr(C)]
57/// The mumble context as stored inside the context field of CMumbleLink.
58/// the first 48 bytes Mumble uses for identification is upto build_id field
59/// the rest of the fields after build_id are provided by gw2 for addon devs.
60pub struct CMumbleContext {
61    /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets.
62    pub server_address: [u8; 28], // contains sockaddr_in or sockaddr_in6
63    pub map_id: u32,
64    pub map_type: u32,
65    pub shard_id: u32,
66    pub instance: u32,
67    pub build_id: u32,
68    // Additional data that gw2 provides us
69    pub ui_state: u32, // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat
70    pub compass_width: u16, // pixels
71    pub compass_height: u16, // pixels
72    pub compass_rotation: f32, // radians
73    pub player_x: f32, // continentCoords
74    pub player_y: f32, // continentCoords
75    pub map_center_x: f32, // continentCoords
76    pub map_center_y: f32, // continentCoords
77    pub map_scale: f32,
78    /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data.
79    /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to consider the change of pid
80    pub process_id: u32,
81    /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match api
82    pub mount_index: u8,
83}
84/// This is completely different from the api's mount ids. so, its find to define this here
85#[derive(Debug, FromPrimitive, ToPrimitive, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
86pub enum Mount {
87    None = 0,
88    Jackal = 1,
89    Griffon = 2,
90    Springer = 3,
91    Skimmer = 4,
92    Raptor = 5,
93    RollerBeetle = 6,
94    Warclaw = 7,
95    Skyscale = 8,
96}
97impl Default for Mount {
98    fn default() -> Self {
99        Self::None
100    }
101}
102
103bitflags! {
104    /// The Uistate enum to represent what is happening in game
105    #[derive(Default, Serialize, Deserialize)]
106    pub struct UIState: u32 {
107        const IS_MAP_OPEN = 0b00000001;
108        const IS_COMPASS_TOP_RIGHT = 0b00000010;
109        const DOES_COMPASS_HAVE_ROTATION_ENABLED = 0b00000100;
110        const GAME_HAS_FOCUS = 0b00001000;
111        const IN_COMPETITIVE_GAMEMODE = 0b00010000;
112        const TEXTBOX_FOCUS = 0b00100000;
113        const IS_IN_COMBAT = 0b01000000;
114    }
115}
116impl CMumbleContext {
117    pub fn get_ui_state(&self) -> Option<UIState> {
118        UIState::from_bits(self.ui_state)
119    }
120
121    pub fn update(&mut self, link_ptr: *const CMumbleLink) {
122        let mc = unsafe {
123            std::ptr::read_volatile(&(*link_ptr).context as *const u8 as *const CMumbleContext)
124        };
125        *self = mc;
126    }
127    /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets.
128    /// contains sockaddr_in or sockaddr_in6
129    pub fn get_map_ip(&self) -> color_eyre::Result<Ipv4Addr> {
130        if self.server_address[0] != 2 {
131            bail!("ipaddr parsing failed for CMumble Context");
132        }
133
134        let ip = Ipv4Addr::from([
135            self.server_address[4],
136            self.server_address[5],
137            self.server_address[6],
138            self.server_address[7],
139        ]);
140        Ok(ip)
141    }
142
143    pub fn get_mount(&self) -> Option<Mount> {
144        use num_traits::FromPrimitive;
145        Mount::from_u8(self.mount_index)
146    }
147}
148
149#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
150/// The json structure of the Identity field inside Cmumblelink.
151/// the json string is null terminated and utf-16 encoded. so, need to use
152/// Widestring crate's U16Cstring to first parse the bytes and then, convert to
153/// String before deserializing to CIdentity
154pub struct CIdentity {
155    /// The name of the character
156    pub name: String,
157    /// The core profession id of the character. matches the ids of v2/professions endpoint
158    pub profession: u32,
159    /// Character's third specialization, or 0 if no specialization is present. See /v2/specializations for valid IDs.
160    pub spec: u32,
161    /// The race of the character. does not match api
162    pub race: u32,
163    /// API:2/maps
164    pub map_id: u32,
165    /// useless field from pre-megaserver days. is just shard_id from context struct
166    pub world_id: u32,
167    /// Team color per API:2/colors (0 = white)
168    pub team_color_id: u32,
169    /// Whether the character has a commander tag active
170    pub commander: bool,
171    /// Vertical field-of-view
172    pub fov: f32,
173    /// A value corresponding to the user's current UI scaling.
174    pub uisz: u32,
175}
176
177/// represents the ui scale set in settings -> graphics options -> interface size
178#[derive(
179    Debug,
180    Clone,
181    Copy,
182    PartialEq,
183    Eq,
184    PartialOrd,
185    Ord,
186    Hash,
187    Serialize,
188    Deserialize,
189    FromPrimitive,
190    ToPrimitive,
191)]
192pub enum UISize {
193    Small = 0,
194    Normal = 1,
195    Large = 2,
196    Larger = 3,
197}
198
199/// Race Enum that DOES NOT match the gw2 api endpoint ids
200#[derive(
201    Debug,
202    Clone,
203    Copy,
204    PartialEq,
205    Eq,
206    PartialOrd,
207    Ord,
208    Hash,
209    Serialize,
210    Deserialize,
211    FromPrimitive,
212    ToPrimitive,
213)]
214pub enum Race {
215    Asura = 0,
216    Charr = 1,
217    Human = 2,
218    Norn = 3,
219    Sylvari = 4,
220}
221impl CIdentity {
222    pub fn get_uisz(&self) -> Option<UISize> {
223        use num_traits::FromPrimitive;
224        UISize::from_u32(self.uisz)
225    }
226    pub fn get_race(&self) -> Option<Race> {
227        use num_traits::FromPrimitive;
228        Race::from_u32(self.uisz)
229    }
230    pub fn update(&mut self, link_ptr: *const CMumbleLink) -> Result<(), MumbleIdentityError> {
231        use widestring::U16CStr;
232        let id = U16CStr::from_slice_truncate(unsafe { &(*link_ptr).identity })?;
233        let id = id.to_string()?;
234        *self = serde_json::from_str::<CIdentity>(&id)?;
235        Ok(())
236    }
237}
238
239/// The total size of the CMumbleLink struct. used to know the amount of memory to give to win32 call that creates the shared memory
240pub const C_MUMBLE_LINK_SIZE: usize = std::mem::size_of::<CMumbleLink>();
241/// This is how much of the CMumbleLink memory that is actually useful and updated. the rest is just zeroed out. might change in future
242pub const USEFUL_C_MUMBLE_LINK_SIZE: usize = 1193;
243
244/// The CMumblelink is how it is represented in the memory. But we rarely use it as it is and instead convert it into MumbleLink before using it for convenience
245/// Many of the fields are documentad in the actual MumbleLink struct
246#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
247#[repr(C)]
248pub struct CMumbleLink {
249    /// The ui_version will always be same as mumble doesn't change. we will come back to change it IF there's a new version.
250    pub ui_version: u32,
251    /// This tick represents the update count of the link (which is usually the frame count ) since mumble was initialized. not from the start of game, but the start of mumble
252    pub ui_tick: u32,
253    pub f_avatar_position: [f32; 3],
254    pub f_avatar_front: [f32; 3],
255    pub f_avatar_top: [f32; 3],
256    pub name: [u8; 512],
257    pub f_camera_position: [f32; 3],
258    pub f_camera_front: [f32; 3],
259    pub f_camera_top: [f32; 3],
260    pub identity: [u16; 256],
261    pub context_len: u32,
262    pub context: [u8; 256],
263    pub description: [u8; 4096],
264}
265impl CMumbleLink {
266    /// This takes a point and reads out the CMumbleLink struct from it. wrapper for unsafe ptr read
267    pub fn get_cmumble_link(link_ptr: *const CMumbleLink) -> CMumbleLink {
268        unsafe { std::ptr::read_volatile(link_ptr) }
269    }
270
271    /// Checks if the uitick is actually initialized
272    pub fn is_valid(link_ptr: *const CMumbleLink) -> bool {
273        unsafe { (*link_ptr).ui_tick > 0 }
274    }
275
276    /// gets uitick if we want to know the frame number since initialization of CMumbleLink
277    pub fn get_ui_tick(link_ptr: *const CMumbleLink) -> u32 {
278        unsafe { (*link_ptr).ui_tick }
279    }
280    pub fn get_pid(link_ptr: *const CMumbleLink) -> u32 {
281        unsafe { (*(link_ptr as *const u8 as *const CMumbleContext)).process_id }
282    }
283    // /// creates the shared memory using win32 calls and returns the pointer
284    // #[cfg(target_os = "windows")]
285    // pub fn new_ptr(key: &str) -> color_eyre::Result<(_, *const CMumbleLink)> {
286    //     crate::win::create_link_shared_mem(key)
287    // }
288    /// we will copy the bytes of the struct memory into the slice. we check that we will only copy upto C_MUMBLE_LINK_SIZE or buffer.len() whichever is smaller to avoid buffer overflow reads
289    pub fn copy_raw_bytes_into(link_ptr: *const CMumbleLink, buffer: &mut [u8]) {
290        let max_len = usize::min(buffer.len(), C_MUMBLE_LINK_SIZE);
291        unsafe {
292            std::ptr::copy_nonoverlapping(link_ptr as *const u8, buffer.as_mut_ptr(), max_len);
293        }
294    }
295}
296
297#[derive(Debug, thiserror::Error)]
298pub enum MumbleUpdateError {
299    #[error("Mumble Identity error")]
300    MumbleIdentityError(#[from] MumbleIdentityError),
301    #[error("link_ptr.as_ref returned None when trying to update MumbleLink. ptr is null. something is very wrong")]
302    CMLinkPtrAsRefError,
303}
304
305#[derive(Debug, thiserror::Error)]
306pub enum MumbleIdentityError {
307    #[error("Mumble Identity String missing null terminator error")]
308    U16CStrMissingNullTerminator(#[from] widestring::error::MissingNulTerminator),
309    #[error("Mumble Identity String is not valid utf-8")]
310    Utf16To8Error(#[from] widestring::error::Utf16Error),
311    #[error("Mumble Identity is not valid json")]
312    JsonError(#[from] serde_json::Error),
313}