add a separate interface for modifying room state
authoralfadur <mail@none>
Mon, 23 Dec 2019 23:47:06 +0300
changeset 15523 f4f6060b536c
parent 15522 4a0b06b03199
child 15524 fbcee515b946
add a separate interface for modifying room state
rust/hedgewars-server/src/core/server.rs
rust/hedgewars-server/src/handlers.rs
rust/hedgewars-server/src/handlers/common.rs
rust/hedgewars-server/src/handlers/inroom.rs
--- a/rust/hedgewars-server/src/core/server.rs	Mon Dec 23 18:55:25 2019 +0300
+++ b/rust/hedgewars-server/src/core/server.rs	Mon Dec 23 23:47:06 2019 +0300
@@ -3,7 +3,7 @@
     client::HwClient,
     indexslab::IndexSlab,
     room::HwRoom,
-    types::{ClientId, RoomId, ServerVar, TeamInfo},
+    types::{ClientId, GameCfg, RoomId, ServerVar, TeamInfo},
 };
 use crate::{protocol::messages::HwProtocolMessage::Greeting, utils};
 
@@ -11,7 +11,7 @@
 use bitflags::*;
 use log::*;
 use slab::Slab;
-use std::{borrow::BorrowMut, collections::HashSet, iter, mem::replace};
+use std::{borrow::BorrowMut, cmp::min, collections::HashSet, iter, mem::replace};
 
 #[derive(Debug)]
 pub enum CreateRoomError {
@@ -40,11 +40,6 @@
 }
 
 #[derive(Debug)]
-pub enum LeaveRoomError {
-    NoRoom,
-}
-
-#[derive(Debug)]
 pub struct ChangeMasterResult {
     pub old_master_id: Option<ClientId>,
     pub new_master_id: ClientId,
@@ -80,6 +75,25 @@
 }
 
 #[derive(Debug)]
+pub enum SetTeamCountError {
+    InvalidNumber,
+    NotMaster,
+}
+
+#[derive(Debug)]
+pub enum SetHedgehogsError {
+    NoTeam,
+    InvalidNumber(u8),
+    NotMaster,
+}
+
+#[derive(Debug)]
+pub enum SetConfigError {
+    NotMaster,
+    RoomFixed,
+}
+
+#[derive(Debug)]
 pub enum ModifyRoomNameError {
     AccessDenied,
     InvalidName,
@@ -198,12 +212,18 @@
     }
 
     #[inline]
-    pub fn client_and_room_mut(
-        &mut self,
-        client_id: ClientId,
-        room_id: RoomId,
-    ) -> (&HwClient, &HwRoom) {
-        (&self.clients[client_id], &mut self.rooms[room_id])
+    fn client_and_room_mut(&mut self, client_id: ClientId) -> Option<(&mut HwClient, &mut HwRoom)> {
+        let client = &mut self.clients[client_id];
+        if let Some(room_id) = client.room_id {
+            Some((client, &mut self.rooms[room_id]))
+        } else {
+            None
+        }
+    }
+
+    #[inline]
+    pub fn get_room_control(&mut self, client_id: ClientId) -> Option<HwRoomControl> {
+        HwRoomControl::new(self, client_id)
     }
 
     #[inline]
@@ -313,226 +333,6 @@
         }
     }
 
