mirror of
https://github.com/bdnugget/nbnet.git
synced 2026-11-07 05:27:27 +00:00
414 lines
11 KiB
C
414 lines
11 KiB
C
/*
|
|
|
|
Copyright (C) 2024 BIAGINI Nathan
|
|
|
|
This software is provided 'as-is', without any express or implied
|
|
warranty. In no event will the authors be held liable for any damages
|
|
arising from the use of this software.
|
|
|
|
Permission is granted to anyone to use this software for any purpose,
|
|
including commercial applications, and to alter it and redistribute it
|
|
freely, subject to the following restrictions:
|
|
|
|
1. The origin of this software must not be misrepresented; you must not
|
|
claim that you wrote the original software. If you use this software
|
|
in a product, an acknowledgment in the product documentation would be
|
|
appreciated but is not required.
|
|
2. Altered source versions must be plainly marked as such, and must not be
|
|
misrepresented as being the original software.
|
|
3. This notice may not be removed or altered from any source distribution.
|
|
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
|
|
// For Sleep function
|
|
#if defined(__EMSCRIPTEN__)
|
|
#include <emscripten/emscripten.h>
|
|
#elif defined(_WIN32) || defined(_WIN64)
|
|
#include <winsock2.h>
|
|
#include <windows.h>
|
|
#include <synchapi.h>
|
|
#else
|
|
#include <time.h>
|
|
#endif
|
|
|
|
#include "shared.h"
|
|
|
|
// A simple structure to represent connected clients
|
|
typedef struct
|
|
{
|
|
// Underlying nbnet connection handle, used to send messages to that particular client
|
|
NBN_ConnectionHandle client_handle;
|
|
|
|
// Client state
|
|
ClientState state;
|
|
} Client;
|
|
|
|
// Array of connected clients, NULL means that the slot is free (i.e no clients)
|
|
static Client *clients[MAX_CLIENTS] = {NULL};
|
|
|
|
// Number of currently connected clients
|
|
static unsigned int client_count = 0;
|
|
|
|
// Spawn positions
|
|
static Vector2 spawns[] = {
|
|
(Vector2){50, 50},
|
|
(Vector2){GAME_WIDTH - 100, 50},
|
|
(Vector2){50, GAME_HEIGHT - 100},
|
|
(Vector2){GAME_WIDTH - 100, GAME_HEIGHT - 100}
|
|
};
|
|
|
|
static void AcceptConnection(unsigned int x, unsigned int y, NBN_ConnectionHandle conn)
|
|
{
|
|
NBN_WriteStream ws;
|
|
uint8_t data[32];
|
|
|
|
NBN_WriteStream_Init(&ws, data, sizeof(data));
|
|
|
|
NBN_SerializeUInt((NBN_Stream *)&ws, x, 0, GAME_WIDTH);
|
|
NBN_SerializeUInt((NBN_Stream *)&ws, y, 0, GAME_HEIGHT);
|
|
NBN_SerializeUInt((NBN_Stream *)&ws, conn, 0, UINT_MAX);
|
|
|
|
// Accept the connection
|
|
NBN_GameServer_AcceptIncomingConnectionWithData(data, sizeof(data));
|
|
}
|
|
|
|
static int HandleNewConnection(void)
|
|
{
|
|
TraceLog(LOG_INFO, "New connection");
|
|
|
|
// If the server is full
|
|
if (client_count == MAX_CLIENTS)
|
|
{
|
|
// Reject the connection (send a SERVER_FULL_CODE code to the client)
|
|
TraceLog(LOG_INFO, "Connection rejected");
|
|
NBN_GameServer_RejectIncomingConnectionWithCode(SERVER_FULL_CODE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Otherwise...
|
|
|
|
NBN_ConnectionHandle client_handle;
|
|
|
|
client_handle = NBN_GameServer_GetIncomingConnection();
|
|
|
|
// Get a spawning position for the client
|
|
Vector2 spawn = spawns[client_handle % MAX_CLIENTS];
|
|
|
|
// Build some "initial" data that will be sent to the connected client
|
|
|
|
unsigned int x = (unsigned int)spawn.x;
|
|
unsigned int y = (unsigned int)spawn.y;
|
|
|
|
AcceptConnection(x, y, client_handle);
|
|
|
|
TraceLog(LOG_INFO, "Connection accepted (ID: %d)", client_handle);
|
|
|
|
Client *client = NULL;
|
|
|
|
// Find a free slot in the clients array and create a new client
|
|
for (int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if (clients[i] == NULL)
|
|
{
|
|
client = malloc(sizeof(Client));
|
|
clients[i] = client;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert(client != NULL);
|
|
|
|
client->client_handle = client_handle; // Store the nbnet connection ID
|
|
|
|
// Fill the client state with initial spawning data
|
|
client->state = (ClientState){.client_id = client_handle, .x = 200, .y = 400, .color = CLI_RED, .val = 0};
|
|
|
|
client_count++;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static Client *FindClientById(uint32_t client_id)
|
|
{
|
|
for (int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if (clients[i] && clients[i]->state.client_id == client_id)
|
|
return clients[i];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void DestroyClient(Client *client)
|
|
{
|
|
for (int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if (clients[i] && clients[i]->state.client_id == client->state.client_id)
|
|
{
|
|
clients[i] = NULL;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
free(client);
|
|
}
|
|
|
|
static void HandleClientDisconnection()
|
|
{
|
|
NBN_ConnectionHandle client_handle = NBN_GameServer_GetDisconnectedClient(); // Get the disconnected client
|
|
|
|
TraceLog(LOG_INFO, "Client has disconnected (id: %d)", client_handle);
|
|
|
|
Client *client = FindClientById(client_handle);
|
|
|
|
assert(client);
|
|
|
|
DestroyClient(client);
|
|
|
|
client_count--;
|
|
}
|
|
|
|
static void HandleUpdateStateMessage(UpdateStateMessage *msg, Client *sender)
|
|
{
|
|
// Update the state of the client with the data from the received UpdateStateMessage message
|
|
sender->state.x = msg->x;
|
|
sender->state.y = msg->y;
|
|
sender->state.val = msg->val;
|
|
|
|
UpdateStateMessage_Destroy(msg);
|
|
}
|
|
|
|
static void HandleChangeColorMessage(ChangeColorMessage *msg, Client *sender)
|
|
{
|
|
// Update the client color
|
|
sender->state.color = msg->color;
|
|
|
|
ChangeColorMessage_Destroy(msg);
|
|
}
|
|
|
|
static void HandleReceivedMessage(void)
|
|
{
|
|
// Fetch info about the last received message
|
|
NBN_MessageInfo msg_info = NBN_GameServer_GetMessageInfo();
|
|
|
|
// Find the client that sent the message
|
|
Client *sender = FindClientById(msg_info.sender);
|
|
|
|
assert(sender != NULL);
|
|
|
|
switch (msg_info.type)
|
|
{
|
|
case UPDATE_STATE_MESSAGE:
|
|
// The server received a client state update
|
|
HandleUpdateStateMessage(msg_info.data, sender);
|
|
break;
|
|
|
|
case CHANGE_COLOR_MESSAGE:
|
|
// The server received a client switch color action
|
|
HandleChangeColorMessage(msg_info.data, sender);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int HandleGameServerEvent(int ev)
|
|
{
|
|
switch (ev)
|
|
{
|
|
case NBN_NEW_CONNECTION:
|
|
// A new client has requested a connection
|
|
if (HandleNewConnection() < 0)
|
|
return -1;
|
|
break;
|
|
|
|
case NBN_CLIENT_DISCONNECTED:
|
|
// A previously connected client has disconnected
|
|
HandleClientDisconnection();
|
|
break;
|
|
|
|
case NBN_CLIENT_MESSAGE_RECEIVED:
|
|
// A message from a client has been received
|
|
HandleReceivedMessage();
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Broadcasts the latest game state to all connected clients
|
|
static int BroadcastGameState(void)
|
|
{
|
|
ClientState client_states[MAX_CLIENTS];
|
|
unsigned int client_index = 0;
|
|
|
|
// Loop over the clients and build an array of ClientState
|
|
for (int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
Client *client = clients[i];
|
|
|
|
if (client == NULL) continue;
|
|
|
|
client_states[client_index] = (ClientState) {
|
|
.client_id = client->state.client_id,
|
|
.x = client->state.x,
|
|
.y = client->state.y,
|
|
.val = client->state.val,
|
|
.color = client->state.color
|
|
};
|
|
client_index++;
|
|
}
|
|
|
|
assert(client_index == client_count);
|
|
|
|
// Broadcast the game state to all clients
|
|
for (int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
Client *client = clients[i];
|
|
|
|
if (client == NULL) continue;
|
|
|
|
GameStateMessage *msg = GameStateMessage_Create();
|
|
|
|
// Fill message data
|
|
msg->client_count = client_count;
|
|
memcpy(msg->client_states, client_states, sizeof(ClientState) * client_count);
|
|
|
|
// Unreliably send the message to all connected clients
|
|
NBN_GameServer_SendUnreliableMessageTo(client->client_handle, GAME_STATE_MESSAGE, msg);
|
|
|
|
// TraceLog(LOG_DEBUG, "Sent game state to client %d (%d, %d)", client->client_id, client_count, client_index);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool running = true;
|
|
|
|
#ifndef __EMSCRIPTEN__
|
|
|
|
static void SigintHandler(int dummy)
|
|
{
|
|
running = false;
|
|
}
|
|
|
|
#endif
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
#ifndef __EMSCRIPTEN__
|
|
signal(SIGINT, SigintHandler);
|
|
#endif
|
|
|
|
// Read command line arguments
|
|
if (ReadCommandLine(argc, argv))
|
|
{
|
|
printf("Usage: server [--packet_loss=<value>] [--packet_duplication=<value>] [--ping=<value>] \
|
|
[--jitter=<value>]\n");
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Even though we do not display anything we still use raylib logging capacibilities
|
|
SetTraceLogLevel(LOG_DEBUG);
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
NBN_WebRTC_Register(); // Register the WebRTC driver
|
|
#else
|
|
NBN_UDP_Register(); // Register the UDP driver
|
|
|
|
#endif // __EMSCRIPTEN__
|
|
|
|
// Start the server with a protocol name and a port
|
|
if (NBN_GameServer_StartEx(RAYLIB_EXAMPLE_PROTOCOL_NAME, RAYLIB_EXAMPLE_PORT) < 0)
|
|
{
|
|
TraceLog(LOG_ERROR, "Game server failed to start. Exit");
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Register messages, have to be done after NBN_GameServer_StartEx
|
|
NBN_GameServer_RegisterMessage(
|
|
CHANGE_COLOR_MESSAGE,
|
|
(NBN_MessageBuilder)ChangeColorMessage_Create,
|
|
(NBN_MessageDestructor)ChangeColorMessage_Destroy,
|
|
(NBN_MessageSerializer)ChangeColorMessage_Serialize);
|
|
NBN_GameServer_RegisterMessage(
|
|
UPDATE_STATE_MESSAGE,
|
|
(NBN_MessageBuilder)UpdateStateMessage_Create,
|
|
(NBN_MessageDestructor)UpdateStateMessage_Destroy,
|
|
(NBN_MessageSerializer)UpdateStateMessage_Serialize);
|
|
NBN_GameServer_RegisterMessage(
|
|
GAME_STATE_MESSAGE,
|
|
(NBN_MessageBuilder)GameStateMessage_Create,
|
|
(NBN_MessageDestructor)GameStateMessage_Destroy,
|
|
(NBN_MessageSerializer)GameStateMessage_Serialize);
|
|
|
|
// Network conditions simulated variables (read from the command line, default is always 0)
|
|
NBN_GameServer_SetPing(GetOptions().ping);
|
|
NBN_GameServer_SetJitter(GetOptions().jitter);
|
|
NBN_GameServer_SetPacketLoss(GetOptions().packet_loss);
|
|
NBN_GameServer_SetPacketDuplication(GetOptions().packet_duplication);
|
|
|
|
float tick_dt = 1.f / TICK_RATE; // Tick delta time
|
|
|
|
while (running)
|
|
{
|
|
int ev;
|
|
|
|
// Poll for server events
|
|
while ((ev = NBN_GameServer_Poll()) != NBN_NO_EVENT)
|
|
{
|
|
if (ev < 0)
|
|
{
|
|
TraceLog(LOG_ERROR, "An occured while polling network events. Exit");
|
|
|
|
break;
|
|
}
|
|
|
|
if (HandleGameServerEvent(ev) < 0)
|
|
break;
|
|
}
|
|
|
|
// Broadcast latest game state
|
|
if (BroadcastGameState() < 0)
|
|
{
|
|
TraceLog(LOG_ERROR, "An occured while broadcasting game states. Exit");
|
|
|
|
break;
|
|
}
|
|
|
|
// Pack all enqueued messages as packets and send them
|
|
if (NBN_GameServer_SendPackets() < 0)
|
|
{
|
|
TraceLog(LOG_ERROR, "An occured while flushing the send queue. Exit");
|
|
|
|
break;
|
|
}
|
|
|
|
NBN_GameServerStats stats = NBN_GameServer_GetStats();
|
|
|
|
TraceLog(LOG_TRACE, "Upload: %f Bps | Download: %f Bps", stats.upload_bandwidth, stats.download_bandwidth);
|
|
|
|
// Cap the simulation rate to TICK_RATE ticks per second (just like for the client)
|
|
#if defined(__EMSCRIPTEN__)
|
|
emscripten_sleep(tick_dt * 1000);
|
|
#elif defined(_WIN32) || defined(_WIN64)
|
|
Sleep(tick_dt * 1000);
|
|
#else
|
|
long nanos = tick_dt * 1e9;
|
|
struct timespec t = {.tv_sec = nanos / 999999999, .tv_nsec = nanos % 999999999};
|
|
|
|
nanosleep(&t, &t);
|
|
#endif
|
|
}
|
|
|
|
// Stop the server
|
|
NBN_GameServer_Stop();
|
|
|
|
return 0;
|
|
}
|