Trade implementation progress.

- State machine for modifying trades.
- ServerGeneral/ClientGeneral messages.
This commit is contained in:
Avi Weinstock 2021-02-10 23:54:31 -05:00
parent e9b811b62b
commit ae528124fc
15 changed files with 258 additions and 78 deletions

View File

@ -69,7 +69,11 @@ 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 }, InviteComplete {
target: Uid,
answer: InviteAnswer,
kind: InviteKind,
},
Disconnect, Disconnect,
DisconnectionNotification(u64), DisconnectionNotification(u64),
InventoryUpdated(InventoryUpdateEvent), InventoryUpdated(InventoryUpdateEvent),
@ -534,7 +538,8 @@ impl Client {
| ClientGeneral::TerrainChunkRequest { .. } | ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::UnlockSkill(_) | ClientGeneral::UnlockSkill(_)
| ClientGeneral::RefundSkill(_) | ClientGeneral::RefundSkill(_)
| ClientGeneral::UnlockSkillGroup(_) => &mut self.in_game_stream, | ClientGeneral::UnlockSkillGroup(_)
| ClientGeneral::UpdatePendingTrade(_, _) => &mut self.in_game_stream,
//Always possible //Always possible
ClientGeneral::ChatMsg(_) | ClientGeneral::Terminate => { ClientGeneral::ChatMsg(_) | ClientGeneral::Terminate => {
&mut self.general_stream &mut self.general_stream
@ -638,7 +643,11 @@ impl Client {
} }
pub fn is_dead(&self) -> bool { pub fn is_dead(&self) -> bool {
self.state.ecs().read_storage::<comp::Health>().get(self.entity).map_or(false, |h| h.is_dead) 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) {
@ -674,7 +683,9 @@ impl Client {
} }
if let Some(uid) = self.state.read_component_copied(counterparty) { if let Some(uid) = self.state.read_component_copied(counterparty) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateTrade(uid))); self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateTrade(
uid,
)));
} }
} }
@ -741,7 +752,9 @@ 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, InviteKind)> { pub fn group_invite(
&self,
) -> Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)> {
self.group_invite self.group_invite
} }
@ -1472,7 +1485,11 @@ impl Client {
}, },
} }
}, },
ServerGeneral::GroupInvite { inviter, timeout, kind } => { ServerGeneral::GroupInvite {
inviter,
timeout,
kind,
} => {
self.group_invite = Some((inviter, std::time::Instant::now(), timeout, kind)); self.group_invite = Some((inviter, std::time::Instant::now(), timeout, kind));
}, },
ServerGeneral::InvitePending(uid) => { ServerGeneral::InvitePending(uid) => {
@ -1480,14 +1497,22 @@ impl Client {
warn!("Received message about pending invite that was already pending"); warn!("Received message about pending invite that was already pending");
} }
}, },
ServerGeneral::InviteComplete { target, answer, kind } => { 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"
) )
} }
frontend_events.push(Event::InviteComplete { target, answer, kind }); frontend_events.push(Event::InviteComplete {
target,
answer,
kind,
});
}, },
// 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

@ -4,6 +4,7 @@ use common::{
comp, comp,
comp::{Skill, SkillGroupKind}, comp::{Skill, SkillGroupKind},
terrain::block::Block, terrain::block::Block,
trade::TradeActionMsg,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vek::*; use vek::*;
@ -79,6 +80,7 @@ pub enum ClientGeneral {
//Always possible //Always possible
ChatMsg(String), ChatMsg(String),
Terminate, Terminate,
UpdatePendingTrade(usize, TradeActionMsg),
} }
impl ClientMsg { impl ClientMsg {
@ -114,7 +116,8 @@ impl ClientMsg {
| ClientGeneral::TerrainChunkRequest { .. } | ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::UnlockSkill(_) | ClientGeneral::UnlockSkill(_)
| ClientGeneral::RefundSkill(_) | ClientGeneral::RefundSkill(_)
| ClientGeneral::UnlockSkillGroup(_) => { | ClientGeneral::UnlockSkillGroup(_)
| ClientGeneral::UpdatePendingTrade(_, _) => {
c_type == ClientType::Game && presence.is_some() c_type == ClientType::Game && presence.is_some()
}, },
//Always possible //Always possible

View File

@ -7,6 +7,7 @@ use common::{
outcome::Outcome, outcome::Outcome,
recipe::RecipeBook, recipe::RecipeBook,
resources::TimeOfDay, resources::TimeOfDay,
trade::PendingTrade,
terrain::{Block, TerrainChunk}, terrain::{Block, TerrainChunk},
uid::Uid, uid::Uid,
}; };
@ -122,6 +123,7 @@ pub enum ServerGeneral {
Disconnect(DisconnectReason), Disconnect(DisconnectReason),
/// Send a popup notification such as "Waypoint Saved" /// Send a popup notification such as "Waypoint Saved"
Notification(Notification), Notification(Notification),
UpdatePendingTrade(usize, PendingTrade),
} }
impl ServerGeneral { impl ServerGeneral {
@ -227,7 +229,8 @@ impl ServerMsg {
| ServerGeneral::TerrainBlockUpdates(_) | ServerGeneral::TerrainBlockUpdates(_)
| ServerGeneral::SetViewDistance(_) | ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_) | ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) => { | ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _) => {
c_type == ClientType::Game && presence.is_some() c_type == ClientType::Game && presence.is_some()
}, },
// Always possible // Always possible

View File

@ -10,7 +10,6 @@ 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'.
@ -320,25 +319,3 @@ 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

@ -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, PendingTrade, SlotManip, InventoryManip, LoadoutManip, MountState, Mounting, SlotManip,
}; };
pub use energy::{Energy, EnergyChange, EnergySource}; pub use energy::{Energy, EnergyChange, EnergySource};
pub use group::Group; pub use group::Group;