-    pub fn leave_room(&mut self, client_id: ClientId) -> Result<LeaveRoomResult, LeaveRoomError> {
-        let client = &mut self.clients[client_id];
-        if let Some(room_id) = client.room_id {
-            let room = &mut self.rooms[room_id];
-
-            room.players_number -= 1;
-            client.room_id = None;
-
-            let is_empty = room.players_number == 0;
-            let is_fixed = room.is_fixed();
-            let was_master = room.master_id == Some(client_id);
-            let was_in_game = client.is_in_game();
-            let mut removed_teams = vec![];
-
-            if is_empty && !is_fixed {
-                if client.is_ready() && room.ready_players_number > 0 {
-                    room.ready_players_number -= 1;
-                }
-
-                removed_teams = room
-                    .client_teams(client.id)
-                    .map(|t| t.name.clone())
-                    .collect();
-
-                for team_name in &removed_teams {
-                    room.remove_team(team_name);
-                }
-
-                if client.is_master() && !is_fixed {
-                    client.set_is_master(false);
-                    room.master_id = None;
-                }
-            }
-
-            client.set_is_ready(false);
-            client.set_is_in_game(false);
-
-            if !is_fixed {
-                if room.players_number == 0 {
-                    self.rooms.remove(room_id);
-                } else if room.master_id == None {
-                    let new_master_id = self.room_clients(room_id).next();
-                    if let Some(new_master_id) = new_master_id {
-                        let room = &mut self.rooms[room_id];
-                        room.master_id = Some(new_master_id);
-                        let new_master = &mut self.clients[new_master_id];
-                        new_master.set_is_master(true);
-
-                        if room.protocol_number < 42 {
-                            room.name = new_master.nick.clone();
-                        }
-
-                        room.set_join_restriction(false);
-                        room.set_team_add_restriction(false);
-                        room.set_unregistered_players_restriction(true);
-                    }
-                }
-            }
-
-            if is_empty && !is_fixed {
-                Ok(LeaveRoomResult::RoomRemoved)
-            } else {
-                Ok(LeaveRoomResult::RoomRemains {
-                    is_empty,
-                    was_master,
-                    was_in_game,
-                    new_master: self.rooms[room_id].master_id,
-                    removed_teams,
-                })
-            }
-        } else {
-            Err(LeaveRoomError::NoRoom)
-        }
-    }
-
-    pub fn change_master(
-        &mut self,
-        client_id: ClientId,
-        room_id: RoomId,
-        new_master_nick: String,
-    ) -> Result<ChangeMasterResult, ChangeMasterError> {
-        let client = &mut self.clients[client_id];
-        let room = &mut self.rooms[room_id];
-
-        if client.is_admin() || room.master_id == Some(client_id) {
-            let new_master_id = self
-                .clients
-                .iter()
-                .find(|(_, c)| c.nick == new_master_nick)
-                .map(|(id, _)| id);
-
-            match new_master_id {
-                Some(new_master_id) if new_master_id == client_id => {
-                    Err(ChangeMasterError::AlreadyMaster)
-                }
-                Some(new_master_id) => {
-                    let new_master = &mut self.clients[new_master_id];
-                    if new_master.room_id == Some(room_id) {
-                        self.clients[new_master_id].set_is_master(true);
-                        let old_master_id = room.master_id;
-                        if let Some(master_id) = old_master_id {
-                            self.clients[master_id].set_is_master(false);
-                        }
-                        room.master_id = Some(new_master_id);
-                        Ok(ChangeMasterResult {
-                            old_master_id,
-                            new_master_id,
-                        })
-                    } else {
-                        Err(ChangeMasterError::ClientNotInRoom)
-                    }
-                }
-                None => Err(ChangeMasterError::NoClient),
-            }
-        } else {
-            Err(ChangeMasterError::NoAccess)
-        }
-    }
-
-    pub fn start_game(&mut self, room_id: RoomId) -> Result<Vec<String>, StartGameError> {
-        let (room_clients, room_nicks): (Vec<_>, Vec<_>) = self
-            .clients
-            .iter()
-            .map(|(id, c)| (id, c.nick.clone()))
-            .unzip();
-
-        let room = &mut self.rooms[room_id];
-
-        if !room.has_multiple_clans() {
-            Err(StartGameError::NotEnoughClans)
-        } else if room.protocol_number <= 43 && room.players_number != room.ready_players_number {
-            Err(StartGameError::NotReady)
-        } else if room.game_info.is_some() {
-            Err(StartGameError::AlreadyInGame)
-        } else {
-            room.start_round();
-            for id in room_clients {
-                let c = &mut self.clients[id];
-                c.set_is_in_game(true);
-                c.team_indices = room.client_team_indices(c.id);
-            }
-            Ok(room_nicks)
-        }
-    }
-
-    pub fn leave_game(&mut self, client_id: ClientId) -> Option<Vec<String>> {
-        let client = &mut self.clients[client_id];
-        let client_left = client.is_in_game();
-        if client_left {
-            client.set_is_in_game(false);
-            let room = &mut self.rooms[client.room_id.expect("Client should've been in the game")];
-
-            let team_names: Vec<_> = room
-                .client_teams(client_id)
-                .map(|t| t.name.clone())
-                .collect();
-
-            if let Some(ref mut info) = room.game_info {
-                info.teams_in_game -= team_names.len() as u8;
-
-                for team_name in &team_names {
-                    let remove_msg =
-                        utils::to_engine_msg(std::iter::once(b'F').chain(team_name.bytes()));
-                    if let Some(m) = &info.sync_msg {
-                        info.msg_log.push(m.clone());
-                    }
-                    if info.sync_msg.is_some() {
-                        info.sync_msg = None
-                    }
-                    info.msg_log.push(remove_msg);
-                }
-                Some(team_names)
-            } else {
-                unreachable!();
-            }
-        } else {
-            None
-        }
-    }
-
-    pub fn end_game(&mut self, room_id: RoomId) -> EndGameResult {
-        let room = &mut self.rooms[room_id];
-        room.ready_players_number = room.master_id.is_some() as u8;
-
-        if let Some(info) = replace(&mut room.game_info, None) {
-            let joined_mid_game_clients = self
-                .clients
-                .iter()
-                .filter(|(_, c)| c.room_id == Some(room_id) && c.is_joined_mid_game())
-                .map(|(_, c)| c.id)
-                .collect();
-
-            let unreadied_nicks: Vec<_> = self
-                .clients
-                .iter_mut()
-                .filter(|(_, c)| c.room_id == Some(room_id))
-                .map(|(_, c)| {
-                    c.set_is_ready(c.is_master());
-                    c.set_is_joined_mid_game(false);
-                    c
-                })
-                .filter_map(|c| {
-                    if !c.is_master() {
-                        Some(c.nick.clone())
-                    } else {
-                        None
-                    }
-                })
-                .collect();
-
-            EndGameResult {
-                joined_mid_game_clients,
-                left_teams: info.left_teams.clone(),
-                unreadied_nicks,
-            }
-        } else {
-            unreachable!()
-        }
-    }
-
     pub fn enable_super_power(&mut self, client_id: ClientId) -> bool {
         let client = &mut self.clients[client_id];
         if client.is_admin() {
@@ -541,116 +341,6 @@
         client.is_admin()
     }
 
-    pub fn set_room_name(
-        &mut self,
-        client_id: ClientId,
-        room_id: RoomId,
-        mut name: String,
-    ) -> Result<String, ModifyRoomNameError> {
-        let room_exists = self.has_room(&name);
-        let room = &mut self.rooms[room_id];
-        if room.is_fixed() || room.master_id != Some(client_id) {
-            Err(ModifyRoomNameError::AccessDenied)
-        } else if utils::is_name_illegal(&name) {
-            Err(ModifyRoomNameError::InvalidName)
-        } else if room_exists {
-            Err(ModifyRoomNameError::DuplicateName)
-        } else {
-            std::mem::swap(&mut room.name, &mut name);
-            Ok(name)
-        }
-    }
-
-    pub fn add_team(
-        &mut self,
-        client_id: ClientId,
-        mut info: Box<TeamInfo>,
-    ) -> Result<&TeamInfo, AddTeamError> {
-        let client = &mut self.clients[client_id];
-        if let Some(room_id) = client.room_id {
-            let room = &mut self.rooms[room_id];
-            if room.teams.len() >= room.max_teams as usize {
-                Err(AddTeamError::TooManyTeams)
-            } else if room.addable_hedgehogs() == 0 {
-                Err(AddTeamError::TooManyHedgehogs)
-            } else if room.find_team(|t| t.name == info.name) != None {
-                Err(AddTeamError::TeamAlreadyExists)
-            } else if room.game_info.is_some() {
-                Err(AddTeamError::GameInProgress)
-            } else if room.is_team_add_restricted() {
-                Err(AddTeamError::Restricted)
-            } else {
-                info.owner = client.nick.clone();
-                let team = room.add_team(client.id, *info, client.protocol_number < 42);
-                client.teams_in_game += 1;
-                client.clan = Some(team.color);
-                Ok(team)
-            }
-        } else {
-            unreachable!()
-        }
-    }
-
-    pub fn remove_team(
-        &mut self,
-        client_id: ClientId,
-        team_name: &str,
-    ) -> Result<(), RemoveTeamError> {
-        let client = &mut self.clients[client_id];
-        if let Some(room_id) = client.room_id {
-            let room = &mut self.rooms[room_id];
-            match room.find_team_owner(team_name) {
-                None => Err(RemoveTeamError::NoTeam),
-                Some((id, _)) if id != client_id => Err(RemoveTeamError::TeamNotOwned),
-                Some(_) => {
-                    client.teams_in_game -= 1;
-                    client.clan = room.find_team_color(client.id);
-                    room.remove_team(team_name);
-                    Ok(())
-                }
-            }
-        } else {
-            unreachable!();
-        }
-    }
-
-    pub fn set_team_color(
-        &mut self,
-        client_id: ClientId,
-        room_id: RoomId,
-        team_name: &str,
-        color: u8,
-    ) -> Result<(), ModifyTeamError> {
-        let client = &self.clients[client_id];
-        let room = &mut self.rooms[room_id];
-        if let Some((owner, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) {
-            if !client.is_master() {
-                Err(ModifyTeamError::NotMaster)
-            } else {
-                team.color = color;
-                self.clients[owner].clan = Some(color);
-                Ok(())
-            }
-        } else {
-            Err(ModifyTeamError::NoTeam)
-        }
-    }
-
-    pub fn toggle_ready(&mut self, client_id: ClientId) -> bool {
-        let client = &mut self.clients[client_id];
-        if let Some(room_id) = client.room_id {
-            let room = &mut self.rooms[room_id];
-
-            client.set_is_ready(!client.is_ready());
-            if client.is_ready() {
-                room.ready_players_number += 1;
-            } else {
-                room.ready_players_number -= 1;
-            }
-        }
-        client.is_ready()
-    }
-
     #[inline]
     pub fn set_var(&mut self, client_id: ClientId, var: ServerVar) -> Result<(), AccessError> {
         if self.clients[client_id].is_admin() {
@@ -790,6 +480,489 @@
     }
 }
 
+pub struct HwRoomControl<'a> {
+    server: &'a mut HwServer,
+    client_id: ClientId,
+    room_id: RoomId,
+}
+
+impl<'a> HwRoomControl<'a> {
+    #[inline]
+    pub fn new(server: &'a mut HwServer, client_id: ClientId) -> Option<Self> {
+        if let Some(room_id) = server.clients[client_id].room_id {
+            Some(Self {
+                server,
+                client_id,
+                room_id,
+            })
+        } else {
+            None
+        }
+    }
+
+    #[inline]
+    pub fn server(&self) -> &HwServer {
+        self.server
+    }
+
+    #[inline]
+    pub fn client(&self) -> &HwClient {
+        &self.server.clients[self.client_id]
+    }
+
+    #[inline]
+    fn client_mut(&mut self) -> &mut HwClient {
+        &mut self.server.clients[self.client_id]
+    }
+
+    #[inline]
+    pub fn room(&self) -> &HwRoom {
+        &self.server.rooms[self.room_id]
+    }
+
+    #[inline]
+    fn room_mut(&mut self) -> &mut HwRoom {
+        &mut self.server.rooms[self.room_id]
+    }
+
+    #[inline]
+    pub fn get(&self) -> (&HwClient, &HwRoom) {
+        (self.client(), self.room())
+    }
+
+    #[inline]
+    fn get_mut(&mut self) -> (&mut HwClient, &mut HwRoom) {
+        (
+            &mut self.server.clients[self.client_id],
+            &mut self.server.rooms[self.room_id],
+        )
+    }
+
+    pub fn leave_room(&mut self) -> LeaveRoomResult {
+        let (client, room) = self.get_mut();
+        room.players_number -= 1;
+        client.room_id = None;
+
+        let is_empty = room.players_number == 0;
+        let is_fixed = room.is_fixed();
+        let was_master = room.master_id == Some(client.id);
+        let was_in_game = client.is_in_game();
+        let mut removed_teams = vec![];
+
+        if is_empty && !is_fixed {
+            if client.is_ready() && room.ready_players_number > 0 {
+                room.ready_players_number -= 1;
+            }
+
+            removed_teams = room
+                .client_teams(client.id)
+                .map(|t| t.name.clone())
+                .collect();
+
+            for team_name in &removed_teams {
+                room.remove_team(team_name);
+            }
+
+            if client.is_master() && !is_fixed {
+                client.set_is_master(false);
+                room.master_id = None;
+            }
+        }
+
+        client.set_is_ready(false);
+        client.set_is_in_game(false);
+
+        if !is_fixed {
+            if room.players_number == 0 {
+                self.server.rooms.remove(self.room_id);
+            } else if room.master_id == None {
+                let protocol_number = room.protocol_number;
+                let new_master_id = self.server.room_clients(self.room_id).next();
+
+                if let Some(new_master_id) = new_master_id {
+                    let room = self.room_mut();
+                    room.master_id = Some(new_master_id);
+                    let new_master = &mut self.server.clients[new_master_id];
+                    new_master.set_is_master(true);
+
+                    if protocol_number < 42 {
+                        todo!();
+                        let nick = new_master.nick.clone();
+                        self.room_mut().name = nick;
+                    }
+
+                    let room = self.room_mut();
+                    room.set_join_restriction(false);
+                    room.set_team_add_restriction(false);
+                    room.set_unregistered_players_restriction(true);
+                }
+            }
+        }
+
+        if is_empty && !is_fixed {
+            LeaveRoomResult::RoomRemoved
+        } else {
+            LeaveRoomResult::RoomRemains {
+                is_empty,
+                was_master,
+                was_in_game,
+                new_master: self.room().master_id,
+                removed_teams,
+            }
+        }
+    }
+
+    pub fn change_master(
+        &mut self,
+        new_master_nick: String,
+    ) -> Result<ChangeMasterResult, ChangeMasterError> {
+        use ChangeMasterError::*;
+        let (client, room) = self.get_mut();
+
+        if client.is_admin() || room.master_id == Some(client.id) {
+            let new_master_id = self
+                .server
+                .clients
+                .iter()
+                .find(|(_, c)| c.nick == new_master_nick)
+                .map(|(id, _)| id);
+
+            match new_master_id {
+                Some(new_master_id) if new_master_id == self.client_id => Err(AlreadyMaster),
+                Some(new_master_id) => {
+                    let new_master = &mut self.server.clients[new_master_id];
+                    if new_master.room_id == Some(self.room_id) {
+                        self.server.clients[new_master_id].set_is_master(true);
+                        let room = self.room_mut();
+                        let old_master_id = self.room().master_id;
+
+                        if let Some(master_id) = old_master_id {
+                            self.server.clients[master_id].set_is_master(false);
+                        }
+                        self.room_mut().master_id = Some(new_master_id);
+                        Ok(ChangeMasterResult {
+                            old_master_id,
+                            new_master_id,
+                        })
+                    } else {
+                        Err(ClientNotInRoom)
+                    }
+                }
+                None => Err(NoClient),
+            }
+        } else {
+            Err(NoAccess)
+        }
+    }
+
+    pub fn vote(&mut self) {
+        todo!("port from the room handler")
+    }
+
+    pub fn add_engine_message(&mut self) {
+        todo!("port from the room handler")
+    }
+
+    pub fn toggle_flag(&mut self, flags: super::room::RoomFlags) -> bool {
+        let (client, room) = self.get_mut();
+        if client.is_master() {
+            room.flags.toggle(flags);
+        }
+        client.is_master()
+    }
+
+    pub fn fix_room(&mut self) -> Result<(), AccessError> {
+        let (client, room) = self.get_mut();
+        if client.is_admin() {
+            room.set_is_fixed(true);
+            room.set_join_restriction(false);
+            room.set_team_add_restriction(false);
+            room.set_unregistered_players_restriction(true);
+            Ok(())
+        } else {
+            Err(AccessError())
+        }
+    }
+
+    pub fn unfix_room(&mut self) -> Result<(), AccessError> {
+        let (client, room) = self.get_mut();
+        if client.is_admin() {
+            room.set_is_fixed(false);
+            Ok(())
+        } else {
+            Err(AccessError())
+        }
+    }
+
+    pub fn set_room_name(&mut self, mut name: String) -> Result<String, ModifyRoomNameError> {
+        use ModifyRoomNameError::*;
+        let room_exists = self.server.has_room(&name);
+        let (client, room) = self.get_mut();
+        if room.is_fixed() || room.master_id != Some(client.id) {
+            Err(AccessDenied)
+        } else if utils::is_name_illegal(&name) {
+            Err(InvalidName)
+        } else if room_exists {
+            Err(DuplicateName)
+        } else {
+            std::mem::swap(&mut room.name, &mut name);
+            Ok(name)
+        }
+    }
+
+    pub fn set_room_greeting(&mut self, greeting: Option<String>) -> Result<(), AccessError> {
+        let (client, room) = self.get_mut();
+        if client.is_admin() {
+            room.greeting = greeting.unwrap_or(String::new());
+            Ok(())
+        } else {
+            Err(AccessError())
+        }
+    }
+
+    pub fn set_room_max_teams(&mut self, count: u8) -> Result<(), SetTeamCountError> {
+        use SetTeamCountError::*;
+        let (client, room) = self.get_mut();
+        if !client.is_master() {
+            Err(NotMaster)
+        } else if !(2..=super::room::MAX_TEAMS_IN_ROOM).contains(&count) {
+            Err(InvalidNumber)
+        } else {
+            room.max_teams = count;
+            Ok(())
+        }
+    }
+
+    pub fn set_team_hedgehogs_number(
+        &mut self,
+        team_name: &str,
+        number: u8,
+    ) -> Result<(), SetHedgehogsError> {
+        use SetHedgehogsError::*;
+        let (client, room) = self.get_mut();
+        let addable_hedgehogs = room.addable_hedgehogs();
+        if let Some((_, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) {
+            let max_hedgehogs = min(
+                super::room::MAX_HEDGEHOGS_IN_ROOM,
+                addable_hedgehogs + team.hedgehogs_number,
+            );
+            if !client.is_master() {
+                Err(NotMaster)
+            } else if !(1..=max_hedgehogs).contains(&number) {
+                Err(InvalidNumber(team.hedgehogs_number))
+            } else {
+                team.hedgehogs_number = number;
+                Ok(())
+            }
+        } else {
+            Err(NoTeam)
+        }
+    }
+
+    pub fn add_team(&mut self, mut info: Box<TeamInfo>) -> Result<&TeamInfo, AddTeamError> {
+        use AddTeamError::*;
+        let (client, room) = self.get_mut();
+        if room.teams.len() >= room.max_teams as usize {
+            Err(TooManyTeams)
+        } else if room.addable_hedgehogs() == 0 {
+            Err(TooManyHedgehogs)
+        } else if room.find_team(|t| t.name == info.name) != None {
+            Err(TeamAlreadyExists)
+        } else if room.game_info.is_some() {
+            Err(GameInProgress)
+        } else if room.is_team_add_restricted() {
+            Err(Restricted)
+        } else {
+            info.owner = client.nick.clone();
+            let team = room.add_team(client.id, *info, client.protocol_number < 42);
+            client.teams_in_game += 1;
+            client.clan = Some(team.color);
+            Ok(team)
+        }
+    }
+
+    pub fn remove_team(&mut self, team_name: &str) -> Result<(), RemoveTeamError> {
+        use RemoveTeamError::*;
+        let (client, room) = self.get_mut();
+        match room.find_team_owner(team_name) {
+            None => Err(NoTeam),
+            Some((id, _)) if id != client.id => Err(RemoveTeamError::TeamNotOwned),
+            Some(_) => {
+                client.teams_in_game -= 1;
+                client.clan = room.find_team_color(client.id);
+                room.remove_team(team_name);
+                Ok(())
+            }
+        }
+    }
+
+    pub fn set_team_color(&mut self, team_name: &str, color: u8) -> Result<(), ModifyTeamError> {
+        use ModifyTeamError::*;
+        let (client, room) = self.get_mut();
+        if let Some((owner, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) {
+            if !client.is_master() {
+                Err(NotMaster)
+            } else {
+                team.color = color;
+                self.server.clients[owner].clan = Some(color);
+                Ok(())
+            }
+        } else {
+            Err(NoTeam)
+        }
+    }
+
+    pub fn set_config(&mut self, cfg: GameCfg) -> Result<(), SetConfigError> {
+        use SetConfigError::*;
+        let (client, room) = self.get_mut();
+        if room.is_fixed() {
+            Err(RoomFixed)
+        } else if !client.is_master() {
+            Err(NotMaster)
+        } else {
+            let cfg = match cfg {
+                GameCfg::Scheme(name, mut values) => {
+                    if client.protocol_number == 49 && values.len() >= 2 {
+                        let mut s = "X".repeat(50);
+                        s.push_str(&values.pop().unwrap());
+                        values.push(s);
+                    }
+                    GameCfg::Scheme(name, values)
+                }
+                cfg => cfg,
+            };
+
+            room.set_config(cfg);
+            Ok(())
+        }
+    }
+
+    pub fn save_config(&mut self, name: String, location: String) {
+        self.room_mut().save_config(name, location);
+    }
+
+    pub fn delete_config(&mut self, name: &str) -> bool {
+        self.room_mut().delete_config(name)
+    }
+
+    pub fn toggle_ready(&mut self) -> bool {
+        let (client, room) = self.get_mut();
+        client.set_is_ready(!client.is_ready());
+        if client.is_ready() {
+            room.ready_players_number += 1;
+        } else {
+            room.ready_players_number -= 1;
+        }
+        client.is_ready()
+    }
+
+    pub fn start_game(&mut self) -> Result<Vec<String>, StartGameError> {
+        use StartGameError::*;
+        let (room_clients, room_nicks): (Vec<_>, Vec<_>) = self
+            .server
+            .clients
+            .iter()
+            .map(|(id, c)| (id, c.nick.clone()))
+            .unzip();
+
+        let room = self.room_mut();
+
+        if !room.has_multiple_clans() {
+            Err(NotEnoughClans)
+        } else if room.protocol_number <= 43 && room.players_number != room.ready_players_number {
+            Err(NotReady)
+        } else if room.game_info.is_some() {
+            Err(AlreadyInGame)
+        } else {
+            room.start_round();
+            for id in room_clients {
+                let team_indices = self.room().client_team_indices(id);
+                let c = &mut self.server.clients[id];
+                c.set_is_in_game(true);
+                c.team_indices = team_indices;
+            }
+            Ok(room_nicks)
+        }
+    }
+
+    pub fn leave_game(&mut self) -> Option<Vec<String>> {
+        let (client, room) = self.get_mut();
+        let client_left = client.is_in_game();
+        if client_left {
+            client.set_is_in_game(false);
+
+            let team_names: Vec<_> = room
+                .client_teams(client.id)
+                .map(|t| t.name.clone())
+                .collect();
+
+            if let Some(ref mut info) = room.game_info {
+                info.teams_in_game -= team_names.len() as u8;
+
+                for team_name in &team_names {
+                    let remove_msg =
+                        utils::to_engine_msg(std::iter::once(b'F').chain(team_name.bytes()));
+                    if let Some(m) = &info.sync_msg {
+                        info.msg_log.push(m.clone());
+                    }
+                    if info.sync_msg.is_some() {
+                        info.sync_msg = None
+                    }
+                    info.msg_log.push(remove_msg);
+                }
+                Some(team_names)
+            } else {
+                unreachable!();
+            }
+        } else {
+            None
+        }
+    }
+
+    pub fn end_game(&mut self) -> Option<EndGameResult> {
+        let room = self.room_mut();
+        room.ready_players_number = room.master_id.is_some() as u8;
+
+        if let Some(info) = replace(&mut room.game_info, None) {
+            let room_id = room.id;
+            let joined_mid_game_clients = self
+                .server
+                .clients
+                .iter()
+                .filter(|(_, c)| c.room_id == Some(self.room_id) && c.is_joined_mid_game())
+                .map(|(_, c)| c.id)
+                .collect();
+
+            let unreadied_nicks: Vec<_> = self
+                .server
+                .clients
+                .iter_mut()
+                .filter(|(_, c)| c.room_id == Some(room_id))
+                .map(|(_, c)| {
+                    c.set_is_ready(c.is_master());
+                    c.set_is_joined_mid_game(false);
+                    c
+                })
+                .filter_map(|c| {
+                    if !c.is_master() {
+                        Some(c.nick.clone())
+                    } else {
+                        None
+                    }
+                })
+                .collect();
+
+            Some(EndGameResult {
+                joined_mid_game_clients,
+                left_teams: info.left_teams.clone(),
+                unreadied_nicks,
+            })
+        } else {
+            None
+        }
+    }
+}
+
 fn allocate_room(rooms: &mut Slab<HwRoom>) -> &mut HwRoom {
     let entry = rooms.vacant_entry();
     let room = HwRoom::new(entry.key());
--- a/rust/hedgewars-server/src/handlers.rs	Mon Dec 23 18:55:25 2019 +0300
+++ b/rust/hedgewars-server/src/handlers.rs	Mon Dec 23 23:47:06 2019 +0300
@@ -357,11 +357,9 @@
                             response.warn(REPLAY_NOT_SUPPORTED);
                         }
                     }
-                    _ => match state.server.client(client_id).room_id {
+                    _ => match state.server.get_room_control(client_id) {
                         None => inlobby::handle(&mut state.server, client_id, response, message),
-                        Some(room_id) => {
-                            inroom::handle(&mut state.server, client_id, response, room_id, message)
-                        }
+                        Some(control) => inroom::handle(control, response, message),
                     },
                 }
             }
--- a/rust/hedgewars-server/src/handlers/common.rs	Mon Dec 23 18:55:25 2019 +0300
+++ b/rust/hedgewars-server/src/handlers/common.rs	Mon Dec 23 23:47:06 2019 +0300
@@ -2,9 +2,7 @@
     core::{
         client::HwClient,
         room::HwRoom,
-        server::{
-            EndGameResult, HwServer, JoinRoomError, LeaveRoomError, LeaveRoomResult, StartGameError,
-        },
+        server::{EndGameResult, HwServer, JoinRoomError, LeaveRoomResult, StartGameError},
         types::{ClientId, GameCfg, RoomId, TeamInfo, Vote, VoteType},
     },
     protocol::messages::{
@@ -74,7 +72,8 @@
     let server_msg = ServerMessage(server.get_greetings(client).to_string());
 
     let rooms_msg = Rooms(
-        server.iter_rooms()
+        server
+            .iter_rooms()
             .filter(|r| r.protocol_number == client.protocol_number)
             .flat_map(|r| r.info(r.master_id.map(|id| server.client(id))))
             .collect(),
@@ -260,30 +259,15 @@
     }
 }
 
-pub fn get_room_leave_data(
-    server: &HwServer,
-    room_id: RoomId,
-    leave_message: &str,
-    result: Result<LeaveRoomResult, LeaveRoomError>,
-    response: &mut Response,
-) {
-    match result {
-        Ok(result) => {
-            let room = server.room(room_id);
-            get_room_leave_result(server, room, leave_message, result, response)
-        }
-        Err(_) => (),
-    }
-}
-
 pub fn remove_client(server: &mut HwServer, response: &mut Response, msg: String) {
     let client_id = response.client_id();
     let client = server.client(client_id);
     let nick = client.nick.clone();
 
-    if let Some(room_id) = client.room_id {
-        let result = server.leave_room(client_id);
-        get_room_leave_data(server, room_id, &msg, result, response);
+    if let Some(mut room_control) = server.get_room_control(client_id) {
+        let room_id = room_control.room().id;
+        let result = room_control.leave_room();
+        get_room_leave_result(server, server.room(room_id), &msg, result, response);
     }
 
     server.remove_client(client_id);
@@ -365,12 +349,20 @@
 ) {
     match kind {
         VoteType::Kick(nick) => {
-            if let Some(client) = server.find_client(&nick) {
-                if client.room_id == Some(room_id) {
-                    let id = client.id;
-                    response.add(Kicked.send(id));
-                    let result = server.leave_room(id);
-                    get_room_leave_data(server, room_id, "kicked", result, response);
+            if let Some(kicked_client) = server.find_client(&nick) {
+                let kicked_id = kicked_client.id;
+                if kicked_client.room_id == Some(room_id) {
+                    if let Some(mut room_control) = server.get_room_control(kicked_client.id) {
+                        response.add(Kicked.send(kicked_id));
+                        let result = room_control.leave_room();
+                        get_room_leave_result(
+                            room_control.server(),
+                            room_control.room(),
+                            "kicked",
+                            result,
+                            response,
+                        );
+                    }
                 }
             }
         }
--- a/rust/hedgewars-server/src/handlers/inroom.rs	Mon Dec 23 18:55:25 2019 +0300
+++ b/rust/hedgewars-server/src/handlers/inroom.rs	Mon Dec 23 23:47:06 2019 +0300
@@ -1,11 +1,11 @@
 use super::{common::rnd_reply, strings::*};
 use crate::core::room::GameInfo;
-use crate::core::server::AddTeamError;
+use crate::core::server::{AddTeamError, SetTeamCountError};
 use crate::{
     core::{
         room::{HwRoom, RoomFlags, MAX_TEAMS_IN_ROOM},
         server::{
-            ChangeMasterError, ChangeMasterResult, HwServer, LeaveRoomResult, ModifyTeamError,
+            ChangeMasterError, ChangeMasterResult, HwRoomControl, LeaveRoomResult, ModifyTeamError,
             StartGameError,
         },
         types,
@@ -106,13 +106,12 @@
 }
 
 pub fn handle(
-    server: &mut HwServer,
-    client_id: ClientId,
+    mut room_control: HwRoomControl,
     response: &mut super::Response,
-    room_id: RoomId,
     message: HwProtocolMessage,
 ) {
-    let (client, room) = server.client_and_room_mut(client_id, room_id);
+    let (client, room) = room_control.get();
+    let (client_id, room_id) = (client.id, room.id);
 
     use crate::protocol::messages::HwProtocolMessage::*;
     match message {
@@ -122,8 +121,14 @@
                 None => "part".to_string(),
             };
 
-            let result = server.leave_room(client_id);
-            super::common::get_room_leave_data(server, room_id, &msg, result, response);
+            let result = room_control.leave_room();
+            super::common::get_room_leave_result(
+                room_control.server(),
+                room_control.room(),
+                &msg,
+                result,
+                response,
+            );
         }
         Chat(msg) => {
             response.add(
@@ -146,41 +151,35 @@
             }
         }
         Fix => {
-            if client.is_admin() {
-                room.set_is_fixed(true);
-                room.set_join_restriction(false);
-                room.set_team_add_restriction(false);
-                room.set_unregistered_players_restriction(true);
-            } else {
+            if let Err(_) = room_control.fix_room() {
                 response.warn(ACCESS_DENIED)
             }
         }
         Unfix => {
-            if client.is_admin() {
-                room.set_is_fixed(false);
-            } else {
+            if let Err(_) = room_control.unfix_room() {
                 response.warn(ACCESS_DENIED)
             }
         }
         Greeting(text) => {
-            if client.is_admin() || client.is_master() && !room.is_fixed() {
-                room.greeting = text.unwrap_or(String::new());
+            if let Err(_) = room_control.set_room_greeting(text) {
+                response.warn(ACCESS_DENIED)
             }
         }
         MaxTeams(count) => {
-            if !client.is_master() {
-                response.warn(NOT_MASTER);
-            } else if !(2..=MAX_TEAMS_IN_ROOM).contains(&count) {
-                response.warn("/maxteams: specify number from 2 to 8");
-            } else {
-                room.max_teams = count;
-            }
+            use crate::core::server::SetTeamCountError;
+            match room_control.set_room_max_teams(count) {
+                Ok(()) => {}
+                Err(SetTeamCountError::NotMaster) => response.warn(NOT_MASTER),
+                Err(SetTeamCountError::InvalidNumber) => {
+                    response.warn("/maxteams: specify number from 2 to 8")
+                }
+            };
         }
         RoomName(new_name) => {
             use crate::core::server::ModifyRoomNameError;
-            match server.set_room_name(client_id, room_id, new_name) {
+            match room_control.set_room_name(new_name) {
                 Ok(old_name) => {
-                    let (client, room) = server.client_and_room(client_id, room_id);
+                    let (client, room) = room_control.get();
                     super::common::get_room_update(Some(old_name), room, Some(client), response)
                 }
                 Err(ModifyRoomNameError::AccessDenied) => response.warn(ACCESS_DENIED),
@@ -189,12 +188,12 @@
             }
         }
         ToggleReady => {
-            let flags = if server.toggle_ready(client_id) {
+            let flags = if room_control.toggle_ready() {
                 add_flags(&[Flags::Ready])
             } else {
                 remove_flags(&[Flags::Ready])
             };
-            let (client, room) = server.client_and_room(client_id, room_id);
+            let (client, room) = room_control.get();
 
             let msg = if client.protocol_number < 38 {
                 LegacyReady(client.is_ready(), vec![client.nick.clone()])
@@ -204,13 +203,18 @@
             response.add(msg.send_all().in_room(room_id));
 
             if room.is_fixed() && room.ready_players_number == room.players_number {
-                let result = server.start_game(room_id);
-                super::common::get_start_game_data(server, room_id, result, response);
+                let result = room_control.start_game();
+                super::common::get_start_game_data(
+                    room_control.server(),
+                    room_id,
+                    result,
+                    response,
+                );
             }
         }
         AddTeam(info) => {
             use crate::core::server::AddTeamError;
-            match server.add_team(client_id, info) {
+            match room_control.add_team(info) {
                 Ok(team) => {
                     response.add(TeamAccepted(team.name.clone()).send_self());
                     response.add(
@@ -230,9 +234,9 @@
                             .in_room(room_id),
                     );
 
-                    let room = server.room(room_id);
+                    let room = room_control.room();
                     let room_master = if let Some(id) = room.master_id {
-                        Some(server.client(id))
+                        Some(room_control.server().client(id))
                     } else {
                         None
                     };
@@ -247,9 +251,9 @@
         }
         RemoveTeam(name) => {
             use crate::core::server::RemoveTeamError;
-            match server.remove_team(client_id, &name) {
+            match room_control.remove_team(&name) {
                 Ok(()) => {
-                    let (client, room) = server.client_and_room(client_id, room_id);
+                    let (client, room) = room_control.get();
 
                     let removed_teams = vec![name];
                     super::common::get_remove_teams_data(
@@ -261,8 +265,14 @@
 
                     match room.game_info {
                         Some(ref info) if info.teams_in_game == 0 => {
-                            let result = server.end_game(room_id);
-                            super::common::get_end_game_result(server, room_id, result, response);
+                            if let Some(result) = room_control.end_game() {
+                                super::common::get_end_game_result(
+                                    room_control.server(),
+                                    room_id,
+                                    result,
+                                    response,
+                                );
+                            }
                         }
                         _ => (),
                     }
@@ -272,62 +282,42 @@
             }
         }
         SetHedgehogsNumber(team_name, number) => {
-            let addable_hedgehogs = room.addable_hedgehogs();
-            if let Some((_, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) {
-                let max_hedgehogs = min(
-                    MAX_HEDGEHOGS_PER_TEAM,
-                    addable_hedgehogs + team.hedgehogs_number,
-                );
-                if !client.is_master() {
-                    response.error(NOT_MASTER);
-                } else if !(1..=max_hedgehogs).contains(&number) {
-                    response
-                        .add(HedgehogsNumber(team.name.clone(), team.hedgehogs_number).send_self());
-                } else {
-                    team.hedgehogs_number = number;
+            use crate::core::server::SetHedgehogsError;
+            match room_control.set_team_hedgehogs_number(&team_name, number) {
+                Ok(()) => {
                     response.add(
-                        HedgehogsNumber(team.name.clone(), number)
+                        HedgehogsNumber(team_name.clone(), number)
                             .send_all()
                             .in_room(room_id)
                             .but_self(),
                     );
                 }
-            } else {
-                response.warn(NO_TEAM);
-            }
-        }
-        SetTeamColor(team_name, color) => {
-            match server.set_team_color(client_id, room_id, &team_name, color) {
-                Ok(()) => response.add(
-                    TeamColor(team_name, color)
-                        .send_all()
-                        .in_room(room_id)
-                        .but_self(),
-                ),
-                Err(ModifyTeamError::NoTeam) => response.warn(NO_TEAM),
-                Err(ModifyTeamError::NotMaster) => response.error(NOT_MASTER),
+                Err(SetHedgehogsError::NotMaster) => response.error(NOT_MASTER),
+                Err(SetHedgehogsError::NoTeam) => response.warn(NO_TEAM),
+                Err(SetHedgehogsError::InvalidNumber(previous_number)) => {
+                    response.add(HedgehogsNumber(team_name.clone(), previous_number).send_self())
+                }
             }
         }
+        SetTeamColor(team_name, color) => match room_control.set_team_color(&team_name, color) {
+            Ok(()) => response.add(
+                TeamColor(team_name, color)
+                    .send_all()
+                    .in_room(room_id)
+                    .but_self(),
+            ),
+            Err(ModifyTeamError::NoTeam) => response.warn(NO_TEAM),
+            Err(ModifyTeamError::NotMaster) => response.error(NOT_MASTER),
+        },
         Cfg(cfg) => {
-            if room.is_fixed() {
-                response.warn(ACCESS_DENIED);
-            } else if !client.is_master() {
-                response.error(NOT_MASTER);
-            } else {
-                let cfg = match cfg {
-                    GameCfg::Scheme(name, mut values) => {
-                        if client.protocol_number == 49 && values.len() >= 2 {
-                            let mut s = "X".repeat(50);
-                            s.push_str(&values.pop().unwrap());
-                            values.push(s);
-                        }
-                        GameCfg::Scheme(name, values)
-                    }
-                    cfg => cfg,
-                };
-
-                response.add(cfg.to_server_msg().send_all().in_room(room.id).but_self());
-                room.set_config(cfg);
+            use crate::core::server::SetConfigError;
+            let msg = cfg.to_server_msg();
+            match room_control.set_config(cfg) {
+                Ok(()) => {
+                    response.add(msg.send_all().in_room(room_control.room().id).but_self());
+                }
+                Err(SetConfigError::NotMaster) => response.error(NOT_MASTER),
+                Err(SetConfigError::RoomFixed) => response.warn(ACCESS_DENIED),
             }
         }
         Save(name, location) => {
@@ -336,7 +326,7 @@
                     .send_all()
                     .in_room(room_id),
             );
-            room.save_config(name, location);
+            room_control.save_config(name, location);
         }
         #[cfg(feature = "official-server")]
         SaveRoom(filename) => {
@@ -361,7 +351,7 @@
             }
         }
         Delete(name) => {
-            if !room.delete_config(&name) {
+            if !room_control.delete_config(&name) {
                 response.add(Warning(format!("Save doesn't exist: {}", name)).send_self());
             } else {
                 response.add(
@@ -375,11 +365,11 @@
             response.add(server_chat("Available callvote commands: kick <nickname>, map <name>, pause, newseed, hedgehogs <number>".to_string())
                 .send_self());
         }
-        CallVote(Some(kind)) => {
+        /*CallVote(Some(kind)) => {
             let is_in_game = room.game_info.is_some();
             let error = match &kind {
                 VoteType::Kick(nick) => {
-                    if server
+                    if room_control.server()
                         .find_client(&nick)
                         .filter(|c| c.room_id == Some(room_id))
                         .is_some()
@@ -390,7 +380,7 @@
                     }
                 }
                 VoteType::Map(None) => {
-                    let names: Vec<_> = server.room(room_id).saves.keys().cloned().collect();
+                    let names: Vec<_> = room.saves.keys().cloned().collect();
                     if names.is_empty() {
                         Some("/callvote map: No maps saved in this room!".to_string())
                     } else {
@@ -421,12 +411,12 @@
             match error {
                 None => {
                     let msg = voting_description(&kind);
-                    let voting = Voting::new(kind, server.room_clients(client_id).collect());
-                    let room = server.room_mut(room_id);
+                    let voting = Voting::new(kind, room_control.server().room_clients(client_id).collect());
+                    let room = room_control.server().room_mut(room_id);
                     room.voting = Some(voting);
                     response.add(server_chat(msg).send_all().in_room(room_id));
                     super::common::submit_vote(
-                        server,
+                        room_control.server(),
                         types::Vote {
                             is_pro: true,
                             is_forced: false,
@@ -438,39 +428,39 @@
                     response.add(server_chat(msg).send_self());
                 }
             }
-        }
-        Vote(vote) => {
+        }*/
+        /*Vote(vote) => {
             super::common::submit_vote(
-                server,
+                room_control.server(),
                 types::Vote {
                     is_pro: vote,
                     is_forced: false,
                 },
                 response,
             );
-        }
-        ForceVote(vote) => {
+        }*/
+        /*ForceVote(vote) => {
             let is_forced = client.is_admin();
             super::common::submit_vote(
-                server,
+                room_control.server(),
                 types::Vote {
                     is_pro: vote,
                     is_forced,
                 },
                 response,
             );
-        }
+        }*/
         ToggleRestrictJoin | ToggleRestrictTeams | ToggleRegisteredOnly => {
-            if client.is_master() {
-                room.flags.toggle(room_message_flag(&message));
+            if room_control.toggle_flag(room_message_flag(&message)) {
+                let (client, room) = room_control.get();
                 super::common::get_room_update(None, room, Some(&client), response);
             }
         }
         StartGame => {
-            let result = server.start_game(room_id);
-            super::common::get_start_game_data(server, room_id, result, response);
+            let result = room_control.start_game();
+            super::common::get_start_game_data(room_control.server(), room_id, result, response);
         }
-        EngineMessage(em) => {
+        /*EngineMessage(em) => {
             if client.teams_in_game > 0 {
                 let decoding = decode(&em[..]).unwrap();
                 let messages = by_msg(&decoding);
@@ -503,10 +493,10 @@
                     }
                 }
             }
-        }
+        }*/
         RoundFinished => {
-            if let Some(team_names) = server.leave_game(client_id) {
-                let (client, room) = server.client_and_room(client_id, room_id);
+            if let Some(team_names) = room_control.leave_game() {
+                let (client, room) = room_control.get();
                 response.add(
                     ClientFlags(remove_flags(&[Flags::InGame]), vec![client.nick.clone()])
                         .send_all()
@@ -527,8 +517,14 @@
                     teams_in_game: 0, ..
                 }) = room.game_info
                 {
-                    let result = server.end_game(room_id);
-                    super::common::get_end_game_result(server, room_id, result, response);
+                    if let Some(result) = room_control.end_game() {
+                        super::common::get_end_game_result(
+                            room_control.server(),
+                            room_id,
+                            result,
+                            response,
+                        );
+                    }
                 }
             }
         }
@@ -537,13 +533,13 @@
             let mut echo = vec!["/rnd".to_string()];
             echo.extend(v.into_iter());
             let chat_msg = ChatMsg {
-                nick: server.client(client_id).nick.clone(),
+                nick: client.nick.clone(),
                 msg: echo.join(" "),
             };
             response.add(chat_msg.send_all().in_room(room_id));
             response.add(result.send_all().in_room(room_id));
         }
-        Delegate(nick) => match server.change_master(client_id, room_id, nick) {
+        Delegate(nick) => match room_control.change_master(nick) {
             Ok(ChangeMasterResult {
                 old_master_id,
                 new_master_id,
@@ -552,7 +548,7 @@
                     response.add(
                         ClientFlags(
                             remove_flags(&[Flags::RoomMaster]),
-                            vec![server.client(master_id).nick.clone()],
+                            vec![room_control.server().client(master_id).nick.clone()],
                         )
                         .send_all()
                         .in_room(room_id),
@@ -561,7 +557,7 @@
                 response.add(
                     ClientFlags(
                         add_flags(&[Flags::RoomMaster]),
-                        vec![server.client(new_master_id).nick.clone()],
+                        vec![room_control.server().client(new_master_id).nick.clone()],
                     )
                     .send_all()
                     .in_room(room_id),