diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index a52a535254..1a067a3229 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -65,7 +65,7 @@ VoxygenLocalization( "common.create": "Create", "common.okay": "Okay", "common.accept": "Accept", - "common.reject": "Reject", + "common.decline": "Decline", "common.disclaimer": "Disclaimer", "common.cancel": "Cancel", "common.none": "None", @@ -386,6 +386,8 @@ magically infused items?"#, "gameinput.autowalk": "Auto Walk", "gameinput.dance": "Dance", "gameinput.select": "Select Entity", + "gameinput.acceptgroupinvite": "Accept Group Invite", + "gameinput.declinegroupinvite": "Decline Group Invite", /// End GameInput section diff --git a/client/src/lib.rs b/client/src/lib.rs index 8e4f15fe96..6c861c7ee2 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -18,7 +18,7 @@ use common::{ character::CharacterItem, comp::{ self, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, - InventoryManip, InventoryUpdateEvent, + InventoryManip, InventoryUpdateEvent, group, }, msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, @@ -34,7 +34,7 @@ use common::{ use futures_executor::block_on; use futures_timer::Delay; use futures_util::{select, FutureExt}; -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashMap; use image::DynamicImage; use network::{ Network, Participant, Pid, ProtocolAddr, Stream, PROMISES_CONSISTENCY, PROMISES_ORDERED, @@ -74,7 +74,6 @@ pub struct Client { pub server_info: ServerInfo, pub world_map: (Arc, Vec2), pub player_list: HashMap, - pub group_members: HashSet, pub character_list: CharacterList, pub active_character_id: Option, recipe_book: RecipeBook, @@ -82,6 +81,7 @@ pub struct Client { group_invite: Option, group_leader: Option, + group_members: HashMap, _network: Network, participant: Option, @@ -212,7 +212,7 @@ impl Client { server_info, world_map, player_list: HashMap::new(), - group_members: HashSet::new(), + group_members: HashMap::new(), character_list: CharacterList::default(), active_character_id: None, recipe_book, @@ -434,11 +434,15 @@ impl Client { pub fn group_invite(&self) -> Option { self.group_invite } - pub fn group_leader(&self) -> Option { self.group_leader } + pub fn group_info(&self) -> Option<(String, Uid)> { self.group_leader.map(|l| ("TODO".into(), l)) } + + pub fn group_members(&self) -> &HashMap { &self.group_members } pub fn send_group_invite(&mut self, invitee: Uid) { self.singleton_stream - .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Invite(invitee) ))) + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Invite(invitee), + ))) .unwrap() } @@ -448,37 +452,42 @@ impl Client { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Accept, - ))).unwrap(); + ))) + .unwrap(); } - pub fn reject_group_invite(&mut self) { + pub fn decline_group_invite(&mut self) { // Clear invite self.group_invite.take(); self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( - GroupManip::Reject, - ))).unwrap(); + GroupManip::Decline, + ))) + .unwrap(); } pub fn leave_group(&mut self) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Leave, - ))).unwrap(); + ))) + .unwrap(); } pub fn kick_from_group(&mut self, uid: Uid) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Kick(uid), - ))).unwrap(); + ))) + .unwrap(); } pub fn assign_group_leader(&mut self, uid: Uid) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::AssignLeader(uid), - ))).unwrap(); + ))) + .unwrap(); } pub fn is_mounted(&self) -> bool { @@ -993,47 +1002,47 @@ impl Client { } }, ServerMsg::GroupUpdate(change_notification) => { - use comp::group::ChangeNotification::*; - // Note: we use a hashmap since this would not work with entities outside - // the view distance - match change_notification { - Added(uid) => { - if !self.group_members.insert(uid) { - warn!( - "Received msg to add uid {} to the group members but they \ - were already there", - uid - ); - } - }, - Removed(uid) => { - if !self.group_members.remove(&uid) { - warn!( - "Received msg to remove uid {} from group members but by \ - they weren't in there!", - uid - ); - } - }, - NewLeader(leader) => { - self.group_leader = Some(leader); - }, - NewGroup { leader, members } => { - self.group_leader = Some(leader); - self.group_members = members.into_iter().collect(); - // Currently add/remove messages treat client as an implicit member - // of the group whereas this message explicitly included them so to - // be consistent for now we will remove the client from the - // received hashset - if let Some(uid) = self.uid() { - self.group_members.remove(&uid); - } - }, - NoGroup => { - self.group_leader = None; - self.group_members = HashSet::new(); + use comp::group::ChangeNotification::*; + // Note: we use a hashmap since this would not work with entities outside + // the view distance + match change_notification { + Added(uid, role) => { + if self.group_members.insert(uid, role) == Some(role) { + warn!( + "Received msg to add uid {} to the group members but they \ + were already there", + uid + ); } - } + }, + Removed(uid) => { + if self.group_members.remove(&uid).is_none() { + warn!( + "Received msg to remove uid {} from group members but by they \ + weren't in there!", + uid + ); + } + }, + NewLeader(leader) => { + self.group_leader = Some(leader); + }, + NewGroup { leader, members } => { + self.group_leader = Some(leader); + self.group_members = members.into_iter().collect(); + // Currently add/remove messages treat client as an implicit member + // of the group whereas this message explicitly included them so to + // be consistent for now we will remove the client from the + // received hashset + if let Some(uid) = self.uid() { + self.group_members.remove(&uid); + } + }, + NoGroup => { + self.group_leader = None; + self.group_members = HashMap::new(); + }, + } }, ServerMsg::GroupInvite(uid) => { self.group_invite = Some(uid); @@ -1189,9 +1198,7 @@ impl Client { pub fn entity(&self) -> EcsEntity { self.entity } /// Get the player's Uid. - pub fn uid(&self) -> Option { - self.state.read_component_copied(self.entity) - } + pub fn uid(&self) -> Option { self.state.read_component_copied(self.entity) } /// Get the client state pub fn get_client_state(&self) -> ClientState { self.client_state } diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 517a4e705d..3c2c6c8828 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -22,7 +22,7 @@ pub enum InventoryManip { pub enum GroupManip { Invite(Uid), Accept, - Reject, + Decline, Leave, Kick(Uid), AssignLeader(Uid), diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index acbcf17fa1..2fcc570467 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -11,7 +11,6 @@ use tracing::{error, warn}; // - no support for more complex group structures // - lack of complex enemy npc integration // - relies on careful management of groups to maintain a valid state -// - clients don't know what entities are their pets // - the possesion rod could probably wreck this #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -35,15 +34,21 @@ pub struct GroupInfo { 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), + Added(E, Role), // :( Removed(E), NewLeader(E), // Use to put in a group overwriting existing group - NewGroup { leader: E, members: Vec }, + NewGroup { leader: E, members: Vec<(E, Role)> }, // No longer in a group NoGroup, } @@ -54,14 +59,17 @@ pub enum ChangeNotification { impl ChangeNotification { pub fn try_map(self, f: impl Fn(E) -> Option) -> Option> { match self { - Self::Added(e) => f(e).map(ChangeNotification::Added), + 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(f).collect(), + members: members + .into_iter() + .filter_map(|(e, r)| f(e).map(|t| (t, r))) + .collect(), }) }, Self::NoGroup => Some(ChangeNotification::NoGroup), @@ -79,23 +87,21 @@ pub struct GroupManager { groups: Slab, } -// Gather list of pets of the group member + member themselves +// Gather list of pets of the group member // Note: iterating through all entities here could become slow at higher entity // counts -fn with_pets( +fn pets( entity: specs::Entity, uid: Uid, alignments: &Alignments, entities: &specs::Entities, ) -> Vec { - let mut list = (entities, alignments) + (entities, alignments) .join() .filter_map(|(e, a)| { matches!(a, Alignment::Owned(owner) if *owner == uid && e != entity).then_some(e) }) - .collect::>(); - list.push(entity); - list + .collect::>() } /// Returns list of current members of a group @@ -103,10 +109,23 @@ pub fn members<'a>( group: Group, groups: impl Join + 'a, entities: &'a specs::Entities, -) -> impl Iterator + 'a { - (entities, groups) + alignments: &'a Alignments, + uids: &'a Uids, +) -> impl Iterator + 'a { + (entities, groups, alignments, uids) .join() - .filter_map(move |(e, g)| (*g == group).then_some(e)) + .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 @@ -194,23 +213,30 @@ impl GroupManager { new_group }); - let member_plus_pets = with_pets(new_member, new_member_uid, alignments, entities); + let new_pets = pets(new_member, new_member_uid, alignments, entities); // Inform - members(group, &*groups, entities).for_each(|a| { - member_plus_pets.iter().for_each(|b| { - notifier(a, ChangeNotification::Added(*b)); - notifier(*b, ChangeNotification::Added(a)); - }) + 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)); + }, }); - // Note: pets not informed 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 - member_plus_pets.iter().for_each(|e| { + let _ = groups.insert(new_member, group).unwrap(); + new_pets.iter().for_each(|e| { let _ = groups.insert(*e, group).unwrap(); }); } @@ -221,6 +247,8 @@ impl GroupManager { 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() { @@ -235,17 +263,15 @@ impl GroupManager { }; // Inform - members(group, &*groups, entities).for_each(|a| { - notifier(a, ChangeNotification::Added(pet)); - notifier(pet, ChangeNotification::Added(a)); + 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(); - - if let Some(info) = self.group_info(group) { - notifier(pet, ChangeNotification::NewLeader(info.leader)); - } } pub fn leave_group( @@ -341,32 +367,30 @@ impl GroupManager { .for_each(|(owner, pets)| { if let Some(owner) = owner { if !pets.is_empty() { - let mut members = pets.clone(); - members.push(owner); + 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); - for &member in &members { - groups.insert(member, new_group).unwrap(); + for (member, _) in &members { + groups.insert(*member, new_group).unwrap(); } - let notification = ChangeNotification::NewGroup { + notifier(owner, ChangeNotification::NewGroup { leader: owner, members, - }; - - // TODO: don't clone - notifier(owner, notification.clone()); - pets.into_iter() - .for_each(|pet| notifier(pet, notification.clone())); + }); } else { // If no pets just remove group groups.remove(owner); notifier(owner, ChangeNotification::NoGroup) } } else { - pets.into_iter() - .for_each(|pet| notifier(pet, ChangeNotification::NoGroup)); + // Owner not found, potentially the were removed from the world + pets.into_iter().for_each(|pet| { + groups.remove(pet); + }); } }); } else { @@ -378,36 +402,47 @@ impl GroupManager { return; }; - let leaving = with_pets(member, leaving_member_uid, alignments, entities); + let leaving_pets = pets(member, leaving_member_uid, alignments, entities); // If pets and not about to be deleted form new group - if leaving.len() > 1 && !to_be_deleted { + if !leaving_pets.is_empty() && !to_be_deleted { let new_group = self.create_group(member); - let notification = ChangeNotification::NewGroup { + notifier(member, ChangeNotification::NewGroup { leader: member, - members: leaving.clone(), - }; + members: leaving_pets + .iter() + .map(|p| (*p, Role::Pet)) + .chain(std::iter::once((member, Role::Member))) + .collect(), + }); - leaving.iter().for_each(|&e| { + let _ = groups.insert(member, new_group).unwrap(); + leaving_pets.iter().for_each(|&e| { let _ = groups.insert(e, new_group).unwrap(); - notifier(e, notification.clone()); }); } else { - leaving.iter().for_each(|&e| { + let _ = groups.remove(member); + notifier(member, ChangeNotification::NoGroup); + leaving_pets.iter().for_each(|&e| { let _ = groups.remove(e); - notifier(e, ChangeNotification::NoGroup); }); } if let Some(info) = self.group_info(group) { // Inform remaining members let mut num_members = 0; - members(group, &*groups, entities).for_each(|a| { + members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| { num_members += 1; - leaving.iter().for_each(|b| { - notifier(a, ChangeNotification::Removed(*b)); - }) + 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 @@ -430,6 +465,8 @@ impl GroupManager { 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) { @@ -441,8 +478,9 @@ impl GroupManager { self.groups[group.0 as usize].leader = new_leader; // Point to new leader - members(group, groups, entities).for_each(|e| { - notifier(e, ChangeNotification::NewLeader(new_leader)); + members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role { + Role::Member => notifier(e, ChangeNotification::NewLeader(new_leader)), + Role::Pet => {}, }); } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index eb14d24e24..3c5601f4e0 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -569,6 +569,8 @@ fn handle_spawn( target, &mut state.ecs().write_storage(), &state.ecs().entities(), + &state.ecs().read_storage(), + &uids, &mut |entity, group_change| { clients .get_mut(entity) diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index bfc15bb946..e73a617fdc 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -136,7 +136,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani ); } }, - GroupManip::Reject => { + GroupManip::Decline => { let mut clients = state.ecs().write_storage::(); if let Some(inviter) = clients .get_mut(entity) @@ -144,8 +144,8 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani { // Inform inviter of rejection if let Some(client) = clients.get_mut(inviter) { - // TODO: say who rejected the invite - client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); + // TODO: say who declined the invite + client.notify(ChatType::Meta.server_msg("Invite declined".to_owned())); } } }, @@ -296,6 +296,8 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani target, &groups, &state.ecs().entities(), + &state.ecs().read_storage(), + &uids, |entity, group_change| { clients .get_mut(entity) diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index e1d658fb04..549804d6f8 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -236,6 +236,8 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv entity, &mut state.ecs().write_storage(), &state.ecs().entities(), + &state.ecs().read_storage(), + &uids, &mut |entity, group_change| { clients .get_mut(entity) diff --git a/server/src/events/player.rs b/server/src/events/player.rs index 1e777c1daf..c81482c199 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -57,6 +57,8 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { new_entity, &state.ecs().read_storage(), &state.ecs().entities(), + &state.ecs().read_storage(), + &state.ecs().read_storage(), // Nothing actually changing |_, _| {}, ); diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 759c469004..0528e8cfba 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -5,8 +5,8 @@ use crate::{ }; use common::{ comp::{ - Admin, AdminList, CanBuild, ChatMode, UnresolvedChatMsg, ChatType, ControlEvent, Controller, - ForceUpdate, Ori, Player, Pos, Stats, Vel, + Admin, AdminList, CanBuild, ChatMode, ChatType, ControlEvent, Controller, ForceUpdate, Ori, + Player, Pos, Stats, UnresolvedChatMsg, Vel, }, event::{EventBus, ServerEvent}, msg::{ diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 24b43e360b..5a6124dcac 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -1,13 +1,16 @@ use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, TEXT_COLOR_GREY, UI_MAIN}; -use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; +use crate::{ + i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts, window::GameInput, +}; use client::{self, Client}; use common::{ - comp::Stats, + comp::{group::Role, Stats}, sync::{Uid, WorldSyncExt}, }; use conrod_core::{ color, + position::{Place, Relative}, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; @@ -25,60 +28,51 @@ widget_ids! { btn_link, btn_kick, btn_leave, + scroll_area, + scrollbar, members[], invite_bubble, bubble_frame, btn_accept, btn_decline, - // TEST - test_leader, - test_member1, - test_member2, - test_member3, - test_member4, - test_member5, } } pub struct State { ids: Ids, - // Holds the time when selection is made since this selection can be overriden - // by selecting an entity in-game - selected_uid: Option<(Uid, Instant)>, // Selected group member selected_member: Option, } #[derive(WidgetCommon)] pub struct Group<'a> { - show: &'a Show, + show: &'a mut Show, client: &'a Client, + settings: &'a Settings, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - selected_entity: Option<(specs::Entity, Instant)>, - #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> Group<'a> { pub fn new( - show: &'a Show, + show: &'a mut Show, client: &'a Client, + settings: &'a Settings, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - selected_entity: Option<(specs::Entity, Instant)>, ) -> Self { Self { show, client, + settings, imgs, fonts, localized_strings, - selected_entity, common: widget::CommonBuilder::default(), } } @@ -87,7 +81,7 @@ impl<'a> Group<'a> { pub enum Event { Close, Accept, - Reject, + Decline, Kick(Uid), LeaveGroup, AssignLeader(Uid), @@ -101,7 +95,6 @@ impl<'a> Widget for Group<'a> { fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { Self::State { ids: Ids::new(id_gen), - selected_uid: None, selected_member: None, } } @@ -114,23 +107,63 @@ impl<'a> Widget for Group<'a> { let mut events = Vec::new(); - let player_leader = true; - let in_group = true; - let open_invite = false; + // Don't show pets + let group_members = self + .client + .group_members() + .iter() + .filter_map(|(u, r)| match r { + Role::Member => Some(u), + Role::Pet => None, + }) + .collect::>(); - if in_group || open_invite { + // Not considered in group for ui purposes if it is just pets + let in_group = !group_members.is_empty(); + + // Helper + let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) { + Some(player_info) => player_info + .character + .as_ref() + .map_or_else(|| format!("Player<{}>", uid), |c| c.name.clone()), + None => client + .state() + .ecs() + .entity_from_uid(uid.0) + .and_then(|entity| { + client + .state() + .ecs() + .read_storage::() + .get(entity) + .map(|stats| stats.name.clone()) + }) + .unwrap_or_else(|| format!("Npc<{}>", uid)), + }; + + let open_invite = self.client.group_invite(); + + let my_uid = self.client.uid(); + + // TODO show something to the player when they click on the group button while + // they are not in a group so that it doesn't look like the button is + // broken + + if in_group || open_invite.is_some() { // Frame Rectangle::fill_with([220.0, 230.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) .bottom_left_with_margins_on(ui.window, 220.0, 10.0) .set(state.ids.bg, ui); - if open_invite { - // yellow animated border - } + if open_invite.is_some() { + // yellow animated border + } } // Buttons - if in_group { - Text::new("Group Name") + if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) { + let selected = state.selected_member; + Text::new(&group_name) .mid_top_with_margin_on(state.ids.bg, 2.0) .font_size(20) .font_id(self.fonts.cyri.conrod_id) @@ -144,7 +177,7 @@ impl<'a> Widget for Group<'a> { .label("Add to Friends") .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) + .label_font_size(self.fonts.cyri.scale(10)) .set(state.ids.btn_friend, ui) .was_clicked() {}; @@ -153,176 +186,209 @@ impl<'a> Widget for Group<'a> { .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Leave Group") - .label_color(TEXT_COLOR) + .label(&self.localized_strings.get("hud.group.leave")) + .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) + .label_font_size(self.fonts.cyri.scale(10)) .set(state.ids.btn_leave, ui) .was_clicked() - {}; + { + events.push(Event::LeaveGroup); + }; // Group leader functions - if player_leader { - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label("Assign Leader") - .label_color(TEXT_COLOR) // Grey when no player is selected - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_leader, ui) - .was_clicked() - {}; - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label("Link Group") - .label_color(TEXT_COLOR_GREY) // Change this when the linking is working - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_link, ui) - .was_clicked() - {}; - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) - .down_from(state.ids.btn_link, 5.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label("Kick") - .label_color(TEXT_COLOR) // Grey when no player is selected - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_kick, ui) - .was_clicked() - {}; - } - // Group Members, only character names, cut long names when they exceed the button size - // TODO Insert loop here - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) + if my_uid == Some(leader) { + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.assign_leader")) + .label_color(if state.selected_member.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_GREY + }) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_leader, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::AssignLeader(uid)); + state.update(|s| { + s.selected_member = None; + }); + } + }; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("Link Group") // TODO: Localize + .label_color(TEXT_COLOR_GREY) // Change this when the linking is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_link, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) + .down_from(state.ids.btn_link, 5.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.kick")) + .label_color(if state.selected_member.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_GREY + }) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_kick, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::Kick(uid)); + state.update(|s| { + s.selected_member = None; + }); + } + }; + } + // Group Members, only character names, cut long names when they exceed the + // button size + let group_size = group_members.len() + 1; + if state.ids.members.len() < group_size { + state.update(|s| { + s.ids + .members + .resize(group_size, &mut ui.widget_id_generator()) + }) + } + // Scrollable area for group member names + Rectangle::fill_with([110.0, 192.0], color::TRANSPARENT) .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Leader") // Grey when no player is selected - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_leader, ui) - .was_clicked() + .scroll_kids() + .scroll_kids_vertically() + .set(state.ids.scroll_area, ui); + Scrollbar::y_axis(state.ids.scroll_area) + .thickness(5.0) + .rgba(0.33, 0.33, 0.33, 1.0) + .set(state.ids.scrollbar, ui); + // List member names + for (i, &uid) in self + .client + .uid() + .iter() + .chain(group_members.iter().copied()) + .enumerate() { - //Select the Leader - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_leader, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member1, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member1, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member2, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member2, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member3, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member3, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member4, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member4, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member5, ui) - .was_clicked() - { - // Select the group member - }; - // Maximum of 6 Players/Npcs per Group - // Player pets count as group members, too. They are not counted into the maximum group size. + let selected = state.selected_member.map_or(false, |u| u == uid); + let char_name = uid_to_name_text(uid, &self.client); + // TODO: Do something special visually if uid == leader + if Button::image(if selected { + self.imgs.selection + } else { + self.imgs.nothing + }) + .w_h(100.0, 22.0) + .and(|w| { + if i == 0 { + w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0) + } else { + w.down_from(state.ids.members[i - 1], 10.0) + } + }) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .crop_kids() + .label_x(Relative::Place(Place::Start(Some(4.0)))) + .label(&char_name) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.members[i], ui) + .was_clicked() + { + // Do nothing when clicking yourself + if Some(uid) != my_uid { + // Select the group member + state.update(|s| { + s.selected_member = if selected { None } else { Some(uid) } + }); + } + }; + } + // Maximum of 6 Players/Npcs per Group + // Player pets count as group members, too. They are not counted + // into the maximum group size. } - if open_invite { - //self.show.group = true; Auto open group menu - Text::new("Player wants to invite you!") + if let Some(invite_uid) = open_invite { + self.show.group = true; // Auto open group menu + // TODO: add group name here too + // Invite text + let name = uid_to_name_text(invite_uid, &self.client); + let invite_text = self + .localized_strings + .get("hud.group.invite_to_join") + .replace("{name}", &name); + Text::new(&invite_text) .mid_top_with_margin_on(state.ids.bg, 20.0) .font_size(20) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(state.ids.title, ui); + // Accept Button + let accept_key = self + .settings + .controls + .get_binding(GameInput::AcceptGroupInvite) + .map_or_else(|| "".into(), |key| key.to_string()); if Button::image(self.imgs.button) .w_h(90.0, 22.0) .bottom_left_with_margins_on(state.ids.bg, 15.0, 15.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label("[U] Accept") - .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&format!( + "[{}] {}", + &accept_key, + &self.localized_strings.get("common.accept") + )) + .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(15)) - .set(state.ids.btn_friend, ui) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_accept, ui) .was_clicked() - {}; + { + events.push(Event::Accept); + }; + // Decline button + let decline_key = self + .settings + .controls + .get_binding(GameInput::DeclineGroupInvite) + .map_or_else(|| "".into(), |key| key.to_string()); if Button::image(self.imgs.button) .w_h(90.0, 22.0) .bottom_right_with_margins_on(state.ids.bg, 15.0, 15.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label("[I] Decline") - .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&format!( + "[{}] {}", + &decline_key, + &self.localized_strings.get("common.decline") + )) + .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(15)) - .set(state.ids.btn_friend, ui) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_decline, ui) .was_clicked() - {}; - + { + events.push(Event::Decline); + }; } events } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 773e4897e9..ad6bd8689c 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -310,7 +310,7 @@ pub enum Event { CraftRecipe(String), InviteMember(common::sync::Uid), AcceptInvite, - RejectInvite, + DeclineInvite, KickMember(common::sync::Uid), LeaveGroup, AssignLeader(common::sync::Uid), @@ -1083,7 +1083,7 @@ impl Hud { h, i, uid, - client.group_members.contains(uid), + client.group_members().contains_key(uid), ) }) .filter(|(entity, pos, _, stats, _, _, _, _, hpfl, _, in_group)| { @@ -1945,30 +1945,25 @@ impl Hud { self.show.open_social_tab(social_tab) }, social::Event::Invite(uid) => events.push(Event::InviteMember(uid)), - social::Event::Accept => events.push(Event::AcceptInvite), - social::Event::Reject => events.push(Event::RejectInvite), - social::Event::Kick(uid) => events.push(Event::KickMember(uid)), - social::Event::LeaveGroup => events.push(Event::LeaveGroup), - social::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), } } } // Group Window if self.show.group { for event in Group::new( - &self.show, + &mut self.show, client, + &global_state.settings, &self.imgs, &self.fonts, &self.voxygen_i18n, - info.selected_entity, ) .set(self.ids.group_window, ui_widgets) { match event { group::Event::Close => self.show.social(false), group::Event::Accept => events.push(Event::AcceptInvite), - group::Event::Reject => events.push(Event::RejectInvite), + group::Event::Decline => events.push(Event::DeclineInvite), group::Event::Kick(uid) => events.push(Event::KickMember(uid)), group::Event::LeaveGroup => events.push(Event::LeaveGroup), group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index a13532f2a0..b42c347398 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -2,16 +2,12 @@ use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_MAIN}; use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; use client::{self, Client}; -use common::{ - comp::Stats, - sync::{Uid, WorldSyncExt}, -}; +use common::sync::Uid; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; -use specs::WorldExt; use std::time::Instant; widget_ids! { @@ -31,15 +27,7 @@ widget_ids! { friends_test, faction_test, player_names[], - group, - group_invite, - member_names[], - accept_invite_button, - reject_invite_button, invite_button, - kick_button, - assign_leader_button, - leave_button, } } @@ -48,8 +36,6 @@ pub struct State { // Holds the time when selection is made since this selection can be overriden // by selecting an entity in-game selected_uid: Option<(Uid, Instant)>, - // Selected group member - selected_member: Option, } pub enum SocialTab { @@ -95,13 +81,8 @@ impl<'a> Social<'a> { pub enum Event { Close, - ChangeSocialTab(SocialTab), Invite(Uid), - Accept, - Reject, - Kick(Uid), - LeaveGroup, - AssignLeader(Uid), + ChangeSocialTab(SocialTab), } impl<'a> Widget for Social<'a> { @@ -113,7 +94,6 @@ impl<'a> Widget for Social<'a> { Self::State { ids: Ids::new(id_gen), selected_uid: None, - selected_member: None, } } @@ -270,86 +250,11 @@ impl<'a> Widget for Social<'a> { } } - Text::new(&self.localized_strings.get("hud.group")) - .down(10.0) - .font_size(self.fonts.cyri.scale(20)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.group, ui); - - // Helper - let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) { - Some(player_info) => { - let alias = &player_info.player_alias; - let character_name_level = match &player_info.character { - Some(character) => format!("{} Lvl {}", &character.name, &character.level), - None => "".to_string(), // character select or spectating - }; - format!("[{}] {}", alias, character_name_level) - }, - None => self - .client - .state() - .ecs() - .entity_from_uid(uid.0) - .and_then(|entity| { - self.client - .state() - .ecs() - .read_storage::() - .get(entity) - .map(|stats| stats.name.clone()) - }) - .unwrap_or_else(|| format!("NPC Uid: {}", uid)), - }; - - // Accept/Reject Invite - if let Some(invite_uid) = self.client.group_invite() { - let name = uid_to_name_text(invite_uid, &self.client); - let text = self - .localized_strings - .get("hud.group.invite_to_join") - .replace("{name}", &name); - Text::new(&text) - .down(10.0) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.group_invite, ui); - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("common.accept")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.accept_invite_button, ui) - .was_clicked() - { - events.push(Event::Accept); - } - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("common.reject")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.reject_invite_button, ui) - .was_clicked() - { - events.push(Event::Reject); - } - } else if self // Invite Button + // Invite Button + if self .client - .group_leader() - .map_or(true, |l_uid| self.client.uid() == Some(l_uid)) + .group_info() + .map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid)) { let selected = state.selected_uid.map(|s| s.0).or_else(|| { self.selected_entity @@ -381,129 +286,6 @@ impl<'a> Widget for Social<'a> { } } } - - // Show group members - if let Some(leader) = self.client.group_leader() { - let group_size = self.client.group_members.len() + 1; - if state.ids.member_names.len() < group_size { - state.update(|s| { - s.ids - .member_names - .resize(group_size, &mut ui.widget_id_generator()) - }) - } - // List member names - for (i, &uid) in self - .client - .uid() - .iter() - .chain(self.client.group_members.iter()) - .enumerate() - { - let selected = state.selected_member.map_or(false, |u| u == uid); - let text = uid_to_name_text(uid, &self.client); - let text = if selected { - format!("-> {}", &text) - } else { - text - }; - let text = if uid == leader { - format!("{} (Leader)", &text) - } else { - text - }; - Text::new(&text) - .down(3.0) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.member_names[i], ui); - // Check for click - if ui - .widget_input(state.ids.member_names[i]) - .clicks() - .left() - .next() - .is_some() - { - state.update(|s| { - s.selected_member = if selected { None } else { Some(uid) } - }); - } - } - - // Show more buttons if leader - if self.client.uid() == Some(leader) { - let selected = state.selected_member; - // Kick - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.kick")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(if selected.is_some() { - TEXT_COLOR - } else { - TEXT_COLOR_3 - }) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.kick_button, ui) - .was_clicked() - { - if let Some(uid) = selected { - events.push(Event::Kick(uid)); - state.update(|s| { - s.selected_member = None; - }); - } - } - // Assign leader - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.assign_leader")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(if selected.is_some() { - TEXT_COLOR - } else { - TEXT_COLOR_3 - }) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.assign_leader_button, ui) - .was_clicked() - { - if let Some(uid) = selected { - events.push(Event::AssignLeader(uid)); - state.update(|s| { - s.selected_member = None; - }); - } - } - } - - // Leave group button - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.leave")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.leave_button, ui) - .was_clicked() - { - events.push(Event::LeaveGroup); - } - } } // Friends Tab diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index b37af8c2ee..79d0c46975 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -957,8 +957,8 @@ impl PlayState for SessionState { HudEvent::AcceptInvite => { self.client.borrow_mut().accept_group_invite(); }, - HudEvent::RejectInvite => { - self.client.borrow_mut().reject_group_invite(); + HudEvent::DeclineInvite => { + self.client.borrow_mut().decline_group_invite(); }, HudEvent::KickMember(uid) => { self.client.borrow_mut().kick_from_group(uid); diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index d89833dd23..441108bdb7 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -169,7 +169,9 @@ impl ControlSettings { GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9), GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q), GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt), - GameInput::Select => KeyMouse::Key(VirtualKeyCode::I), + GameInput::Select => KeyMouse::Key(VirtualKeyCode::Y), + GameInput::AcceptGroupInvite => KeyMouse::Key(VirtualKeyCode::U), + GameInput::DeclineGroupInvite => KeyMouse::Key(VirtualKeyCode::I), } } } @@ -236,6 +238,8 @@ impl Default for ControlSettings { GameInput::Slot10, GameInput::SwapLoadout, GameInput::Select, + GameInput::AcceptGroupInvite, + GameInput::DeclineGroupInvite, ]; for game_input in game_inputs { new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input)); diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index d338d2ca10..b8bf69c08b 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -68,6 +68,8 @@ pub enum GameInput { AutoWalk, CycleCamera, Select, + AcceptGroupInvite, + DeclineGroupInvite, } impl GameInput { @@ -125,6 +127,8 @@ impl GameInput { GameInput::Slot10 => "gameinput.slot10", GameInput::SwapLoadout => "gameinput.swaploadout", GameInput::Select => "gameinput.select", + GameInput::AcceptGroupInvite => "gameinput.acceptgroupinvite", + GameInput::DeclineGroupInvite => "gameinput.declinegroupinvite", } }