From 92106a3c449105d3dff5b5b659484c141a29e887 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Sat, 20 Sep 2025 18:58:24 +0100 Subject: [PATCH] [Add] Client/server networking and 'ls' command. --- client/CMakeLists.txt | 7 ++- client/src/client_network.cpp | 76 ++++++++++++++++++++++++++ client/src/client_network.h | 17 ++++++ client/src/terminal.cpp | 16 ++++++ client/src/terminal.h | 3 ++ common/src/vfs.h | 26 +++++++++ server/CMakeLists.txt | 8 ++- server/src/main.cpp | 14 ++++- server/src/network_manager.cpp | 97 ++++++++++++++++++++++++++++++++++ server/src/network_manager.h | 25 +++++++++ server/src/player.cpp | 7 +++ server/src/player.h | 13 +++++ server/src/vfs_manager.cpp | 37 +++++++++++++ server/src/vfs_manager.h | 7 +++ 14 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 client/src/client_network.cpp create mode 100644 client/src/client_network.h create mode 100644 common/src/vfs.h create mode 100644 server/src/network_manager.cpp create mode 100644 server/src/network_manager.h create mode 100644 server/src/player.cpp create mode 100644 server/src/player.h create mode 100644 server/src/vfs_manager.cpp create mode 100644 server/src/vfs_manager.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 9c3db6c..0356d53 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -5,10 +5,13 @@ add_executable(bettolac ) find_package(SDL3 REQUIRED) +find_package(SDL3_net REQUIRED) find_package(GLEW REQUIRED) find_package(OpenGL REQUIRED) find_package(Freetype REQUIRED) -target_link_libraries(bettolac PRIVATE bettola SDL3::SDL3 GLEW::glew OpenGL::GL Freetype::Freetype) +target_link_libraries(bettolac PRIVATE bettola SDL3::SDL3 SDL3_net + GLEW::glew OpenGL::GL Freetype::Freetype) -target_include_directories(bettolac PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_include_directories(bettolac PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src + /usr/local/include/SDL3_net) diff --git a/client/src/client_network.cpp b/client/src/client_network.cpp new file mode 100644 index 0000000..b36a683 --- /dev/null +++ b/client/src/client_network.cpp @@ -0,0 +1,76 @@ +#include +#include +#include "SDL_net.h" + +#include "client_network.h" +#include + +ClientNetwork::ClientNetwork(void) { + if(!NET_Init()) { + printf("Error: NET_Init: %s\n", SDL_GetError()); + } + _socket = nullptr; +} + +ClientNetwork::~ClientNetwork(void) { + if(_socket) { + NET_DestroyStreamSocket(_socket); + } + NET_Quit(); +} + +bool ClientNetwork::connect(const char* host, int port) { + NET_Address* address = NET_ResolveHostname(host); + if(!address) { + printf("Error: NET_ResolveHostname %s\n", SDL_GetError()); + return false; + } + + /* This seems impossible, but I'll try it.. + * Pass the port to CreateClient as the compiler insists we do. + */ + /* FUCK ME!! Wait up to 5 seconds for DNS resolution to complete. */ + if(NET_WaitUntilResolved(address, 5000) <= 0) { + printf("Error: NET_WaitUntilResolved: %s\n", SDL_GetError()); + NET_UnrefAddress(address); + return false; + } + + _socket = NET_CreateClient(address, port); + if(!_socket) { + printf("Error: NET_CreateClient: %s\n", SDL_GetError()); + NET_UnrefAddress(address); + return false; + } + + /* Wait up to 5 seconds for the connection to establish. */ + if(NET_WaitUntilConnected((_socket), 5000) > 0) { + printf("connected server.\n"); + NET_UnrefAddress(address); + return true; + } + + printf("Error: connection timeout.\n"); + NET_UnrefAddress(address); + return false; +} + +void ClientNetwork::send_command(const char* command) { + if(!_socket) return; + NET_WriteToStreamSocket(_socket, command, strlen(command)+1); +} + +std::string ClientNetwork::receive_response(void) { + if(!_socket) return ""; + char buffer[2048]; + memset(buffer, 0, sizeof(buffer)); + + void* socket_ptr = _socket; + if(NET_WaitUntilInputAvailable(&socket_ptr, 1, 2000) > 0) { + if(NET_ReadFromStreamSocket(_socket, buffer, sizeof(buffer)) > 0) { + return std::string(buffer); + } + } + return ""; /* Return empty on timeout or error. */ +} + diff --git a/client/src/client_network.h b/client/src/client_network.h new file mode 100644 index 0000000..dfa23a0 --- /dev/null +++ b/client/src/client_network.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +class ClientNetwork { +public: + ClientNetwork(void); + ~ClientNetwork(void); + + bool connect(const char* host, int port); + void send_command(const char* command); + std::string receive_response(void); + +private: + NET_StreamSocket* _socket; +}; diff --git a/client/src/terminal.cpp b/client/src/terminal.cpp index b620bdb..c3e44ff 100644 --- a/client/src/terminal.cpp +++ b/client/src/terminal.cpp @@ -3,6 +3,7 @@ #include #include +#include "client_network.h" #include "gfx/txt_renderer.h" #include "terminal.h" @@ -10,6 +11,16 @@ Terminal::Terminal(void) { /* Placeholder welcome message to history. */ _history.push_back("Welcome to Bettola"); _scroll_offset = 0; + _network = new ClientNetwork(); + if(_network->connect("localhost", 8080)) { + _history.push_back("Connection to server Success!"); + } else { + _history.push_back("Connection to server failed!"); + } +} + +Terminal::~Terminal(void) { + delete _network; } void Terminal::_on_ret_press(void) { @@ -20,6 +31,11 @@ void Terminal::_on_ret_press(void) { * TODO: Parse and execute commands. */ printf("Command entered: %s\n", _input_buffer.c_str()); + _network->send_command(_input_buffer.c_str()); + std::string response = _network->receive_response(); + if(!response.empty()) { + _history.push_back(response); + } if(_input_buffer == "clear") { _history.clear(); diff --git a/client/src/terminal.h b/client/src/terminal.h index b68c483..ab7a938 100644 --- a/client/src/terminal.h +++ b/client/src/terminal.h @@ -4,10 +4,12 @@ #include #include "gfx/txt_renderer.h" +#include "client_network.h" class Terminal { public: Terminal(void); + ~Terminal(void); void handle_input(SDL_Event* event); void render(TextRenderer* renderer, int x, int y, int width, int height, bool show_cursor); @@ -19,4 +21,5 @@ private: std::string _input_buffer; std::vector _history; int _scroll_offset; + ClientNetwork* _network; }; diff --git a/common/src/vfs.h b/common/src/vfs.h new file mode 100644 index 0000000..3d99743 --- /dev/null +++ b/common/src/vfs.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +struct vfs_node; + +/* Store children for quick lookup by name. */ +typedef std::map vfs_child_map; + +enum vfs_node_type { + FILE_NODE, + DIR_NODE +}; + +struct vfs_node { + std::string name; + vfs_node_type type; + + /* Files. */ + std::string content; + + /* Directories. */ + vfs_child_map children; + vfs_node* parent; +}; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 6e7c919..00cf308 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -4,5 +4,11 @@ add_executable(bettolas ${BETTOLAS_SOURCES} ) -target_link_libraries(bettolas PRIVATE bettola) +find_package(SDL3 REQUIRED) +find_package(SDL3_net REQUIRED) +target_link_libraries(bettolas PRIVATE bettola SDL3::SDL3 SDL3_net) + +target_include_directories(bettolas PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + /usr/local/include/SDL3_net) diff --git a/server/src/main.cpp b/server/src/main.cpp index df48ef9..b401e28 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -1,8 +1,18 @@ #include -#include "bettola.h" + +#include "vfs_manager.h" +#include "network_manager.h" int main(int argc, char** argv) { printf("=== Server starting ===\n"); - bettola_function(); + + vfs_node* root_vfs = vfs_manager::create_root_system(); + printf("Virtual file system created. Root contains '%s' directory.\n", + root_vfs->children["home"]->name.c_str()); + + NetworkManager* net_manager = new NetworkManager(root_vfs); + net_manager->start(); /* Our loop. */ + + delete net_manager; /* Shouldn't get here. */ return 0; } diff --git a/server/src/network_manager.cpp b/server/src/network_manager.cpp new file mode 100644 index 0000000..ee47137 --- /dev/null +++ b/server/src/network_manager.cpp @@ -0,0 +1,97 @@ +#include +#include + +#include "vfs.h" + +#include "network_manager.h" + +NetworkManager::NetworkManager(vfs_node* vfs_root) { + _vfs_root = vfs_root; + + if(!NET_Init()) { + printf("Error: SDLNet_Init: %s\n", SDL_GetError()); + return; + } + + _server_socket = NET_CreateServer(NULL, 8080); + if(!_server_socket) { + printf("Error: NET_CreateServer: %s\n", SDL_GetError()); + return; + } +} + +NetworkManager::~NetworkManager(void) { + NET_DestroyServer(_server_socket); + NET_Quit(); +} + +void NetworkManager::start(void) { + printf("BettolaServer listening on port 8080...\n"); + while(true) { + /* Check for and accept any new client connections. */ + _handle_new_connections(); + + /* Check all existing clients for incoming data. */ + _handle_client_activity(); + + /* Let's not be burning CPU cycles. */ + SDL_Delay(0); + } +} + +void NetworkManager::_handle_new_connections(void) { + NET_StreamSocket* client_socket; + if(NET_AcceptClient(_server_socket, &client_socket)) { + if(client_socket) { + Player* new_player = new Player(client_socket); + new_player->current_dir = _vfs_root; + _players.push_back(new_player); + printf("Client connected. Total players: %zu\n", _players.size()); + } + } +} + +void NetworkManager::_handle_client_activity(void) { + char buffer [512]; + + /* Iterate backwards so we can safely remove disconnected clients. */ + for(int i = _players.size()-1; i >= 0; --i) { + Player* player = _players[i]; + + /* NET_ReadFromStreamSocket returns > 0 if data was received. + * 0 if no data, and < 0 on error (disconnect). + */ + int bytes_received = NET_ReadFromStreamSocket(player->socket, buffer, 512); + + if(bytes_received > 0) { + _process_command(player, buffer); + } else if(bytes_received < 0 ) { + _disconnect_client(player, i); + } + } +} + +void NetworkManager::_process_command(Player* player, char* command) { + /* We'll just do 'ls' for now. */ + if(strncmp(command, "ls", 2) == 0) { + std::string response = ""; + if(player->current_dir && player->current_dir->type == DIR_NODE) { + for(auto const& [name, node] : player->current_dir->children) { + response += name; + if(node->type == DIR_NODE) { + response += "/"; + } + response += "\n"; + } + } + /* NET_WriteToStreamSocket is essentially the new send function in SDL3. */ + NET_WriteToStreamSocket(player->socket, response.c_str(), response.length()+1); + } +} + +void NetworkManager::_disconnect_client(Player* player, int index) { + printf("Client disconnected.\n"); + NET_DestroyStreamSocket(player->socket); + _players.erase(_players.begin() + index); + delete player; +} diff --git a/server/src/network_manager.h b/server/src/network_manager.h new file mode 100644 index 0000000..33cd16c --- /dev/null +++ b/server/src/network_manager.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include "SDL_net.h" +#include "player.h" + +class NetworkManager { +public: + NetworkManager(vfs_node* vfs_root); + ~NetworkManager(void); + + void start(void); + +private: + void _handle_new_connections(void); + void _handle_client_activity(void); + void _process_command(Player* player, char* command); + void _disconnect_client(Player* player, int index); + + NET_Server* _server_socket; + std::vector _players; + vfs_node* _vfs_root; +}; diff --git a/server/src/player.cpp b/server/src/player.cpp new file mode 100644 index 0000000..316191d --- /dev/null +++ b/server/src/player.cpp @@ -0,0 +1,7 @@ +#include "player.h" +#include "SDL_net.h" + +Player::Player(NET_StreamSocket* new_socket) { + socket = new_socket; + current_dir = nullptr; /* Will set to VFS root on connection. */ +} diff --git a/server/src/player.h b/server/src/player.h new file mode 100644 index 0000000..fb2ffc5 --- /dev/null +++ b/server/src/player.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include "vfs.h" + +class Player { +public: + Player(NET_StreamSocket* socket); + + NET_StreamSocket* socket; + vfs_node* current_dir; +private: +}; diff --git a/server/src/vfs_manager.cpp b/server/src/vfs_manager.cpp new file mode 100644 index 0000000..19c6f89 --- /dev/null +++ b/server/src/vfs_manager.cpp @@ -0,0 +1,37 @@ +#include "vfs_manager.h" +#include "vfs.h" + +/* Create a new node. */ +vfs_node* new_node(std::string name, vfs_node_type type, vfs_node* parent) { + vfs_node* node = new vfs_node(); + node->name = name; + node->type = type; + node->parent = parent; + return node; +} + +vfs_node* vfs_manager::create_root_system(void) { + /* Rood directory. */ + vfs_node* root = new_node("/", DIR_NODE, nullptr); + + /* Subdirectories. */ + vfs_node* home = new_node("home", DIR_NODE, root); + vfs_node* bin = new_node("bin", DIR_NODE, root); + root->children["home"] = home; + root->children["bin"] = bin; + + /* User diractory. */ + vfs_node* user = new_node("user", DIR_NODE, home); + home->children["user"] = user; + + /* Create file. */ + vfs_node* readme = new_node("readme.txt", FILE_NODE, user); + readme->content = "Welcome to your new virtual machine."; + user->children["readme.txt"] = readme; + + vfs_node* ls_exe = new_node("ls", FILE_NODE, bin); + ls_exe->content = "[executable]"; + bin->children["ls"] = ls_exe; + + return root; +} diff --git a/server/src/vfs_manager.h b/server/src/vfs_manager.h new file mode 100644 index 0000000..87c8077 --- /dev/null +++ b/server/src/vfs_manager.h @@ -0,0 +1,7 @@ +#pragma once + +#include "vfs.h" + +namespace vfs_manager { + vfs_node* create_root_system(void); +}