--- 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<String>)>),
- Vote(String),
- ForceVote(String),
+ CallVote(Option<VoteType>),
+ Vote(bool),
+ ForceVote(bool),
Save(String, String),
Delete(String),
SaveRoom(String),
@@ -90,6 +93,7 @@
TeamColor(String, u8),
HedgehogsNumber(String, u8),
ConfigEntry(String, Vec<String>),
+ Kicked,
RunGame,
ForwardEngineMessage(Vec<String>),
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<String>) {
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),
--- 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<String> >, 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 >>
--- 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<String>)>),
- 50 => Vote(Ascii),
- 51 => ForceVote(Ascii),
+ 50 => Vote(bool),
+ 51 => ForceVote(bool),
//52 => Save(String, String),
53 => Delete(Ascii),
54 => SaveRoom(Ascii),
--- 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));
--- 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<usize>,
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,
--- 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<String>),
+ Pause,
+ NewSeed,
+ HedgehogsPerTeam(u8)
+}
+
+#[derive(Clone, Debug)]
+pub struct Voting {
+ pub ttl: u32,
+ pub voters: Vec<ClientId>,
+ pub votes: Vec<(ClientId, bool)>,
+ pub kind: VoteType
+}
+
+impl Voting {
+ pub fn new(kind: VoteType, voters: Vec<ClientId>) -> Voting {
+ Voting {
+ kind, voters, ttl: 2,
+ votes: Vec::new()
+ }
+ }
+}
\ No newline at end of file
--- 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 <nickname>, map <name>, pause, newseed, hedgehogs <number>")
+ .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)]
--- 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::{
--- 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::{
--- 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 {
--- 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<String>,
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<Voting>,
pub game_info: Option<GameInfo>
}
@@ -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<String> {
+ 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<F>(&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))
--- 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<F>(&self, f: F) -> Vec<ClientId>
where F: Fn(&(usize, &HWClient)) -> bool {
self.clients.iter().filter(f)