Add timeout's to group invites, and configurable limit to group size

Fix a few group bugs, enable invite timeout and group limits in ui
This commit is contained in:
Imbris 2020-08-06 21:59:28 -04:00 committed by Monty Marz
parent def68302c7
commit 390d289d35
17 changed files with 467 additions and 172 deletions

View File

@ -5,3 +5,5 @@ rustflags = [
[alias]
generate = "run --package tools --"
test-server = "run --bin veloren-server-cli --no-default-features"

32
Cargo.lock generated
View File

@ -219,16 +219,6 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "auth-common"
version = "0.1.0"
source = "git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086#223a4097f7ebc8d451936dccb5e6517194bbf086"
dependencies = [
"rand 0.7.3",
"serde",
"uuid",
]
[[package]]
name = "auth-common"
version = "0.1.0"
@ -239,26 +229,12 @@ dependencies = [
"uuid",
]
[[package]]
name = "authc"
version = "1.0.0"
source = "git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086#223a4097f7ebc8d451936dccb5e6517194bbf086"
dependencies = [
"auth-common 0.1.0 (git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086)",
"fxhash",
"hex",
"rust-argon2 0.8.2",
"serde_json",
"ureq",
"uuid",
]
[[package]]
name = "authc"
version = "1.0.0"
source = "git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe#b943c85e4a38f5ec60cd18c34c73097640162bfe"
dependencies = [
"auth-common 0.1.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)",
"auth-common",
"fxhash",
"hex",
"rust-argon2 0.8.2",
@ -4625,7 +4601,7 @@ dependencies = [
name = "veloren-client"
version = "0.6.0"
dependencies = [
"authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)",
"authc",
"byteorder 1.3.4",
"futures-executor",
"futures-timer",
@ -4646,7 +4622,7 @@ name = "veloren-common"
version = "0.6.0"
dependencies = [
"arraygen",
"authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086)",
"authc",
"criterion",
"crossbeam",
"dot_vox",
@ -4674,7 +4650,7 @@ dependencies = [
name = "veloren-server"
version = "0.6.0"
dependencies = [
"authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)",
"authc",
"chrono",
"crossbeam",
"diesel",

View File

@ -23,7 +23,7 @@ use common::{
msg::{
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification,
PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg,
MAX_BYTES_CHAT_MSG,
MAX_BYTES_CHAT_MSG, InviteAnswer,
},
recipe::RecipeBook,
state::State,
@ -79,9 +79,14 @@ pub struct Client {
recipe_book: RecipeBook,
available_recipes: HashSet<String>,
group_invite: Option<Uid>,
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_leader: Option<Uid>,
// Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>,
// Pending invites that this client has sent out
pending_invites: HashSet<Uid>,
_network: Network,
participant: Option<Participant>,
@ -130,13 +135,14 @@ impl Client {
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
// Wait for initial sync
let (state, entity, server_info, world_map, recipe_book) = block_on(async {
let (state, entity, server_info, world_map, recipe_book, max_group_size) = block_on(async {
loop {
match stream.recv().await? {
ServerMsg::InitialSync {
entity_package,
server_info,
time_of_day,
max_group_size,
world_map: (map_size, world_map),
recipe_book,
} => {
@ -188,6 +194,7 @@ impl Client {
server_info,
(world_map, map_size),
recipe_book,
max_group_size,
));
},
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
@ -212,14 +219,16 @@ impl Client {
server_info,
world_map,
player_list: HashMap::new(),
group_members: HashMap::new(),
character_list: CharacterList::default(),
active_character_id: None,
recipe_book,
available_recipes: HashSet::default(),
max_group_size,
group_invite: None,
group_leader: None,
group_members: HashMap::new(),
pending_invites: HashSet::new(),
_network: network,
participant: Some(participant),
@ -432,7 +441,9 @@ impl Client {
.unwrap();
}
pub fn group_invite(&self) -> Option<Uid> { self.group_invite }
pub fn max_group_size(&self) -> u32 { self.max_group_size }
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> { self.group_invite }
pub fn group_info(&self) -> Option<(String, Uid)> {
self.group_leader.map(|l| ("Group".into(), l)) // TODO
@ -440,6 +451,8 @@ impl Client {
pub fn group_members(&self) -> &HashMap<Uid, group::Role> { &self.group_members }
pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
pub fn send_group_invite(&mut self, invitee: Uid) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
@ -758,6 +771,10 @@ impl Client {
frontend_events.append(&mut self.handle_new_messages()?);
// 3) Update client local data
// Check if the group invite has timed out and remove if so
if self.group_invite.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur) {
self.group_invite = None;
}
// 4) Tick the client's LocalState
self.state.tick(dt, add_foreign_systems, true);
@ -1046,9 +1063,28 @@ impl Client {
},
}
},
ServerMsg::GroupInvite(uid) => {
self.group_invite = Some(uid);
ServerMsg::GroupInvite { inviter, timeout } => {
self.group_invite = Some((inviter, std::time::Instant::now(), timeout));
},
ServerMsg::InvitePending(uid) => {
if !self.pending_invites.insert(uid) {
warn!("Received message about pending invite that was already pending");
}
}
ServerMsg::InviteComplete { target, answer } => {
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)));
}
ServerMsg::Ping => {
self.singleton_stream.send(ClientMsg::Pong)?;
},

View File

@ -9,9 +9,11 @@ use tracing::{error, warn};
// Primitive group system
// Shortcomings include:
// - no support for more complex group structures
// - lack of complex enemy npc integration
// - lack of npc group integration
// - relies on careful management of groups to maintain a valid state
// - the possesion rod could probably wreck this
// - clients don't know which pets are theirs (could be easy to solve by
// putting owner uid in Role::Pet)
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Group(u32);
@ -26,11 +28,26 @@ impl Component for Group {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}
pub struct Invite(pub specs::Entity);
impl Component for Invite {
type Storage = IdvStorage<Self>;
}
// 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)>);
impl Component for PendingInvites {
type Storage = IdvStorage<Self>;
}
#[derive(Clone, Debug)]
pub struct GroupInfo {
// TODO: what about enemy groups, either the leader will constantly change because they have to
// be loaded or we create a dummy entity or this needs to be optional
pub leader: specs::Entity,
// Number of group members (excluding pets)
pub num_members: u32,
// Name of the group
pub name: String,
}
@ -134,9 +151,14 @@ impl GroupManager {
self.groups.get(group.0 as usize)
}
fn create_group(&mut self, leader: specs::Entity) -> Group {
fn group_info_mut(&mut self, group: Group) -> Option<&mut GroupInfo> {
self.groups.get_mut(group.0 as usize)
}
fn create_group(&mut self, leader: specs::Entity, num_members: u32) -> Group {
Group(self.groups.insert(GroupInfo {
leader,
num_members,
name: "Group".into(),
}) as u32)
}
@ -204,15 +226,20 @@ impl GroupManager {
None => None,
};
let group = group.unwrap_or_else(|| {
let new_group = self.create_group(leader);
let group = if let Some(group) = group {
// Increment group size
// Note: unwrap won't fail since we just retrieved the group successfully above
self.group_info_mut(group).unwrap().num_members += 1;
group
} else {
let new_group = self.create_group(leader, 2);
// Unwrap should not fail since we just found these entities and they should
// still exist Note: if there is an issue replace with a warn
groups.insert(leader, new_group).unwrap();
// Inform
notifier(leader, ChangeNotification::NewLeader(leader));
new_group
});
};
let new_pets = pets(new_member, new_member_uid, alignments, entities);
@ -256,7 +283,7 @@ impl GroupManager {
let group = match groups.get(owner).copied() {
Some(group) => group,
None => {
let new_group = self.create_group(owner);
let new_group = self.create_group(owner, 1);
groups.insert(owner, new_group).unwrap();
// Inform
notifier(owner, ChangeNotification::NewLeader(owner));
@ -348,7 +375,7 @@ impl GroupManager {
(entities, uids, &*groups, alignments.maybe())
.join()
.filter(|(e, _, g, _)| **g == group && (!to_be_deleted || *e == member))
.filter(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member))
.fold(
HashMap::<Uid, (Option<specs::Entity>, Vec<specs::Entity>)>::new(),
|mut acc, (e, uid, _, alignment)| {
@ -356,9 +383,11 @@ impl GroupManager {
Alignment::Owned(owner) if uid != owner => Some(owner),
_ => None,
}) {
// A pet
// Assumes owner will be in the group
acc.entry(*owner).or_default().1.push(e);
} else {
// Not a pet
acc.entry(*uid).or_default().0 = Some(e);
}
@ -375,7 +404,7 @@ impl GroupManager {
members.push((owner, Role::Member));
// New group
let new_group = self.create_group(owner);
let new_group = self.create_group(owner, 1);
for (member, _) in &members {
groups.insert(*member, new_group).unwrap();
}
@ -401,7 +430,7 @@ impl GroupManager {
let leaving_member_uid = if let Some(uid) = uids.get(member) {
*uid
} else {
error!("Failed to retrieve uid for the new group member");
error!("Failed to retrieve uid for the leaving member");
return;
};
@ -409,7 +438,7 @@ impl GroupManager {
// If pets and not about to be deleted form new group
if !leaving_pets.is_empty() && !to_be_deleted {
let new_group = self.create_group(member);
let new_group = self.create_group(member, 1);
notifier(member, ChangeNotification::NewGroup {
leader: member,
@ -432,12 +461,11 @@ impl GroupManager {
});
}
if let Some(info) = self.group_info(group) {
if let Some(info) = self.group_info_mut(group) {
info.num_members -= 1;
// Inform remaining members
let mut num_members = 0;
members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| {
num_members += 1;
match role {
members(group, &*groups, entities, alignments, uids).for_each(
|(e, role)| match role {
Role::Member => {
notifier(e, ChangeNotification::Removed(member));
leaving_pets.iter().for_each(|p| {
@ -445,16 +473,16 @@ impl GroupManager {
})
},
Role::Pet => {},
}
});
},
);
// If leader is the last one left then disband the group
// Assumes last member is the leader
if num_members == 1 {
if info.num_members == 1 {
let leader = info.leader;
self.remove_group(group);
groups.remove(leader);
notifier(leader, ChangeNotification::NoGroup);
} else if num_members == 0 {
} else if info.num_members == 0 {
error!("Somehow group has no members")
}
}

View File

@ -7,7 +7,7 @@ pub use self::{
client::ClientMsg,
ecs_packet::EcsCompPacket,
server::{
CharacterInfo, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
CharacterInfo, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
RequestStateError, ServerInfo, ServerMsg,
},
};

View File

@ -47,6 +47,13 @@ pub struct CharacterInfo {
pub level: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InviteAnswer {
Accepted,
Declined,
TimedOut,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Notification {
WaypointSaved,
@ -59,6 +66,7 @@ pub enum ServerMsg {
entity_package: sync::EntityPackage<EcsCompPacket>,
server_info: ServerInfo,
time_of_day: state::TimeOfDay,
max_group_size: u32,
world_map: (Vec2<u32>, Vec<u32>),
recipe_book: RecipeBook,
},
@ -70,7 +78,21 @@ pub enum ServerMsg {
CharacterActionError(String),
PlayerListUpdate(PlayerListUpdate),
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
GroupInvite(sync::Uid),
// Indicate to the client that they are invited to join a group
GroupInvite {
inviter: sync::Uid,
timeout: std::time::Duration,
},
// Indicate to the client that their sent invite was not invalid and is currently pending
InvitePending(sync::Uid),
// Note: this could potentially include all the failure cases such as inviting yourself in
// which case the `InvitePending` message could be removed and the client could consider their
// invite pending until they receive this message
// Indicate to the client the result of their invite
InviteComplete {
target: sync::Uid,
answer: InviteAnswer,
},
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
/// Trigger cleanup for when the client goes back to the `Registered` state
/// from an ingame state

View File

@ -158,6 +158,8 @@ impl State {
ecs.register::<comp::ItemDrop>();
ecs.register::<comp::ChatMode>();
ecs.register::<comp::Faction>();
ecs.register::<comp::group::Invite>();
ecs.register::<comp::group::PendingInvites>();
// Register synced resources used by the ECS.
ecs.insert(TimeOfDay(0.0));

View File

@ -3,9 +3,11 @@ use crate::{
self,
agent::Activity,
group,
group::Invite,
item::{tool::ToolKind, ItemKind},
Agent, Alignment, Body, CharacterState, ControlAction, Controller, Loadout, MountState,
Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, Vel,
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller,
GroupManip, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg,
Vel,
},
event::{EventBus, ServerEvent},
path::{Chaser, TraversalConfig},
@ -49,6 +51,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Agent>,
WriteStorage<'a, Controller>,
ReadStorage<'a, MountState>,
ReadStorage<'a, Invite>,
);
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
@ -77,6 +80,7 @@ impl<'a> System<'a> for Sys {
mut agents,
mut controllers,
mount_states,
invites,
): Self::SystemData,
) {
for (
@ -494,5 +498,23 @@ impl<'a> System<'a> for Sys {
debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
}
// Proccess group invites
for (_invite, alignment, agent, controller) in
(&invites, &alignments, &mut agents, &mut controllers).join()
{
let accept = matches!(alignment, Alignment::Npc);
if accept {
// Clear agent comp
*agent = Agent::default();
controller
.events
.push(ControlEvent::GroupManip(GroupManip::Accept));
} else {
controller
.events
.push(ControlEvent::GroupManip(GroupManip::Decline));
}
}
}
}

View File

@ -18,7 +18,6 @@ pub struct Client {
pub network_error: AtomicBool,
pub last_ping: f64,
pub login_msg_sent: bool,
pub invited_to_group: Option<specs::Entity>,
}
impl Component for Client {

View File

@ -2,18 +2,25 @@ use crate::{client::Client, Server};
use common::{
comp::{
self,
group::{self, GroupManager},
group::{self, Group, GroupManager, Invite, PendingInvites},
ChatType, GroupManip,
},
msg::ServerMsg,
msg::{InviteAnswer, ServerMsg},
sync,
sync::WorldSyncExt,
};
use specs::world::WorldExt;
use tracing::warn;
use std::time::{Duration, Instant};
use tracing::{error, warn};
/// Time before invite times out
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) {
let max_group_size = server.settings().max_player_group_size;
let state = server.state_mut();
match manip {
@ -44,38 +51,62 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
return;
}
let alignments = state.ecs().read_storage::<comp::Alignment>();
let agents = state.ecs().read_storage::<comp::Agent>();
let mut already_has_invite = false;
let mut add_to_group = false;
// If client comp
if let (Some(client), Some(inviter_uid)) = (clients.get_mut(invitee), uids.get(entity))
{
if client.invited_to_group.is_some() {
already_has_invite = true;
} else {
client.notify(ServerMsg::GroupInvite(*inviter_uid));
client.invited_to_group = Some(entity);
// Disallow inviting entity that is already in your group
let groups = state.ecs().read_storage::<Group>();
let group_manager = state.ecs().read_resource::<GroupManager>();
if groups.get(entity).map_or(false, |group| {
group_manager
.group_info(*group)
.map_or(false, |g| g.leader == entity)
&& groups.get(invitee) == Some(group)
}) {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
"Invite failed, can't invite someone already in your group".to_owned(),
));
}
// Would be cool to do this in agent system (e.g. add an invited
// component to replace the field on Client)
// TODO: move invites to component and make them time out
} else if matches!(
(alignments.get(invitee), agents.get(invitee)),
(Some(comp::Alignment::Npc), Some(_))
) {
add_to_group = true;
// Wipe agent state
drop(agents);
let _ = state
.ecs()
.write_storage()
.insert(invitee, comp::Agent::default());
} else if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg("Invite rejected.".to_owned()));
return;
}
if already_has_invite {
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
// Check if group max size is already reached
// Adding the current number of pending invites
if 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
{
// Inform inviter that they have reached the group size limit
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta.server_msg(
"Invite failed, pending invites plus current group size have reached \
the group size limit"
.to_owned(),
),
);
}
return;
}
let agents = state.ecs().read_storage::<comp::Agent>();
let mut invites = state.ecs().write_storage::<Invite>();
if invites.contains(invitee) {
// Inform inviter that there is already an invite
if let Some(client) = clients.get_mut(entity) {
client.notify(
@ -83,37 +114,94 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
.server_msg("This player already has a pending invite.".to_owned()),
);
}
return;
}
if add_to_group {
let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.add_group_member(
entity,
invitee,
&state.ecs().entities(),
&mut state.ecs().write_storage(),
&alignments,
&uids,
|entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
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_mut(invitee), uids.get(entity).copied())
{
if send_invite() {
client.notify(ServerMsg::GroupInvite {
inviter,
timeout: PRESENTED_INVITE_TIMEOUT_DUR,
});
}
} else if agents.contains(invitee) {
send_invite();
} else {
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta.server_msg("Can't invite, not a player or npc".to_owned()),
);
}
}
// Notify inviter that the invite is pending
if invite_sent {
if let Some(client) = clients.get_mut(entity) {
client.notify(ServerMsg::InvitePending(uid));
}
}
},
GroupManip::Accept => {
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
if let Some(inviter) = clients
.get_mut(entity)
.and_then(|c| c.invited_to_group.take())
{
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list
let invite_index = pending.iter().position(|p| p.0 == entity)?;
pending.swap_remove(invite_index);
// If no pending invites remain remove the component
if pending.is_empty() {
pending_invites.remove(inviter);
}
Some(inviter)
}) {
if let (Some(client), Some(target)) =
(clients.get_mut(inviter), uids.get(entity).copied())
{
client.notify(ServerMsg::InviteComplete {
target,
answer: InviteAnswer::Accepted,
})
}
let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.add_group_member(
inviter,
@ -137,14 +225,30 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
},
GroupManip::Decline => {
let mut clients = state.ecs().write_storage::<Client>();
if let Some(inviter) = clients
.get_mut(entity)
.and_then(|c| c.invited_to_group.take())
{
let uids = state.ecs().read_storage::<sync::Uid>();
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list
let invite_index = pending.iter().position(|p| p.0 == entity)?;
pending.swap_remove(invite_index);
// If no pending invites remain remove the component
if pending.is_empty() {
pending_invites.remove(inviter);
}
Some(inviter)
}) {
// Inform inviter of rejection
if let Some(client) = clients.get_mut(inviter) {
// TODO: say who declined the invite
client.notify(ChatType::Meta.server_msg("Invite declined.".to_owned()));
if let (Some(client), Some(target)) =
(clients.get_mut(inviter), uids.get(entity).copied())
{
client.notify(ServerMsg::InviteComplete {
target,
answer: InviteAnswer::Declined,
})
}
}
},

View File

@ -59,7 +59,7 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
&state.ecs().entities(),
&state.ecs().read_storage(),
&state.ecs().read_storage(),
// Nothing actually changing
// Nothing actually changing since Uid is transferred
|_, _| {},
);
}

View File

@ -1,6 +1,6 @@
#![deny(unsafe_code)]
#![allow(clippy::option_map_unit_fn)]
#![feature(drain_filter, option_zip)]
#![feature(bool_to_option, drain_filter, option_zip)]
pub mod alias_validator;
pub mod chunk_generator;
@ -127,6 +127,7 @@ impl Server {
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default());
state.ecs_mut().insert(sys::WaypointTimer::default());
state.ecs_mut().insert(sys::InviteTimeoutTimer::default());
state.ecs_mut().insert(sys::PersistenceTimer::default());
// System schedulers to control execution of systems
@ -508,12 +509,13 @@ impl Server {
.nanos as i64;
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().nanos as i64;
let waypoint_nanos = self.state.ecs().read_resource::<sys::WaypointTimer>().nanos as i64;
let invite_timeout_nanos = self.state.ecs().read_resource::<sys::InviteTimeoutTimer>().nanos as i64;
let stats_persistence_nanos = self
.state
.ecs()
.read_resource::<sys::PersistenceTimer>()
.nanos as i64;
let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos;
let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos + invite_timeout_nanos;
// Report timing info
self.tick_metrics
@ -575,6 +577,10 @@ impl Server {
.tick_time
.with_label_values(&["waypoint"])
.set(waypoint_nanos);
self.tick_metrics
.tick_time
.with_label_values(&["invite timeout"])
.set(invite_timeout_nanos);
self.tick_metrics
.tick_time
.with_label_values(&["persistence:stats"])
@ -656,7 +662,6 @@ impl Server {
network_error: std::sync::atomic::AtomicBool::new(false),
last_ping: self.state.get_time(),
login_msg_sent: false,
invited_to_group: None,
};
if self.settings().max_players
@ -685,6 +690,7 @@ impl Server {
.create_entity_package(entity, None, None, None),
server_info: self.get_server_info(),
time_of_day: *self.state.ecs().read_resource(),
max_group_size: self.settings().max_player_group_size,
world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()),
recipe_book: (&*default_recipe_book()).clone(),
});

View File

@ -26,6 +26,7 @@ pub struct ServerSettings {
pub persistence_db_dir: String,
pub max_view_distance: Option<u32>,
pub banned_words_files: Vec<PathBuf>,
pub max_player_group_size: u32,
}
impl Default for ServerSettings {
@ -65,6 +66,7 @@ impl Default for ServerSettings {
persistence_db_dir: "saves".to_owned(),
max_view_distance: Some(30),
banned_words_files: Vec::new(),
max_player_group_size: 6,
}
}
}

View File

@ -0,0 +1,71 @@
use super::SysTimer;
use crate::client::Client;
use common::{
comp::group::{Invite, PendingInvites},
msg::{InviteAnswer, ServerMsg},
sync::Uid,
};
use specs::{Entities, Join, ReadStorage, System, Write, WriteStorage};
/// This system removes timed out group invites
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)] // TODO: Pending review in #587
type SystemData = (
Entities<'a>,
WriteStorage<'a, Invite>,
WriteStorage<'a, PendingInvites>,
WriteStorage<'a, Client>,
ReadStorage<'a, Uid>,
Write<'a, SysTimer<Self>>,
);
fn run(
&mut self,
(entities, mut invites, mut pending_invites, mut clients, uids, mut timer): Self::SystemData,
) {
timer.start();
let now = std::time::Instant::now();
let timed_out_invites = (&entities, &invites)
.join()
.filter_map(|(invitee, Invite(inviter))| {
// 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 {
return None;
}
// Remove pending entry
pending.swap_remove(index);
// If no pending invites remain remove the component
if pending.is_empty() {
pending_invites.remove(*inviter);
}
// Inform inviter of timeout
if let (Some(client), Some(target)) =
(clients.get_mut(*inviter), uids.get(invitee).copied())
{
client.notify(ServerMsg::InviteComplete {
target,
answer: InviteAnswer::TimedOut,
})
}
Some(invitee)
})
.collect::<Vec<_>>();
for entity in timed_out_invites {
invites.remove(entity);
}
timer.end();
}
}

View File

@ -1,4 +1,5 @@
pub mod entity_sync;
pub mod invite_timeout;
pub mod message;
pub mod object;
pub mod persistence;
@ -21,6 +22,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type InviteTimeoutTimer = SysTimer<invite_timeout::Sys>;
pub type PersistenceTimer = SysTimer<persistence::Sys>;
pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
@ -32,12 +34,14 @@ pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
//const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys";
const WAYPOINT_SYS: &str = "server_waypoint_sys";
const INVITE_TIMEOUT_SYS: &str = "server_invite_timeout_sys";
const PERSISTENCE_SYS: &str = "server_persistence_sys";
const OBJECT_SYS: &str = "server_object_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
dispatch_builder.add(invite_timeout::Sys, INVITE_TIMEOUT_SYS, &[]);
dispatch_builder.add(persistence::Sys, PERSISTENCE_SYS, &[]);
dispatch_builder.add(object::Sys, OBJECT_SYS, &[]);
}

View File

@ -182,23 +182,22 @@ impl<'a> Widget for Group<'a> {
.crop_kids()
.set(state.ids.bg, ui);
}
if open_invite.is_some() {
if let Some((_, timeout_start, timeout_dur)) = open_invite {
// Group Menu button
Button::image(self.imgs.group_icon)
.w_h(49.0, 26.0)
.bottom_left_with_margins_on(ui.window, 10.0, 490.0)
.set(state.ids.group_button, ui);
// Show timeout bar
let max_time = 90.0;
let time = 50.0;
let progress_perc = time / max_time;
let timeout_progress =
1.0 - timeout_start.elapsed().as_secs_f32() / timeout_dur.as_secs_f32();
Image::new(self.imgs.progress_frame)
.w_h(100.0, 10.0)
.middle_of(state.ids.bg)
.color(Some(UI_MAIN))
.set(state.ids.timeout_bg, ui);
Image::new(self.imgs.progress)
.w_h(98.0 * progress_perc, 8.0)
.w_h(98.0 * timeout_progress as f64, 8.0)
.top_left_with_margins_on(state.ids.timeout_bg, 1.0, 1.0)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.timeout, ui);
@ -613,7 +612,7 @@ impl<'a> Widget for Group<'a> {
// into the maximum group size.
}
}
if let Some(invite_uid) = open_invite {
if let Some((invite_uid, _, _)) = open_invite {
self.show.group = true; // Auto open group menu
// TODO: add group name here too
// Invite text

View File

@ -8,7 +8,7 @@ use crate::{
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
};
use client::{self, Client};
use common::sync::Uid;
use common::{comp::group, sync::Uid};
use conrod_core::{
color,
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
@ -467,68 +467,90 @@ impl<'a> Widget for Social<'a> {
}
// Invite Button
let selected_ingame = state
.selected_uid
.as_ref()
.map(|(s, _)| *s)
.filter(|selected| {
self.client
.player_list
.get(selected)
.map_or(false, |selected_player| {
selected_player.is_online && selected_player.character.is_some()
let is_leader_or_not_in_group = self
.client
.group_info()
.map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid));
let current_members = self
.client
.group_members()
.iter()
.filter(|(_, role)| matches!(role, group::Role::Member))
.count()
+ 1;
let current_invites = self.client.pending_invites().len();
let max_members = self.client.max_group_size() as usize;
let group_not_full = current_members + current_invites < max_members;
let selected_to_invite = (is_leader_or_not_in_group && group_not_full)
.then(|| {
state
.selected_uid
.as_ref()
.map(|(s, _)| *s)
.filter(|selected| {
self.client
.player_list
.get(selected)
.map_or(false, |selected_player| {
selected_player.is_online && selected_player.character.is_some()
})
})
.or_else(|| {
self.selected_entity
.and_then(|s| self.client.state().read_component_copied(s.0))
})
.filter(|selected| {
// Prevent inviting entities already in the same group
!self.client.group_members().contains_key(selected)
})
})
.or_else(|| {
self.selected_entity
.and_then(|s| self.client.state().read_component_copied(s.0))
});
// TODO: Prevent inviting players with the same group uid
// TODO: Show current amount of group members as a tooltip for the invite button
// if the player is the group leader TODO: Grey out the invite
// button if the group has 6/6 members
let current_members = 4;
let tooltip_txt = if selected_ingame.is_some() {
format!(
"{}/6 {}",
&current_members,
&self.localized_strings.get("hud.group.members")
)
} else {
(&self.localized_strings.get("hud.group.members")).to_string()
};
if Button::image(self.imgs.button)
.flatten();
let invite_button = Button::image(self.imgs.button)
.w_h(106.0, 26.0)
.bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0)
.hover_image(if selected_ingame.is_some() {
.hover_image(if selected_to_invite.is_some() {
self.imgs.button_hover
} else {
self.imgs.button
})
.press_image(if selected_ingame.is_some() {
.press_image(if selected_to_invite.is_some() {
self.imgs.button_press
} else {
self.imgs.button
})
.label(&self.localized_strings.get("hud.group.invite"))
.label(self.localized_strings.get("hud.group.invite"))
.label_y(conrod_core::position::Relative::Scalar(3.0))
.label_color(if selected_ingame.is_some() {
.label_color(if selected_to_invite.is_some() {
TEXT_COLOR
} else {
TEXT_COLOR_3
})
.image_color(if selected_ingame.is_some() {
.image_color(if selected_to_invite.is_some() {
TEXT_COLOR
} else {
TEXT_COLOR_3
})
.label_font_size(self.fonts.cyri.scale(15))
.label_font_id(self.fonts.cyri.conrod_id)
.with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip)
.set(state.ids.invite_button, ui)
.was_clicked()
.label_font_id(self.fonts.cyri.conrod_id);
if if self.client.group_info().is_some() {
let tooltip_txt = format!(
"{}/{} {}",
current_members + current_invites,
max_members,
&self.localized_strings.get("hud.group.members")
);
invite_button
.with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip)
.set(state.ids.invite_button, ui)
} else {
invite_button.set(state.ids.invite_button, ui)
}
.was_clicked()
{
if let Some(uid) = selected_ingame {
if let Some(uid) = selected_to_invite {
events.push(Event::Invite(uid));
state.update(|s| {
s.selected_uid = None;