veloren_server/events/
invite.rs

1use super::{
2    ServerEvent, event_dispatch,
3    group_manip::{self, update_map_markers},
4};
5use crate::{Settings, client::Client};
6use common::{
7    comp::{
8        self, CharacterState, ChatType, Content, Group, Health, Pos,
9        agent::{Agent, AgentEvent},
10        group::GroupManager,
11        invite::{Invite, InviteKind, InviteResponse, PendingInvites},
12    },
13    consts::MAX_TRADE_RANGE,
14    event::{InitiateInviteEvent, InviteResponseEvent},
15    trade::{TradeResult, Trades},
16    uid::{IdMaps, Uid},
17};
18use common_net::msg::{InviteAnswer, ServerGeneral};
19#[cfg(feature = "worldgen")]
20use specs::ReadExpect;
21use specs::{
22    DispatcherBuilder, Entities, Entity, Read, ReadStorage, SystemData, Write, WriteStorage, shred,
23};
24use std::time::{Duration, Instant};
25use tracing::{error, warn};
26#[cfg(feature = "worldgen")]
27use world::IndexOwned;
28
29/// Time before invite times out
30const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31);
31/// Reduced duration shown to the client to help alleviate latency issues
32const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30);
33
34pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
35    event_dispatch::<InitiateInviteEvent>(builder, &[]);
36    event_dispatch::<InviteResponseEvent>(builder, &[]);
37}
38
39impl ServerEvent for InitiateInviteEvent {
40    type SystemData<'a> = (
41        Entities<'a>,
42        Write<'a, Trades>,
43        Read<'a, Settings>,
44        Read<'a, IdMaps>,
45        Read<'a, GroupManager>,
46        WriteStorage<'a, PendingInvites>,
47        WriteStorage<'a, Agent>,
48        WriteStorage<'a, Invite>,
49        ReadStorage<'a, Uid>,
50        ReadStorage<'a, Client>,
51        ReadStorage<'a, Pos>,
52        ReadStorage<'a, Group>,
53        ReadStorage<'a, Health>,
54        ReadStorage<'a, CharacterState>,
55    );
56
57    fn handle(
58        events: impl ExactSizeIterator<Item = Self>,
59        (
60            entities,
61            mut trades,
62            settings,
63            id_maps,
64            group_manager,
65            mut pending_invites,
66            mut agents,
67            mut invites,
68            uids,
69            clients,
70            positions,
71            groups,
72            healths,
73            character_states,
74        ): Self::SystemData<'_>,
75    ) {
76        for InitiateInviteEvent(inviter, invitee_uid, kind) in events {
77            let max_group_size = settings.max_player_group_size;
78            let invitee = match id_maps.uid_entity(invitee_uid) {
79                Some(t) => t,
80                None => {
81                    // Inform of failure
82                    if let Some(client) = clients.get(inviter) {
83                        client.send_fallible(ServerGeneral::server_msg(
84                            ChatType::Meta,
85                            Content::Plain("Invite failed, target does not exist.".to_string()),
86                        ));
87                    }
88                    continue;
89                },
90            };
91
92            // Check if entity is trying to invite themselves
93            if uids
94                .get(inviter)
95                .is_some_and(|inviter_uid| *inviter_uid == invitee_uid)
96            {
97                warn!("Entity tried to invite themselves into a group/trade");
98                continue;
99            }
100
101            if matches!(kind, InviteKind::Trade) {
102                let is_alive_and_well = |entity| {
103                    entities.is_alive(entity)
104                        && !comp::is_downed_or_dead(
105                            healths.get(entity),
106                            character_states.get(entity),
107                        )
108                };
109                // Check whether the inviter is in range of the invitee or dead
110                if !within_trading_range(positions.get(inviter), positions.get(invitee))
111                    || !is_alive_and_well(inviter)
112                    || !is_alive_and_well(invitee)
113                {
114                    continue;
115                }
116            }
117
118            if let InviteKind::Group = kind {
119                if !group_manip::can_invite(
120                    &clients,
121                    &groups,
122                    &group_manager,
123                    &mut pending_invites,
124                    max_group_size,
125                    inviter,
126                    invitee,
127                ) {
128                    continue;
129                }
130            } else {
131                // cancel current trades for inviter before inviting someone else to trade
132                if let Some(inviter_uid) = uids.get(inviter).copied() {
133                    if let Some(active_trade) = trades.entity_trades.get(&inviter_uid).copied() {
134                        trades
135                            .decline_trade(active_trade, inviter_uid)
136                            .and_then(|u| id_maps.uid_entity(u))
137                            .map(|e| {
138                                if let Some(client) = clients.get(e) {
139                                    client.send_fallible(ServerGeneral::FinishedTrade(
140                                        TradeResult::Declined,
141                                    ));
142                                }
143                                if let Some(agent) = agents.get_mut(e) {
144                                    agent.inbox.push_back(AgentEvent::FinishedTrade(
145                                        TradeResult::Declined,
146                                    ));
147                                }
148                            });
149                    }
150                };
151            }
152
153            if invites.contains(invitee) {
154                // Inform inviter that there is already an invite
155                if let Some(client) = clients.get(inviter) {
156                    client.send_fallible(ServerGeneral::server_msg(
157                        ChatType::Meta,
158                        Content::Plain("This player already has a pending invite.".to_string()),
159                    ));
160                }
161                continue;
162            }
163
164            let mut invite_sent = false;
165            // Returns true if insertion was successful
166            let mut send_invite = || {
167                match invites.insert(invitee, Invite { inviter, kind }) {
168                    Err(err) => {
169                        error!("Failed to insert Invite component: {:?}", err);
170                        false
171                    },
172                    Ok(_) => {
173                        match pending_invites.entry(inviter) {
174                            Ok(entry) => {
175                                entry.or_insert_with(|| PendingInvites(Vec::new())).0.push((
176                                    invitee,
177                                    kind,
178                                    Instant::now() + INVITE_TIMEOUT_DUR,
179                                ));
180                                invite_sent = true;
181                                true
182                            },
183                            Err(err) => {
184                                error!(
185                                    "Failed to get entry for pending invites component: {:?}",
186                                    err
187                                );
188                                // Cleanup
189                                invites.remove(invitee);
190                                false
191                            },
192                        }
193                    },
194                }
195            };
196
197            // If client comp
198            if let (Some(client), Some(inviter)) =
199                (clients.get(invitee), uids.get(inviter).copied())
200            {
201                if send_invite() {
202                    client.send_fallible(ServerGeneral::Invite {
203                        inviter,
204                        timeout: PRESENTED_INVITE_TIMEOUT_DUR,
205                        kind,
206                    });
207                }
208            } else if let Some(agent) = agents.get_mut(invitee) {
209                if send_invite() {
210                    if let Some(inviter) = uids.get(inviter) {
211                        agent.inbox.push_back(AgentEvent::TradeInvite(*inviter));
212                        invite_sent = true;
213                    }
214                }
215            } else if let Some(client) = clients.get(inviter) {
216                client.send_fallible(ServerGeneral::server_msg(
217                    ChatType::Meta,
218                    Content::Plain("Can't invite, not a player or npc".to_string()),
219                ));
220            }
221
222            // Notify inviter that the invite is pending
223            if invite_sent {
224                if let Some(client) = clients.get(inviter) {
225                    client.send_fallible(ServerGeneral::InvitePending(invitee_uid));
226                }
227            }
228        }
229    }
230}
231
232#[derive(SystemData)]
233pub struct InviteResponseData<'a> {
234    entities: Entities<'a>,
235    group_manager: Write<'a, GroupManager>,
236    trades: Write<'a, Trades>,
237    #[cfg(feature = "worldgen")]
238    index: ReadExpect<'a, IndexOwned>,
239    id_maps: Read<'a, IdMaps>,
240    invites: WriteStorage<'a, Invite>,
241    pending_invites: WriteStorage<'a, PendingInvites>,
242    groups: WriteStorage<'a, Group>,
243    agents: WriteStorage<'a, comp::Agent>,
244    uids: ReadStorage<'a, Uid>,
245    clients: ReadStorage<'a, Client>,
246    alignments: ReadStorage<'a, comp::Alignment>,
247    map_markers: ReadStorage<'a, comp::MapMarker>,
248}
249
250impl ServerEvent for InviteResponseEvent {
251    type SystemData<'a> = InviteResponseData<'a>;
252
253    fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
254        for InviteResponseEvent(entity, response) in events {
255            match response {
256                InviteResponse::Accept => handle_invite_accept(&mut data, entity),
257                InviteResponse::Decline => handle_invite_decline(&mut data, entity),
258            }
259        }
260    }
261}
262
263pub fn handle_invite_accept(data: &mut InviteResponseData, entity: Entity) {
264    if let Some((inviter, kind)) = get_inviter_and_kind(entity, data) {
265        handle_invite_answer(data, inviter, entity, InviteAnswer::Accepted, kind);
266
267        match kind {
268            InviteKind::Group => {
269                data.group_manager.add_group_member(
270                    inviter,
271                    entity,
272                    &data.entities,
273                    &mut data.groups,
274                    &data.alignments,
275                    &data.uids,
276                    |entity, group_change| {
277                        data.clients
278                            .get(entity)
279                            .and_then(|c| {
280                                group_change
281                                    .try_map_ref(|e| data.uids.get(*e).copied())
282                                    .map(|g| (g, c))
283                            })
284                            .map(|(g, c)| {
285                                update_map_markers(&data.map_markers, &data.uids, c, &group_change);
286                                c.send_fallible(ServerGeneral::GroupUpdate(g));
287                            });
288                    },
289                );
290            },
291            InviteKind::Trade => {
292                if let (Some(inviter_uid), Some(invitee_uid)) = (
293                    data.uids.get(inviter).copied(),
294                    data.uids.get(entity).copied(),
295                ) {
296                    // check if the person that invited me has started a new trade since the
297                    // invitation was sent
298                    if data
299                        .trades
300                        .entity_trades
301                        .get(&inviter_uid)
302                        .copied()
303                        .is_some()
304                    {
305                        for client in data
306                            .clients
307                            .get(entity)
308                            .into_iter()
309                            .chain(data.clients.get(inviter))
310                        {
311                            client.send_fallible(ServerGeneral::server_msg(
312                                ChatType::Meta,
313                                Content::Plain(
314                                    "Trade failed, inviter initiated new trade since sending \
315                                     trade request."
316                                        .to_string(),
317                                ),
318                            ));
319                        }
320                        return;
321                    }
322                    let id = data.trades.begin_trade(inviter_uid, invitee_uid);
323                    let trade = data.trades.trades[&id].clone();
324                    if let Some(agent) = data.agents.get_mut(inviter) {
325                        agent
326                            .inbox
327                            .push_back(AgentEvent::TradeAccepted(invitee_uid));
328                    }
329                    #[cfg(feature = "worldgen")]
330                    let pricing = data
331                        .agents
332                        .get(inviter)
333                        .and_then(|a| {
334                            a.behavior
335                                .trade_site()
336                                .and_then(|id| data.index.get_site_prices(id))
337                        })
338                        .or_else(|| {
339                            data.agents.get(entity).and_then(|a| {
340                                a.behavior
341                                    .trade_site()
342                                    .and_then(|id| data.index.get_site_prices(id))
343                            })
344                        });
345                    #[cfg(not(feature = "worldgen"))]
346                    let pricing = None;
347
348                    data.clients.get(inviter).map(|c| {
349                        c.send(ServerGeneral::UpdatePendingTrade(
350                            id,
351                            trade.clone(),
352                            pricing.clone(),
353                        ))
354                    });
355                    data.clients
356                        .get(entity)
357                        .map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade, pricing)));
358                }
359            },
360        }
361    }
362}
363
364fn get_inviter_and_kind(
365    entity: Entity,
366    data: &mut InviteResponseData,
367) -> Option<(Entity, InviteKind)> {
368    data.invites.remove(entity).and_then(|invite| {
369        let Invite { inviter, kind } = invite;
370        let pending = &mut data.pending_invites.get_mut(inviter)?.0;
371        // Check that inviter has a pending invite and remove it from the list
372        let invite_index = pending.iter().position(|p| p.0 == entity)?;
373        pending.swap_remove(invite_index);
374        // If no pending invites remain remove the component
375        if pending.is_empty() {
376            data.pending_invites.remove(inviter);
377        }
378
379        Some((inviter, kind))
380    })
381}
382
383fn handle_invite_answer(
384    data: &mut InviteResponseData,
385    inviter: Entity,
386    entity: Entity,
387    invite_answer: InviteAnswer,
388    kind: InviteKind,
389) {
390    if matches!(kind, InviteKind::Trade) && matches!(invite_answer, InviteAnswer::Accepted) {
391        // invitee must close current trade if one exists before accepting new one
392        if let Some(invitee_uid) = data.uids.get(entity).copied() {
393            if let Some(active_trade) = data.trades.entity_trades.get(&invitee_uid).copied() {
394                data.trades
395                    .decline_trade(active_trade, invitee_uid)
396                    .and_then(|u| data.id_maps.uid_entity(u))
397                    .map(|e| {
398                        if let Some(client) = data.clients.get(e) {
399                            client
400                                .send_fallible(ServerGeneral::FinishedTrade(TradeResult::Declined));
401                        }
402                        if let Some(agent) = data.agents.get_mut(e) {
403                            agent
404                                .inbox
405                                .push_back(AgentEvent::FinishedTrade(TradeResult::Declined));
406                        }
407                    });
408            }
409        };
410    }
411    if let (Some(client), Some(target)) =
412        (data.clients.get(inviter), data.uids.get(entity).copied())
413    {
414        client.send_fallible(ServerGeneral::InviteComplete {
415            target,
416            answer: invite_answer,
417            kind,
418        });
419    }
420}
421
422pub fn handle_invite_decline(data: &mut InviteResponseData, entity: Entity) {
423    if let Some((inviter, kind)) = get_inviter_and_kind(entity, data) {
424        // Inform inviter of rejection
425        handle_invite_answer(data, inviter, entity, InviteAnswer::Declined, kind)
426    }
427}
428
429fn within_trading_range(requester_position: Option<&Pos>, invitee_position: Option<&Pos>) -> bool {
430    match (requester_position, invitee_position) {
431        (Some(rpos), Some(ipos)) => rpos.0.distance_squared(ipos.0) < MAX_TRADE_RANGE.powi(2),
432        _ => false,
433    }
434}