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]
|
||||
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",
|
||||
]
|
||||
|
||||
[[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",
|
||||
|
@ -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)?;
|
||||
},
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
|_, _| {},
|
||||
);
|
||||
}
|
||||
|
@ -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(),
|
||||
});
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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 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, &[]);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {}",
|
||||
¤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)
|
||||
.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;
|
||||
|
Loading…
Reference in New Issue
Block a user