#include #include #include #include #include #include #include // TODO: move `GameServer` into its own files (h/cpp). // Note: This is compiled with SFML 2.6.2 in mind. // It would work similarly with slightly older versions of SFML. // A thourough rework is necessary for SFML 3.0. enum GameMessageType : unsigned char { JOIN_GAME = 0x01, PLACE_TOKEN = 0x02, START_GAME = 0x03, GAME_OVER = 0x04 }; enum Token : unsigned char { NOUGHTS = 0x01, CROSSES = 0x02 }; class GameServer { public: GameServer(unsigned short tcp_port) : m_tcp_port(tcp_port) {} bool send_start_game_to_clients() { char buf[1] = { START_GAME }; std::cout << "Starting the game..." << std::endl; return broadcast_message(buf, nullptr); } bool send_game_over_to_clients() { char buf[1] = { GAME_OVER }; std::cout << "Game Over!" << std::endl; return broadcast_message(buf, nullptr); } // Binds to a port and then loops around. For every client that connects, // we start a new thread receiving their messages. void tcp_start() { // BINDING sf::TcpListener listener; sf::Socket::Status status = listener.listen(m_tcp_port, sf::IpAddress("152.105.66.120")); // Make sure to change this! if (status != sf::Socket::Status::Done) { std::cerr << "Error binding listener to port" << std::endl; return; } std::cout << "TCP Server is listening to port " << m_tcp_port << ", waiting for connections..." << std::endl; while (true) { // ACCEPTING if(m_player_count < 2) { sf::TcpSocket* client = new sf::TcpSocket; status = listener.accept(*client); if (status != sf::Socket::Status::Done) { delete client; } else { { std::lock_guard lock(m_clients_mutex); m_clients.push_back(client); } std::cout << "New client connected: " << client->getRemoteAddress() << std::endl; m_player_count++; std::thread(&GameServer::handle_client, this, client, m_player_count).detach(); if(m_player_count == 2) { // -------------------------------------------------------------- // Slight pause to ensure the all threads have started // -------------------------------------------------------------- std::this_thread::sleep_for(std::chrono::milliseconds(250)); if(!send_start_game_to_clients()) { std::cerr << "Could not start game. One or both players did not recieve START_GAME" << std::endl; } } } } } // No need to call close of the listener. // The connection is closed automatically when the listener object is out of scope. } private: unsigned short m_tcp_port; unsigned short m_player_count { 0 }; unsigned short m_turns_played { 0 }; int board[3][3]; std::vector m_clients; std::mutex m_clients_mutex; // Loop around, receive messages from client and send them to all // the other connected clients. void handle_client(sf::TcpSocket* client, unsigned short player_num) { sf::Socket::Status status; if(player_num == 1) { std::cout << "Player " << player_num << " is NOUGHTS" << std::endl; char buffer[2] = { GameMessageType::JOIN_GAME, Token::NOUGHTS }; status = client->send(buffer, message_size(GameMessageType::JOIN_GAME)); if (status != sf::Socket::Status::Done) { std::cerr << "Error sending JOIN_GAME to player 1" << std::endl; return; } } else if(player_num == 2) { std::cout << "Player " << player_num << " is CROSSES" << std::endl; char buffer[2] = { GameMessageType::JOIN_GAME, Token::CROSSES }; status = client->send(buffer, message_size(GameMessageType::JOIN_GAME)); if (status != sf::Socket::Status::Done) { std::cerr << "Error sending JOIN_GAME to player 2" << std::endl; return; } } else { return; // No more players please!!! } while (true) { // RECEIVING char payload[1024]; std::memset(payload, 0, 1024); size_t received; sf::Socket::Status status = client->receive(payload, 1024, received); if (status != sf::Socket::Status::Done) { std::cerr << "Error receiving message from client" << std::endl; break; } else { // Actually, there is no need to print the message if the message is not a string debug_message(payload); broadcast_message(payload, client); std::this_thread::sleep_for(std::chrono::milliseconds(250)); if(++m_turns_played == 9) { send_game_over_to_clients(); } } } // Everything that follows only makes sense if we have a graceful way to exiting the loop. // Remove the client from the list when done { std::lock_guard lock(m_clients_mutex); m_clients.erase(std::remove(m_clients.begin(), m_clients.end(), client), m_clients.end()); } delete client; } // Sends `message` from `sender` to all the other connected clients bool broadcast_message(const char *buffer, sf::TcpSocket* sender) { size_t msgSize { message_size(buffer[0]) }; // You might want to validate the message before you send it. // A few reasons for that: // 1. Make sure the message makes sense in the game. // 2. Make sure the sender is not cheating. // 3. First need to synchronise the players inputs (usually done in Lockstep). // 4. Compensate for latency and perform rollbacks (usually done in Ded Reckoning). // 5. Delay the sending of messages to make the game fairer wrt high ping players. // This is where you can write the authoritative part of the server. std::lock_guard lock(m_clients_mutex); for (auto& client : m_clients) { if (client != sender) { // SENDING sf::Socket::Status status = client->send(buffer, msgSize) ; if (status != sf::Socket::Status::Done) { std::cerr << "Error sending message to client" << std::endl; return false; } } } return true; } constexpr size_t message_size(const char messageType) { switch(messageType) { case JOIN_GAME: return 2; case PLACE_TOKEN: return sizeof(int) * 2 + 2; case START_GAME: return 1; case GAME_OVER: return 1; default: return 0; } } void debug_message(const char *buf) { const unsigned char msgType = buf[0]; switch(msgType) { case JOIN_GAME: { std::cout << "Player Joined The Game" << std::endl; break; } case PLACE_TOKEN: { const unsigned char *row { (unsigned char* )buf + 1 }; const unsigned char *col { (unsigned char* )buf + 1 + sizeof(int) }; unsigned int rowI = be32toh(*((unsigned int *) row)); unsigned int colI = be32toh(*((unsigned int *) col)); std::cout << "Player Placed A Token: (" << rowI << ", " << colI << ")" << std::endl; break; } } } }; int main() { GameServer server(4331); server.tcp_start(); return 0; }