Files

257 lines
8.4 KiB
C++

#include <SFML/Network.hpp>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
// 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<std::mutex> 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<sf::TcpSocket*> 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<std::mutex> 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<std::mutex> 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;
}