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
//! User information provided by Unofficial Extras.

use crate::util::{str_from_cstr, strip_account_prefix};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::{os::raw::c_char, slice};

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

#[cfg(feature = "strum")]
use strum::{Display, EnumCount, EnumIter, IntoStaticStr, VariantNames};

/// Role of a user in the squad.
#[derive(
    Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, IntoPrimitive, TryFromPrimitive,
)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
    feature = "strum",
    derive(Display, EnumCount, EnumIter, IntoStaticStr, VariantNames)
)]
#[repr(u8)]
pub enum UserRole {
    /// User is leader (commander tag).
    SquadLeader = 0,

    /// User is lieutenant.
    Lieutenant = 1,

    /// User is regular member.
    Member = 2,

    /// User is invited.
    Invited = 3,

    /// User has requested to join.
    Applied = 4,

    /// User has left.
    None = 5,

    /// Internal only.
    Invalid = 6,
}

/// Information about a player related to the squad.
///
/// Strings are available for the duration of the call.
/// If you need it for longer than that, consider converting it to [`UserInfoOwned`].
///
/// ```no_run
/// # use arcdps::extras::{UserInfo, UserInfoOwned};
/// # let user: UserInfo = todo!();
/// let owned = user.to_owned();
/// let owned: UserInfoOwned = user.into();
/// ```
#[derive(Debug)]
#[repr(C)]
pub struct UserInfo {
    /// Account name with leading `':'`.
    account_name: *const c_char,

    /// Unix timestamp when the user joined the squad.
    ///
    /// `0` if time could not be determined.
    pub join_time: u64,

    /// Role in squad, or [`UserRole::None`] if the user was removed from the squad.
    pub role: UserRole,

    /// Subgroup the user is in.
    ///
    /// `0` when no subgroup could be found, which is either the first subgroup or no subgroup.
    pub subgroup: u8,

    /// Whether this player is ready or not (in a squad ready check).
    ///
    /// ### Remarks
    /// `role` set to [`UserRole::SquadLeader`] and `ready_status == true` implies that a ready check was just started.
    /// Similarly, `role` set to [`UserRole::SquadLeader`] and `ready_status == false` implies that a ready check either finished or was cancelled.
    /// If everyone in the squad had an event sent with `ready_status == true` then that means that the ready check finished successfully.
    /// After which there will be events sent for each user where their `ready_status == false`.
    pub ready_status: bool,

    /// Unused space.
    pub _unused1: u8,

    /// Unused space.
    pub _unused2: u32,
}

impl UserInfo {
    /// Returns the user account name without leading `':'`.
    #[inline]
    pub fn account_name(&self) -> Option<&str> {
        unsafe { str_from_cstr(self.account_name).map(strip_account_prefix) }
    }

    /// Returns the raw pointer to the user account name.
    #[inline]
    pub fn account_name_ptr(&self) -> *const c_char {
        self.account_name
    }

    /// Converts the [`UserInfo`] to the owned version [`UserInfoOwned`].
    #[inline]
    pub fn to_owned(self) -> UserInfoOwned {
        self.into()
    }
}

/// [`UserInfo`] with an owned [`String`] name.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct UserInfoOwned {
    /// Account name, without leading ':'.
    pub account_name: Option<String>,

    /// Unix timestamp when the user joined the squad.
    ///
    /// `0` if time could not be determined.
    pub join_time: u64,

    /// Role in squad, or [`UserRole::None`] if the user was removed from the squad.
    pub role: UserRole,

    /// Subgroup the user is in.
    ///
    /// `0` when no subgroup could be found, which is either the first subgroup or no subgroup.
    pub subgroup: u8,

    /// Whether this player is ready or not (in a squad ready check).
    ///
    /// ### Remarks
    /// `role` set to [`UserRole::SquadLeader`] and `ready_status == true` implies that a ready check was just started.
    /// Similarly, `role` set to [`UserRole::SquadLeader`] and `ready_status == false` implies that a ready check either finished or was cancelled.
    /// If everyone in the squad had an event sent with `ready_status == true` then that means that the ready check finished successfully.
    /// After which there will be events sent for each user where their `ready_status == false`.
    pub ready_status: bool,
}

impl From<UserInfo> for UserInfoOwned {
    #[inline]
    fn from(user: UserInfo) -> Self {
        Self {
            account_name: user.account_name().map(|x| x.to_string()),
            join_time: user.join_time,
            role: user.role,
            subgroup: user.subgroup,
            ready_status: user.ready_status,
        }
    }
}

/// Iterator over changed users.
pub type UserInfoIter<'a> = slice::Iter<'a, UserInfo>;

/// Helper to generate an iterator over [`UserInfo`] structs.
#[inline]
pub unsafe fn to_user_info_iter<'a>(ptr: *const UserInfo, len: u64) -> UserInfoIter<'a> {
    slice::from_raw_parts(ptr, len as usize).iter()
}