View File

@ -49,6 +49,7 @@ pub mod states;
pub mod store; pub mod store;
pub mod terrain; pub mod terrain;
pub mod time; pub mod time;
pub mod trade;
pub mod typed; pub mod typed;
pub mod uid; pub mod uid;
pub mod util; pub mod util;

138
common/src/trade.rs Normal file
View File

@ -0,0 +1,138 @@
use crate::{comp::inventory::slot::InvSlotId, uid::Uid};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use tracing::warn;
/// Clients submit `TradeActionMsg` to the server, which adds the Uid of the
/// player out-of-band (i.e. without trusting the client to say who it's
/// accepting on behalf of)
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum TradeActionMsg {
AddItem { item: InvSlotId, quantity: usize },
RemoveItem { item: InvSlotId, quantity: usize },
Phase1Accept,
Phase2Accept,
}
/// 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 {
/// `parties[0]` is the entity that initiated the trade, parties[1] is the
/// other entity that's being traded with
pub parties: [Uid; 2],
/// `offers[i]` represents the items and quantities of the party i's items
/// being offered
pub offers: [HashMap<InvSlotId, usize>; 2],
/// phase1_accepts indicate that the parties wish to proceed to review
pub phase1_accepts: [bool; 2],
/// phase2_accepts indicate that the parties have reviewed the trade and
/// wish to commit it
pub phase2_accepts: [bool; 2],
}
impl PendingTrade {
pub fn new(party: Uid, counterparty: Uid) -> PendingTrade {
PendingTrade {
parties: [party, counterparty],
offers: [HashMap::new(), HashMap::new()],
phase1_accepts: [false, false],
phase2_accepts: [false, false],
}
}
pub fn in_phase1(&self) -> bool { !self.phase1_accepts[0] || !self.phase1_accepts[1] }
pub fn in_phase2(&self) -> bool {
(self.phase1_accepts[0] && self.phase1_accepts[1]) && (!self.phase2_accepts[0] || !self.phase2_accepts[1])
}
pub fn should_commit(&self) -> bool {
self.phase1_accepts[0] && self.phase1_accepts[1] && self.phase2_accepts[0] && self.phase2_accepts[1]
}
pub fn which_party(&self, party: Uid) -> Option<usize> {
self.parties
.iter()
.enumerate()
.find(|(_, x)| **x == party)
.map(|(i, _)| i)
}
pub fn process_msg(&mut self, who: usize, msg: TradeActionMsg) {
use TradeActionMsg::*;
match msg {
AddItem { item, quantity } => {
if self.in_phase1() {
let total = self.offers[who].entry(item).or_insert(0);
*total = total.saturating_add(quantity);
}
},
RemoveItem { item, quantity } => {
if self.in_phase1() {
let total = self.offers[who].entry(item).or_insert(0);
*total = total.saturating_sub(quantity);
}
},
Phase1Accept => {
if self.in_phase1() {
self.phase1_accepts[who] = true;
}
},
Phase2Accept => {
if self.in_phase2() {
self.phase2_accepts[who] = true;
}
},
}
}
}
pub struct Trades {
pub next_id: usize,
pub trades: HashMap<usize, PendingTrade>,
}
impl Trades {
pub fn begin_trade(&mut self, party: Uid, counterparty: Uid) -> usize {
let id = self.next_id;
self.next_id = id.wrapping_add(1);
self.trades
.insert(id, PendingTrade::new(party, counterparty));
id
}
pub fn process_trade_action(&mut self, id: usize, who: Uid, msg: TradeActionMsg) {
if let Some(trade) = self.trades.get_mut(&id) {
if let Some(party) = trade.which_party(who) {
trade.process_msg(party, msg);
} else {
warn!("An entity who is not a party to trade {} tried to modify it", id);
}
} else {
warn!("Attempt to modify nonexistent trade id {}", id);
}
}
pub fn decline_trade(&mut self, id: usize, who: Uid) {
if let Some(trade) = self.trades.remove(&id) {
if let None = trade.which_party(who) {
warn!("An entity who is not a party to trade {} tried to decline it", id);
// put it back
self.trades.insert(id, trade);
}
} else {
warn!("Attempt to decline nonexistent trade id {}", id);
}
}
}
impl Default for Trades {
fn default() -> Trades {
Trades {
next_id: 0,
trades: HashMap::new(),
}
}
}

