use crate::{comp::Alignment, uid::Uid}; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use slab::Slab; use specs::{Component, DerefFlaggedStorage, Join}; use specs_idvs::IdvStorage; use tracing::{error, warn}; // Primitive group system // Shortcomings include: // - no support for more complex group structures // - lack of npc group integration // - relies on careful management of groups to maintain a valid state // - the possession rod could probably wreck this // - clients don't know which pets are theirs (could be easy to solve by // putting owner uid in Role::Pet) #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Group(u32); // TODO: Hack // Corresponds to Alignment::Enemy pub const ENEMY: Group = Group(u32::MAX); // Corresponds to Alignment::Npc | Alignment::Tame pub const NPC: Group = Group(u32::MAX - 1); impl Component for Group { type Storage = DerefFlaggedStorage>; } #[derive(Clone, Debug)] pub struct GroupInfo { // TODO: what about enemy groups, either the leader will constantly change because they have to // be loaded or we create a dummy entity or this needs to be optional pub leader: specs::Entity, // Number of group members (excluding pets) pub num_members: u32, // Name of the group pub name: String, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum Role { Member, Pet, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ChangeNotification { // :D Added(E, Role), // :( Removed(E), NewLeader(E), // Use to put in a group overwriting existing group NewGroup { leader: E, members: Vec<(E, Role)> }, // No longer in a group NoGroup, } // Note: now that we are dipping into uids here consider just using // ChangeNotification everywhere // Also note when the same notification is sent to multiple destinations the // mapping might be duplicated effort impl ChangeNotification { pub fn try_map(self, f: impl Fn(E) -> Option) -> Option> { match self { Self::Added(e, r) => f(e).map(|t| ChangeNotification::Added(t, r)), Self::Removed(e) => f(e).map(ChangeNotification::Removed), Self::NewLeader(e) => f(e).map(ChangeNotification::NewLeader), // Note just discards members that fail map Self::NewGroup { leader, members } => { f(leader).map(|leader| ChangeNotification::NewGroup { leader, members: members .into_iter() .filter_map(|(e, r)| f(e).map(|t| (t, r))) .collect(), }) }, Self::NoGroup => Some(ChangeNotification::NoGroup), } } } type GroupsMut<'a> = specs::WriteStorage<'a, Group>; type Groups<'a> = specs::ReadStorage<'a, Group>; type Alignments<'a> = specs::ReadStorage<'a, Alignment>; type Uids<'a> = specs::ReadStorage<'a, Uid>; #[derive(Debug, Default)] pub struct GroupManager { groups: Slab, } // Gather list of pets of the group member // Note: iterating through all entities here could become slow at higher entity // counts fn pets( entity: specs::Entity, uid: Uid, alignments: &Alignments, entities: &specs::Entities, ) -> Vec { (entities, alignments) .join() .filter_map(|(e, a)| { matches!(a, Alignment::Owned(owner) if *owner == uid && e != entity).then_some(e) }) .collect::>() } /// Returns list of current members of a group pub fn members<'a>( group: Group, groups: impl Join + 'a, entities: &'a specs::Entities, alignments: &'a Alignments, uids: &'a Uids, ) -> impl Iterator + 'a { (entities, groups, alignments, uids) .join() .filter_map(move |(e, g, a, u)| { (*g == group).then(|| { ( e, if matches!(a, Alignment::Owned(owner) if owner != u) { Role::Pet } else { Role::Member }, ) }) }) } // TODO: optimize add/remove for massive NPC groups impl GroupManager { pub fn group_info(&self, group: Group) -> Option<&GroupInfo> { self.groups.get(group.0 as usize) } fn group_info_mut(&mut self, group: Group) -> Option<&mut GroupInfo> { self.groups.get_mut(group.0 as usize) } fn create_group(&mut self, leader: specs::Entity, num_members: u32) -> Group { Group(self.groups.insert(GroupInfo { leader, num_members, name: "Group".into(), }) as u32) } fn remove_group(&mut self, group: Group) { self.groups.remove(group.0 as usize); } // Add someone to a group // Also used to create new groups #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 pub fn add_group_member( &mut self, leader: specs::Entity, new_member: specs::Entity, entities: &specs::Entities, groups: &mut GroupsMut, alignments: &Alignments, uids: &Uids, mut notifier: impl FnMut(specs::Entity, ChangeNotification), ) { // Ensure leader is not inviting themselves if leader == new_member { warn!("Attempt to form group with leader as the only member (this is disallowed)"); return; } // Get uid let new_member_uid = if let Some(uid) = uids.get(new_member) { *uid } else { error!("Failed to retrieve uid for the new group member"); return; }; // If new member is a member of a different group remove that if groups .get(new_member) .and_then(|g| self.group_info(*g)) .is_some() { self.leave_group( new_member, groups, alignments, uids, entities, &mut notifier, ) } let group = match groups.get(leader).copied() { Some(id) if self .group_info(id) .map(|info| info.leader == leader) .unwrap_or(false) => { Some(id) }, // Member of an existing group can't be a leader // If the lead is a member of another group leave that group first Some(_) => { self.leave_group(leader, groups, alignments, uids, entities, &mut notifier); None }, None => None, }; let group = if let Some(group) = group { // Increment group size // Note: unwrap won't fail since we just retrieved the group successfully above self.group_info_mut(group).unwrap().num_members += 1; group } else { let new_group = self.create_group(leader, 2); // Unwrap should not fail since we just found these entities and they should // still exist Note: if there is an issue replace with a warn groups.insert(leader, new_group).unwrap(); // Inform notifier(leader, ChangeNotification::NewLeader(leader)); new_group }; let new_pets = pets(new_member, new_member_uid, alignments, entities); // Inform members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role { Role::Member => { notifier(e, ChangeNotification::Added(new_member, Role::Member)); notifier(new_member, ChangeNotification::Added(e, Role::Member)); new_pets.iter().for_each(|p| { notifier(e, ChangeNotification::Added(*p, Role::Pet)); }) }, Role::Pet => { notifier(new_member, ChangeNotification::Added(e, Role::Pet)); }, }); notifier(new_member, ChangeNotification::NewLeader(leader)); // Add group id for new member and pets // Unwrap should not fail since we just found these entities and they should // still exist // Note: if there is an issue replace with a warn let _ = groups.insert(new_member, group).unwrap(); new_pets.iter().for_each(|e| { let _ = groups.insert(*e, group).unwrap(); }); } #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 pub fn new_pet( &mut self, pet: specs::Entity, owner: specs::Entity, groups: &mut GroupsMut, entities: &specs::Entities, alignments: &Alignments, uids: &Uids, notifier: &mut impl FnMut(specs::Entity, ChangeNotification), ) { let group = match groups.get(owner).copied() { Some(group) => group, None => { let new_group = self.create_group(owner, 1); groups.insert(owner, new_group).unwrap(); // Inform notifier(owner, ChangeNotification::NewLeader(owner)); new_group }, }; // Inform members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role { Role::Member => { notifier(e, ChangeNotification::Added(pet, Role::Pet)); }, Role::Pet => {}, }); // Add groups.insert(pet, group).unwrap(); } pub fn leave_group( &mut self, member: specs::Entity, groups: &mut GroupsMut, alignments: &Alignments, uids: &Uids, entities: &specs::Entities, notifier: &mut impl FnMut(specs::Entity, ChangeNotification), ) { // Pets can't leave if matches!(alignments.get(member), Some(Alignment::Owned(uid)) if uids.get(member).map_or(true, |u| u != uid)) { return; } self.remove_from_group(member, groups, alignments, uids, entities, notifier, false); // Set NPC back to their group if let Some(alignment) = alignments.get(member) { match alignment { Alignment::Npc => { let _ = groups.insert(member, NPC); }, Alignment::Enemy => { let _ = groups.insert(member, ENEMY); }, _ => {}, } } } pub fn entity_deleted( &mut self, member: specs::Entity, groups: &mut GroupsMut, alignments: &Alignments, uids: &Uids, entities: &specs::Entities, notifier: &mut impl FnMut(specs::Entity, ChangeNotification), ) { self.remove_from_group(member, groups, alignments, uids, entities, notifier, true); } // Remove someone from a group if they are in one // Don't need to check if they are in a group before calling this // Also removes pets (ie call this if the pet no longer exists) #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 fn remove_from_group( &mut self, member: specs::Entity, groups: &mut GroupsMut, alignments: &Alignments, uids: &Uids, entities: &specs::Entities, notifier: &mut impl FnMut(specs::Entity, ChangeNotification), to_be_deleted: bool, ) { let group = match groups.get(member) { Some(group) => *group, None => return, }; // If leaving entity was the leader disband the group if self .group_info(group) .map(|info| info.leader == member) .unwrap_or(false) { // Remove group self.remove_group(group); (entities, uids, &*groups, alignments.maybe()) .join() .filter(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member)) .fold( HashMap::, Vec)>::new(), |mut acc, (e, uid, _, alignment)| { if let Some(owner) = alignment.and_then(|a| match a { Alignment::Owned(owner) if uid != owner => Some(owner), _ => None, }) { // A pet // Assumes owner will be in the group acc.entry(*owner).or_default().1.push(e); } else { // Not a pet acc.entry(*uid).or_default().0 = Some(e); } acc }, ) .into_iter() .map(|(_, v)| v) .for_each(|(owner, pets)| { if let Some(owner) = owner { if !pets.is_empty() { let mut members = pets.iter().map(|e| (*e, Role::Pet)).collect::>(); members.push((owner, Role::Member)); // New group let new_group = self.create_group(owner, 1); for (member, _) in &members { groups.insert(*member, new_group).unwrap(); } notifier(owner, ChangeNotification::NewGroup { leader: owner, members, }); } else { // If no pets just remove group groups.remove(owner); notifier(owner, ChangeNotification::NoGroup) } } else { // Owner not found, potentially the were removed from the world pets.into_iter().for_each(|pet| { groups.remove(pet); }); } }); } else { // Not leader let leaving_member_uid = if let Some(uid) = uids.get(member) { *uid } else { error!("Failed to retrieve uid for the leaving member"); return; }; let leaving_pets = pets(member, leaving_member_uid, alignments, entities); // If pets and not about to be deleted form new group if !leaving_pets.is_empty() && !to_be_deleted { let new_group = self.create_group(member, 1); notifier(member, ChangeNotification::NewGroup { leader: member, members: leaving_pets .iter() .map(|p| (*p, Role::Pet)) .chain(std::iter::once((member, Role::Member))) .collect(), }); let _ = groups.insert(member, new_group).unwrap(); leaving_pets.iter().for_each(|&e| { let _ = groups.insert(e, new_group).unwrap(); }); } else { let _ = groups.remove(member); notifier(member, ChangeNotification::NoGroup); leaving_pets.iter().for_each(|&e| { let _ = groups.remove(e); }); } if let Some(info) = self.group_info_mut(group) { // If not pet, decrement number of members if !matches!(alignments.get(member), Some(Alignment::Owned(owner)) if uids.get(member).map_or(true, |uid| uid != owner)) { if info.num_members > 0 { info.num_members -= 1; } else { error!("Group with invalid number of members") } } let mut remaining_count = 0; // includes pets // Inform remaining members members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| { remaining_count += 1; match role { Role::Member => { notifier(e, ChangeNotification::Removed(member)); leaving_pets.iter().for_each(|p| { notifier(e, ChangeNotification::Removed(*p)); }) }, Role::Pet => {}, } }); // If leader is the last one left then disband the group // Assumes last member is the leader if remaining_count == 1 { let leader = info.leader; self.remove_group(group); groups.remove(leader); notifier(leader, ChangeNotification::NoGroup); } else if remaining_count == 0 { error!("Somehow group has no members") } } } } // Assign new group leader // Does nothing if new leader is not part of a group pub fn assign_leader( &mut self, new_leader: specs::Entity, groups: &Groups, entities: &specs::Entities, alignments: &Alignments, uids: &Uids, mut notifier: impl FnMut(specs::Entity, ChangeNotification), ) { let group = match groups.get(new_leader) { Some(group) => *group, None => return, }; // Set new leader self.groups[group.0 as usize].leader = new_leader; // Point to new leader members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role { Role::Member => notifier(e, ChangeNotification::NewLeader(new_leader)), Role::Pet => {}, }); } }