diff --git a/assets/voxygen/i18n/en/hud/group.ron b/assets/voxygen/i18n/en/hud/group.ron index 9e726cccce..a24b65ed99 100644 --- a/assets/voxygen/i18n/en/hud/group.ron +++ b/assets/voxygen/i18n/en/hud/group.ron @@ -5,6 +5,7 @@ string_map: { "hud.group": "Group", "hud.group.invite_to_join": "{name} invited you to their group!", + "hud.group.invite_to_trade": "{name} would like to trade with you.", "hud.group.invite": "Invite", "hud.group.kick": "Kick", "hud.group.assign_leader": "Assign Leader", diff --git a/client/src/lib.rs b/client/src/lib.rs index 043f2536fc..2fd035232b 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -20,7 +20,7 @@ use common::{ comp::{ self, chat::{KillSource, KillType}, - group, + group::{self, InviteKind}, skills::Skill, slot::Slot, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, @@ -69,6 +69,7 @@ const PING_ROLLING_AVERAGE_SECS: usize = 10; pub enum Event { Chat(comp::ChatMsg), + InviteComplete { target: Uid, answer: InviteAnswer, kind: InviteKind }, Disconnect, DisconnectionNotification(u64), InventoryUpdated(InventoryUpdateEvent), @@ -130,7 +131,7 @@ pub struct Client { max_group_size: u32, // Client has received an invite (inviter uid, time out instant) - group_invite: Option<(Uid, std::time::Instant, std::time::Duration)>, + group_invite: Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)>, group_leader: Option, // Note: potentially representable as a client only component group_members: HashMap, @@ -636,18 +637,16 @@ impl Client { } } + pub fn is_dead(&self) -> bool { + self.state.ecs().read_storage::().get(self.entity).map_or(false, |h| h.is_dead) + } + pub fn pick_up(&mut self, entity: EcsEntity) { // Get the health component from the entity if let Some(uid) = self.state.read_component_copied(entity) { // If we're dead, exit before sending the message - if self - .state - .ecs() - .read_storage::() - .get(self.entity) - .map_or(false, |h| h.is_dead) - { + if self.is_dead() { return; } @@ -659,13 +658,7 @@ impl Client { pub fn npc_interact(&mut self, npc_entity: EcsEntity) { // If we're dead, exit before sending message - if self - .state - .ecs() - .read_storage::() - .get(self.entity) - .map_or(false, |h| h.is_dead) - { + if self.is_dead() { return; } @@ -674,6 +667,17 @@ impl Client { } } + pub fn initiate_trade(&mut self, counterparty: EcsEntity) { + // If we're dead, exit before sending message + if self.is_dead() { + return; + } + + if let Some(uid) = self.state.read_component_copied(counterparty) { + self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateTrade(uid))); + } + } + pub fn player_list(&self) -> &HashMap { &self.player_list } pub fn character_list(&self) -> &CharacterList { &self.character_list } @@ -737,7 +741,7 @@ impl Client { pub fn max_group_size(&self) -> u32 { self.max_group_size } - pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> { + pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)> { self.group_invite } @@ -1094,7 +1098,7 @@ impl Client { // Check if the group invite has timed out and remove if so if self .group_invite - .map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur) + .map_or(false, |(_, timeout, dur, _)| timeout.elapsed() > dur) { self.group_invite = None; } @@ -1468,30 +1472,22 @@ impl Client { }, } }, - ServerGeneral::GroupInvite { inviter, timeout } => { - self.group_invite = Some((inviter, std::time::Instant::now(), timeout)); + ServerGeneral::GroupInvite { inviter, timeout, kind } => { + self.group_invite = Some((inviter, std::time::Instant::now(), timeout, kind)); }, ServerGeneral::InvitePending(uid) => { if !self.pending_invites.insert(uid) { warn!("Received message about pending invite that was already pending"); } }, - ServerGeneral::InviteComplete { target, answer } => { + ServerGeneral::InviteComplete { target, answer, kind } => { if !self.pending_invites.remove(&target) { warn!( "Received completed invite message for invite that was not in the list of \ pending invites" ) } - // TODO: expose this as a new event variant instead of going - // through the chat - let msg = match answer { - // TODO: say who accepted/declined/timed out the invite - InviteAnswer::Accepted => "Invite accepted", - InviteAnswer::Declined => "Invite declined", - InviteAnswer::TimedOut => "Invite timed out", - }; - frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg(msg))); + frontend_events.push(Event::InviteComplete { target, answer, kind }); }, // Cleanup for when the client goes back to the `presence = None` ServerGeneral::ExitInGameSuccess => { diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index bf8723da77..f64d412570 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -3,7 +3,7 @@ use crate::sync; use authc::AuthClientError; use common::{ character::{self, CharacterItem}, - comp, + comp::{self, group::InviteKind}, outcome::Outcome, recipe::RecipeBook, resources::TimeOfDay, @@ -80,6 +80,7 @@ pub enum ServerGeneral { GroupInvite { inviter: sync::Uid, timeout: std::time::Duration, + kind: InviteKind, }, /// Indicate to the client that their sent invite was not invalid and is /// currently pending @@ -92,6 +93,7 @@ pub enum ServerGeneral { InviteComplete { target: sync::Uid, answer: InviteAnswer, + kind: InviteKind, }, /// Trigger cleanup for when the client goes back to the `Registered` state /// from an ingame state diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 5b99e695f7..2b3abf535f 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage}; use specs_idvs::IdvStorage; use std::time::Duration; +use hashbrown::HashMap; use vek::*; /// Default duration before an input is considered 'held'. @@ -83,6 +84,7 @@ pub enum ControlEvent { EnableLantern, DisableLantern, Interact(Uid), + InitiateTrade(Uid), Mount(Uid), Unmount, InventoryManip(InventoryManip), @@ -318,3 +320,25 @@ pub struct Mounting(pub Uid); impl Component for Mounting { type Storage = DerefFlaggedStorage>; } + +/// A PendingTrade is associated with the entity that initiated the trade. +/// +/// Items are not removed from the inventory during a PendingTrade: all the items are moved +/// atomically (if there's space and both parties agree) upon completion +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PendingTrade { + /// `counterparty` is the other entity that's being traded with + counterparty: Uid, + /// `self_offer` represents the items and quantities of the initiator's items being offered + self_offer: HashMap, + /// `other_offer` represents the items and quantities of the counterparty's items being offered + other_offer: HashMap, + /// `locked` is set when going from the first (mutable) screen to the second (readonly, review) + /// screen + locked: bool, +} + +impl Component for PendingTrade { + type Storage = DerefFlaggedStorage>; +} + diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index f137f3a829..73d428c11c 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -28,14 +28,24 @@ impl Component for Group { type Storage = DerefFlaggedStorage>; } -pub struct Invite(pub specs::Entity); +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub enum InviteKind { + Group, + Trade, +} + +pub struct Invite { + pub inviter: specs::Entity, + pub kind: InviteKind, +} + impl Component for Invite { type Storage = IdvStorage; } // Pending invites that an entity currently has sent out // (invited entity, instant when invite times out) -pub struct PendingInvites(pub Vec<(specs::Entity, std::time::Instant)>); +pub struct PendingInvites(pub Vec<(specs::Entity, InviteKind, std::time::Instant)>); impl Component for PendingInvites { type Storage = IdvStorage; } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 54c3ad67be..083183b384 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -46,7 +46,7 @@ pub use chat::{ }; pub use controller::{ Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input, - InventoryManip, LoadoutManip, MountState, Mounting, SlotManip, + InventoryManip, LoadoutManip, MountState, Mounting, PendingTrade, SlotManip, }; pub use energy::{Energy, EnergyChange, EnergySource}; pub use group::Group; diff --git a/common/src/event.rs b/common/src/event.rs index a375898815..30db1e8726 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -79,6 +79,7 @@ pub enum ServerEvent { EnableLantern(EcsEntity), DisableLantern(EcsEntity), NpcInteract(EcsEntity, EcsEntity), + InitiateTrade(EcsEntity, EcsEntity), Mount(EcsEntity, EcsEntity), Unmount(EcsEntity), Possess(Uid, Uid), diff --git a/common/sys/src/controller.rs b/common/sys/src/controller.rs index 93f08e00f1..acf5cc92c0 100644 --- a/common/sys/src/controller.rs +++ b/common/sys/src/controller.rs @@ -98,6 +98,13 @@ impl<'a> System<'a> for Sys { server_emitter.emit(ServerEvent::NpcInteract(entity, npc_entity)); } }, + ControlEvent::InitiateTrade(counterparty_uid) => { + if let Some(counterparty_entity) = + uid_allocator.retrieve_entity_internal(counterparty_uid.id()) + { + server_emitter.emit(ServerEvent::InitiateTrade(entity, counterparty_entity)); + } + }, ControlEvent::InventoryManip(manip) => { server_emitter.emit(ServerEvent::InventoryManip(entity, manip.into())); }, diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index b65fa49d7e..56cb35f999 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -2,7 +2,7 @@ use crate::{client::Client, Server}; use common::{ comp::{ self, - group::{self, Group, GroupManager, Invite, PendingInvites}, + group::{self, Group, GroupManager, Invite, InviteKind, PendingInvites}, ChatType, GroupManip, }, uid::Uid, @@ -20,169 +20,176 @@ const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31); /// Reduced duration shown to the client to help alleviate latency issues const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30); -// TODO: turn chat messages into enums -pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) { +pub fn handle_invite(server: &mut Server, inviter: specs::Entity, invitee_uid: Uid, kind: InviteKind) { let max_group_size = server.settings().max_player_group_size; let state = server.state_mut(); - - match manip { - GroupManip::Invite(uid) => { - let clients = state.ecs().read_storage::(); - let invitee = match state.ecs().entity_from_uid(uid.into()) { - Some(t) => t, - None => { - // Inform of failure - if let Some(client) = clients.get(entity) { - client.send_fallible(ServerGeneral::server_msg( - ChatType::Meta, - "Invite failed, target does not exist.", - )); - } - return; - }, - }; - - let uids = state.ecs().read_storage::(); - - // Check if entity is trying to invite themselves to a group - if uids - .get(entity) - .map_or(false, |inviter_uid| *inviter_uid == uid) - { - warn!("Entity tried to invite themselves into a group"); - return; - } - - // Disallow inviting entity that is already in your group - let groups = state.ecs().read_storage::(); - let group_manager = state.ecs().read_resource::(); - let already_in_same_group = groups.get(entity).map_or(false, |group| { - group_manager - .group_info(*group) - .map_or(false, |g| g.leader == entity) - && groups.get(invitee) == Some(group) - }); - if already_in_same_group { - // Inform of failure - if let Some(client) = clients.get(entity) { - client.send_fallible(ServerGeneral::server_msg( - ChatType::Meta, - "Invite failed, can't invite someone already in your group", - )); - } - return; - } - - let mut pending_invites = state.ecs().write_storage::(); - - // Check if group max size is already reached - // Adding the current number of pending invites - let group_size_limit_reached = state - .ecs() - .read_storage() - .get(entity) - .copied() - .and_then(|group| { - // If entity is currently the leader of a full group then they can't invite - // anyone else - group_manager - .group_info(group) - .filter(|i| i.leader == entity) - .map(|i| i.num_members) - }) - .unwrap_or(1) as usize - + pending_invites.get(entity).map_or(0, |p| p.0.len()) - >= max_group_size as usize; - if group_size_limit_reached { - // Inform inviter that they have reached the group size limit - if let Some(client) = clients.get(entity) { - client.send_fallible(ServerGeneral::server_msg( - ChatType::Meta, - "Invite failed, pending invites plus current group size have reached the \ - group size limit" - .to_owned(), - )); - } - return; - } - - let agents = state.ecs().read_storage::(); - let mut invites = state.ecs().write_storage::(); - - if invites.contains(invitee) { - // Inform inviter that there is already an invite - if let Some(client) = clients.get(entity) { - client.send_fallible(ServerGeneral::server_msg( - ChatType::Meta, - "This player already has a pending invite.", - )); - } - return; - } - - let mut invite_sent = false; - // Returns true if insertion was succesful - let mut send_invite = || { - match invites.insert(invitee, group::Invite(entity)) { - Err(err) => { - error!("Failed to insert Invite component: {:?}", err); - false - }, - Ok(_) => { - match pending_invites.entry(entity) { - Ok(entry) => { - entry - .or_insert_with(|| PendingInvites(Vec::new())) - .0 - .push((invitee, Instant::now() + INVITE_TIMEOUT_DUR)); - invite_sent = true; - true - }, - Err(err) => { - error!( - "Failed to get entry for pending invites component: {:?}", - err - ); - // Cleanup - invites.remove(invitee); - false - }, - } - }, - } - }; - - // If client comp - if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(entity).copied()) - { - if send_invite() { - client.send_fallible(ServerGeneral::GroupInvite { - inviter, - timeout: PRESENTED_INVITE_TIMEOUT_DUR, - }); - } - } else if agents.contains(invitee) { - send_invite(); - } else if let Some(client) = clients.get(entity) { + let clients = state.ecs().read_storage::(); + let invitee = match state.ecs().entity_from_uid(invitee_uid.into()) { + Some(t) => t, + None => { + // Inform of failure + if let Some(client) = clients.get(inviter) { client.send_fallible(ServerGeneral::server_msg( ChatType::Meta, - "Can't invite, not a player or npc", + "Invite failed, target does not exist.", )); } + return; + }, + }; - // Notify inviter that the invite is pending - if invite_sent { - if let Some(client) = clients.get(entity) { - client.send_fallible(ServerGeneral::InvitePending(uid)); - } + let uids = state.ecs().read_storage::(); + + // Check if entity is trying to invite themselves + if uids + .get(inviter) + .map_or(false, |inviter_uid| *inviter_uid == invitee_uid) + { + warn!("Entity tried to invite themselves into a group/trade"); + return; + } + + let mut pending_invites = state.ecs().write_storage::(); + + if let InviteKind::Group = kind { + // Disallow inviting entity that is already in your group + let groups = state.ecs().read_storage::(); + let group_manager = state.ecs().read_resource::(); + let already_in_same_group = groups.get(inviter).map_or(false, |group| { + group_manager + .group_info(*group) + .map_or(false, |g| g.leader == inviter) + && groups.get(invitee) == Some(group) + }); + if already_in_same_group { + // Inform of failure + if let Some(client) = clients.get(inviter) { + client.send_fallible(ServerGeneral::server_msg( + ChatType::Meta, + "Invite failed, can't invite someone already in your group", + )); } + return; + } + + // Check if group max size is already reached + // Adding the current number of pending invites + let group_size_limit_reached = state + .ecs() + .read_storage() + .get(inviter) + .copied() + .and_then(|group| { + // If entity is currently the leader of a full group then they can't invite + // anyone else + group_manager + .group_info(group) + .filter(|i| i.leader == inviter) + .map(|i| i.num_members) + }) + .unwrap_or(1) as usize + + pending_invites.get(inviter).map_or(0, |p| p.0.len()) + >= max_group_size as usize; + if group_size_limit_reached { + // Inform inviter that they have reached the group size limit + if let Some(client) = clients.get(inviter) { + client.send_fallible(ServerGeneral::server_msg( + ChatType::Meta, + "Invite failed, pending invites plus current group size have reached the \ + group size limit" + .to_owned(), + )); + } + return; + } + } + + let agents = state.ecs().read_storage::(); + let mut invites = state.ecs().write_storage::(); + + if invites.contains(invitee) { + // Inform inviter that there is already an invite + if let Some(client) = clients.get(inviter) { + client.send_fallible(ServerGeneral::server_msg( + ChatType::Meta, + "This player already has a pending invite.", + )); + } + return; + } + + let mut invite_sent = false; + // Returns true if insertion was succesful + let mut send_invite = || { + match invites.insert(invitee, group::Invite { inviter, kind }) { + Err(err) => { + error!("Failed to insert Invite component: {:?}", err); + false + }, + Ok(_) => { + match pending_invites.entry(inviter) { + Ok(entry) => { + entry + .or_insert_with(|| PendingInvites(Vec::new())) + .0 + .push((invitee, kind, Instant::now() + INVITE_TIMEOUT_DUR)); + invite_sent = true; + true + }, + Err(err) => { + error!( + "Failed to get entry for pending invites component: {:?}", + err + ); + // Cleanup + invites.remove(invitee); + false + }, + } + }, + } + }; + + // If client comp + if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(inviter).copied()) + { + if send_invite() { + client.send_fallible(ServerGeneral::GroupInvite { + inviter, + timeout: PRESENTED_INVITE_TIMEOUT_DUR, + kind, + }); + } + } else if agents.contains(invitee) { + send_invite(); + } else if let Some(client) = clients.get(inviter) { + client.send_fallible(ServerGeneral::server_msg( + ChatType::Meta, + "Can't invite, not a player or npc", + )); + } + + // Notify inviter that the invite is pending + if invite_sent { + if let Some(client) = clients.get(inviter) { + client.send_fallible(ServerGeneral::InvitePending(invitee_uid)); + } + } +} + +// TODO: turn chat messages into enums +pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) { + match manip { + GroupManip::Invite(uid) => { + handle_invite(server, entity, uid, InviteKind::Group); }, GroupManip::Accept => { + let state = server.state_mut(); let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); let mut invites = state.ecs().write_storage::(); - if let Some(inviter) = invites.remove(entity).and_then(|invite| { - let inviter = invite.0; + if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| { + let Invite { inviter, kind } = invite; let mut pending_invites = state.ecs().write_storage::(); let pending = &mut pending_invites.get_mut(inviter)?.0; // Check that inviter has a pending invite and remove it from the list @@ -193,7 +200,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani pending_invites.remove(inviter); } - Some(inviter) + Some((inviter, kind)) }) { if let (Some(client), Some(target)) = (clients.get(inviter), uids.get(entity).copied()) @@ -201,35 +208,39 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani client.send_fallible(ServerGeneral::InviteComplete { target, answer: InviteAnswer::Accepted, + kind, }); } - let mut group_manager = state.ecs().write_resource::(); - group_manager.add_group_member( - inviter, - entity, - &state.ecs().entities(), - &mut state.ecs().write_storage(), - &state.ecs().read_storage(), - &uids, - |entity, group_change| { - clients - .get(entity) - .and_then(|c| { - group_change - .try_map(|e| uids.get(e).copied()) - .map(|g| (g, c)) - }) - .map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g))); - }, - ); + if let InviteKind::Group = kind { + let mut group_manager = state.ecs().write_resource::(); + group_manager.add_group_member( + inviter, + entity, + &state.ecs().entities(), + &mut state.ecs().write_storage(), + &state.ecs().read_storage(), + &uids, + |entity, group_change| { + clients + .get(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g))); + }, + ); + } } }, GroupManip::Decline => { + let state = server.state_mut(); let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); let mut invites = state.ecs().write_storage::(); - if let Some(inviter) = invites.remove(entity).and_then(|invite| { - let inviter = invite.0; + if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| { + let Invite { inviter, kind } = invite; let mut pending_invites = state.ecs().write_storage::(); let pending = &mut pending_invites.get_mut(inviter)?.0; // Check that inviter has a pending invite and remove it from the list @@ -240,7 +251,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani pending_invites.remove(inviter); } - Some(inviter) + Some((inviter, kind)) }) { // Inform inviter of rejection if let (Some(client), Some(target)) = @@ -249,11 +260,13 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani client.send_fallible(ServerGeneral::InviteComplete { target, answer: InviteAnswer::Declined, + kind, }); } } }, GroupManip::Leave => { + let state = server.state_mut(); let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); let mut group_manager = state.ecs().write_resource::(); @@ -276,6 +289,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani ); }, GroupManip::Kick(uid) => { + let state = server.state_mut(); let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); let alignments = state.ecs().read_storage::(); @@ -379,6 +393,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani } }, GroupManip::AssignLeader(uid) => { + let state = server.state_mut(); let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); let target = match state.ecs().entity_from_uid(uid.into()) { diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index bf69e76cd1..7fac953a5d 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -1,8 +1,8 @@ use specs::{world::WorldExt, Entity as EcsEntity}; -use tracing::error; +use tracing::{error, warn}; use common::{ - comp::{self, agent::AgentEvent, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos}, + comp::{self, agent::AgentEvent, group::InviteKind, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos}, consts::MAX_MOUNT_RANGE, uid::Uid, }; @@ -10,6 +10,7 @@ use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use crate::{ client::Client, + events::group_manip::handle_invite, presence::{Presence, RegionSubscription}, Server, }; @@ -68,6 +69,14 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en } } +pub fn handle_initiate_trade(server: &mut Server, interactor: EcsEntity, counterparty: EcsEntity) { + if let Some(uid) = server.state_mut().ecs().uid_from_entity(counterparty) { + handle_invite(server, interactor, uid, InviteKind::Trade); + } else { + warn!("Entity tried to trade with an entity that lacks an uid"); + } +} + pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity) { let state = server.state_mut(); diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index a66352ee3d..fbeb82345d 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -13,7 +13,7 @@ use entity_manipulation::{ }; use group_manip::handle_group; use interaction::{ - handle_lantern, handle_mount, handle_npc_interaction, handle_possess, handle_unmount, + handle_lantern, handle_initiate_trade, handle_mount, handle_npc_interaction, handle_possess, handle_unmount, }; use inventory_manip::handle_inventory; use player::{handle_client_disconnect, handle_exit_ingame}; @@ -103,6 +103,9 @@ impl Server { ServerEvent::NpcInteract(interactor, target) => { handle_npc_interaction(self, interactor, target) }, + ServerEvent::InitiateTrade(interactor, target) => { + handle_initiate_trade(self, interactor, target) + }, ServerEvent::Mount(mounter, mountee) => handle_mount(self, mounter, mountee), ServerEvent::Unmount(mounter) => handle_unmount(self, mounter), ServerEvent::Possess(possessor_uid, possesse_uid) => { diff --git a/server/src/sys/invite_timeout.rs b/server/src/sys/invite_timeout.rs index 92995fba35..e0e4098c5a 100644 --- a/server/src/sys/invite_timeout.rs +++ b/server/src/sys/invite_timeout.rs @@ -32,13 +32,13 @@ impl<'a> System<'a> for Sys { let timed_out_invites = (&entities, &invites) .join() - .filter_map(|(invitee, Invite(inviter))| { + .filter_map(|(invitee, Invite { inviter, kind })| { // Retrieve timeout invite from pending invites let pending = &mut pending_invites.get_mut(*inviter)?.0; let index = pending.iter().position(|p| p.0 == invitee)?; // Stop if not timed out - if pending[index].1 > now { + if pending[index].2 > now { return None; } @@ -57,6 +57,7 @@ impl<'a> System<'a> for Sys { client.send_fallible(ServerGeneral::InviteComplete { target, answer: InviteAnswer::TimedOut, + kind: *kind, }); } diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 7de3595493..b6f4f79684 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -16,7 +16,7 @@ use crate::{ use client::{self, Client}; use common::{ combat, - comp::{group::Role, BuffKind, Stats}, + comp::{group::{InviteKind, Role}, BuffKind, Stats}, uid::{Uid, UidAllocator}, }; use common_net::sync::WorldSyncExt; @@ -219,7 +219,7 @@ impl<'a> Widget for Group<'a> { .crop_kids() .set(state.ids.bg, ui); } - if let Some((_, timeout_start, timeout_dur)) = open_invite { + if let Some((_, timeout_start, timeout_dur, kind)) = open_invite { // Group Menu button Button::image(self.imgs.group_icon) .w_h(49.0, 26.0) @@ -792,16 +792,26 @@ impl<'a> Widget for Group<'a> { // into the maximum group size. } } - if let Some((invite_uid, _, _)) = open_invite { + if let Some((invite_uid, _, _, kind)) = 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); + let invite_text = match kind { + InviteKind::Group => { + self + .localized_strings + .get("hud.group.invite_to_join") + .replace("{name}", &name) + }, + InviteKind::Trade => { + self + .localized_strings + .get("hud.group.invite_to_trade") + .replace("{name}", &name) + }, + }; Text::new(&invite_text) .mid_top_with_margin_on(state.ids.bg, 5.0) .font_size(12) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 8c15f9f750..f518b28b3d 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -2810,10 +2810,6 @@ impl Hud { self.show.toggle_bag(); true }, - GameInput::Trade if state => { - self.show.toggle_trade(); - true - }, GameInput::Social if state => { self.show.toggle_social(); true diff --git a/voxygen/src/hud/trade.rs b/voxygen/src/hud/trade.rs index 345cde15f4..b703c6cf40 100644 --- a/voxygen/src/hud/trade.rs +++ b/voxygen/src/hud/trade.rs @@ -6,9 +6,6 @@ use super::{ util::loadout_slot_text, Show, CRITICAL_HP_COLOR, LOW_HP_COLOR, QUALITY_COMMON, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, }; -use common::{ - comp::{item::Quality,}, -}; use crate::{ hud::get_quality_col, i18n::Localization, @@ -19,6 +16,7 @@ use crate::{ }, }; use client::Client; +use common::comp::item::Quality; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, @@ -129,36 +127,28 @@ impl<'a> Widget for Trade<'a> { // BG Image::new(self.imgs.inv_bg_bag) - .w_h(424.0, 708.0) - .middle() - .color(Some(UI_MAIN)) - .set(state.ids.bg, ui); + .w_h(424.0, 708.0) + .middle() + .color(Some(UI_MAIN)) + .set(state.ids.bg, ui); Image::new(self.imgs.inv_frame_bag) - .w_h(424.0, 708.0) - .middle_of(state.ids.bg) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.bg_frame, ui); + .w_h(424.0, 708.0) + .middle_of(state.ids.bg) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.bg_frame, ui); // Title - Text::new( - &self - .localized_strings - .get("hud.trade.trade_window") - ) - .mid_top_with_margin_on(state.ids.bg_frame, 9.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(20)) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.trade_title_bg, ui); - Text::new( - &self - .localized_strings - .get("hud.trade.trade_window") - ) - .top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(20)) - .color(TEXT_COLOR) - .set(state.ids.trade_title, ui); + Text::new(&self.localized_strings.get("hud.trade.trade_window")) + .mid_top_with_margin_on(state.ids.bg_frame, 9.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(20)) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.trade_title_bg, ui); + Text::new(&self.localized_strings.get("hud.trade.trade_window")) + .top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(20)) + .color(TEXT_COLOR) + .set(state.ids.trade_title, ui); event } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index a0dc128cdb..33c1f0b64a 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -9,7 +9,7 @@ use client::{self, Client}; use common::{ assets::AssetExt, comp, - comp::{inventory::slot::Slot, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel}, + comp::{inventory::slot::Slot, group::InviteKind, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel}, consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE}, outcome::Outcome, span, @@ -20,7 +20,7 @@ use common::{ }, vol::ReadVol, }; -use common_net::msg::PresenceKind; +use common_net::msg::{server::InviteAnswer, PresenceKind}; use crate::{ audio::sfx::SfxEvent, @@ -121,6 +121,24 @@ impl SessionState { client::Event::Chat(m) => { self.hud.new_message(m); }, + client::Event::InviteComplete { target, answer, kind } => { + // TODO: i18n + let kind_str = match kind { + InviteKind::Group => "Group", + InviteKind::Trade => "Trade", + }; + let target_name = match client.player_list().get(&target) { + Some(info) => info.player_alias.clone(), + None => "".to_string(), + }; + let answer_str = match answer { + InviteAnswer::Accepted => "accepted", + InviteAnswer::Declined => "declined", + InviteAnswer::TimedOut => "timed out", + }; + let msg = format!("{} invite to {} {}", kind_str, target_name, answer_str); + self.hud.new_message(ChatType::Meta.chat_msg(msg)); + } client::Event::InventoryUpdated(inv_event) => { let sfx_triggers = self.scene.sfx_mgr.triggers.read(); @@ -524,6 +542,33 @@ impl PlayState for SessionState { } } } + Event::InputUpdate(GameInput::Trade, state) + if state != self.key_state.collect => + { + self.key_state.collect = state; + + if state { + if let Some(interactable) = self.interactable { + let mut client = self.client.borrow_mut(); + match interactable { + Interactable::Block(_, _) => {}, + Interactable::Entity(entity) => { + if client + .state() + .ecs() + .read_storage::() + .get(entity) + .is_some() + { + client.pick_up(entity); + } else { + client.initiate_trade(entity); + } + }, + } + } + } + } /*Event::InputUpdate(GameInput::Charge, state) => { self.inputs.charge.set_state(state); },*/