View File

@ -102,7 +102,8 @@ impl<'a> System<'a> for Sys {
if let Some(counterparty_entity) = if let Some(counterparty_entity) =
uid_allocator.retrieve_entity_internal(counterparty_uid.id()) uid_allocator.retrieve_entity_internal(counterparty_uid.id())
{ {
server_emitter.emit(ServerEvent::InitiateTrade(entity, counterparty_entity)); server_emitter
.emit(ServerEvent::InitiateTrade(entity, counterparty_entity));
} }
}, },
ControlEvent::InventoryManip(manip) => { ControlEvent::InventoryManip(manip) => {

View File

@ -10,6 +10,7 @@ use common::{
span, span,
terrain::{Block, TerrainChunk, TerrainGrid}, terrain::{Block, TerrainChunk, TerrainGrid},
time::DayPeriod, time::DayPeriod,
trade::Trades,
vol::{ReadVol, WriteVol}, vol::{ReadVol, WriteVol},
}; };
use common_net::sync::WorldSyncExt; use common_net::sync::WorldSyncExt;
@ -190,6 +191,7 @@ impl State {
ecs.insert(RegionMap::new()); ecs.insert(RegionMap::new());
ecs.insert(SysMetrics::default()); ecs.insert(SysMetrics::default());
ecs.insert(PhysicsMetrics::default()); ecs.insert(PhysicsMetrics::default());
ecs.insert(Trades::default());
// Load plugins from asset directory // Load plugins from asset directory
#[cfg(feature = "plugins")] #[cfg(feature = "plugins")]

View File

@ -88,7 +88,8 @@ impl Client {
| ServerGeneral::TerrainBlockUpdates(_) | ServerGeneral::TerrainBlockUpdates(_)
| ServerGeneral::SetViewDistance(_) | ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_) | ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) => { | ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _) => {
self.in_game_stream.try_lock().unwrap().send(g) self.in_game_stream.try_lock().unwrap().send(g)
}, },
// Always possible // Always possible
@ -166,7 +167,10 @@ impl Client {
| ServerGeneral::TerrainBlockUpdates(_) | ServerGeneral::TerrainBlockUpdates(_)
| ServerGeneral::SetViewDistance(_) | ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_) | ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) => PreparedMsg::new(2, &g, &self.in_game_stream), | ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _) => {
PreparedMsg::new(2, &g, &self.in_game_stream)
},
// Always possible // Always possible
ServerGeneral::PlayerListUpdate(_) ServerGeneral::PlayerListUpdate(_)
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)

View File

