# HG changeset patch # User alfadur # Date 1531154355 -10800 # Node ID 38f9097b6bbc9c76227b0e47ae7506841468d739 # Parent d79795acaa73466688105cb1e5ff0a1a5d534b18# Parent 7a0a56c52fd2f826f4c622ef24da242351abb196 merge diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/protocol/messages.rs --- a/gameServer2/src/protocol/messages.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/protocol/messages.rs Mon Jul 09 19:39:15 2018 +0300 @@ -1,4 +1,7 @@ -use server::coretypes::{ServerVar, GameCfg, TeamInfo, HedgehogInfo}; +use server::coretypes::{ + ServerVar, GameCfg, TeamInfo, + HedgehogInfo, VoteType +}; use std::{ops, convert::From, iter::once}; #[derive(PartialEq, Eq, Clone, Debug)] @@ -56,9 +59,9 @@ Fix, Unfix, Greeting(String), - CallVote(Option<(String, Option)>), - Vote(String), - ForceVote(String), + CallVote(Option), + Vote(bool), + ForceVote(bool), Save(String, String), Delete(String), SaveRoom(String), @@ -90,6 +93,7 @@ TeamColor(String, u8), HedgehogsNumber(String, u8), ConfigEntry(String, Vec), + Kicked, RunGame, ForwardEngineMessage(Vec), RoundFinished, @@ -101,6 +105,10 @@ Unreachable, } +pub fn server_chat(msg: &str) -> HWServerMessage { + HWServerMessage::ChatMsg{ nick: "[server]".to_string(), msg: msg.to_string() } +} + impl GameCfg { pub fn to_protocol(&self) -> (String, Vec) { use server::coretypes::GameCfg::*; @@ -280,6 +288,7 @@ HedgehogsNumber(name, number) => msg!["HH_NUM", name, number], ConfigEntry(name, values) => construct_message(&["CFG", name], &values), + Kicked => msg!["KICKED"], RunGame => msg!["RUN_GAME"], ForwardEngineMessage(em) => construct_message(&["EM"], &em), diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/protocol/parser.rs --- a/gameServer2/src/protocol/parser.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/protocol/parser.rs Mon Jul 09 19:39:15 2018 +0300 @@ -18,7 +18,7 @@ test::gen_proto_msg }; use server::coretypes::{ - HedgehogInfo, TeamInfo, GameCfg + HedgehogInfo, TeamInfo, GameCfg, VoteType }; named!(end_of_message, tag!("\n\n")); @@ -26,6 +26,9 @@ named!( a_line<&[u8], String>, map!(str_line, String::from)); named!( u8_line<&[u8], u8>, map_res!(str_line, FromStr::from_str)); named!(u32_line<&[u8], u32>, map_res!(str_line, FromStr::from_str)); +named!(yes_no_line<&[u8], bool>, alt!( + do_parse!(tag_no_case!("YES") >> (true)) + | do_parse!(tag_no_case!("NO") >> (false)))); named!(opt_param<&[u8], Option >, alt!( do_parse!(peek!(tag!("\n\n")) >> (None)) | do_parse!(tag!("\n") >> s: str_line >> (Some(s.to_string()))))); @@ -42,6 +45,18 @@ h5: hog_line >> eol >> h6: hog_line >> eol >> h7: hog_line >> eol >> h8: hog_line >> ([h1, h2, h3, h4, h5, h6, h7, h8]))); +named!(voting<&[u8], VoteType>, alt!( + do_parse!(tag_no_case!("KICK") >> spaces >> n: a_line >> + (VoteType::Kick(n))) + | do_parse!(tag_no_case!("MAP") >> + n: opt!(preceded!(spaces, a_line)) >> + (VoteType::Map(n))) + | do_parse!(tag_no_case!("PAUSE") >> + (VoteType::Pause)) + | do_parse!(tag_no_case!("NEWSEED") >> + (VoteType::NewSeed)) + | do_parse!(tag_no_case!("HEDGEHOGS") >> spaces >> n: u8_line >> + (VoteType::HedgehogsPerTeam(n))))); /** Recognizes messages which do not take any parameters */ named!(basic_message<&[u8], HWProtocolMessage>, alt!( @@ -94,10 +109,12 @@ | do_parse!(tag_no_case!("GLOBAL") >> spaces >> m: a_line >> (Global(m))) | do_parse!(tag_no_case!("WATCH") >> spaces >> i: a_line >> (Watch(i))) | do_parse!(tag_no_case!("GREETING") >> spaces >> m: a_line >> (Greeting(m))) - | do_parse!(tag_no_case!("VOTE") >> spaces >> m: a_line >> (Vote(m))) - | do_parse!(tag_no_case!("FORCE") >> spaces >> m: a_line >> (ForceVote(m))) + | do_parse!(tag_no_case!("VOTE") >> spaces >> m: yes_no_line >> (Vote(m))) + | do_parse!(tag_no_case!("FORCE") >> spaces >> m: yes_no_line >> (ForceVote(m))) | do_parse!(tag_no_case!("INFO") >> spaces >> n: a_line >> (Info(n))) | do_parse!(tag_no_case!("MAXTEAMS") >> spaces >> n: u8_line >> (MaxTeams(n))) + | do_parse!(tag_no_case!("CALLVOTE") >> + v: opt!(preceded!(spaces, voting)) >> (CallVote(v))) | do_parse!( tag_no_case!("RND") >> alt!(spaces | peek!(end_of_message)) >> v: str_line >> diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/protocol/test.rs --- a/gameServer2/src/protocol/test.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/protocol/test.rs Mon Jul 09 19:39:15 2018 +0300 @@ -166,8 +166,8 @@ 47 => Unfix(), 48 => Greeting(Ascii), //49 => CallVote(Option<(String, Option)>), - 50 => Vote(Ascii), - 51 => ForceVote(Ascii), + 50 => Vote(bool), + 51 => ForceVote(bool), //52 => Save(String, String), 53 => Delete(Ascii), 54 => SaveRoom(Ascii), diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/actions.rs --- a/gameServer2/src/server/actions.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/actions.rs Mon Jul 09 19:39:15 2018 +0300 @@ -1,19 +1,21 @@ use std::{ io, io::Write, iter::once, - mem::swap + mem::replace }; use super::{ server::HWServer, - room::{RoomId, GameInfo}, - client::{ClientId, HWClient}, + room::{GameInfo}, + client::HWClient, + coretypes::{ClientId, RoomId, VoteType}, room::HWRoom, handlers }; use protocol::messages::{ HWProtocolMessage, HWServerMessage, - HWServerMessage::* + HWServerMessage::*, + server_chat }; use utils::to_engine_msg; @@ -103,6 +105,8 @@ SendTeamRemovalMessage(String), FinishRoomGame(RoomId), SendRoomData{to: ClientId, teams: bool, config: bool, flags: bool}, + AddVote{vote: bool, is_forced: bool}, + ApplyVoting(VoteType, RoomId), Warn(String), ProtocolError(String) } @@ -227,6 +231,10 @@ RoomJoined(vec![c.nick.clone()]).send_all().in_room(room_id).action(), ClientFlags("+i".to_string(), vec![c.nick.clone()]).send_all().action(), SendRoomUpdate(None)]; + if !r.greeting.is_empty() { + v.push(ChatMsg {nick: "[greeting]".to_string(), msg: r.greeting.clone()} + .send_self().action()); + } if !c.is_master { let team_names: Vec<_>; if let Some(ref mut info) = r.game_info { @@ -320,6 +328,80 @@ } server.react(client_id, actions); } + AddVote{vote, is_forced} => { + let mut actions = Vec::new(); + if let (c, Some(r)) = server.client_and_room(client_id) { + let mut result = None; + if let Some(ref mut voting) = r.voting { + if is_forced || voting.votes.iter().find(|(id, _)| client_id == *id).is_none() { + actions.push(server_chat("Your vote has been counted.").send_self().action()); + voting.votes.push((client_id, vote)); + let i = voting.votes.iter(); + let pro = i.clone().filter(|(_, v)| *v).count(); + let contra = i.filter(|(_, v)| !*v).count(); + let success_quota = voting.voters.len() / 2 + 1; + if is_forced && vote || pro >= success_quota { + result = Some(true); + } else if is_forced && !vote || contra > voting.voters.len() - success_quota { + result = Some(false); + } + } else { + actions.push(server_chat("You already have voted.").send_self().action()); + } + } else { + actions.push(server_chat("There's no voting going on.").send_self().action()); + } + + if let Some(res) = result { + actions.push(server_chat("Voting closed.") + .send_all().in_room(r.id).action()); + let voting = replace(&mut r.voting, None).unwrap(); + if res { + actions.push(ApplyVoting(voting.kind, r.id)); + } + } + } + + server.react(client_id, actions); + } + ApplyVoting(kind, room_id) => { + let mut actions = Vec::new(); + let mut id = client_id; + match kind { + VoteType::Kick(nick) => { + if let Some(c) = server.find_client(&nick) { + if c.room_id == Some(room_id) { + id = c.id; + actions.push(Kicked.send_self().action()); + actions.push(MoveToLobby("kicked".to_string())); + } + } + }, + VoteType::Map(_) => { + unimplemented!(); + }, + VoteType::Pause => { + if let Some(ref mut info) = server.room(client_id).unwrap().game_info { + info.is_paused = !info.is_paused; + actions.push(server_chat("Pause toggled.") + .send_all().in_room(room_id).action()); + actions.push(ForwardEngineMessage(vec![to_engine_msg(once(b'I'))]) + .send_all().in_room(room_id).action()); + } + }, + VoteType::NewSeed => { + unimplemented!(); + }, + VoteType::HedgehogsPerTeam(number) => { + let r = &mut server.rooms[room_id]; + let nicks = r.set_hedgehogs_number(number); + actions.extend(nicks.into_iter().map(|n| + HedgehogsNumber(n, number).send_all().in_room(room_id).action() + )); + }, + } + server.react(id, actions); + } MoveToLobby(msg) => { let mut actions = Vec::new(); let lobby_id = server.lobby_id; @@ -328,23 +410,24 @@ if c.is_ready && r.ready_players_number > 0 { r.ready_players_number -= 1; } - if r.players_number > 0 && c.is_master { + if c.is_master && (r.players_number > 0 || r.is_fixed) { actions.push(ChangeMaster(r.id, None)); } - actions.push(RemoveClientTeams); - actions.push(RoomLeft(c.nick.clone(), msg) - .send_all().in_room(r.id).but_self().action()); actions.push(ClientFlags("-i".to_string(), vec![c.nick.clone()]) .send_all().action()); - actions.push(SendRoomUpdate(Some(r.name.clone()))); } server.react(client_id, actions); actions = Vec::new(); if let (c, Some(r)) = server.client_and_room(client_id) { c.room_id = Some(lobby_id); - if r.players_number == 0 { + if r.players_number == 0 && !r.is_fixed { actions.push(RemoveRoom(r.id)); + } else { + actions.push(RemoveClientTeams); + actions.push(RoomLeft(c.nick.clone(), msg) + .send_all().in_room(r.id).but_self().action()); + actions.push(SendRoomUpdate(Some(r.name.clone()))); } } server.react(client_id, actions) @@ -352,8 +435,12 @@ ChangeMaster(room_id, new_id) => { let mut actions = Vec::new(); let room_client_ids = server.room_clients(room_id); - let new_id = new_id.or_else(|| - room_client_ids.iter().find(|id| **id != client_id).map(|id| *id)); + let new_id = if server.room(client_id).map(|r| r.is_fixed).unwrap_or(false) { + new_id + } else { + new_id.or_else(|| + room_client_ids.iter().find(|id| **id != client_id).map(|id| *id)) + }; let new_nick = new_id.map(|id| server.clients[id].nick.clone()); if let (c, Some(r)) = server.client_and_room(client_id) { @@ -461,10 +548,10 @@ } FinishRoomGame(room_id) => { let mut actions = Vec::new(); - let mut old_info = None; + let old_info; { let r = &mut server.rooms[room_id]; - swap(&mut old_info, &mut r.game_info); + old_info = replace(&mut r.game_info, None); r.game_info = None; r.ready_players_number = 1; actions.push(SendRoomUpdate(None)); diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/client.rs --- a/gameServer2/src/server/client.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/client.rs Mon Jul 09 19:39:15 2018 +0300 @@ -1,10 +1,11 @@ -pub type ClientId = usize; +use super::coretypes::ClientId; pub struct HWClient { pub id: ClientId, pub room_id: Option, pub nick: String, pub protocol_number: u32, + pub is_admin: bool, pub is_master: bool, pub is_ready: bool, pub is_in_game: bool, @@ -21,6 +22,7 @@ room_id: None, nick: String::new(), protocol_number: 0, + is_admin: false, is_master: false, is_ready: false, is_in_game: false, diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/coretypes.rs --- a/gameServer2/src/server/coretypes.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/coretypes.rs Mon Jul 09 19:39:15 2018 +0300 @@ -1,3 +1,6 @@ +pub type ClientId = usize; +pub type RoomId = usize; + #[derive(PartialEq, Eq, Clone, Debug)] pub enum ServerVar { MOTDNew(String), @@ -39,3 +42,29 @@ pub name: String, pub hat: String, } + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum VoteType { + Kick(String), + Map(Option), + Pause, + NewSeed, + HedgehogsPerTeam(u8) +} + +#[derive(Clone, Debug)] +pub struct Voting { + pub ttl: u32, + pub voters: Vec, + pub votes: Vec<(ClientId, bool)>, + pub kind: VoteType +} + +impl Voting { + pub fn new(kind: VoteType, voters: Vec) -> Voting { + Voting { + kind, voters, ttl: 2, + votes: Vec::new() + } + } +} \ No newline at end of file diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/handlers/inroom.rs --- a/gameServer2/src/server/handlers/inroom.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/handlers/inroom.rs Mon Jul 09 19:39:15 2018 +0300 @@ -2,11 +2,12 @@ use protocol::messages::{ HWProtocolMessage, - HWServerMessage::* + HWServerMessage::*, + server_chat }; use server::{ + coretypes::{ClientId, Voting, VoteType}, server::HWServer, - client::ClientId, room::HWRoom, actions::{Action, Action::*} }; @@ -89,10 +90,29 @@ }; server.react(client_id, actions); }, + Fix => { + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.is_admin { r.is_fixed = true } + } + } + Unfix => { + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.is_admin { r.is_fixed = false } + } + } + Greeting(text) => { + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.is_admin || c.is_master && !r.is_fixed { + r.greeting = text + } + } + } RoomName(new_name) => { let actions = if is_name_illegal(&new_name) { vec![Warn("Illegal room name! A room name must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string())] + } else if server.room(client_id).map(|r| r.is_fixed).unwrap_or(false) { + vec![Warn("Access denied.".to_string())] } else if server.has_room(&new_name) { vec![Warn("A room with the same name already exists.".to_string())] } else { @@ -116,8 +136,13 @@ "+r" }; c.is_ready = !c.is_ready; - vec![ClientFlags(flags.to_string(), vec![c.nick.clone()]) - .send_all().in_room(r.id).action()] + let mut v = + vec![ClientFlags(flags.to_string(), vec![c.nick.clone()]) + .send_all().in_room(r.id).action()]; + if r.is_fixed && r.ready_players_number as u32 == r.players_number { + v.push(StartRoomGame(r.id)) + } + v } else { Vec::new() }; @@ -223,7 +248,9 @@ }, Cfg(cfg) => { let actions = if let (c, Some(r)) = server.client_and_room(client_id) { - if !c.is_master { + if r.is_fixed { + vec![Warn("Access denied.".to_string())] + } else if !c.is_master { vec![ProtocolError("You're not the room master!".to_string())] } else { let v = vec![cfg.to_server_msg() @@ -236,6 +263,76 @@ }; server.react(client_id, actions); } + CallVote(None) => { + server.react(client_id, vec![ + server_chat("Available callvote commands: kick , map , pause, newseed, hedgehogs ") + .send_self().action()]) + } + CallVote(Some(kind)) => { + let (room_id, is_in_game) = server.room(client_id) + .map(|r| (r.id, r.game_info.is_some())).unwrap(); + let error = match &kind { + VoteType::Kick(nick) => { + if server.find_client(&nick).filter(|c| c.room_id == Some(room_id)).is_some() { + None + } else { + Some("/callvote kick: No such user!") + } + }, + VoteType::Map(None) => { + Some("/callvote map: Not implemented") + }, + VoteType::Map(Some(name)) => { + Some("/callvote map: Not implemented") + }, + VoteType::Pause => { + if is_in_game { + None + } else { + Some("/callvote pause: No game in progress!") + } + }, + VoteType::NewSeed => { + None + }, + VoteType::HedgehogsPerTeam(number) => { + match number { + 1...8 => None, + _ => Some("/callvote hedgehogs: Specify number from 1 to 8.") + } + }, + }; + match error { + None => { + let voting = Voting::new(kind, server.room_clients(client_id)); + server.room(client_id).unwrap().voting = Some(voting); + server.react(client_id, vec![ + server_chat("New voting started: ") + .send_all().in_room(room_id).action(), + AddVote{ vote: true, is_forced: false}]); + } + Some(msg) => { + server.react(client_id, vec![ + server_chat(msg).send_self().action()]) + } + } + } + Vote(vote) => { + let actions = if let (c, Some(r)) = server.client_and_room(client_id) { + vec![AddVote{ vote, is_forced: false }] + } else { + Vec::new() + }; + server.react(client_id, actions); + } + ForceVote(vote) => { + let actions = if let (c, Some(r)) = server.client_and_room(client_id) { + vec![AddVote{ vote, is_forced: c.is_admin} ] + } else { + Vec::new() + }; + server.react(client_id, actions); + } StartGame => { let actions = if let (_, Some(r)) = server.client_and_room(client_id) { vec![StartRoomGame(r.id)] diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/handlers/lobby.rs --- a/gameServer2/src/server/handlers/lobby.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/handlers/lobby.rs Mon Jul 09 19:39:15 2018 +0300 @@ -2,7 +2,7 @@ use server::{ server::HWServer, - client::ClientId, + coretypes::ClientId, actions::{Action, Action::*} }; use protocol::messages::{ diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/handlers/loggingin.rs --- a/gameServer2/src/server/handlers/loggingin.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/handlers/loggingin.rs Mon Jul 09 19:39:15 2018 +0300 @@ -2,7 +2,7 @@ use server::{ server::HWServer, - client::ClientId, + coretypes::ClientId, actions::{Action, Action::*} }; use protocol::messages::{ diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/network.rs --- a/gameServer2/src/server/network.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/network.rs Mon Jul 09 19:39:15 2018 +0300 @@ -4,7 +4,7 @@ io, io::{Error, ErrorKind, Write}, net::{SocketAddr, IpAddr, Ipv4Addr}, collections::HashSet, - mem::swap + mem::{swap, replace} }; use mio::{ @@ -18,7 +18,7 @@ use protocol::{ProtocolDecoder, messages::*}; use super::{ server::{HWServer}, - client::ClientId + coretypes::ClientId }; const MAX_BYTES_PER_READ: usize = 2048; @@ -277,8 +277,7 @@ pub fn on_idle(&mut self, poll: &Poll) -> io::Result<()> { if self.has_pending_operations() { - let mut cache = Vec::new(); - swap(&mut cache, &mut self.pending_cache); + let mut cache = replace(&mut self.pending_cache, Vec::new()); cache.extend(self.pending.drain()); for (id, state) in cache.drain(..) { match state { diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/room.rs --- a/gameServer2/src/server/room.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/room.rs Mon Jul 09 19:39:15 2018 +0300 @@ -1,11 +1,10 @@ use std::{iter}; use server::{ - coretypes::{TeamInfo, GameCfg, GameCfg::*}, - client::{ClientId, HWClient} + coretypes::{ClientId, RoomId, TeamInfo, GameCfg, GameCfg::*, Voting}, + client::{HWClient} }; const MAX_HEDGEHOGS_IN_ROOM: u8 = 48; -pub type RoomId = usize; #[derive(Clone)] struct Ammo { @@ -113,6 +112,8 @@ pub name: String, pub password: Option, pub protocol_number: u32, + pub greeting: String, + pub is_fixed: bool, pub players_number: u32, pub default_hedgehog_number: u8, @@ -120,6 +121,7 @@ pub ready_players_number: u8, pub teams: Vec<(ClientId, TeamInfo)>, config: RoomConfig, + pub voting: Option, pub game_info: Option } @@ -130,6 +132,8 @@ master_id: None, name: String::new(), password: None, + greeting: "".to_string(), + is_fixed: false, protocol_number: 0, players_number: 0, default_hedgehog_number: 4, @@ -137,6 +141,7 @@ ready_players_number: 0, teams: Vec::new(), config: RoomConfig::new(), + voting: None, game_info: None } } @@ -169,6 +174,23 @@ } } + pub fn set_hedgehogs_number(&mut self, n: u8) -> Vec { + let mut names = Vec::new(); + let teams = match self.game_info { + Some(ref mut info) => &mut info.teams_at_start, + None => &mut self.teams + }; + + if teams.len() as u8 * n <= MAX_HEDGEHOGS_IN_ROOM { + for (_, team) in teams.iter_mut() { + team.hedgehogs_number = n; + names.push(team.name.clone()) + }; + self.default_hedgehog_number = n; + } + names + } + pub fn find_team_and_owner_mut(&mut self, f: F) -> Option<(ClientId, &mut TeamInfo)> where F: Fn(&TeamInfo) -> bool { self.teams.iter_mut().find(|(_, t)| f(t)).map(|(id, t)| (*id, t)) diff -r 7a0a56c52fd2 -r 38f9097b6bbc gameServer2/src/server/server.rs --- a/gameServer2/src/server/server.rs Sun Jul 08 02:46:59 2018 +0200 +++ b/gameServer2/src/server/server.rs Mon Jul 09 19:39:15 2018 +0300 @@ -1,7 +1,8 @@ use slab; use utils; use super::{ - client::*, room::*, actions, handlers, + client::HWClient, room::HWRoom, actions, handlers, + coretypes::{ClientId, RoomId}, actions::{Destination, PendingMessage} }; use protocol::messages::*; @@ -105,6 +106,14 @@ self.rooms.iter_mut().find(|(_, r)| r.name == name).map(|(_, r)| r) } + pub fn find_client(&self, nick: &str) -> Option<&HWClient> { + self.clients.iter().find(|(_, c)| c.nick == nick).map(|(_, c)| c) + } + + pub fn find_client_mut(&mut self, nick: &str) -> Option<&mut HWClient> { + self.clients.iter_mut().find(|(_, c)| c.nick == nick).map(|(_, c)| c) + } + pub fn select_clients(&self, f: F) -> Vec where F: Fn(&(usize, &HWClient)) -> bool { self.clients.iter().filter(f)