mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
parent
def68302c7
commit
390d289d35
@ -4,4 +4,6 @@ rustflags = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[alias]
|
[alias]
|
||||||
generate = "run --package tools --"
|
generate = "run --package tools --"
|
||||||
|
test-server = "run --bin veloren-server-cli --no-default-features"
|
||||||
|
|
||||||
|
32
Cargo.lock
generated
32
Cargo.lock
generated
@ -219,16 +219,6 @@ dependencies = [
|
|||||||
"winapi 0.3.8",
|
"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]]
|
[[package]]
|
||||||
name = "auth-common"
|
name = "auth-common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -239,26 +229,12 @@ dependencies = [
|
|||||||
"uuid",
|
"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]]
|
[[package]]
|
||||||
name = "authc"
|
name = "authc"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe#b943c85e4a38f5ec60cd18c34c73097640162bfe"
|
source = "git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe#b943c85e4a38f5ec60cd18c34c73097640162bfe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"auth-common 0.1.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)",
|
"auth-common",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
"hex",
|
"hex",
|
||||||
"rust-argon2 0.8.2",
|
"rust-argon2 0.8.2",
|
||||||
@ -4625,7 +4601,7 @@ dependencies = [
|
|||||||
name = "veloren-client"
|
name = "veloren-client"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)",
|
"authc",
|
||||||
"byteorder 1.3.4",
|
"byteorder 1.3.4",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
"futures-timer",
|
"futures-timer",
|
||||||
@ -4646,7 +4622,7 @@ name = "veloren-common"
|
|||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arraygen",
|
"arraygen",
|
||||||
"authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086)",
|
"authc",
|
||||||
"criterion",
|
"criterion",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
"dot_vox",
|
"dot_vox",
|
||||||
@ -4674,7 +4650,7 @@ dependencies = [
|
|||||||
name = "veloren-server"
|
name = "veloren-server"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)",
|
"authc",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
"diesel",
|
"diesel",
|
||||||
|
@ -23,7 +23,7 @@ use common::{
|
|||||||
msg::{
|
msg::{
|
||||||
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification,
|
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification,
|
||||||
PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg,
|
PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg,
|
||||||
MAX_BYTES_CHAT_MSG,
|
MAX_BYTES_CHAT_MSG, InviteAnswer,
|
||||||
},
|
},
|
||||||
recipe::RecipeBook,
|
recipe::RecipeBook,
|
||||||
state::State,
|
state::State,
|
||||||
@ -79,9 +79,14 @@ pub struct Client {
|
|||||||
recipe_book: RecipeBook,
|
recipe_book: RecipeBook,
|
||||||
available_recipes: HashSet<String>,
|
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>,
|
group_leader: Option<Uid>,
|
||||||
|
// Note: potentially representable as a client only component
|
||||||
group_members: HashMap<Uid, group::Role>,
|
group_members: HashMap<Uid, group::Role>,
|
||||||
|
// Pending invites that this client has sent out
|
||||||
|
pending_invites: HashSet<Uid>,
|
||||||
|
|
||||||
_network: Network,
|
_network: Network,
|
||||||
participant: Option<Participant>,
|
participant: Option<Participant>,
|
||||||
@ -130,13 +135,14 @@ impl Client {
|
|||||||
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
|
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
|
||||||
|
|
||||||
// Wait for initial sync
|
// 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 {
|
loop {
|
||||||
match stream.recv().await? {
|
match stream.recv().await? {
|
||||||
ServerMsg::InitialSync {
|
ServerMsg::InitialSync {
|
||||||
entity_package,
|
entity_package,
|
||||||
server_info,
|
server_info,
|
||||||
time_of_day,
|
time_of_day,
|
||||||
|
max_group_size,
|
||||||
world_map: (map_size, world_map),
|
world_map: (map_size, world_map),
|
||||||
recipe_book,
|
recipe_book,
|
||||||
} => {
|
} => {
|
||||||
@ -188,6 +194,7 @@ impl Client {
|
|||||||
server_info,
|
server_info,
|
||||||
(world_map, map_size),
|
(world_map, map_size),
|
||||||
recipe_book,
|
recipe_book,
|
||||||
|
max_group_size,
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
|
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
|
||||||
@ -212,14 +219,16 @@ impl Client {
|
|||||||
server_info,
|
server_info,
|
||||||
world_map,
|
world_map,
|
||||||
player_list: HashMap::new(),
|
player_list: HashMap::new(),
|
||||||
group_members: HashMap::new(),
|
|
||||||
character_list: CharacterList::default(),
|
character_list: CharacterList::default(),
|
||||||
active_character_id: None,
|
active_character_id: None,
|
||||||
recipe_book,
|
recipe_book,
|
||||||
available_recipes: HashSet::default(),
|
available_recipes: HashSet::default(),
|
||||||
|
|
||||||
|
max_group_size,
|
||||||
group_invite: None,
|
group_invite: None,
|
||||||
group_leader: None,
|
group_leader: None,
|
||||||
|
group_members: HashMap::new(),
|
||||||
|
pending_invites: HashSet::new(),
|
||||||
|
|
||||||
_network: network,
|
_network: network,
|
||||||
participant: Some(participant),
|
participant: Some(participant),
|
||||||
@ -432,7 +441,9 @@ impl Client {
|
|||||||
.unwrap();
|
.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)> {
|
pub fn group_info(&self) -> Option<(String, Uid)> {
|
||||||
self.group_leader.map(|l| ("Group".into(), l)) // TODO
|
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 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) {
|
pub fn send_group_invite(&mut self, invitee: Uid) {
|
||||||
self.singleton_stream
|
self.singleton_stream
|
||||||
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
|
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
|
||||||
@ -758,6 +771,10 @@ impl Client {
|
|||||||
frontend_events.append(&mut self.handle_new_messages()?);
|
frontend_events.append(&mut self.handle_new_messages()?);
|
||||||
|
|
||||||
// 3) Update client local data
|
// 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
|
// 4) Tick the client's LocalState
|
||||||
self.state.tick(dt, add_foreign_systems, true);
|
self.state.tick(dt, add_foreign_systems, true);
|
||||||
@ -1046,9 +1063,28 @@ impl Client {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ServerMsg::GroupInvite(uid) => {
|
ServerMsg::GroupInvite { inviter, timeout } => {
|
||||||
self.group_invite = Some(uid);
|
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 => {
|
ServerMsg::Ping => {
|
||||||
self.singleton_stream.send(ClientMsg::Pong)?;
|
self.singleton_stream.send(ClientMsg::Pong)?;
|
||||||
},
|
},
|
||||||
|
@ -9,9 +9,11 @@ use tracing::{error, warn};
|
|||||||
// Primitive group system
|
// Primitive group system
|
||||||
// Shortcomings include:
|
// Shortcomings include:
|
||||||
// - no support for more complex group structures
|
// - 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
|
// - relies on careful management of groups to maintain a valid state
|
||||||
// - the possesion rod could probably wreck this
|
// - 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)]
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Group(u32);
|
pub struct Group(u32);
|
||||||
@ -26,11 +28,26 @@ impl Component for Group {
|
|||||||
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GroupInfo {
|
pub struct GroupInfo {
|
||||||
// TODO: what about enemy groups, either the leader will constantly change because they have to
|
// 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
|
// be loaded or we create a dummy entity or this needs to be optional
|
||||||
pub leader: specs::Entity,
|
pub leader: specs::Entity,
|
||||||
|
// Number of group members (excluding pets)
|
||||||
|
pub num_members: u32,
|
||||||
|
// Name of the group
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,9 +151,14 @@ impl GroupManager {
|
|||||||
self.groups.get(group.0 as usize)
|
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 {
|
Group(self.groups.insert(GroupInfo {
|
||||||
leader,
|
leader,
|
||||||
|
num_members,
|
||||||
name: "Group".into(),
|
name: "Group".into(),
|
||||||
}) as u32)
|
}) as u32)
|
||||||
}
|
}
|
||||||
@ -204,15 +226,20 @@ impl GroupManager {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let group = group.unwrap_or_else(|| {
|
let group = if let Some(group) = group {
|
||||||
let new_group = self.create_group(leader);
|
// 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
|
// 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
|
// still exist Note: if there is an issue replace with a warn
|
||||||
groups.insert(leader, new_group).unwrap();
|
groups.insert(leader, new_group).unwrap();
|
||||||
// Inform
|
// Inform
|
||||||
notifier(leader, ChangeNotification::NewLeader(leader));
|
notifier(leader, ChangeNotification::NewLeader(leader));
|
||||||
new_group
|
new_group
|
||||||
});
|
};
|
||||||
|
|
||||||
let new_pets = pets(new_member, new_member_uid, alignments, entities);
|
let new_pets = pets(new_member, new_member_uid, alignments, entities);
|
||||||
|
|
||||||
@ -256,7 +283,7 @@ impl GroupManager {
|
|||||||
let group = match groups.get(owner).copied() {
|
let group = match groups.get(owner).copied() {
|
||||||
Some(group) => group,
|
Some(group) => group,
|
||||||
None => {
|
None => {
|
||||||
let new_group = self.create_group(owner);
|
let new_group = self.create_group(owner, 1);
|
||||||
groups.insert(owner, new_group).unwrap();
|
groups.insert(owner, new_group).unwrap();
|
||||||
// Inform
|
// Inform
|
||||||
notifier(owner, ChangeNotification::NewLeader(owner));
|
notifier(owner, ChangeNotification::NewLeader(owner));
|
||||||
@ -348,7 +375,7 @@ impl GroupManager {
|
|||||||
|
|
||||||
(entities, uids, &*groups, alignments.maybe())
|
(entities, uids, &*groups, alignments.maybe())
|
||||||
.join()
|
.join()
|
||||||
.filter(|(e, _, g, _)| **g == group && (!to_be_deleted || *e == member))
|
.filter(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member))
|
||||||
.fold(
|
.fold(
|
||||||
HashMap::<Uid, (Option<specs::Entity>, Vec<specs::Entity>)>::new(),
|
HashMap::<Uid, (Option<specs::Entity>, Vec<specs::Entity>)>::new(),
|
||||||
|mut acc, (e, uid, _, alignment)| {
|
|mut acc, (e, uid, _, alignment)| {
|
||||||
@ -356,9 +383,11 @@ impl GroupManager {
|
|||||||
Alignment::Owned(owner) if uid != owner => Some(owner),
|
Alignment::Owned(owner) if uid != owner => Some(owner),
|
||||||
_ => None,
|
_ => None,
|
||||||
}) {
|
}) {
|
||||||
|
// A pet
|
||||||
// Assumes owner will be in the group
|
// Assumes owner will be in the group
|
||||||
acc.entry(*owner).or_default().1.push(e);
|
acc.entry(*owner).or_default().1.push(e);
|
||||||
} else {
|
} else {
|
||||||
|
// Not a pet
|
||||||
acc.entry(*uid).or_default().0 = Some(e);
|
acc.entry(*uid).or_default().0 = Some(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,7 +404,7 @@ impl GroupManager {
|
|||||||
members.push((owner, Role::Member));
|
members.push((owner, Role::Member));
|
||||||
|
|
||||||
// New group
|
// New group
|
||||||
let new_group = self.create_group(owner);
|
let new_group = self.create_group(owner, 1);
|
||||||
for (member, _) in &members {
|
for (member, _) in &members {
|
||||||
groups.insert(*member, new_group).unwrap();
|
groups.insert(*member, new_group).unwrap();
|
||||||
}
|
}
|
||||||
@ -401,7 +430,7 @@ impl GroupManager {
|
|||||||
let leaving_member_uid = if let Some(uid) = uids.get(member) {
|
let leaving_member_uid = if let Some(uid) = uids.get(member) {
|
||||||
*uid
|
*uid
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to retrieve uid for the new group member");
|
error!("Failed to retrieve uid for the leaving member");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -409,7 +438,7 @@ impl GroupManager {
|
|||||||
|
|
||||||
// If pets and not about to be deleted form new group
|
// If pets and not about to be deleted form new group
|
||||||
if !leaving_pets.is_empty() && !to_be_deleted {
|
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 {
|
notifier(member, ChangeNotification::NewGroup {
|
||||||
leader: member,
|
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
|
// Inform remaining members
|
||||||
let mut num_members = 0;
|
members(group, &*groups, entities, alignments, uids).for_each(
|
||||||
members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| {
|
|(e, role)| match role {
|
||||||
num_members += 1;
|
|
||||||
match role {
|
|
||||||
Role::Member => {
|
Role::Member => {
|
||||||
notifier(e, ChangeNotification::Removed(member));
|
notifier(e, ChangeNotification::Removed(member));
|
||||||
leaving_pets.iter().for_each(|p| {
|
leaving_pets.iter().for_each(|p| {
|
||||||
@ -445,16 +473,16 @@ impl GroupManager {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
Role::Pet => {},
|
Role::Pet => {},
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
// If leader is the last one left then disband the group
|
// If leader is the last one left then disband the group
|
||||||
// Assumes last member is the leader
|
// Assumes last member is the leader
|
||||||
if num_members == 1 {
|
if info.num_members == 1 {
|
||||||
let leader = info.leader;
|
let leader = info.leader;
|
||||||
self.remove_group(group);
|
self.remove_group(group);
|
||||||
groups.remove(leader);
|
groups.remove(leader);
|
||||||
notifier(leader, ChangeNotification::NoGroup);
|
notifier(leader, ChangeNotification::NoGroup);
|
||||||
} else if num_members == 0 {
|
} else if info.num_members == 0 {
|
||||||
error!("Somehow group has no members")
|
error!("Somehow group has no members")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ pub use self::{
|
|||||||
client::ClientMsg,
|
client::ClientMsg,
|
||||||
ecs_packet::EcsCompPacket,
|
ecs_packet::EcsCompPacket,
|
||||||
server::{
|
server::{
|
||||||
CharacterInfo, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
|
CharacterInfo, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
|
||||||
RequestStateError, ServerInfo, ServerMsg,
|
RequestStateError, ServerInfo, ServerMsg,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -47,6 +47,13 @@ pub struct CharacterInfo {
|
|||||||
pub level: u32,
|
pub level: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum InviteAnswer {
|
||||||
|
Accepted,
|
||||||
|
Declined,
|
||||||
|
TimedOut,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum Notification {
|
pub enum Notification {
|
||||||
WaypointSaved,
|
WaypointSaved,
|
||||||
@ -59,6 +66,7 @@ pub enum ServerMsg {
|
|||||||
entity_package: sync::EntityPackage<EcsCompPacket>,
|
entity_package: sync::EntityPackage<EcsCompPacket>,
|
||||||
server_info: ServerInfo,
|
server_info: ServerInfo,
|
||||||
time_of_day: state::TimeOfDay,
|
time_of_day: state::TimeOfDay,
|
||||||
|
max_group_size: u32,
|
||||||
world_map: (Vec2<u32>, Vec<u32>),
|
world_map: (Vec2<u32>, Vec<u32>),
|
||||||
recipe_book: RecipeBook,
|
recipe_book: RecipeBook,
|
||||||
},
|
},
|
||||||
@ -70,7 +78,21 @@ pub enum ServerMsg {
|
|||||||
CharacterActionError(String),
|
CharacterActionError(String),
|
||||||
PlayerListUpdate(PlayerListUpdate),
|
PlayerListUpdate(PlayerListUpdate),
|
||||||
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
|
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)>),
|
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
|
||||||
/// 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
|
||||||
|
@ -158,6 +158,8 @@ impl State {
|
|||||||
ecs.register::<comp::ItemDrop>();
|
ecs.register::<comp::ItemDrop>();
|
||||||
ecs.register::<comp::ChatMode>();
|
ecs.register::<comp::ChatMode>();
|
||||||
ecs.register::<comp::Faction>();
|
ecs.register::<comp::Faction>();
|
||||||
|
ecs.register::<comp::group::Invite>();
|
||||||
|
ecs.register::<comp::group::PendingInvites>();
|
||||||
|
|
||||||
// Register synced resources used by the ECS.
|
// Register synced resources used by the ECS.
|
||||||
ecs.insert(TimeOfDay(0.0));
|
ecs.insert(TimeOfDay(0.0));
|
||||||
|
@ -3,9 +3,11 @@ use crate::{
|
|||||||
self,
|
self,
|
||||||
agent::Activity,
|
agent::Activity,
|
||||||
group,
|
group,
|
||||||
|
group::Invite,
|
||||||
item::{tool::ToolKind, ItemKind},
|
item::{tool::ToolKind, ItemKind},
|
||||||
Agent, Alignment, Body, CharacterState, ControlAction, Controller, Loadout, MountState,
|
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller,
|
||||||
Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, Vel,
|
GroupManip, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg,
|
||||||
|
Vel,
|
||||||
},
|
},
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
path::{Chaser, TraversalConfig},
|
path::{Chaser, TraversalConfig},
|
||||||
@ -49,6 +51,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
WriteStorage<'a, Agent>,
|
WriteStorage<'a, Agent>,
|
||||||
WriteStorage<'a, Controller>,
|
WriteStorage<'a, Controller>,
|
||||||
ReadStorage<'a, MountState>,
|
ReadStorage<'a, MountState>,
|
||||||
|
ReadStorage<'a, Invite>,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
|
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
|
||||||
@ -77,6 +80,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
mut agents,
|
mut agents,
|
||||||
mut controllers,
|
mut controllers,
|
||||||
mount_states,
|
mount_states,
|
||||||
|
invites,
|
||||||
): Self::SystemData,
|
): Self::SystemData,
|
||||||
) {
|
) {
|
||||||
for (
|
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.move_dir.map(|e| !e.is_nan()).reduce_and());
|
||||||
debug_assert!(inputs.look_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ pub struct Client {
|
|||||||
pub network_error: AtomicBool,
|
pub network_error: AtomicBool,
|
||||||
pub last_ping: f64,
|
pub last_ping: f64,
|
||||||
pub login_msg_sent: bool,
|
pub login_msg_sent: bool,
|
||||||
pub invited_to_group: Option<specs::Entity>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Client {
|
impl Component for Client {
|
||||||
|
@ -2,18 +2,25 @@ use crate::{client::Client, Server};
|
|||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
group::{self, GroupManager},
|
group::{self, Group, GroupManager, Invite, PendingInvites},
|
||||||
ChatType, GroupManip,
|
ChatType, GroupManip,
|
||||||
},
|
},
|
||||||
msg::ServerMsg,
|
msg::{InviteAnswer, ServerMsg},
|
||||||
sync,
|
sync,
|
||||||
sync::WorldSyncExt,
|
sync::WorldSyncExt,
|
||||||
};
|
};
|
||||||
use specs::world::WorldExt;
|
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
|
// TODO: turn chat messages into enums
|
||||||
pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) {
|
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();
|
let state = server.state_mut();
|
||||||
|
|
||||||
match manip {
|
match manip {
|
||||||
@ -44,38 +51,62 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let alignments = state.ecs().read_storage::<comp::Alignment>();
|
// Disallow inviting entity that is already in your group
|
||||||
let agents = state.ecs().read_storage::<comp::Agent>();
|
let groups = state.ecs().read_storage::<Group>();
|
||||||
let mut already_has_invite = false;
|
let group_manager = state.ecs().read_resource::<GroupManager>();
|
||||||
let mut add_to_group = false;
|
if groups.get(entity).map_or(false, |group| {
|
||||||
// If client comp
|
group_manager
|
||||||
if let (Some(client), Some(inviter_uid)) = (clients.get_mut(invitee), uids.get(entity))
|
.group_info(*group)
|
||||||
{
|
.map_or(false, |g| g.leader == entity)
|
||||||
if client.invited_to_group.is_some() {
|
&& groups.get(invitee) == Some(group)
|
||||||
already_has_invite = true;
|
}) {
|
||||||
} else {
|
// Inform of failure
|
||||||
client.notify(ServerMsg::GroupInvite(*inviter_uid));
|
if let Some(client) = clients.get_mut(entity) {
|
||||||
client.invited_to_group = Some(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
|
return;
|
||||||
// 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()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Inform inviter that there is already an invite
|
||||||
if let Some(client) = clients.get_mut(entity) {
|
if let Some(client) = clients.get_mut(entity) {
|
||||||
client.notify(
|
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()),
|
.server_msg("This player already has a pending invite.".to_owned()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if add_to_group {
|
let mut invite_sent = false;
|
||||||
let mut group_manager = state.ecs().write_resource::<GroupManager>();
|
// Returns true if insertion was succesful
|
||||||
group_manager.add_group_member(
|
let mut send_invite = || {
|
||||||
entity,
|
match invites.insert(invitee, group::Invite(entity)) {
|
||||||
invitee,
|
Err(err) => {
|
||||||
&state.ecs().entities(),
|
error!("Failed to insert Invite component: {:?}", err);
|
||||||
&mut state.ecs().write_storage(),
|
false
|
||||||
&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)));
|
|
||||||
},
|
},
|
||||||
);
|
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 => {
|
GroupManip::Accept => {
|
||||||
let mut clients = state.ecs().write_storage::<Client>();
|
let mut clients = state.ecs().write_storage::<Client>();
|
||||||
let uids = state.ecs().read_storage::<sync::Uid>();
|
let uids = state.ecs().read_storage::<sync::Uid>();
|
||||||
if let Some(inviter) = clients
|
let mut invites = state.ecs().write_storage::<Invite>();
|
||||||
.get_mut(entity)
|
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
|
||||||
.and_then(|c| c.invited_to_group.take())
|
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>();
|
let mut group_manager = state.ecs().write_resource::<GroupManager>();
|
||||||
group_manager.add_group_member(
|
group_manager.add_group_member(
|
||||||
inviter,
|
inviter,
|
||||||
@ -137,14 +225,30 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
|
|||||||
},
|
},
|
||||||
GroupManip::Decline => {
|
GroupManip::Decline => {
|
||||||
let mut clients = state.ecs().write_storage::<Client>();
|
let mut clients = state.ecs().write_storage::<Client>();
|
||||||
if let Some(inviter) = clients
|
let uids = state.ecs().read_storage::<sync::Uid>();
|
||||||
.get_mut(entity)
|
let mut invites = state.ecs().write_storage::<Invite>();
|
||||||
.and_then(|c| c.invited_to_group.take())
|
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
|
// Inform inviter of rejection
|
||||||
if let Some(client) = clients.get_mut(inviter) {
|
if let (Some(client), Some(target)) =
|
||||||
// TODO: say who declined the invite
|
(clients.get_mut(inviter), uids.get(entity).copied())
|
||||||
client.notify(ChatType::Meta.server_msg("Invite declined.".to_owned()));
|
{
|
||||||
|
client.notify(ServerMsg::InviteComplete {
|
||||||
|
target,
|
||||||
|
answer: InviteAnswer::Declined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,7 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
|
|||||||
&state.ecs().entities(),
|
&state.ecs().entities(),
|
||||||
&state.ecs().read_storage(),
|
&state.ecs().read_storage(),
|
||||||
&state.ecs().read_storage(),
|
&state.ecs().read_storage(),
|
||||||
// Nothing actually changing
|
// Nothing actually changing since Uid is transferred
|
||||||
|_, _| {},
|
|_, _| {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
#![allow(clippy::option_map_unit_fn)]
|
#![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 alias_validator;
|
||||||
pub mod chunk_generator;
|
pub mod chunk_generator;
|
||||||
@ -127,6 +127,7 @@ impl Server {
|
|||||||
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
|
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
|
||||||
state.ecs_mut().insert(sys::TerrainTimer::default());
|
state.ecs_mut().insert(sys::TerrainTimer::default());
|
||||||
state.ecs_mut().insert(sys::WaypointTimer::default());
|
state.ecs_mut().insert(sys::WaypointTimer::default());
|
||||||
|
state.ecs_mut().insert(sys::InviteTimeoutTimer::default());
|
||||||
state.ecs_mut().insert(sys::PersistenceTimer::default());
|
state.ecs_mut().insert(sys::PersistenceTimer::default());
|
||||||
|
|
||||||
// System schedulers to control execution of systems
|
// System schedulers to control execution of systems
|
||||||
@ -508,12 +509,13 @@ impl Server {
|
|||||||
.nanos as i64;
|
.nanos as i64;
|
||||||
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().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 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
|
let stats_persistence_nanos = self
|
||||||
.state
|
.state
|
||||||
.ecs()
|
.ecs()
|
||||||
.read_resource::<sys::PersistenceTimer>()
|
.read_resource::<sys::PersistenceTimer>()
|
||||||
.nanos as i64;
|
.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
|
// Report timing info
|
||||||
self.tick_metrics
|
self.tick_metrics
|
||||||
@ -575,6 +577,10 @@ impl Server {
|
|||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["waypoint"])
|
.with_label_values(&["waypoint"])
|
||||||
.set(waypoint_nanos);
|
.set(waypoint_nanos);
|
||||||
|
self.tick_metrics
|
||||||
|
.tick_time
|
||||||
|
.with_label_values(&["invite timeout"])
|
||||||
|
.set(invite_timeout_nanos);
|
||||||
self.tick_metrics
|
self.tick_metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["persistence:stats"])
|
.with_label_values(&["persistence:stats"])
|
||||||
@ -656,7 +662,6 @@ impl Server {
|
|||||||
network_error: std::sync::atomic::AtomicBool::new(false),
|
network_error: std::sync::atomic::AtomicBool::new(false),
|
||||||
last_ping: self.state.get_time(),
|
last_ping: self.state.get_time(),
|
||||||
login_msg_sent: false,
|
login_msg_sent: false,
|
||||||
invited_to_group: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.settings().max_players
|
if self.settings().max_players
|
||||||
@ -685,6 +690,7 @@ impl Server {
|
|||||||
.create_entity_package(entity, None, None, None),
|
.create_entity_package(entity, None, None, None),
|
||||||
server_info: self.get_server_info(),
|
server_info: self.get_server_info(),
|
||||||
time_of_day: *self.state.ecs().read_resource(),
|
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()),
|
world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()),
|
||||||
recipe_book: (&*default_recipe_book()).clone(),
|
recipe_book: (&*default_recipe_book()).clone(),
|
||||||
});
|
});
|
||||||
|
@ -26,6 +26,7 @@ pub struct ServerSettings {
|
|||||||
pub persistence_db_dir: String,
|
pub persistence_db_dir: String,
|
||||||
pub max_view_distance: Option<u32>,
|
pub max_view_distance: Option<u32>,
|
||||||
pub banned_words_files: Vec<PathBuf>,
|
pub banned_words_files: Vec<PathBuf>,
|
||||||
|
pub max_player_group_size: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerSettings {
|
impl Default for ServerSettings {
|
||||||
@ -65,6 +66,7 @@ impl Default for ServerSettings {
|
|||||||
persistence_db_dir: "saves".to_owned(),
|
persistence_db_dir: "saves".to_owned(),
|
||||||
max_view_distance: Some(30),
|
max_view_distance: Some(30),
|
||||||
banned_words_files: Vec::new(),
|
banned_words_files: Vec::new(),
|
||||||
|
max_player_group_size: 6,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
71
server/src/sys/invite_timeout.rs
Normal file
71
server/src/sys/invite_timeout.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
pub mod entity_sync;
|
pub mod entity_sync;
|
||||||
|
pub mod invite_timeout;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod object;
|
pub mod object;
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
@ -21,6 +22,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
|
|||||||
pub type TerrainTimer = SysTimer<terrain::Sys>;
|
pub type TerrainTimer = SysTimer<terrain::Sys>;
|
||||||
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
|
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
|
||||||
pub type WaypointTimer = SysTimer<waypoint::Sys>;
|
pub type WaypointTimer = SysTimer<waypoint::Sys>;
|
||||||
|
pub type InviteTimeoutTimer = SysTimer<invite_timeout::Sys>;
|
||||||
pub type PersistenceTimer = SysTimer<persistence::Sys>;
|
pub type PersistenceTimer = SysTimer<persistence::Sys>;
|
||||||
pub type PersistenceScheduler = SysScheduler<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_SYNC_SYS: &str = "server_terrain_sync_sys";
|
||||||
const TERRAIN_SYS: &str = "server_terrain_sys";
|
const TERRAIN_SYS: &str = "server_terrain_sys";
|
||||||
const WAYPOINT_SYS: &str = "server_waypoint_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 PERSISTENCE_SYS: &str = "server_persistence_sys";
|
||||||
const OBJECT_SYS: &str = "server_object_sys";
|
const OBJECT_SYS: &str = "server_object_sys";
|
||||||
|
|
||||||
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||||
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
|
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
|
||||||
dispatch_builder.add(waypoint::Sys, WAYPOINT_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(persistence::Sys, PERSISTENCE_SYS, &[]);
|
||||||
dispatch_builder.add(object::Sys, OBJECT_SYS, &[]);
|
dispatch_builder.add(object::Sys, OBJECT_SYS, &[]);
|
||||||
}
|
}
|
||||||
|
@ -182,23 +182,22 @@ impl<'a> Widget for Group<'a> {
|
|||||||
.crop_kids()
|
.crop_kids()
|
||||||
.set(state.ids.bg, ui);
|
.set(state.ids.bg, ui);
|
||||||
}
|
}
|
||||||
if open_invite.is_some() {
|
if let Some((_, timeout_start, timeout_dur)) = 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)
|
||||||
.bottom_left_with_margins_on(ui.window, 10.0, 490.0)
|
.bottom_left_with_margins_on(ui.window, 10.0, 490.0)
|
||||||
.set(state.ids.group_button, ui);
|
.set(state.ids.group_button, ui);
|
||||||
// Show timeout bar
|
// Show timeout bar
|
||||||
let max_time = 90.0;
|
let timeout_progress =
|
||||||
let time = 50.0;
|
1.0 - timeout_start.elapsed().as_secs_f32() / timeout_dur.as_secs_f32();
|
||||||
let progress_perc = time / max_time;
|
|
||||||
Image::new(self.imgs.progress_frame)
|
Image::new(self.imgs.progress_frame)
|
||||||
.w_h(100.0, 10.0)
|
.w_h(100.0, 10.0)
|
||||||
.middle_of(state.ids.bg)
|
.middle_of(state.ids.bg)
|
||||||
.color(Some(UI_MAIN))
|
.color(Some(UI_MAIN))
|
||||||
.set(state.ids.timeout_bg, ui);
|
.set(state.ids.timeout_bg, ui);
|
||||||
Image::new(self.imgs.progress)
|
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)
|
.top_left_with_margins_on(state.ids.timeout_bg, 1.0, 1.0)
|
||||||
.color(Some(UI_HIGHLIGHT_0))
|
.color(Some(UI_HIGHLIGHT_0))
|
||||||
.set(state.ids.timeout, ui);
|
.set(state.ids.timeout, ui);
|
||||||
@ -613,7 +612,7 @@ 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, _, _)) = 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
|
||||||
|
@ -8,7 +8,7 @@ use crate::{
|
|||||||
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
|
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
|
||||||
};
|
};
|
||||||
use client::{self, Client};
|
use client::{self, Client};
|
||||||
use common::sync::Uid;
|
use common::{comp::group, sync::Uid};
|
||||||
use conrod_core::{
|
use conrod_core::{
|
||||||
color,
|
color,
|
||||||
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
|
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
|
||||||
@ -467,68 +467,90 @@ impl<'a> Widget for Social<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Invite Button
|
// Invite Button
|
||||||
let selected_ingame = state
|
let is_leader_or_not_in_group = self
|
||||||
.selected_uid
|
.client
|
||||||
.as_ref()
|
.group_info()
|
||||||
.map(|(s, _)| *s)
|
.map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid));
|
||||||
.filter(|selected| {
|
|
||||||
self.client
|
let current_members = self
|
||||||
.player_list
|
.client
|
||||||
.get(selected)
|
.group_members()
|
||||||
.map_or(false, |selected_player| {
|
.iter()
|
||||||
selected_player.is_online && selected_player.character.is_some()
|
.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(|| {
|
.flatten();
|
||||||
self.selected_entity
|
|
||||||
.and_then(|s| self.client.state().read_component_copied(s.0))
|
let invite_button = Button::image(self.imgs.button)
|
||||||
});
|
|
||||||
// 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 {}",
|
|
||||||
¤t_members,
|
|
||||||
&self.localized_strings.get("hud.group.members")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(&self.localized_strings.get("hud.group.members")).to_string()
|
|
||||||
};
|
|
||||||
if Button::image(self.imgs.button)
|
|
||||||
.w_h(106.0, 26.0)
|
.w_h(106.0, 26.0)
|
||||||
.bottom_right_with_margins_on(state.ids.frame, 9.0, 7.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
|
self.imgs.button_hover
|
||||||
} else {
|
} else {
|
||||||
self.imgs.button
|
self.imgs.button
|
||||||
})
|
})
|
||||||
.press_image(if selected_ingame.is_some() {
|
.press_image(if selected_to_invite.is_some() {
|
||||||
self.imgs.button_press
|
self.imgs.button_press
|
||||||
} else {
|
} else {
|
||||||
self.imgs.button
|
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_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
|
TEXT_COLOR
|
||||||
} else {
|
} else {
|
||||||
TEXT_COLOR_3
|
TEXT_COLOR_3
|
||||||
})
|
})
|
||||||
.image_color(if selected_ingame.is_some() {
|
.image_color(if selected_to_invite.is_some() {
|
||||||
TEXT_COLOR
|
TEXT_COLOR
|
||||||
} else {
|
} else {
|
||||||
TEXT_COLOR_3
|
TEXT_COLOR_3
|
||||||
})
|
})
|
||||||
.label_font_size(self.fonts.cyri.scale(15))
|
.label_font_size(self.fonts.cyri.scale(15))
|
||||||
.label_font_id(self.fonts.cyri.conrod_id)
|
.label_font_id(self.fonts.cyri.conrod_id);
|
||||||
.with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip)
|
|
||||||
.set(state.ids.invite_button, ui)
|
if if self.client.group_info().is_some() {
|
||||||
.was_clicked()
|
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));
|
events.push(Event::Invite(uid));
|
||||||
state.update(|s| {
|
state.update(|s| {
|
||||||
s.selected_uid = None;
|
s.selected_uid = None;
|
||||||
|
Loading…
Reference in New Issue
Block a user