@ -5,6 +5,7 @@ use common::{
group::{self, Group, GroupManager, Invite, InviteKind, PendingInvites}, group::{self, Group, GroupManager, Invite, InviteKind, PendingInvites},
ChatType, GroupManip, ChatType, GroupManip,
}, },
trade::Trades,
uid::Uid, uid::Uid,
}; };
use common_net::{ use common_net::{
@ -20,7 +21,12 @@ 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);
pub fn handle_invite(server: &mut Server, inviter: specs::Entity, invitee_uid: Uid, kind: InviteKind) { 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 max_group_size = server.settings().max_player_group_size;
let state = server.state_mut(); let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
@ -129,10 +135,11 @@ pub fn handle_invite(server: &mut Server, inviter: specs::Entity, invitee_uid: U
Ok(_) => { Ok(_) => {
match pending_invites.entry(inviter) { match pending_invites.entry(inviter) {
Ok(entry) => { Ok(entry) => {
entry entry.or_insert_with(|| PendingInvites(Vec::new())).0.push((
.or_insert_with(|| PendingInvites(Vec::new())) invitee,
.0 kind,
.push((invitee, kind, Instant::now() + INVITE_TIMEOUT_DUR)); Instant::now() + INVITE_TIMEOUT_DUR,
));
invite_sent = true; invite_sent = true;
true true
}, },
@ -151,8 +158,7 @@ pub fn handle_invite(server: &mut Server, inviter: specs::Entity, invitee_uid: U
}; };
// If client comp // If client comp
if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(inviter).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,
@ -211,26 +217,37 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
kind, kind,
}); });
} }
if let InviteKind::Group = kind { match kind {
let mut group_manager = state.ecs().write_resource::<GroupManager>(); InviteKind::Group => {
group_manager.add_group_member( let mut group_manager = state.ecs().write_resource::<GroupManager>();
inviter, group_manager.add_group_member(
entity, inviter,
&state.ecs().entities(), entity,
&mut state.ecs().write_storage(), &state.ecs().entities(),
&state.ecs().read_storage(), &mut state.ecs().write_storage(),
&uids, &state.ecs().read_storage(),
|entity, group_change| { &uids,
clients |entity, group_change| {
.get(entity) clients
.and_then(|c| { .get(entity)
group_change .and_then(|c| {
.try_map(|e| uids.get(e).copied()) group_change
.map(|g| (g, c)) .try_map(|e| uids.get(e).copied())
}) .map(|g| (g, c))
.map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g))); })
}, .map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g)));
); },
);
},
InviteKind::Trade => {
if let (Some(inviter_uid), Some(invitee_uid)) = (uids.get(inviter).copied(), uids.get(entity).copied()) {
let mut trades = state.ecs().write_resource::<Trades>();
let id = trades.begin_trade(inviter_uid, invitee_uid);
let trade = trades.trades[&id].clone();
clients.get(inviter).map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade.clone())));
clients.get(entity).map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade)));
}
},
} }
} }
}, },

View File

@ -2,7 +2,10 @@ use specs::{world::WorldExt, Entity as EcsEntity};
use tracing::{error, warn}; use tracing::{error, warn};
use common::{ use common::{
comp::{self, agent::AgentEvent, group::InviteKind, 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,
}; };

View File

@ -13,7 +13,8 @@ use entity_manipulation::{
}; };
use group_manip::handle_group; use group_manip::handle_group;
use interaction::{ use interaction::{
handle_lantern, handle_initiate_trade, handle_mount, handle_npc_interaction, handle_possess, handle_unmount, handle_initiate_trade, handle_lantern, 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};

View File

@ -16,7 +16,10 @@ use crate::{
use client::{self, Client}; use client::{self, Client};
use common::{ use common::{
combat, combat,
comp::{group::{InviteKind, 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;
@ -799,18 +802,14 @@ impl<'a> Widget for Group<'a> {
let name = uid_to_name_text(invite_uid, &self.client); let name = uid_to_name_text(invite_uid, &self.client);
let invite_text = match kind { let invite_text = match kind {
InviteKind::Group => { InviteKind::Group => self
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
InviteKind::Trade => {
self
.localized_strings .localized_strings
.get("hud.group.invite_to_trade") .get("hud.group.invite_to_trade")
.replace("{name}", &name) .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)

View File

@ -9,7 +9,9 @@ use client::{self, Client};
use common::{ use common::{
assets::AssetExt, assets::AssetExt,
comp, comp,
comp::{inventory::slot::Slot, group::InviteKind, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel}, comp::{
group::InviteKind, inventory::slot::Slot, 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,
@ -121,7 +123,11 @@ 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 } => { client::Event::InviteComplete {
target,
answer,
kind,
} => {
// TODO: i18n // TODO: i18n
let kind_str = match kind { let kind_str = match kind {
InviteKind::Group => "Group", InviteKind::Group => "Group",
@ -138,7 +144,7 @@ impl SessionState {
}; };
let msg = format!("{} invite to {} {}", kind_str, target_name, answer_str); let msg = format!("{} invite to {} {}", kind_str, target_name, answer_str);
self.hud.new_message(ChatType::Meta.chat_msg(msg)); 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();