aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/agent.rs732
-rw-r--r--src/lib.rs739
-rw-r--r--src/processing.rs20
3 files changed, 750 insertions, 741 deletions
diff --git a/src/agent.rs b/src/agent.rs
new file mode 100644
index 0000000..11f81d1
--- /dev/null
+++ b/src/agent.rs
@@ -0,0 +1,732 @@
+use std::convert::TryFrom;
+use std::marker::PhantomData;
+
+use getset::{CopyGetters, Getters, Setters};
+use num_traits::FromPrimitive;
+
+use super::{
+ gamedata::{EliteSpec, Profession},
+ raw, EvtcError,
+};
+
+/// Player-specific agent data.
+///
+/// Player agents are characters controlled by a player and as such, they contain data about the
+/// account and character used (name, profession), as well as the squad composition.
+///
+/// Note that a `Player` is only the player character itself. Any additional entities that are
+/// spawned by the player (clones, illusions, banners, ...) are either a [`Character`][Character]
+/// or a [`Gadget`][Gadget].
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
+pub struct Player {
+ /// The player's profession.
+ #[get_copy = "pub"]
+ profession: Profession,
+
+ /// The player's elite specialization, if any is equipped.
+ #[get_copy = "pub"]
+ elite: Option<EliteSpec>,
+
+ character_name: String,
+
+ account_name: String,
+
+ /// The subgroup the player was in.
+ #[get_copy = "pub"]
+ subgroup: u8,
+}
+
+impl Player {
+ /// The player's character name.
+ pub fn character_name(&self) -> &str {
+ &self.character_name
+ }
+
+ /// The player's account name.
+ ///
+ /// This includes the leading colon and the 4-digit denominator.
+ pub fn account_name(&self) -> &str {
+ &self.account_name
+ }
+}
+
+/// Gadget-specific agent data.
+///
+/// Gadgets are entities that are spawned by certain skills. They are mostly inanimate objects that
+/// only exist to achieve a certain skill effect.
+///
+/// Examples of this include the [banners](https://wiki.guildwars2.com/wiki/Banner) spawned by
+/// Warriors, but also skill effects like the roots created by
+/// [Entangle](https://wiki.guildwars2.com/wiki/Entangle) or the other objects in the arena.
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
+pub struct Gadget {
+ /// The id of the gadget.
+ ///
+ /// Note that gadgets do not have true ids and the id is generated "through a combination of
+ /// gadget parameters".
+ #[get_copy = "pub"]
+ id: u16,
+ name: String,
+}
+
+impl Gadget {
+ /// The name of the gadget.
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+}
+
+/// Character-specific agent data.
+///
+/// Characters are NPCs such as the bosses themselves, additional mobs that they spawn, but also
+/// friendly characters like Mesmer's clones and illusions, Necromancer minions, and so on.
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
+pub struct Character {
+ /// The id of the character.
+ #[get_copy = "pub"]
+ id: u16,
+ name: String,
+}
+
+impl Character {
+ /// The name of the character.
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+}
+
+/// The type of an agent.
+///
+/// arcdps differentiates between three types of agents: [`Player`][Player],
+/// [`Character`][Character] and [`Gadget`][Gadget]. This enum unifies handling between them by
+/// allowing you to pattern match or use one of the accessor methods.
+///
+/// The main way to obtain a `AgentKind` is by using the [`.kind()`][Agent::kind] method on an
+/// [`Agent`][Agent]. In cases where you already have a [`raw::Agent`][raw::Agent] available, you
+/// can also use the [`TryFrom`][TryFrom]/[`TryInto`][std::convert::TryInto] traits to convert a
+/// `raw::Agent` or `&raw::Agent` to a `AgentKind`:
+///
+/// ```no_run
+/// # use evtclib::{AgentKind, raw};
+/// use std::convert::TryInto;
+/// // Get a raw::Agent from somewhere
+/// let raw_agent: raw::Agent = panic!();
+/// // Convert it
+/// let agent: AgentKind = raw_agent.try_into().unwrap();
+/// ```
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub enum AgentKind {
+ /// The agent is a player.
+ ///
+ /// The player-specific data is in the included [`Player`][Player] struct.
+ Player(Player),
+ /// The agent is a gadget.
+ ///
+ /// The gadget-specific data is in the included [`Gadget`][Gadget] struct.
+ Gadget(Gadget),
+ /// The agent is a character.
+ ///
+ /// The character-specific data is in the included [`Character`][Character] struct.
+ Character(Character),
+}
+
+impl AgentKind {
+ fn from_raw_character(raw_agent: &raw::Agent) -> Result<Character, EvtcError> {
+ assert!(raw_agent.is_character());
+ let name = raw::cstr_up_to_nul(&raw_agent.name).ok_or(EvtcError::InvalidData)?;
+ Ok(Character {
+ id: raw_agent.prof as u16,
+ name: name.to_str()?.to_owned(),
+ })
+ }
+
+ fn from_raw_gadget(raw_agent: &raw::Agent) -> Result<Gadget, EvtcError> {
+ assert!(raw_agent.is_gadget());
+ let name = raw::cstr_up_to_nul(&raw_agent.name).ok_or(EvtcError::InvalidData)?;
+ Ok(Gadget {
+ id: raw_agent.prof as u16,
+ name: name.to_str()?.to_owned(),
+ })
+ }
+
+ fn from_raw_player(raw_agent: &raw::Agent) -> Result<Player, EvtcError> {
+ assert!(raw_agent.is_player());
+ let character_name = raw::cstr_up_to_nul(&raw_agent.name)
+ .ok_or(EvtcError::InvalidData)?
+ .to_str()?;
+ let account_name = raw::cstr_up_to_nul(&raw_agent.name[character_name.len() + 1..])
+ .ok_or(EvtcError::InvalidData)?
+ .to_str()?;
+ let subgroup = raw_agent.name[character_name.len() + account_name.len() + 2] - b'0';
+ let elite = if raw_agent.is_elite == 0 {
+ None
+ } else {
+ Some(
+ EliteSpec::from_u32(raw_agent.is_elite)
+ .ok_or(EvtcError::InvalidEliteSpec(raw_agent.is_elite))?,
+ )
+ };
+ Ok(Player {
+ profession: Profession::from_u32(raw_agent.prof)
+ .ok_or(EvtcError::InvalidProfession(raw_agent.prof))?,
+ elite,
+ character_name: character_name.to_owned(),
+ account_name: account_name.to_owned(),
+ subgroup,
+ })
+ }
+
+ /// Accesses the inner [`Player`][Player] struct, if available.
+ pub fn as_player(&self) -> Option<&Player> {
+ if let AgentKind::Player(ref player) = *self {
+ Some(player)
+ } else {
+ None
+ }
+ }
+
+ /// Determines whether this `AgentKind` contains a player.
+ pub fn is_player(&self) -> bool {
+ self.as_player().is_some()
+ }
+
+ /// Accesses the inner [`Gadget`][Gadget] struct, if available.
+ pub fn as_gadget(&self) -> Option<&Gadget> {
+ if let AgentKind::Gadget(ref gadget) = *self {
+ Some(gadget)
+ } else {
+ None
+ }
+ }
+
+ /// Determines whether this `AgentKind` contains a gadget.
+ pub fn is_gadget(&self) -> bool {
+ self.as_gadget().is_some()
+ }
+
+ /// Accesses the inner [`Character`][Character] struct, if available.
+ pub fn as_character(&self) -> Option<&Character> {
+ if let AgentKind::Character(ref character) = *self {
+ Some(character)
+ } else {
+ None
+ }
+ }
+
+ /// Determines whether this `AgentKind` contains a character.
+ pub fn is_character(&self) -> bool {
+ self.as_character().is_some()
+ }
+}
+
+impl TryFrom<raw::Agent> for AgentKind {
+ type Error = EvtcError;
+ /// Convenience method to avoid manual borrowing.
+ ///
+ /// Note that this conversion will consume the agent, so if you plan on re-using it, use the
+ /// `TryFrom<&raw::Agent>` implementation that works with a reference.
+ fn try_from(raw_agent: raw::Agent) -> Result<Self, Self::Error> {
+ Self::try_from(&raw_agent)
+ }
+}
+
+impl TryFrom<&raw::Agent> for AgentKind {
+ type Error = EvtcError;
+
+ /// Extract the correct `AgentKind` from the given [raw agent][raw::Agent].
+ ///
+ /// This automatically discerns between player, gadget and characters.
+ ///
+ /// Note that in most cases, you probably want to use `Agent::try_from` or even
+ /// [`process`][process] instead of this function.
+ fn try_from(raw_agent: &raw::Agent) -> Result<Self, Self::Error> {
+ if raw_agent.is_character() {
+ Ok(AgentKind::Character(AgentKind::from_raw_character(
+ raw_agent,
+ )?))
+ } else if raw_agent.is_gadget() {
+ Ok(AgentKind::Gadget(AgentKind::from_raw_gadget(raw_agent)?))
+ } else if raw_agent.is_player() {
+ Ok(AgentKind::Player(AgentKind::from_raw_player(raw_agent)?))
+ } else {
+ Err(EvtcError::InvalidData)
+ }
+ }
+}
+
+/// An agent.
+///
+/// Agents in arcdps are very versatile, as a lot of things end up being an "agent". This includes:
+/// * Players
+/// * Bosses
+/// * Any additional mobs that spawn
+/// * Mesmer illusions
+/// * Ranger spirits, pets
+/// * Guardian spirit weapons
+/// * ...
+///
+/// Generally, you can divide them into three kinds ([`AgentKind`][AgentKind]):
+/// * [`Player`][Player]: All players themselves.
+/// * [`Character`][Character]: Non-player mobs, including most bosses, "adds" and player-generated
+/// characters.
+/// * [`Gadget`][Gadget]: Some additional gadgets, such as ley rifts, continuum split, ...
+///
+/// All of these agents share some common fields, which are the ones accessible in `Agent<Kind>`.
+/// The kind can be retrieved using [`.kind()`][Agent::kind], which can be matched on.
+///
+/// # Obtaining an agent
+///
+/// The normal way to obtain the agents is to use the [`.agents()`](Log::agents) method on a
+/// [`Log`][Log], or one of the other accessor methods (like [`.players()`][Log::players] or
+/// [`.agent_by_addr()`][Log::agent_by_addr]).
+///
+/// In the cases where you already have a [`raw::Agent`][raw::Agent] available, you can also
+/// convert it to an [`Agent`][Agent] by using the standard
+/// [`TryFrom`][TryFrom]/[`TryInto`][std::convert::TryInto] traits:
+///
+/// ```no_run
+/// # use evtclib::{Agent, raw};
+/// use std::convert::TryInto;
+/// let raw_agent: raw::Agent = panic!();
+/// let agent: Agent = raw_agent.try_into().unwrap();
+/// ```
+///
+/// Note that you can convert references as well, so if you plan on re-using the raw agent
+/// afterwards, you should opt for `Agent::try_from(&raw_agent)` instead.
+///
+/// # The `Kind` parameter
+///
+/// The type parameter is not actually used and only exists at the type level. It can be used to
+/// tag `Agent`s containing a known kind. For example, `Agent<Player>` implements
+/// [`.player()`][Agent::player], which returns a `&Player` directly (instead of a
+/// `Option<&Player>`). This works because such tagged `Agent`s can only be constructed (safely)
+/// using [`.as_player()`][Agent::as_player], [`.as_gadget()`][Agent::as_gadget] or
+/// [`.as_character()`][Agent::as_character]. This is useful since functions like
+/// [`Log::players`][Log::players], which already filter only players, don't require the consumer
+/// to do another check/pattern match for the right agent kind.
+///
+/// The unit type `()` is used to tag `Agent`s which contain an undetermined type, and it is the
+/// default if you write `Agent` without any parameters.
+///
+/// The downside is that methods which work on `Agent`s theoretically should be generic over
+/// `Kind`. An escape hatch is the method [`.erase()`][Agent::erase], which erases the kind
+/// information and produces the default `Agent<()>`. Functions/methods that only take `Agent<()>`
+/// can therefore be used by any other agent as well.
+#[cfg_attr(feature = "serde", derive(serde::Serialize))]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Getters, CopyGetters, Setters)]
+// For the reasoning of #[repr(C)] see Agent::transmute.
+#[repr(C)]
+pub struct Agent<Kind = ()> {
+ /// The address of this agent.
+ ///
+ /// This is not actually the address of the in-memory Rust object, but rather a serialization
+ /// detail of arcdps. You should consider this as an opaque number and only compare it to other
+ /// agent addresses.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ addr: u64,
+
+ /// The kind of this agent.
+ #[getset(get = "pub", set = "pub(crate)")]
+ kind: AgentKind,
+
+ /// The toughness of this agent.
+ ///
+ /// This is not an absolute number, but a relative indicator that indicates this agent's
+ /// toughness relative to the other people in the squad.
+ ///
+ /// 0 means lowest toughness, 10 means highest toughness.
+ #[get_copy = "pub"]
+ toughness: i16,
+
+ /// The concentration of this agent.
+ ///
+ /// This is not an absolute number, but a relative indicator that indicates this agent's
+ /// concentration relative to the other people in the squad.
+ ///
+ /// 0 means lowest concentration, 10 means highest concentration.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ concentration: i16,
+
+ /// The healing power of this agent.
+ ///
+ /// This is not an absolute number, but a relative indicator that indicates this agent's
+ /// healing power relative to the other people in the squad.
+ ///
+ /// 0 means lowest healing power, 10 means highest healing power.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ healing: i16,
+
+ /// The condition damage of this agent.
+ ///
+ /// This is not an absolute number, but a relative indicator that indicates this agent's
+ /// condition damage relative to the other people in the squad.
+ ///
+ /// 0 means lowest condition damage, 10 means highest condition damage.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ condition: i16,
+
+ /// The instance ID of this agent.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ instance_id: u16,
+
+ /// The timestamp of the first event entry with this agent.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ first_aware: u64,
+
+ /// The timestamp of the last event entry with this agent.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ last_aware: u64,
+
+ /// The master agent's address.
+ #[getset(get_copy = "pub", set = "pub(crate)")]
+ master_agent: Option<u64>,
+
+ #[cfg_attr(feature = "serde", serde(skip_serializing))]
+ phantom_data: PhantomData<Kind>,
+}
+
+// We could derive this, however that would derive Deserialize generically for Agent<T>, where T is
+// deserializable. In particular, this would mean that you could deserialize Agent<Character>,
+// Agent<Player> and Agent<Gadget> directly, which would not be too bad - the problem is that serde
+// has no way of knowing if the serialized agent actually is a character/player/gadget agent, as
+// that information only exists on the type level. This meant that you could "coerce" agents into
+// the wrong type, safely, using a serialization followed by a deserialization.
+// Now this doesn't actually lead to memory unsafety or other bad behaviour, but it could mean that
+// the program would panic if you tried to access a non-existing field, e.g. by calling
+// Agent<Character>::id() on a non-character agent.
+// In order to prevent this, we manually implement Deserialize only for Agent<()>, so that the
+// usual conversion functions with the proper checks have to be used.
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for Agent {
+ fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ use serde::de::{self, MapAccess, Visitor};
+ use std::fmt;
+
+ #[derive(serde::Deserialize)]
+ #[serde(field_identifier, rename_all = "snake_case")]
+ enum Field {
+ Addr,
+ Kind,
+ Toughness,
+ Concentration,
+ Healing,
+ Condition,
+ InstanceId,
+ FirstAware,
+ LastAware,
+ MasterAgent,
+ };
+
+ struct AgentVisitor;
+
+ impl<'de> Visitor<'de> for AgentVisitor {
+ type Value = Agent;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("struct Agent")
+ }
+
+ fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Agent, V::Error> {
+ let mut addr = None;
+ let mut kind = None;
+ let mut toughness = None;
+ let mut concentration = None;
+ let mut healing = None;
+ let mut condition = None;
+ let mut instance_id = None;
+ let mut first_aware = None;
+ let mut last_aware = None;
+ let mut master_agent = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Addr => {
+ if addr.is_some() {
+ return Err(de::Error::duplicate_field("addr"));
+ }
+ addr = Some(map.next_value()?);
+ }
+ Field::Kind => {
+ if kind.is_some() {
+ return Err(de::Error::duplicate_field("kind"));
+ }
+ kind = Some(map.next_value()?);
+ }
+ Field::Toughness => {
+ if toughness.is_some() {
+ return Err(de::Error::duplicate_field("toughness"));
+ }
+ toughness = Some(map.next_value()?);
+ }
+ Field::Concentration => {
+ if concentration.is_some() {
+ return Err(de::Error::duplicate_field("concentration"));
+ }
+ concentration = Some(map.next_value()?);
+ }
+ Field::Healing => {
+ if healing.is_some() {
+ return Err(de::Error::duplicate_field("healing"));
+ }
+ healing = Some(map.next_value()?);
+ }
+ Field::Condition => {
+ if condition.is_some() {
+ return Err(de::Error::duplicate_field("condition"));
+ }
+ condition = Some(map.next_value()?);
+ }
+ Field::InstanceId => {
+ if instance_id.is_some() {
+ return Err(de::Error::duplicate_field("instance_id"));
+ }
+ instance_id = Some(map.next_value()?);
+ }
+ Field::FirstAware => {
+ if first_aware.is_some() {
+ return Err(de::Error::duplicate_field("first_aware"));
+ }
+ first_aware = Some(map.next_value()?);
+ }
+ Field::LastAware => {
+ if last_aware.is_some() {
+ return Err(de::Error::duplicate_field("last_aware"));
+ }
+ last_aware = Some(map.next_value()?);
+ }
+ Field::MasterAgent => {
+ if master_agent.is_some() {
+ return Err(de::Error::duplicate_field("master_agent"));
+ }
+ master_agent = Some(map.next_value()?);
+ }
+ }
+ }
+
+ Ok(Agent {
+ addr: addr.ok_or_else(|| de::Error::missing_field("addr"))?,
+ kind: kind.ok_or_else(|| de::Error::missing_field("kind"))?,
+ toughness: toughness.ok_or_else(|| de::Error::missing_field("toughness"))?,
+ concentration: concentration
+ .ok_or_else(|| de::Error::missing_field("concentration"))?,
+ healing: healing.ok_or_else(|| de::Error::missing_field("healing"))?,
+ condition: condition.ok_or_else(|| de::Error::missing_field("condition"))?,
+ instance_id: instance_id
+ .ok_or_else(|| de::Error::missing_field("instance_id"))?,
+ first_aware: first_aware
+ .ok_or_else(|| de::Error::missing_field("first_aware"))?,
+ last_aware: last_aware.ok_or_else(|| de::Error::missing_field("last_aware"))?,
+ master_agent: master_agent
+ .ok_or_else(|| de::Error::missing_field("master_agent"))?,
+ phantom_data: PhantomData,
+ })
+ }
+ };
+
+ const FIELDS: &'static [&'static str] = &[
+ "addr",
+ "kind",
+ "toughness",
+ "concentration",
+ "healing",
+ "condition",
+ "instance_id",
+ "first_aware",
+ "last_aware",
+ "master_agent",
+ ];
+ deserializer.deserialize_struct("Agent", FIELDS, AgentVisitor)
+ }
+}
+
+impl TryFrom<&raw::Agent> for Agent {
+ type Error = EvtcError;
+
+ /// Parse a raw agent.
+ fn try_from(raw_agent: &raw::Agent) -> Result<Self, Self::Error> {
+ let kind = AgentKind::try_from(raw_agent)?;
+ Ok(Agent {
+ addr: raw_agent.addr,
+ kind,
+ toughness: raw_agent.toughness,
+ concentration: raw_agent.concentration,
+ healing: raw_agent.healing,
+ condition: raw_agent.condition,
+ instance_id: 0,
+ first_aware: 0,
+ last_aware: u64::max_value(),
+ master_agent: None,
+ phantom_data: PhantomData,
+ })
+ }
+}
+
+impl TryFrom<raw::Agent> for Agent {
+ type Error = EvtcError;
+
+ /// Convenience method to avoid manual borrowing.
+ ///
+ /// Note that this conversion will consume the agent, so if you plan on re-using it, use the
+ /// `TryFrom<&raw::Agent>` implementation that works with a reference.
+ fn try_from(raw_agent: raw::Agent) -> Result<Self, Self::Error> {
+ Agent::try_from(&raw_agent)
+ }
+}
+
+impl<Kind> Agent<Kind> {
+ /// Unconditionally change the tagged type.
+ #[inline]
+ fn transmute<T>(&self) -> &Agent<T> {
+ // Beware, unsafe code ahead!
+ //
+ // What are we doing here?
+ // In Agent<T>, T is a marker type that only exists at the type level. There is no actual
+ // value of type T being held, instead, we use PhantomData under the hood. This is so we
+ // can implement special methods on Agent<Player>, Agent<Gadget> and Agent<Character>,
+ // which allows us in some cases to avoid the "second check" (e.g. Log::players() can
+ // return Agent<Player>, as the function already makes sure all returned agents are
+ // players). This makes the interface more ergonomical, as we can prove to the type checker
+ // at compile time that a given Agent has a certain AgentKind.
+ //
+ // Why is this safe?
+ // PhantomData<T> (which is what Agent<T> boils down to) is a zero-sized type, which means
+ // it does not actually change the layout of the struct. There is some discussion in [1],
+ // which suggests that this is true for #[repr(C)] structs (which Agent is). We can
+ // therefore safely transmute from Agent<U> to Agent<T>, for any U and T.
+ //
+ // Can this lead to unsafety?
+ // No, the actual data access is still done through safe rust and a if-let. In the worst
+ // case it can lead to an unexpected panic, but the "guarantee" made by T is rather weak in
+ // that regard.
+ //
+ // What are the alternatives?
+ // None, as far as I'm aware. Going from Agent<U> to Agent<T> is possible in safe Rust by
+ // destructuring the struct, or alternatively by [2] (if it would be implemented). However,
+ // when dealing with references, there seems to be no way to safely go from Agent<U> to
+ // Agent<T>, even if they share the same layout.
+ //
+ // [1]: https://www.reddit.com/r/rust/comments/avrbvc/is_it_safe_to_transmute_foox_to_fooy_if_the/
+ // [2]: https://github.com/rust-lang/rfcs/pull/2528
+ unsafe { &*(self as *const Agent<Kind> as *const Agent<T>) }
+ }
+
+ /// Erase any extra information about the contained agent kind.
+ #[inline]
+ pub fn erase(&self) -> &Agent {
+ self.transmute()
+ }
+
+ /// Try to convert this `Agent` to an `Agent` representing a `Player`.
+ #[inline]
+ pub fn as_player(&self) -> Option<&Agent<Player>> {
+ if self.kind.is_player() {
+ Some(self.transmute())
+ } else {
+ None
+ }
+ }
+
+ /// Try to convert this `Agent` to an `Agent` representing a `Gadget`.
+ #[inline]
+ pub fn as_gadget(&self) -> Option<&Agent<Gadget>> {
+ if self.kind.is_gadget() {
+ Some(self.transmute())
+ } else {
+ None
+ }
+ }
+
+ /// Try to convert this `Agent` to an `Agent` representing a `Character`.
+ #[inline]
+ pub fn as_character(&self) -> Option<&Agent<Character>> {
+ if self.kind.is_character() {
+ Some(self.transmute())
+ } else {
+ None
+ }
+ }
+}
+
+impl Agent<Player> {
+ /// Directly access the underlying player data.
+ #[inline]
+ pub fn player(&self) -> &Player {
+ self.kind.as_player().expect("Agent<Player> had no player!")
+ }
+
+ /// Shorthand to get the player's account name.
+ #[inline]
+ pub fn account_name(&self) -> &str {
+ self.player().account_name()
+ }
+
+ /// Shorthand to get the player's character name.
+ #[inline]
+ pub fn character_name(&self) -> &str {
+ self.player().character_name()
+ }
+
+ /// Shorthand to get the player's elite specialization.
+ #[inline]
+ pub fn elite(&self) -> Option<EliteSpec> {
+ self.player().elite()
+ }
+
+ /// Shorthand to get the player's profession.
+ #[inline]
+ pub fn profession(&self) -> Profession {
+ self.player().profession()
+ }
+
+ /// Shorthand to get the player's subgroup.
+ #[inline]
+ pub fn subgroup(&self) -> u8 {
+ self.player().subgroup()
+ }
+}
+
+impl Agent<Gadget> {
+ /// Directly access the underlying gadget data.
+ #[inline]
+ pub fn gadget(&self) -> &Gadget {
+ self.kind.as_gadget().expect("Agent<Gadget> had no gadget!")
+ }
+
+ /// Shorthand to get the gadget's id.
+ #[inline]
+ pub fn id(&self) -> u16 {
+ self.gadget().id()
+ }
+
+ /// Shorthand to get the gadget's name.
+ #[inline]
+ pub fn name(&self) -> &str {
+ self.gadget().name()
+ }
+}
+
+impl Agent<Character> {
+ /// Directly access the underlying character data.
+ #[inline]
+ pub fn character(&self) -> &Character {
+ self.kind
+ .as_character()
+ .expect("Agent<Character> had no character!")
+ }
+
+ /// Shorthand to get the character's id.
+ #[inline]
+ pub fn id(&self) -> u16 {
+ self.character().id()
+ }
+
+ /// Shorthand to get the character's name.
+ #[inline]
+ pub fn name(&self) -> &str {
+ self.character().name()
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 5ba6340..bb71c77 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -88,15 +88,14 @@
//! While there are legitimate use cases for writing/modification support, they are currently not
//! implemented (but might be in a future version).
-use std::convert::TryFrom;
-use std::marker::PhantomData;
-
-use getset::{CopyGetters, Getters};
use num_traits::FromPrimitive;
use thiserror::Error;
pub mod raw;
+mod agent;
+pub use agent::{Agent, AgentKind, Character, Gadget, Player};
+
pub mod event;
pub use event::{Event, EventKind};
@@ -137,728 +136,6 @@ pub enum EvtcError {
Utf8Error(#[from] std::str::Utf8Error),
}
-/// Player-specific agent data.
-///
-/// Player agents are characters controlled by a player and as such, they contain data about the
-/// account and character used (name, profession), as well as the squad composition.
-///
-/// Note that a `Player` is only the player character itself. Any additional entities that are
-/// spawned by the player (clones, illusions, banners, ...) are either a [`Character`][Character]
-/// or a [`Gadget`][Gadget].
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
-pub struct Player {
- /// The player's profession.
- #[get_copy = "pub"]
- profession: Profession,
-
- /// The player's elite specialization, if any is equipped.
- #[get_copy = "pub"]
- elite: Option<EliteSpec>,
-
- character_name: String,
-
- account_name: String,
-
- /// The subgroup the player was in.
- #[get_copy = "pub"]
- subgroup: u8,
-}
-
-impl Player {
- /// The player's character name.
- pub fn character_name(&self) -> &str {
- &self.character_name
- }
-
- /// The player's account name.
- ///
- /// This includes the leading colon and the 4-digit denominator.
- pub fn account_name(&self) -> &str {
- &self.account_name
- }
-}
-
-/// Gadget-specific agent data.
-///
-/// Gadgets are entities that are spawned by certain skills. They are mostly inanimate objects that
-/// only exist to achieve a certain skill effect.
-///
-/// Examples of this include the [banners](https://wiki.guildwars2.com/wiki/Banner) spawned by
-/// Warriors, but also skill effects like the roots created by
-/// [Entangle](https://wiki.guildwars2.com/wiki/Entangle) or the other objects in the arena.
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
-pub struct Gadget {
- /// The id of the gadget.
- ///
- /// Note that gadgets do not have true ids and the id is generated "through a combination of
- /// gadget parameters".
- #[get_copy = "pub"]
- id: u16,
- name: String,
-}
-
-impl Gadget {
- /// The name of the gadget.
- pub fn name(&self) -> &str {
- &self.name
- }
-}
-
-/// Character-specific agent data.
-///
-/// Characters are NPCs such as the bosses themselves, additional mobs that they spawn, but also
-/// friendly characters like Mesmer's clones and illusions, Necromancer minions, and so on.
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
-pub struct Character {
- /// The id of the character.
- #[get_copy = "pub"]
- id: u16,
- name: String,
-}
-
-impl Character {
- /// The name of the character.
- pub fn name(&self) -> &str {
- &self.name
- }
-}
-
-/// The type of an agent.
-///
-/// arcdps differentiates between three types of agents: [`Player`][Player],
-/// [`Character`][Character] and [`Gadget`][Gadget]. This enum unifies handling between them by
-/// allowing you to pattern match or use one of the accessor methods.
-///
-/// The main way to obtain a `AgentKind` is by using the [`.kind()`][Agent::kind] method on an
-/// [`Agent`][Agent]. In cases where you already have a [`raw::Agent`][raw::Agent] available, you
-/// can also use the [`TryFrom`][TryFrom]/[`TryInto`][std::convert::TryInto] traits to convert a
-/// `raw::Agent` or `&raw::Agent` to a `AgentKind`:
-///
-/// ```no_run
-/// # use evtclib::{AgentKind, raw};
-/// use std::convert::TryInto;
-/// // Get a raw::Agent from somewhere
-/// let raw_agent: raw::Agent = panic!();
-/// // Convert it
-/// let agent: AgentKind = raw_agent.try_into().unwrap();
-/// ```
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone, Hash, PartialEq, Eq)]
-pub enum AgentKind {
- /// The agent is a player.
- ///
- /// The player-specific data is in the included [`Player`][Player] struct.
- Player(Player),
- /// The agent is a gadget.
- ///
- /// The gadget-specific data is in the included [`Gadget`][Gadget] struct.
- Gadget(Gadget),
- /// The agent is a character.
- ///
- /// The character-specific data is in the included [`Character`][Character] struct.
- Character(Character),
-}
-
-impl AgentKind {
- fn from_raw_character(raw_agent: &raw::Agent) -> Result<Character, EvtcError> {
- assert!(raw_agent.is_character());
- let name = raw::cstr_up_to_nul(&raw_agent.name).ok_or(EvtcError::InvalidData)?;
- Ok(Character {
- id: raw_agent.prof as u16,
- name: name.to_str()?.to_owned(),
- })
- }
-
- fn from_raw_gadget(raw_agent: &raw::Agent) -> Result<Gadget, EvtcError> {
- assert!(raw_agent.is_gadget());
- let name = raw::cstr_up_to_nul(&raw_agent.name).ok_or(EvtcError::InvalidData)?;
- Ok(Gadget {
- id: raw_agent.prof as u16,
- name: name.to_str()?.to_owned(),
- })
- }
-
- fn from_raw_player(raw_agent: &raw::Agent) -> Result<Player, EvtcError> {
- assert!(raw_agent.is_player());
- let character_name = raw::cstr_up_to_nul(&raw_agent.name)
- .ok_or(EvtcError::InvalidData)?
- .to_str()?;
- let account_name = raw::cstr_up_to_nul(&raw_agent.name[character_name.len() + 1..])
- .ok_or(EvtcError::InvalidData)?
- .to_str()?;
- let subgroup = raw_agent.name[character_name.len() + account_name.len() + 2] - b'0';
- let elite = if raw_agent.is_elite == 0 {
- None
- } else {
- Some(
- EliteSpec::from_u32(raw_agent.is_elite)
- .ok_or(EvtcError::InvalidEliteSpec(raw_agent.is_elite))?,
- )
- };
- Ok(Player {
- profession: Profession::from_u32(raw_agent.prof)
- .ok_or(EvtcError::InvalidProfession(raw_agent.prof))?,
- elite,
- character_name: character_name.to_owned(),
- account_name: account_name.to_owned(),
- subgroup,
- })
- }
-
- /// Accesses the inner [`Player`][Player] struct, if available.
- pub fn as_player(&self) -> Option<&Player> {
- if let AgentKind::Player(ref player) = *self {
- Some(player)
- } else {
- None
- }
- }
-
- /// Determines whether this `AgentKind` contains a player.
- pub fn is_player(&self) -> bool {
- self.as_player().is_some()
- }
-
- /// Accesses the inner [`Gadget`][Gadget] struct, if available.
- pub fn as_gadget(&self) -> Option<&Gadget> {
- if let AgentKind::Gadget(ref gadget) = *self {
- Some(gadget)
- } else {
- None
- }
- }
-
- /// Determines whether this `AgentKind` contains a gadget.
- pub fn is_gadget(&self) -> bool {
- self.as_gadget().is_some()
- }
-
- /// Accesses the inner [`Character`][Character] struct, if available.
- pub fn as_character(&self) -> Option<&Character> {
- if let AgentKind::Character(ref character) = *self {
- Some(character)
- } else {
- None
- }
- }
-
- /// Determines whether this `AgentKind` contains a character.
- pub fn is_character(&self) -> bool {
- self.as_character().is_some()
- }
-}
-
-impl TryFrom<raw::Agent> for AgentKind {
- type Error = EvtcError;
- /// Convenience method to avoid manual borrowing.
- ///
- /// Note that this conversion will consume the agent, so if you plan on re-using it, use the
- /// `TryFrom<&raw::Agent>` implementation that works with a reference.
- fn try_from(raw_agent: raw::Agent) -> Result<Self, Self::Error> {
- Self::try_from(&raw_agent)
- }
-}
-
-impl TryFrom<&raw::Agent> for AgentKind {
- type Error = EvtcError;
-
- /// Extract the correct `AgentKind` from the given [raw agent][raw::Agent].
- ///
- /// This automatically discerns between player, gadget and characters.
- ///
- /// Note that in most cases, you probably want to use `Agent::try_from` or even
- /// [`process`][process] instead of this function.
- fn try_from(raw_agent: &raw::Agent) -> Result<Self, Self::Error> {
- if raw_agent.is_character() {
- Ok(AgentKind::Character(AgentKind::from_raw_character(
- raw_agent,
- )?))
- } else if raw_agent.is_gadget() {
- Ok(AgentKind::Gadget(AgentKind::from_raw_gadget(raw_agent)?))
- } else if raw_agent.is_player() {
- Ok(AgentKind::Player(AgentKind::from_raw_player(raw_agent)?))
- } else {
- Err(EvtcError::InvalidData)
- }
- }
-}
-
-/// An agent.
-///
-/// Agents in arcdps are very versatile, as a lot of things end up being an "agent". This includes:
-/// * Players
-/// * Bosses
-/// * Any additional mobs that spawn
-/// * Mesmer illusions
-/// * Ranger spirits, pets
-/// * Guardian spirit weapons
-/// * ...
-///
-/// Generally, you can divide them into three kinds ([`AgentKind`][AgentKind]):
-/// * [`Player`][Player]: All players themselves.
-/// * [`Character`][Character]: Non-player mobs, including most bosses, "adds" and player-generated
-/// characters.
-/// * [`Gadget`][Gadget]: Some additional gadgets, such as ley rifts, continuum split, ...
-///
-/// All of these agents share some common fields, which are the ones accessible in `Agent<Kind>`.
-/// The kind can be retrieved using [`.kind()`][Agent::kind], which can be matched on.
-///
-/// # Obtaining an agent
-///
-/// The normal way to obtain the agents is to use the [`.agents()`](Log::agents) method on a
-/// [`Log`][Log], or one of the other accessor methods (like [`.players()`][Log::players] or
-/// [`.agent_by_addr()`][Log::agent_by_addr]).
-///
-/// In the cases where you already have a [`raw::Agent`][raw::Agent] available, you can also
-/// convert it to an [`Agent`][Agent] by using the standard
-/// [`TryFrom`][TryFrom]/[`TryInto`][std::convert::TryInto] traits:
-///
-/// ```no_run
-/// # use evtclib::{Agent, raw};
-/// use std::convert::TryInto;
-/// let raw_agent: raw::Agent = panic!();
-/// let agent: Agent = raw_agent.try_into().unwrap();
-/// ```
-///
-/// Note that you can convert references as well, so if you plan on re-using the raw agent
-/// afterwards, you should opt for `Agent::try_from(&raw_agent)` instead.
-///
-/// # The `Kind` parameter
-///
-/// The type parameter is not actually used and only exists at the type level. It can be used to
-/// tag `Agent`s containing a known kind. For example, `Agent<Player>` implements
-/// [`.player()`][Agent::player], which returns a `&Player` directly (instead of a
-/// `Option<&Player>`). This works because such tagged `Agent`s can only be constructed (safely)
-/// using [`.as_player()`][Agent::as_player], [`.as_gadget()`][Agent::as_gadget] or
-/// [`.as_character()`][Agent::as_character]. This is useful since functions like
-/// [`Log::players`][Log::players], which already filter only players, don't require the consumer
-/// to do another check/pattern match for the right agent kind.
-///
-/// The unit type `()` is used to tag `Agent`s which contain an undetermined type, and it is the
-/// default if you write `Agent` without any parameters.
-///
-/// The downside is that methods which work on `Agent`s theoretically should be generic over
-/// `Kind`. An escape hatch is the method [`.erase()`][Agent::erase], which erases the kind
-/// information and produces the default `Agent<()>`. Functions/methods that only take `Agent<()>`
-/// can therefore be used by any other agent as well.
-#[cfg_attr(feature = "serde", derive(serde::Serialize))]
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Getters, CopyGetters)]
-// For the reasoning of #[repr(C)] see Agent::transmute.
-#[repr(C)]
-pub struct Agent<Kind = ()> {
- /// The address of this agent.
- ///
- /// This is not actually the address of the in-memory Rust object, but rather a serialization
- /// detail of arcdps. You should consider this as an opaque number and only compare it to other
- /// agent addresses.
- #[get_copy = "pub"]
- addr: u64,
-
- /// The kind of this agent.
- #[get = "pub"]
- kind: AgentKind,
-
- /// The toughness of this agent.
- ///
- /// This is not an absolute number, but a relative indicator that indicates this agent's
- /// toughness relative to the other people in the squad.
- ///
- /// 0 means lowest toughness, 10 means highest toughness.
- #[get_copy = "pub"]
- toughness: i16,
-
- /// The concentration of this agent.
- ///
- /// This is not an absolute number, but a relative indicator that indicates this agent's
- /// concentration relative to the other people in the squad.
- ///
- /// 0 means lowest concentration, 10 means highest concentration.
- #[get_copy = "pub"]
- concentration: i16,
-
- /// The healing power of this agent.
- ///
- /// This is not an absolute number, but a relative indicator that indicates this agent's
- /// healing power relative to the other people in the squad.
- ///
- /// 0 means lowest healing power, 10 means highest healing power.
- #[get_copy = "pub"]
- healing: i16,
-
- /// The condition damage of this agent.
- ///
- /// This is not an absolute number, but a relative indicator that indicates this agent's
- /// condition damage relative to the other people in the squad.
- ///
- /// 0 means lowest condition damage, 10 means highest condition damage.
- #[get_copy = "pub"]
- condition: i16,
-
- /// The instance ID of this agent.
- #[get_copy = "pub"]
- instance_id: u16,
-
- /// The timestamp of the first event entry with this agent.
- #[get_copy = "pub"]
- first_aware: u64,
-
- /// The timestamp of the last event entry with this agent.
- #[get_copy = "pub"]
- last_aware: u64,
-
- /// The master agent's address.
- #[get_copy = "pub"]
- master_agent: Option<u64>,
-
- #[cfg_attr(feature = "serde", serde(skip_serializing))]
- phantom_data: PhantomData<Kind>,
-}
-
-// We could derive this, however that would derive Deserialize generically for Agent<T>, where T is
-// deserializable. In particular, this would mean that you could deserialize Agent<Character>,
-// Agent<Player> and Agent<Gadget> directly, which would not be too bad - the problem is that serde
-// has no way of knowing if the serialized agent actually is a character/player/gadget agent, as
-// that information only exists on the type level. This meant that you could "coerce" agents into
-// the wrong type, safely, using a serialization followed by a deserialization.
-// Now this doesn't actually lead to memory unsafety or other bad behaviour, but it could mean that
-// the program would panic if you tried to access a non-existing field, e.g. by calling
-// Agent<Character>::id() on a non-character agent.
-// In order to prevent this, we manually implement Deserialize only for Agent<()>, so that the
-// usual conversion functions with the proper checks have to be used.
-#[cfg(feature = "serde")]
-impl<'de> serde::Deserialize<'de> for Agent {
- fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
- use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
- use std::fmt;
-
- #[derive(serde::Deserialize)]
- #[serde(field_identifier, rename_all = "snake_case")]
- enum Field {
- Addr,
- Kind,
- Toughness,
- Concentration,
- Healing,
- Condition,
- InstanceId,
- FirstAware,
- LastAware,
- MasterAgent,
- };
-
- struct AgentVisitor;
-
- impl<'de> Visitor<'de> for AgentVisitor {
- type Value = Agent;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("struct Agent")
- }
-
- fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Agent, V::Error> {
- let mut addr = None;
- let mut kind = None;
- let mut toughness = None;
- let mut concentration = None;
- let mut healing = None;
- let mut condition = None;
- let mut instance_id = None;
- let mut first_aware = None;
- let mut last_aware = None;
- let mut master_agent = None;
-
- while let Some(key) = map.next_key()? {
- match key {
- Field::Addr => {
- if addr.is_some() {
- return Err(de::Error::duplicate_field("addr"));
- }
- addr = Some(map.next_value()?);
- }
- Field::Kind => {
- if kind.is_some() {
- return Err(de::Error::duplicate_field("kind"));
- }
- kind = Some(map.next_value()?);
- }
- Field::Toughness => {
- if toughness.is_some() {
- return Err(de::Error::duplicate_field("toughness"));
- }
- toughness = Some(map.next_value()?);
- }
- Field::Concentration => {
- if concentration.is_some() {
- return Err(de::Error::duplicate_field("concentration"));
- }
- concentration = Some(map.next_value()?);
- }
- Field::Healing => {
- if healing.is_some() {
- return Err(de::Error::duplicate_field("healing"));
- }
- healing = Some(map.next_value()?);
- }
- Field::Condition => {
- if condition.is_some() {
- return Err(de::Error::duplicate_field("condition"));
- }
- condition = Some(map.next_value()?);
- }
- Field::InstanceId=> {
- if instance_id.is_some() {
- return Err(de::Error::duplicate_field("instance_id"));
- }
- instance_id = Some(map.next_value()?);
- }
- Field::FirstAware=> {
- if first_aware.is_some() {
- return Err(de::Error::duplicate_field("first_aware"));
- }
- first_aware = Some(map.next_value()?);
- }
- Field::LastAware => {
- if last_aware.is_some() {
- return Err(de::Error::duplicate_field("last_aware"));
- }
- last_aware = Some(map.next_value()?);
- }
- Field::MasterAgent => {
- if master_agent.is_some() {
- return Err(de::Error::duplicate_field("master_agent"));
- }
- master_agent = Some(map.next_value()?);
- }
- }
- }
-
- Ok(Agent {
- addr: addr.ok_or_else(|| de::Error::missing_field("addr"))?,
- kind: kind.ok_or_else(|| de::Error::missing_field("kind"))?,
- toughness: toughness.ok_or_else(|| de::Error::missing_field("toughness"))?,
- concentration: concentration
- .ok_or_else(|| de::Error::missing_field("concentration"))?,
- healing: healing.ok_or_else(|| de::Error::missing_field("healing"))?,
- condition: condition.ok_or_else(|| de::Error::missing_field("condition"))?,
- instance_id: instance_id
- .ok_or_else(|| de::Error::missing_field("instance_id"))?,
- first_aware: first_aware
- .ok_or_else(|| de::Error::missing_field("first_aware"))?,
- last_aware: last_aware.ok_or_else(|| de::Error::missing_field("last_aware"))?,
- master_agent: master_agent
- .ok_or_else(|| de::Error::missing_field("master_agent"))?,
- phantom_data: PhantomData,
- })
- }
- };
-
- const FIELDS: &'static [&'static str] = &[
- "addr",
- "kind",
- "toughness",
- "concentration",
- "healing",
- "condition",
- "instance_id",
- "first_aware",
- "last_aware",
- "master_agent",
- ];
- deserializer.deserialize_struct("Agent", FIELDS, AgentVisitor)
- }
-}
-
-impl TryFrom<&raw::Agent> for Agent {
- type Error = EvtcError;
-
- /// Parse a raw agent.
- fn try_from(raw_agent: &raw::Agent) -> Result<Self, Self::Error> {
- let kind = AgentKind::try_from(raw_agent)?;
- Ok(Agent {
- addr: raw_agent.addr,
- kind,
- toughness: raw_agent.toughness,
- concentration: raw_agent.concentration,
- healing: raw_agent.healing,
- condition: raw_agent.condition,
- instance_id: 0,
- first_aware: 0,
- last_aware: u64::max_value(),
- master_agent: None,
- phantom_data: PhantomData,
- })
- }
-}
-
-impl TryFrom<raw::Agent> for Agent {
- type Error = EvtcError;
-
- /// Convenience method to avoid manual borrowing.
- ///
- /// Note that this conversion will consume the agent, so if you plan on re-using it, use the
- /// `TryFrom<&raw::Agent>` implementation that works with a reference.
- fn try_from(raw_agent: raw::Agent) -> Result<Self, Self::Error> {
- Agent::try_from(&raw_agent)
- }
-}
-
-impl<Kind> Agent<Kind> {
- /// Unconditionally change the tagged type.
- #[inline]
- fn transmute<T>(&self) -> &Agent<T> {
- // Beware, unsafe code ahead!
- //
- // What are we doing here?
- // In Agent<T>, T is a marker type that only exists at the type level. There is no actual
- // value of type T being held, instead, we use PhantomData under the hood. This is so we
- // can implement special methods on Agent<Player>, Agent<Gadget> and Agent<Character>,
- // which allows us in some cases to avoid the "second check" (e.g. Log::players() can
- // return Agent<Player>, as the function already makes sure all returned agents are
- // players). This makes the interface more ergonomical, as we can prove to the type checker
- // at compile time that a given Agent has a certain AgentKind.
- //
- // Why is this safe?
- // PhantomData<T> (which is what Agent<T> boils down to) is a zero-sized type, which means
- // it does not actually change the layout of the struct. There is some discussion in [1],
- // which suggests that this is true for #[repr(C)] structs (which Agent is). We can
- // therefore safely transmute from Agent<U> to Agent<T>, for any U and T.
- //
- // Can this lead to unsafety?
- // No, the actual data access is still done through safe rust and a if-let. In the worst
- // case it can lead to an unexpected panic, but the "guarantee" made by T is rather weak in
- // that regard.
- //
- // What are the alternatives?
- // None, as far as I'm aware. Going from Agent<U> to Agent<T> is possible in safe Rust by
- // destructuring the struct, or alternatively by [2] (if it would be implemented). However,
- // when dealing with references, there seems to be no way to safely go from Agent<U> to
- // Agent<T>, even if they share the same layout.
- //
- // [1]: https://www.reddit.com/r/rust/comments/avrbvc/is_it_safe_to_transmute_foox_to_fooy_if_the/
- // [2]: https://github.com/rust-lang/rfcs/pull/2528
- unsafe { &*(self as *const Agent<Kind> as *const Agent<T>) }
- }
-
- /// Erase any extra information about the contained agent kind.
- #[inline]
- pub fn erase(&self) -> &Agent {
- self.transmute()
- }
-
- /// Try to convert this `Agent` to an `Agent` representing a `Player`.
- #[inline]
- pub fn as_player(&self) -> Option<&Agent<Player>> {
- if self.kind.is_player() {
- Some(self.transmute())
- } else {
- None
- }
- }
-
- /// Try to convert this `Agent` to an `Agent` representing a `Gadget`.
- #[inline]
- pub fn as_gadget(&self) -> Option<&Agent<Gadget>> {
- if self.kind.is_gadget() {
- Some(self.transmute())
- } else {
- None
- }
- }
-
- /// Try to convert this `Agent` to an `Agent` representing a `Character`.
- #[inline]
- pub fn as_character(&self) -> Option<&Agent<Character>> {
- if self.kind.is_character() {
- Some(self.transmute())
- } else {
- None
- }
- }
-}
-
-impl Agent<Player> {
- /// Directly access the underlying player data.
- #[inline]
- pub fn player(&self) -> &Player {
- self.kind.as_player().expect("Agent<Player> had no player!")
- }
-
- /// Shorthand to get the player's account name.
- #[inline]
- pub fn account_name(&self) -> &str {
- self.player().account_name()
- }
-
- /// Shorthand to get the player's character name.
- #[inline]
- pub fn character_name(&self) -> &str {
- self.player().character_name()
- }
-
- /// Shorthand to get the player's elite specialization.
- #[inline]
- pub fn elite(&self) -> Option<EliteSpec> {
- self.player().elite()
- }
-
- /// Shorthand to get the player's profession.
- #[inline]
- pub fn profession(&self) -> Profession {
- self.player().profession()
- }
-
- /// Shorthand to get the player's subgroup.
- #[inline]
- pub fn subgroup(&self) -> u8 {
- self.player().subgroup()
- }
-}
-
-impl Agent<Gadget> {
- /// Directly access the underlying gadget data.
- #[inline]
- pub fn gadget(&self) -> &Gadget {
- self.kind.as_gadget().expect("Agent<Gadget> had no gadget!")
- }
-
- /// Shorthand to get the gadget's id.
- #[inline]
- pub fn id(&self) -> u16 {
- self.gadget().id()
- }
-
- /// Shorthand to get the gadget's name.
- #[inline]
- pub fn name(&self) -> &str {
- self.gadget().name()
- }
-}
-
-impl Agent<Character> {
- /// Directly access the underlying character data.
- #[inline]
- pub fn character(&self) -> &Character {
- self.kind
- .as_character()
- .expect("Agent<Character> had no character!")
- }
-
- /// Shorthand to get the character's id.
- #[inline]
- pub fn id(&self) -> u16 {
- self.character().id()
- }
-
- /// Shorthand to get the character's name.
- #[inline]
- pub fn name(&self) -> &str {
- self.character().name()
- }
-}
-
/// A fully processed log file.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
@@ -877,12 +154,12 @@ impl Log {
/// Return an agent based on its address.
pub fn agent_by_addr(&self, addr: u64) -> Option<&Agent> {
- self.agents.iter().find(|a| a.addr == addr)
+ self.agents.iter().find(|a| a.addr() == addr)
}
/// Return an agent based on the instance ID.
pub fn agent_by_instance_id(&self, instance_id: u16) -> Option<&Agent> {
- self.agents.iter().find(|a| a.instance_id == instance_id)
+ self.agents.iter().find(|a| a.instance_id() == instance_id)
}
/// Return the master agent of the given agent.
@@ -890,7 +167,7 @@ impl Log {
/// * `addr` - The address of the agent which to get the master for.
pub fn master_agent(&self, addr: u64) -> Option<&Agent> {
self.agent_by_addr(addr)
- .and_then(|a| a.master_agent)
+ .and_then(|a| a.master_agent())
.and_then(|a| self.agent_by_addr(a))
}
@@ -915,7 +192,7 @@ impl Log {
/// and Xera.
pub fn boss(&self) -> &Agent {
self.characters()
- .find(|c| c.character().id == self.boss_id)
+ .find(|c| c.character().id() == self.boss_id)
.map(Agent::erase)
.expect("Boss has no agent!")
}
@@ -930,7 +207,7 @@ impl Log {
.map(Encounter::bosses)
.unwrap_or(&[] as &[_]);
self.characters()
- .filter(|c| bosses.iter().any(|boss| *boss as u16 == c.character().id))
+ .filter(|c| bosses.iter().any(|boss| *boss as u16 == c.character().id()))
.map(Agent::erase)
.collect()
}
diff --git a/src/processing.rs b/src/processing.rs
index 722440b..11bdda7 100644
--- a/src/processing.rs
+++ b/src/processing.rs
@@ -110,7 +110,7 @@ fn setup_agents(data: &raw::Evtc) -> Result<Vec<Agent>, EvtcError> {
fn get_agent_by_addr(agents: &mut [Agent], addr: u64) -> Option<&mut Agent> {
for agent in agents {
- if agent.addr == addr {
+ if agent.addr() == addr {
return Some(agent);
}
}
@@ -121,11 +121,11 @@ fn set_agent_awares(data: &raw::Evtc, agents: &mut [Agent]) -> Result<(), EvtcEr
for event in &data.events {
if event.is_statechange == raw::CbtStateChange::None {
if let Some(current_agent) = get_agent_by_addr(agents, event.src_agent) {
- current_agent.instance_id = event.src_instid;
- if current_agent.first_aware == 0 {
- current_agent.first_aware = event.time;
+ current_agent.set_instance_id(event.src_instid);
+ if current_agent.first_aware() == 0 {
+ current_agent.set_first_aware(event.time);
}
- current_agent.last_aware = event.time;
+ current_agent.set_last_aware(event.time);
}
}
}
@@ -137,17 +137,17 @@ fn set_agent_masters(data: &raw::Evtc, agents: &mut [Agent]) -> Result<(), EvtcE
if event.src_master_instid != 0 {
let mut master_addr = None;
for agent in &*agents {
- if agent.instance_id == event.src_master_instid
- && agent.first_aware < event.time
- && event.time < agent.last_aware
+ if agent.instance_id() == event.src_master_instid
+ && agent.first_aware() < event.time
+ && event.time < agent.last_aware()
{
- master_addr = Some(agent.addr);
+ master_addr = Some(agent.addr());
break;
}
}
if let Some(master_addr) = master_addr {
if let Some(current_slave) = get_agent_by_addr(agents, event.src_agent) {
- current_slave.master_agent = Some(master_addr);
+ current_slave.set_master_agent(Some(master_addr));
}
}
}