evtc_parse/
agent.rs

1use crate::{
2    util::{read_string_buffer, write_string_buffer, Endian},
3    Parse, ParseError, Save,
4};
5use byteorder::{ReadBytesExt, WriteBytesExt};
6use evtc::AgentKind;
7use std::io;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12/// An EVTC agent.
13///
14/// Could be a player, enemy, minion or other.
15///
16/// If `is_elite == 0xffffffff` and upper half of `prof == 0xffff`, the agent is a gadget with a pseudo id as lower half of `prof` (volatile id).
17/// If `is_elite == 0xffffffff` and upper half of `prof != 0xffff`, the agent is an NPC with species id as lower half of `prof` (reliable id).
18/// If `is_elite != 0xffffffff`, the agent is a player with Profession as `prof` and Elite Specialization as `is_elite`.
19///
20/// Gadgets do not have true ids and are generated through a combination of gadget parameters.
21/// They will collide with NPCs and should be treated separately.
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24pub struct Agent {
25    /// Id of the agent.
26    pub id: u64,
27
28    /// Name information for the agent.
29    ///
30    /// For players this is a combo string containing the character name, account name and subgroup.
31    pub name: Vec<String>,
32
33    /// Profession for player agents
34    pub profession: u32,
35
36    /// Elite specialization for player agents.
37    pub is_elite: u32,
38
39    /// Hitbox width of the agent.
40    pub hitbox_width: u16,
41
42    /// Hitbox height of the agent.
43    pub hitbox_height: u16,
44
45    /// Normalized Toughness attribute of the agent.
46    pub toughness: u16,
47
48    /// Normalized Concentration attribute of the agent.
49    pub concentration: u16,
50
51    /// Normalized Healing attribute of the agent.
52    pub healing: u16,
53
54    /// Normalized Condition Damage attribute of the agent.
55    pub condition: u16,
56}
57
58impl Agent {
59    /// Size of the name combo string.
60    pub const NAME_SIZE: usize = 64;
61
62    /// Determines the kind of agent.
63    #[inline]
64    pub const fn kind(&self) -> AgentKind {
65        AgentKind::new(self.profession, self.is_elite)
66    }
67
68    /// Parses name information from the input.
69    fn parse_name(input: &mut impl io::Read) -> Result<Vec<String>, ParseError> {
70        let string = read_string_buffer::<{ Self::NAME_SIZE }>(input)?;
71        Ok(string
72            .split('\0')
73            .filter(|part| !part.is_empty())
74            .map(|part| part.to_string())
75            .collect())
76    }
77
78    /// Saves name information to the output.
79    fn save_name(&self, output: &mut impl io::Write) -> Result<(), io::Error> {
80        let string = self.name.join("\0");
81        write_string_buffer::<{ Self::NAME_SIZE }>(output, &string)
82    }
83}
84
85impl Parse for Agent {
86    type Error = ParseError;
87
88    fn parse(input: &mut impl io::Read) -> Result<Self, Self::Error> {
89        let id = input.read_u64::<Endian>()?;
90        let profession = input.read_u32::<Endian>()?;
91        let is_elite = input.read_u32::<Endian>()?;
92        let toughness = input.read_u16::<Endian>()?;
93        let concentration = input.read_u16::<Endian>()?;
94        let healing = input.read_u16::<Endian>()?;
95        let hitbox_width = input.read_u16::<Endian>()?;
96        let condition = input.read_u16::<Endian>()?;
97        let hitbox_height = input.read_u16::<Endian>()?;
98
99        let name = Self::parse_name(input)?;
100
101        // padding added by c
102        input.read_u32::<Endian>()?;
103
104        Ok(Self {
105            name,
106            id,
107            profession,
108            is_elite,
109            hitbox_width,
110            hitbox_height,
111            toughness,
112            concentration,
113            healing,
114            condition,
115        })
116    }
117}
118
119impl Save for Agent {
120    type Error = io::Error;
121
122    fn save(&self, output: &mut impl io::Write) -> Result<(), Self::Error> {
123        output.write_u64::<Endian>(self.id)?;
124        output.write_u32::<Endian>(self.profession)?;
125        output.write_u32::<Endian>(self.is_elite)?;
126        output.write_u16::<Endian>(self.toughness)?;
127        output.write_u16::<Endian>(self.concentration)?;
128        output.write_u16::<Endian>(self.healing)?;
129        output.write_u16::<Endian>(self.hitbox_width)?;
130        output.write_u16::<Endian>(self.condition)?;
131        output.write_u16::<Endian>(self.hitbox_height)?;
132
133        self.save_name(output)?;
134
135        // padding added by c
136        output.write_u32::<Endian>(0)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn agent_name() {
146        let name: Vec<String> = vec!["Character".into(), ":Account.1234".into(), "1".into()];
147        let data: &[u8; Agent::NAME_SIZE] = b"Character\0:Account.1234\x001\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
148
149        let parsed = Agent::parse_name(io::Cursor::new(data.as_slice()).get_mut())
150            .expect("failed to parse agent name");
151        assert_eq!(name, parsed, "incorrect agent name");
152
153        let agent = Agent {
154            id: 0,
155            name,
156            profession: 0,
157            is_elite: 0,
158            hitbox_width: 0,
159            hitbox_height: 0,
160            toughness: 0,
161            concentration: 0,
162            healing: 0,
163            condition: 0,
164        };
165
166        let mut buffer = [123u8; Agent::NAME_SIZE];
167        agent
168            .save_name(&mut buffer.as_mut_slice())
169            .expect("failed to save agent");
170        assert_eq!(data, &buffer, "incorrect saved data");
171    }
172}