From 388c6429cf6afa8eb7a25055441b2c3cdcddaf0a Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Tue, 23 Sep 2025 20:54:39 +0100 Subject: [PATCH] [Change] Remove hybrid local/remote network model Client architecture has been refactored to be fully server-authoritative, remove the previous "hybrid" model that supported both local and remote command execution, that was a stupid idea. - Client now connects to server on startup. - The local command processor and VFS have been removed from the Terminal class. - All command processing is now handled by the server. - The client is now just a thin client essentially I'll in the future enable single player mode by running the server on the local machine in a separate thread. --- assets/scripts/bin/ssh.lua | 4 ++ client/src/client_network.cpp | 9 +-- client/src/client_network.h | 2 +- client/src/terminal.cpp | 85 +++++++++--------------- client/src/terminal.h | 7 +- common/src/lua_processor.cpp | 10 ++- common/src/vfs_manager.cpp | 2 +- server/src/main.cpp | 15 +---- server/src/network_manager.cpp | 117 ++++++++++++++++++--------------- server/src/network_manager.h | 8 ++- server/src/player.cpp | 16 +++-- server/src/player.h | 4 ++ 12 files changed, 137 insertions(+), 142 deletions(-) create mode 100644 assets/scripts/bin/ssh.lua diff --git a/assets/scripts/bin/ssh.lua b/assets/scripts/bin/ssh.lua new file mode 100644 index 0000000..1ccf3aa --- /dev/null +++ b/assets/scripts/bin/ssh.lua @@ -0,0 +1,4 @@ +-- /bin/ssh - Connects to a remote host. +local target = arg[1] +if not target then return "ssh: missing operand" end +return { action = "ssh", target = target } diff --git a/client/src/client_network.cpp b/client/src/client_network.cpp index ddba593..0c87b1f 100644 --- a/client/src/client_network.cpp +++ b/client/src/client_network.cpp @@ -1,9 +1,9 @@ #include #include -#include +#include +#include "SDL3_net/SDL_net.h" #include "client_network.h" -#include ClientNetwork::ClientNetwork(void) { if(!NET_Init()) { @@ -55,9 +55,9 @@ bool ClientNetwork::connect(const char* host, int port) { return false; } -void ClientNetwork::send_command(const char* command) { +void ClientNetwork::send_command(const std::string& command) { if(!_socket) return; - NET_WriteToStreamSocket(_socket, command, strlen(command)+1); + NET_WriteToStreamSocket(_socket, command.c_str(), command.length()+1); } void ClientNetwork::disconnect(void) { @@ -80,6 +80,7 @@ std::string ClientNetwork::receive_response(void) { 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 index 2b9fec1..4e27266 100644 --- a/client/src/client_network.h +++ b/client/src/client_network.h @@ -9,7 +9,7 @@ public: ~ClientNetwork(void); bool connect(const char* host, int port); - void send_command(const char* command); + void send_command(const std::string& data); void disconnect(void); std::string receive_response(void); diff --git a/client/src/terminal.cpp b/client/src/terminal.cpp index d772dbf..2ff16e9 100644 --- a/client/src/terminal.cpp +++ b/client/src/terminal.cpp @@ -4,26 +4,26 @@ #include #include "client_network.h" -#include "command_processor.h" #include "gfx/txt_renderer.h" -#include "vfs_manager.h" -#include "vfs.h" #include "terminal.h" Terminal::Terminal(void) { /* Placeholder welcome message to history. */ _history.push_back("Welcome to Bettola (local mode)"); + _history.push_back("Connecting to server..."); _network = new ClientNetwork(); - _is_remote = false; _should_close = false; - - /* Create local file system for the client. */ - VFSManager vfs_manager; - _local_vfs = vfs_manager.create_vfs("local"); - _local_cmd_processor = new CommandProcessor(_local_vfs); - _current_path = get_full_path(_local_cmd_processor->get_current_dir()); _scroll_offset = 0; + + /* TODO: Move network to main.cpp, or better yet, a dedicatated game state file */ + if(_network->connect("localhost", 8080)) { + _history.push_back("Connection successful."); + _prompt = _network->receive_response(); /* Get initial prompt from server. */ + } else { + _history.push_back("Connection failed. Please restart."); + _prompt = "error>"; + } } bool Terminal::close(void) { @@ -41,52 +41,27 @@ void Terminal::_on_ret_press(void) { _input_buffer.clear(); /* Clear the input buffer immediately. */ /* Add the command to history. */ - _history.push_back(_current_path + "> " + command); + _history.push_back(_prompt + "> " + command); - if(_is_remote) { - /* REMOTE STATE. */ - _network->send_command(command.c_str()); + if(command == "exit") { + _should_close = true; + /* TODO: Window will close, and the OS will close the socket. + * server will detect the disconnection. + */ + return; + } else if(command == "clear"){ + _history.clear(); + return; + } - if(command == "exit") { - /* Don't wait for a response, the server booted us. */ - _is_remote = false; - _network->disconnect(); - _current_path = get_full_path(_local_cmd_processor->get_current_dir()); - _history.push_back("Connection closed."); - } else { - /* For all other commands, wait for a response. */ - std::string response = _network->receive_response(); - if(!response.empty()) { _history.push_back(response); } - } - } else { - /* LOCAL STATE! */ - if(command.rfind("ssh ", 0) == 0) { - if(_network->connect("localhost", 8080)) { - _network->send_command(command.c_str()); - std::string response = _network->receive_response(); - if(response.rfind("CONNECTED ", 0) == 0) { - _is_remote = true; - std::string ip = response.substr(10); - _current_path = "root@" + ip; - } else { - _history.push_back(response); - _network->disconnect(); /* Disconnect on fialed ssh login. */ - } - } else { - _history.push_back("Connection failed."); - } - } else if(command == "exit") { - _should_close = true; - } else if(command == "clear") { - _history.clear(); - } else { - std::string response = _local_cmd_processor->process_command(command); - if(response.rfind("/", 0) == 0) { - _current_path = response; - } else if(!response.empty()) { - _history.push_back(response); - } - } + _network->send_command(command); + std::string response = _network->receive_response(); + /* Server sends back "output\nprompt". Split them. */ + size_t last_newline = response.find_last_of('\n'); + if(last_newline != std::string::npos) { + _prompt = response.substr(last_newline+1); + std::string output = response.substr(0, last_newline); + if(!output.empty()) { _history.push_back(output); } } /* TODO: Ugly hack. Refactor to pass window height @@ -151,7 +126,7 @@ void Terminal::render(TextRenderer* renderer, int x, int y, int width, int heigh } /* Draw current input line. */ - std::string line_to_render = _current_path + "> " + _input_buffer; + std::string line_to_render = _prompt + "> " + _input_buffer; if(show_cursor) { line_to_render += "_"; } diff --git a/client/src/terminal.h b/client/src/terminal.h index 303e699..02f0e5f 100644 --- a/client/src/terminal.h +++ b/client/src/terminal.h @@ -5,8 +5,6 @@ #include "gfx/txt_renderer.h" #include "client_network.h" -#include "vfs_manager.h" -#include "command_processor.h" class Terminal { public: @@ -25,9 +23,6 @@ private: bool _should_close; std::vector _history; int _scroll_offset; - std::string _current_path; - bool _is_remote; - vfs_node* _local_vfs; - CommandProcessor* _local_cmd_processor; + std::string _prompt; ClientNetwork* _network; }; diff --git a/common/src/lua_processor.cpp b/common/src/lua_processor.cpp index 19f8f3b..bb395da 100644 --- a/common/src/lua_processor.cpp +++ b/common/src/lua_processor.cpp @@ -1,5 +1,7 @@ #include "lua_processor.h" #include +#include +#include #include "vfs.h" LuaProcessor::LuaProcessor(void) { @@ -27,7 +29,13 @@ sol::object LuaProcessor::execute(const std::string& script, vfs_node* current_d } _lua["arg"] = arg_table; - return _lua.script(script); + sol::protected_function_result result = _lua.safe_script(script); + if(result.valid()) { + return result; + } else { + sol::error err = result; + return sol::make_object(_lua, err.what()); + } } catch(const sol::error& e) { /* Return the error message as a string in a sol::object. */ diff --git a/common/src/vfs_manager.cpp b/common/src/vfs_manager.cpp index 48b0b9c..17d61e7 100644 --- a/common/src/vfs_manager.cpp +++ b/common/src/vfs_manager.cpp @@ -75,7 +75,7 @@ vfs_node* VFSManager::create_vfs(const std::string& system_type) { user->children["readme.txt"] = readme; /* Link to the shared directories from the template. */ - root->children["bin"] = _vfs_root->children["bin"]; + root->children["bin"] = copy_vfs_node(_vfs_root->children["bin"], root); if(system_type == "npc") { vfs_node* npc_file = new_node("npc_system.txt", FILE_NODE, root); diff --git a/server/src/main.cpp b/server/src/main.cpp index 8a4d0cd..4033dd4 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -1,24 +1,11 @@ #include -#include -#include -#include "vfs.h" -#include "vfs_manager.h" #include "network_manager.h" int main(int argc, char** argv) { printf("=== Server starting ===\n"); - VFSManager vfs_manager; - - /* Create the world map of networks. */ - std::map world_vfs; - world_vfs["8.8.8.8"] = vfs_manager.create_vfs("npc"); /* Basic npc system. */ - world_vfs["10.0.2.15"] = vfs_manager.create_vfs("npc"); /* Another one. */ - printf("Created world with %zu networks.\n", world_vfs.size()); - - /* Network manager now managed the whole world. */ - NetworkManager* net_manager = new NetworkManager(world_vfs); + NetworkManager* net_manager = new NetworkManager(); net_manager->start(); /* Our loop. */ delete net_manager; /* Shouldn't get here. */ diff --git a/server/src/network_manager.cpp b/server/src/network_manager.cpp index 2101540..0480efa 100644 --- a/server/src/network_manager.cpp +++ b/server/src/network_manager.cpp @@ -1,15 +1,26 @@ #include #include -#include -#include +#include "SDL3_net/SDL_net.h" #include "vfs.h" +#include "vfs_manager.h" #include "command_processor.h" #include "network_manager.h" -NetworkManager::NetworkManager(std::map world_vfs) { - _world_vfs = world_vfs; +/* Send a length-prefixed string. */ +void send_response_string(NET_StreamSocket* socket, const std::string& data) { + if(!socket) return; + NET_WriteToStreamSocket(socket, data.c_str(), data.length()+1); +} + +NetworkManager::NetworkManager(void) { + /* The VFSManager for the world's NPC's. + * Player VSManagers will be created on a per-player basis. + */ + _world_vfs["8.8.8.8"] = _npc_vfs_manager.create_vfs("npc"); + _world_vfs["10.0.2.15"] = _npc_vfs_manager.create_vfs("npc"); + printf("Created world with %zu networks.\n", _world_vfs.size()); if(!NET_Init()) { printf("Error: SDLNet_Init: %s\n", SDL_GetError()); @@ -24,6 +35,9 @@ NetworkManager::NetworkManager(std::map world_vfs) { } NetworkManager::~NetworkManager(void) { + for(auto player : _players) { + delete player; + } NET_DestroyServer(_server_socket); NET_Quit(); } @@ -47,83 +61,80 @@ void NetworkManager::_handle_new_connections(void) { if(NET_AcceptClient(_server_socket, &client_socket)) { if(client_socket) { Player* new_player = new Player(client_socket); - new_player->cmd_processor = nullptr; /* Not connected to remote host yet. */ _players.push_back(new_player); printf("Client connected. Total players: %zu\n", _players.size()); + + /* Send the initial prompt to the new client. */ + std::string prompt = get_full_path(new_player->cmd_processor->get_current_dir()); + send_response_string(client_socket, prompt); } } } 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_StreamSocket* sock_array[1] = { player->socket }; - /* 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) { - printf("bytes_received: %d\n", bytes_received); + /* Timeout of 0 for non-blocking check. */ + int ready = NET_WaitUntilInputAvailable((void**)sock_array, 1, 0); + if(ready == -1) { + /* An error occured on the socket. */ + fprintf(stderr, "[SERVER] Socket error, disconnecting client.\n"); + _disconnect_client(player, i); + continue; } - if(bytes_received > 0) { - /* _process_command returns true, the client wants to exit, boot it! */ - if(_process_command(player, buffer)) { + if(ready > 0) { + /* Socket has data. If processing fails, disconnect the client. */ + fprintf(stderr, "[SERVER] Socket has data, processing command...\n"); + + if(!_process_command(player)) { + fprintf(stderr, "[SERVER] _process_command failed, disconnecting client.\n"); _disconnect_client(player, i); } - } else if(bytes_received < 0) { - /* Disconnect on error or if the client closes the connection. */ - _disconnect_client(player, i); } } } -bool NetworkManager::_process_command(Player* player, char* command) { - /* Create a clean std::string from the buffer. */ - std::string cmd_str = command; +bool NetworkManager::_process_command(Player* player) { + /* Read the length-prefixed command from the client. */ + char buffer[2048]; + memset(buffer, 0, sizeof(buffer)); - if(cmd_str == "exit") { - /* Client is disconnecting, no response needed. */ - return true; /* Disconnect signal. */ - } else if(strncmp(cmd_str.c_str(), "ssh ", 4) == 0) { - std::string target = cmd_str.substr(4); - std::string target_ip = target; - size_t at_pos = target.find('@'); - if(at_pos != std::string::npos) { - target_ip = target.substr(at_pos + 1); - } - if(_world_vfs.count(target_ip)) { - /* Create new command processor for the player's session on the target VFS. */ - if(player->cmd_processor) { delete player->cmd_processor; } - player->cmd_processor = new CommandProcessor(_world_vfs[target_ip]); - std::string response = "CONNECTED " + target_ip; - NET_WriteToStreamSocket(player->socket, response.c_str(), response.length()+1); - } else { - NET_WriteToStreamSocket(player->socket, "ssh: Could not resolve hostname\n", 32); - } - } else { - /* If not ssh command, and we have a command processor, process it. */ - if(player->cmd_processor) { - std::string response = player->cmd_processor->process_command(cmd_str); - printf("OK: cmd_processor exits. Processing command '%s'\n", cmd_str.c_str()); - NET_WriteToStreamSocket(player->socket, response.c_str(), response.length()+1); - } else { - printf("ERROR: cmd_processor is null. Cannot process command '%s'\n", cmd_str.c_str()); - NET_WriteToStreamSocket(player->socket, "Error: Not in a remote session.\n", 32); - } + int bytes_received = NET_ReadFromStreamSocket(player->socket, buffer, sizeof(buffer)-1); + if(bytes_received <= 0) { + return false; /* Error or disconnect. */ } - return false; /* Don't disconnect. */ + + std::string cmd_str(buffer); + fprintf(stderr, "[SERVER] Received command: \"%s\"\n", cmd_str.c_str()); + + /* Process the command. */ + std::string response; + + if(player->cmd_processor) { + response = player->cmd_processor->process_command(cmd_str); + } else { + response = "Error: No command processor available for this player."; + } + + /* Get the new prompt. Append it to response with a known separator. */ + std::string new_prompt = get_full_path(player->cmd_processor->get_current_dir()); + std::string final_response = response + "\n" + new_prompt; + + send_response_string(player->socket, final_response); + return true; } void NetworkManager::_disconnect_client(Player* player, int index) { + fprintf(stderr, "[SERVER] Disconnecting client.\n"); NET_DestroyStreamSocket(player->socket); /* If we have a valid index, remove the player. Otherwise, find them. */ if(index != -1) { _players.erase(_players.begin() + index); } - if(player->cmd_processor) { delete player->cmd_processor; } delete player; + printf("Client disconnected. Total players: %zu\n", _players.size()); } diff --git a/server/src/network_manager.h b/server/src/network_manager.h index d6c997a..34f4c59 100644 --- a/server/src/network_manager.h +++ b/server/src/network_manager.h @@ -6,10 +6,11 @@ #include #include "player.h" +#include "vfs_manager.h" class NetworkManager { public: - NetworkManager(std::map world_vfs); + NetworkManager(); ~NetworkManager(void); void start(void); @@ -17,10 +18,11 @@ public: private: void _handle_new_connections(void); void _handle_client_activity(void); - bool _process_command(Player* player, char* command); + bool _process_command(Player* player); void _disconnect_client(Player* player, int index); NET_Server* _server_socket; std::vector _players; - std::map _world_vfs; + std::map _world_vfs; /* For NPC's. */ + VFSManager _npc_vfs_manager; }; diff --git a/server/src/player.cpp b/server/src/player.cpp index c24ca64..0230cf5 100644 --- a/server/src/player.cpp +++ b/server/src/player.cpp @@ -1,7 +1,15 @@ #include "player.h" -#include +#include "command_processor.h" -Player::Player(NET_StreamSocket* new_socket) { - socket = new_socket; - cmd_processor = nullptr; +Player::Player(NET_StreamSocket* new_socket) : + socket(new_socket), + vfs_root(vfs_manager.create_vfs("player")), + cmd_processor(new CommandProcessor(vfs_root)) {} + +Player::~Player(void) { + if(cmd_processor) { + delete cmd_processor; + } + + /* TODO: The VFSManager should handle deleting the vfs_root. */ } diff --git a/server/src/player.h b/server/src/player.h index 389f964..f5eeeb4 100644 --- a/server/src/player.h +++ b/server/src/player.h @@ -3,13 +3,17 @@ #include #include "vfs.h" +#include "vfs_manager.h" #include "command_processor.h" class Player { public: Player(NET_StreamSocket* socket); + ~Player(void); NET_StreamSocket* socket; + VFSManager vfs_manager; + vfs_node* vfs_root; CommandProcessor* cmd_processor; /* Manages the VFS state for the remote session. */ private: };