Plumb trade requests through the group invite UI, such that they can be accepted/declined without impacting the counterparty's movement.

This commit is contained in:
Avi Weinstock 2021-02-10 19:59:14 -05:00
parent 250391656f
commit e9b811b62b
16 changed files with 367 additions and 257 deletions

View File

@ -5,6 +5,7 @@
string_map: { string_map: {
"hud.group": "Group", "hud.group": "Group",
"hud.group.invite_to_join": "{name} invited you to their 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.invite": "Invite",
"hud.group.kick": "Kick", "hud.group.kick": "Kick",
"hud.group.assign_leader": "Assign Leader", "hud.group.assign_leader": "Assign Leader",

View File

@ -20,7 +20,7 @@ use common::{
comp::{ comp::{
self, self,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
group, group::{self, InviteKind},
skills::Skill, skills::Skill,
slot::Slot, slot::Slot,
ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip,
@ -69,6 +69,7 @@ const PING_ROLLING_AVERAGE_SECS: usize = 10;
pub enum Event { pub enum Event {
Chat(comp::ChatMsg), Chat(comp::ChatMsg),
InviteComplete { target: Uid, answer: InviteAnswer, kind: InviteKind },
Disconnect, Disconnect,
DisconnectionNotification(u64), DisconnectionNotification(u64),
InventoryUpdated(InventoryUpdateEvent), InventoryUpdated(InventoryUpdateEvent),
@ -130,7 +131,7 @@ pub struct Client {
max_group_size: u32, max_group_size: u32,
// Client has received an invite (inviter uid, time out instant) // 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<Uid>, group_leader: Option<Uid>,
// Note: potentially representable as a client only component // Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>, group_members: HashMap<Uid, group::Role>,
@ -636,18 +637,16 @@ impl Client {
} }
} }
pub fn is_dead(&self) -> bool {
self.state.ecs().read_storage::<comp::Health>().get(self.entity).map_or(false, |h| h.is_dead)
}
pub fn pick_up(&mut self, entity: EcsEntity) { pub fn pick_up(&mut self, entity: EcsEntity) {
// Get the health component from the entity // Get the health component from the entity
if let Some(uid) = self.state.read_component_copied(entity) { if let Some(uid) = self.state.read_component_copied(entity) {
// If we're dead, exit before sending the message // If we're dead, exit before sending the message
if self if self.is_dead() {
.state
.ecs()
.read_storage::<comp::Health>()
.get(self.entity)
.map_or(false, |h| h.is_dead)
{
return; return;
} }
@ -659,13 +658,7 @@ impl Client {
pub fn npc_interact(&mut self, npc_entity: EcsEntity) { pub fn npc_interact(&mut self, npc_entity: EcsEntity) {
// If we're dead, exit before sending message // If we're dead, exit before sending message
if self if self.is_dead() {
.state
.ecs()
.read_storage::<comp::Health>()
.get(self.entity)
.map_or(false, |h| h.is_dead)
{
return; 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<Uid, PlayerInfo> { &self.player_list } pub fn player_list(&self) -> &HashMap<Uid, PlayerInfo> { &self.player_list }
pub fn character_list(&self) -> &CharacterList { &self.character_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 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 self.group_invite
} }
@ -1094,7 +1098,7 @@ impl Client {
// Check if the group invite has timed out and remove if so // Check if the group invite has timed out and remove if so
if self if self
.group_invite .group_invite
.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur) .map_or(false, |(_, timeout, dur, _)| timeout.elapsed() > dur)
{ {
self.group_invite = None; self.group_invite = None;
} }
@ -1468,30 +1472,22 @@ impl Client {
}, },
} }
}, },
ServerGeneral::GroupInvite { inviter, timeout } => { ServerGeneral::GroupInvite { inviter, timeout, kind } => {
self.group_invite = Some((inviter, std::time::Instant::now(), timeout)); self.group_invite = Some((inviter, std::time::Instant::now(), timeout, kind));
}, },
ServerGeneral::InvitePending(uid) => { ServerGeneral::InvitePending(uid) => {
if !self.pending_invites.insert(uid) { if !self.pending_invites.insert(uid) {
warn!("Received message about pending invite that was already pending"); 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) { if !self.pending_invites.remove(&target) {
warn!( warn!(
"Received completed invite message for invite that was not in the list of \ "Received completed invite message for invite that was not in the list of \
pending invites" pending invites"
) )
} }
// TODO: expose this as a new event variant instead of going frontend_events.push(Event::InviteComplete { target, answer, kind });
// 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)));
}, },
// Cleanup for when the client goes back to the `presence = None` // Cleanup for when the client goes back to the `presence = None`
ServerGeneral::ExitInGameSuccess => { ServerGeneral::ExitInGameSuccess => {

View File

@ -3,7 +3,7 @@ use crate::sync;
use authc::AuthClientError; use authc::AuthClientError;
use common::{ use common::{
character::{self, CharacterItem}, character::{self, CharacterItem},
comp, comp::{self, group::InviteKind},
outcome::Outcome, outcome::Outcome,
recipe::RecipeBook, recipe::RecipeBook,
resources::TimeOfDay, resources::TimeOfDay,
@ -80,6 +80,7 @@ pub enum ServerGeneral {
GroupInvite { GroupInvite {
inviter: sync::Uid, inviter: sync::Uid,
timeout: std::time::Duration, timeout: std::time::Duration,
kind: InviteKind,
}, },
/// Indicate to the client that their sent invite was not invalid and is /// Indicate to the client that their sent invite was not invalid and is
/// currently pending /// currently pending
@ -92,6 +93,7 @@ pub enum ServerGeneral {
InviteComplete { InviteComplete {
target: sync::Uid, target: sync::Uid,
answer: InviteAnswer, answer: InviteAnswer,
kind: InviteKind,
}, },
/// Trigger cleanup for when the client goes back to the `Registered` state /// Trigger cleanup for when the client goes back to the `Registered` state
/// from an ingame state /// from an ingame state

View File

@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage}; use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
use std::time::Duration; use std::time::Duration;
use hashbrown::HashMap;
use vek::*; use vek::*;
/// Default duration before an input is considered 'held'. /// Default duration before an input is considered 'held'.
@ -83,6 +84,7 @@ pub enum ControlEvent {
EnableLantern, EnableLantern,
DisableLantern, DisableLantern,
Interact(Uid), Interact(Uid),
InitiateTrade(Uid),
Mount(Uid), Mount(Uid),
Unmount, Unmount,
InventoryManip(InventoryManip), InventoryManip(InventoryManip),
@ -318,3 +320,25 @@ pub struct Mounting(pub Uid);
impl Component for Mounting { impl Component for Mounting {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>; type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
} }
/// 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<InvSlotId, usize>,
/// `other_offer` represents the items and quantities of the counterparty's items being offered
other_offer: HashMap<InvSlotId, usize>,
/// `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<Self, IdvStorage<Self>>;
}

View File

@ -28,14 +28,24 @@ impl Component for Group {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>; type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
} }
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 { impl Component for Invite {
type Storage = IdvStorage<Self>; type Storage = IdvStorage<Self>;
} }
// Pending invites that an entity currently has sent out // Pending invites that an entity currently has sent out
// (invited entity, instant when invite times 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 { impl Component for PendingInvites {
type Storage = IdvStorage<Self>; type Storage = IdvStorage<Self>;
} }

View File

@ -46,7 +46,7 @@ pub use chat::{
}; };
pub use controller::{ pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input, 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 energy::{Energy, EnergyChange, EnergySource};
pub use group::Group; pub use group::Group;

View File

@ -79,6 +79,7 @@ pub enum ServerEvent {
EnableLantern(EcsEntity), EnableLantern(EcsEntity),
DisableLantern(EcsEntity), DisableLantern(EcsEntity),
NpcInteract(EcsEntity, EcsEntity), NpcInteract(EcsEntity, EcsEntity),
InitiateTrade(EcsEntity, EcsEntity),
Mount(EcsEntity, EcsEntity), Mount(EcsEntity, EcsEntity),
Unmount(EcsEntity), Unmount(EcsEntity),
Possess(Uid, Uid), Possess(Uid, Uid),

View File

@ -98,6 +98,13 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::NpcInteract(entity, npc_entity)); 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) => { ControlEvent::InventoryManip(manip) => {
server_emitter.emit(ServerEvent::InventoryManip(entity, manip.into())); server_emitter.emit(ServerEvent::InventoryManip(entity, manip.into()));
}, },

View File

@ -2,7 +2,7 @@ use crate::{client::Client, Server};
use common::{ use common::{
comp::{ comp::{
self, self,
group::{self, Group, GroupManager, Invite, PendingInvites}, group::{self, Group, GroupManager, Invite, InviteKind, PendingInvites},
ChatType, GroupManip, ChatType, GroupManip,
}, },
uid::Uid, uid::Uid,
@ -20,19 +20,15 @@ const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31);
/// Reduced duration shown to the client to help alleviate latency issues /// Reduced duration shown to the client to help alleviate latency issues
const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30); const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30);
// TODO: turn chat messages into enums pub fn handle_invite(server: &mut Server, inviter: specs::Entity, invitee_uid: Uid, kind: InviteKind) {
pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) {
let max_group_size = server.settings().max_player_group_size; let max_group_size = server.settings().max_player_group_size;
let state = server.state_mut(); let state = server.state_mut();
match manip {
GroupManip::Invite(uid) => {
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let invitee = match state.ecs().entity_from_uid(uid.into()) { let invitee = match state.ecs().entity_from_uid(invitee_uid.into()) {
Some(t) => t, Some(t) => t,
None => { None => {
// Inform of failure // Inform of failure
if let Some(client) = clients.get(entity) { if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg( client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta, ChatType::Meta,
"Invite failed, target does not exist.", "Invite failed, target does not exist.",
@ -44,27 +40,30 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
// Check if entity is trying to invite themselves to a group // Check if entity is trying to invite themselves
if uids if uids
.get(entity) .get(inviter)
.map_or(false, |inviter_uid| *inviter_uid == uid) .map_or(false, |inviter_uid| *inviter_uid == invitee_uid)
{ {
warn!("Entity tried to invite themselves into a group"); warn!("Entity tried to invite themselves into a group/trade");
return; return;
} }
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
if let InviteKind::Group = kind {
// Disallow inviting entity that is already in your group // Disallow inviting entity that is already in your group
let groups = state.ecs().read_storage::<Group>(); let groups = state.ecs().read_storage::<Group>();
let group_manager = state.ecs().read_resource::<GroupManager>(); let group_manager = state.ecs().read_resource::<GroupManager>();
let already_in_same_group = groups.get(entity).map_or(false, |group| { let already_in_same_group = groups.get(inviter).map_or(false, |group| {
group_manager group_manager
.group_info(*group) .group_info(*group)
.map_or(false, |g| g.leader == entity) .map_or(false, |g| g.leader == inviter)
&& groups.get(invitee) == Some(group) && groups.get(invitee) == Some(group)
}); });
if already_in_same_group { if already_in_same_group {
// Inform of failure // Inform of failure
if let Some(client) = clients.get(entity) { if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg( client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta, ChatType::Meta,
"Invite failed, can't invite someone already in your group", "Invite failed, can't invite someone already in your group",
@ -73,29 +72,27 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
return; return;
} }
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
// Check if group max size is already reached // Check if group max size is already reached
// Adding the current number of pending invites // Adding the current number of pending invites
let group_size_limit_reached = state let group_size_limit_reached = state
.ecs() .ecs()
.read_storage() .read_storage()
.get(entity) .get(inviter)
.copied() .copied()
.and_then(|group| { .and_then(|group| {
// If entity is currently the leader of a full group then they can't invite // If entity is currently the leader of a full group then they can't invite
// anyone else // anyone else
group_manager group_manager
.group_info(group) .group_info(group)
.filter(|i| i.leader == entity) .filter(|i| i.leader == inviter)
.map(|i| i.num_members) .map(|i| i.num_members)
}) })
.unwrap_or(1) as usize .unwrap_or(1) as usize
+ pending_invites.get(entity).map_or(0, |p| p.0.len()) + pending_invites.get(inviter).map_or(0, |p| p.0.len())
>= max_group_size as usize; >= max_group_size as usize;
if group_size_limit_reached { if group_size_limit_reached {
// Inform inviter that they have reached the group size limit // Inform inviter that they have reached the group size limit
if let Some(client) = clients.get(entity) { if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg( client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta, ChatType::Meta,
"Invite failed, pending invites plus current group size have reached the \ "Invite failed, pending invites plus current group size have reached the \
@ -105,13 +102,14 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
} }
return; return;
} }
}
let agents = state.ecs().read_storage::<comp::Agent>(); let agents = state.ecs().read_storage::<comp::Agent>();
let mut invites = state.ecs().write_storage::<Invite>(); let mut invites = state.ecs().write_storage::<Invite>();
if invites.contains(invitee) { if invites.contains(invitee) {
// Inform inviter that there is already an invite // Inform inviter that there is already an invite
if let Some(client) = clients.get(entity) { if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg( client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta, ChatType::Meta,
"This player already has a pending invite.", "This player already has a pending invite.",
@ -123,18 +121,18 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
let mut invite_sent = false; let mut invite_sent = false;
// Returns true if insertion was succesful // Returns true if insertion was succesful
let mut send_invite = || { let mut send_invite = || {
match invites.insert(invitee, group::Invite(entity)) { match invites.insert(invitee, group::Invite { inviter, kind }) {
Err(err) => { Err(err) => {
error!("Failed to insert Invite component: {:?}", err); error!("Failed to insert Invite component: {:?}", err);
false false
}, },
Ok(_) => { Ok(_) => {
match pending_invites.entry(entity) { match pending_invites.entry(inviter) {
Ok(entry) => { Ok(entry) => {
entry entry
.or_insert_with(|| PendingInvites(Vec::new())) .or_insert_with(|| PendingInvites(Vec::new()))
.0 .0
.push((invitee, Instant::now() + INVITE_TIMEOUT_DUR)); .push((invitee, kind, Instant::now() + INVITE_TIMEOUT_DUR));
invite_sent = true; invite_sent = true;
true true
}, },
@ -153,17 +151,18 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
}; };
// If client comp // If client comp
if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(entity).copied()) if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(inviter).copied())
{ {
if send_invite() { if send_invite() {
client.send_fallible(ServerGeneral::GroupInvite { client.send_fallible(ServerGeneral::GroupInvite {
inviter, inviter,
timeout: PRESENTED_INVITE_TIMEOUT_DUR, timeout: PRESENTED_INVITE_TIMEOUT_DUR,
kind,
}); });
} }
} else if agents.contains(invitee) { } else if agents.contains(invitee) {
send_invite(); send_invite();
} else if let Some(client) = clients.get(entity) { } else if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg( client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta, ChatType::Meta,
"Can't invite, not a player or npc", "Can't invite, not a player or npc",
@ -172,17 +171,25 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
// Notify inviter that the invite is pending // Notify inviter that the invite is pending
if invite_sent { if invite_sent {
if let Some(client) = clients.get(entity) { if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::InvitePending(uid)); 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 => { GroupManip::Accept => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
let mut invites = state.ecs().write_storage::<Invite>(); let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| { if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0; let Invite { inviter, kind } = invite;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>(); let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0; let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list // 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); pending_invites.remove(inviter);
} }
Some(inviter) Some((inviter, kind))
}) { }) {
if let (Some(client), Some(target)) = if let (Some(client), Some(target)) =
(clients.get(inviter), uids.get(entity).copied()) (clients.get(inviter), uids.get(entity).copied())
@ -201,8 +208,10 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
client.send_fallible(ServerGeneral::InviteComplete { client.send_fallible(ServerGeneral::InviteComplete {
target, target,
answer: InviteAnswer::Accepted, answer: InviteAnswer::Accepted,
kind,
}); });
} }
if let InviteKind::Group = kind {
let mut group_manager = state.ecs().write_resource::<GroupManager>(); let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.add_group_member( group_manager.add_group_member(
inviter, inviter,
@ -223,13 +232,15 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
}, },
); );
} }
}
}, },
GroupManip::Decline => { GroupManip::Decline => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
let mut invites = state.ecs().write_storage::<Invite>(); let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| { if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0; let Invite { inviter, kind } = invite;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>(); let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0; let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list // 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); pending_invites.remove(inviter);
} }
Some(inviter) Some((inviter, kind))
}) { }) {
// Inform inviter of rejection // Inform inviter of rejection
if let (Some(client), Some(target)) = 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 { client.send_fallible(ServerGeneral::InviteComplete {
target, target,
answer: InviteAnswer::Declined, answer: InviteAnswer::Declined,
kind,
}); });
} }
} }
}, },
GroupManip::Leave => { GroupManip::Leave => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
let mut group_manager = state.ecs().write_resource::<GroupManager>(); let mut group_manager = state.ecs().write_resource::<GroupManager>();
@ -276,6 +289,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
); );
}, },
GroupManip::Kick(uid) => { GroupManip::Kick(uid) => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
let alignments = state.ecs().read_storage::<comp::Alignment>(); let alignments = state.ecs().read_storage::<comp::Alignment>();
@ -379,6 +393,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
} }
}, },
GroupManip::AssignLeader(uid) => { GroupManip::AssignLeader(uid) => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
let target = match state.ecs().entity_from_uid(uid.into()) { let target = match state.ecs().entity_from_uid(uid.into()) {

View File

@ -1,8 +1,8 @@
use specs::{world::WorldExt, Entity as EcsEntity}; use specs::{world::WorldExt, Entity as EcsEntity};
use tracing::error; use tracing::{error, warn};
use common::{ 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, consts::MAX_MOUNT_RANGE,
uid::Uid, uid::Uid,
}; };
@ -10,6 +10,7 @@ use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use crate::{ use crate::{
client::Client, client::Client,
events::group_manip::handle_invite,
presence::{Presence, RegionSubscription}, presence::{Presence, RegionSubscription},
Server, 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) { pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity) {
let state = server.state_mut(); let state = server.state_mut();

View File

@ -13,7 +13,7 @@ use entity_manipulation::{
}; };
use group_manip::handle_group; use group_manip::handle_group;
use interaction::{ 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 inventory_manip::handle_inventory;
use player::{handle_client_disconnect, handle_exit_ingame}; use player::{handle_client_disconnect, handle_exit_ingame};
@ -103,6 +103,9 @@ impl Server {
ServerEvent::NpcInteract(interactor, target) => { ServerEvent::NpcInteract(interactor, target) => {
handle_npc_interaction(self, 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::Mount(mounter, mountee) => handle_mount(self, mounter, mountee),
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter), ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
ServerEvent::Possess(possessor_uid, possesse_uid) => { ServerEvent::Possess(possessor_uid, possesse_uid) => {

View File

@ -32,13 +32,13 @@ impl<'a> System<'a> for Sys {
let timed_out_invites = (&entities, &invites) let timed_out_invites = (&entities, &invites)
.join() .join()
.filter_map(|(invitee, Invite(inviter))| { .filter_map(|(invitee, Invite { inviter, kind })| {
// Retrieve timeout invite from pending invites // Retrieve timeout invite from pending invites
let pending = &mut pending_invites.get_mut(*inviter)?.0; let pending = &mut pending_invites.get_mut(*inviter)?.0;
let index = pending.iter().position(|p| p.0 == invitee)?; let index = pending.iter().position(|p| p.0 == invitee)?;
// Stop if not timed out // Stop if not timed out
if pending[index].1 > now { if pending[index].2 > now {
return None; return None;
} }
@ -57,6 +57,7 @@ impl<'a> System<'a> for Sys {
client.send_fallible(ServerGeneral::InviteComplete { client.send_fallible(ServerGeneral::InviteComplete {
target, target,
answer: InviteAnswer::TimedOut, answer: InviteAnswer::TimedOut,
kind: *kind,
}); });
} }

View File

@ -16,7 +16,7 @@ use crate::{
use client::{self, Client}; use client::{self, Client};
use common::{ use common::{
combat, combat,
comp::{group::Role, BuffKind, Stats}, comp::{group::{InviteKind, Role}, BuffKind, Stats},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
}; };
use common_net::sync::WorldSyncExt; use common_net::sync::WorldSyncExt;
@ -219,7 +219,7 @@ impl<'a> Widget for Group<'a> {
.crop_kids() .crop_kids()
.set(state.ids.bg, ui); .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 // Group Menu button
Button::image(self.imgs.group_icon) Button::image(self.imgs.group_icon)
.w_h(49.0, 26.0) .w_h(49.0, 26.0)
@ -792,16 +792,26 @@ impl<'a> Widget for Group<'a> {
// into the maximum group size. // 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 self.show.group = true; // Auto open group menu
// TODO: add group name here too // TODO: add group name here too
// Invite text // Invite text
let name = uid_to_name_text(invite_uid, &self.client); let name = uid_to_name_text(invite_uid, &self.client);
let invite_text = self let invite_text = match kind {
InviteKind::Group => {
self
.localized_strings .localized_strings
.get("hud.group.invite_to_join") .get("hud.group.invite_to_join")
.replace("{name}", &name); .replace("{name}", &name)
},
InviteKind::Trade => {
self
.localized_strings
.get("hud.group.invite_to_trade")
.replace("{name}", &name)
},
};
Text::new(&invite_text) Text::new(&invite_text)
.mid_top_with_margin_on(state.ids.bg, 5.0) .mid_top_with_margin_on(state.ids.bg, 5.0)
.font_size(12) .font_size(12)

View File

@ -2810,10 +2810,6 @@ impl Hud {
self.show.toggle_bag(); self.show.toggle_bag();
true true
}, },
GameInput::Trade if state => {
self.show.toggle_trade();
true
},
GameInput::Social if state => { GameInput::Social if state => {
self.show.toggle_social(); self.show.toggle_social();
true true

View File

@ -6,9 +6,6 @@ use super::{
util::loadout_slot_text, util::loadout_slot_text,
Show, CRITICAL_HP_COLOR, LOW_HP_COLOR, QUALITY_COMMON, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, Show, CRITICAL_HP_COLOR, LOW_HP_COLOR, QUALITY_COMMON, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
}; };
use common::{
comp::{item::Quality,},
};
use crate::{ use crate::{
hud::get_quality_col, hud::get_quality_col,
i18n::Localization, i18n::Localization,
@ -19,6 +16,7 @@ use crate::{
}, },
}; };
use client::Client; use client::Client;
use common::comp::item::Quality;
use conrod_core::{ use conrod_core::{
color, color,
widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget::{self, Button, Image, Rectangle, Scrollbar, Text},
@ -139,21 +137,13 @@ impl<'a> Widget for Trade<'a> {
.color(Some(UI_HIGHLIGHT_0)) .color(Some(UI_HIGHLIGHT_0))
.set(state.ids.bg_frame, ui); .set(state.ids.bg_frame, ui);
// Title // Title
Text::new( Text::new(&self.localized_strings.get("hud.trade.trade_window"))
&self
.localized_strings
.get("hud.trade.trade_window")
)
.mid_top_with_margin_on(state.ids.bg_frame, 9.0) .mid_top_with_margin_on(state.ids.bg_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20)) .font_size(self.fonts.cyri.scale(20))
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.trade_title_bg, ui); .set(state.ids.trade_title_bg, ui);
Text::new( Text::new(&self.localized_strings.get("hud.trade.trade_window"))
&self
.localized_strings
.get("hud.trade.trade_window")
)
.top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0) .top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20)) .font_size(self.fonts.cyri.scale(20))

View File

@ -9,7 +9,7 @@ use client::{self, Client};
use common::{ use common::{
assets::AssetExt, assets::AssetExt,
comp, 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}, consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
outcome::Outcome, outcome::Outcome,
span, span,
@ -20,7 +20,7 @@ use common::{
}, },
vol::ReadVol, vol::ReadVol,
}; };
use common_net::msg::PresenceKind; use common_net::msg::{server::InviteAnswer, PresenceKind};
use crate::{ use crate::{
audio::sfx::SfxEvent, audio::sfx::SfxEvent,
@ -121,6 +121,24 @@ impl SessionState {
client::Event::Chat(m) => { client::Event::Chat(m) => {
self.hud.new_message(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 => "<unknown>".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) => { client::Event::InventoryUpdated(inv_event) => {
let sfx_triggers = self.scene.sfx_mgr.triggers.read(); 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::<comp::Item>()
.get(entity)
.is_some()
{
client.pick_up(entity);
} else {
client.initiate_trade(entity);
}
},
}
}
}
}
/*Event::InputUpdate(GameInput::Charge, state) => { /*Event::InputUpdate(GameInput::Charge, state) => {
self.inputs.charge.set_state(state); self.inputs.charge.set_state(state);
},*/ },*/