From fce3b3aad6b3a498708c2efd210a7600f96484f5 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Sun, 5 Oct 2025 22:45:33 +0100 Subject: [PATCH] [Add] Implement database persistance and Login UI. [Build System] - Integrate 'sqlite3' and 'sqlite_modern_cpp' using FetchContent. - Enabled 'C' language to allow compilation of 'sqlite3' lib. [Persistance] - Adds a 'DatabaseManager' class to handle all SQLite operations. - Creates a 'players' table on server startup. - Server uses separate database for single-player 'bettola_sp.db' and 'bettola.db' [UI] - Adds a new 'LoginScreen' UI. - Game flow is now MainMenu -> LoginScreen -> bootSequence -> Desktop. - 'LoginScreen' has interactive tabs to switch between "Login" and "Create Account" 'modes'. - Full client-server communication for creating accounts and authentication. [Server] - Refactor 'NetworkManager' to handle an 'AUTHENTICATING' state for new connectiosn. - Player state is only set to 'ACTIVE' after a successful login --- .gitignore | 1 + client/src/game_state.cpp | 68 +++++++++++++++++++----------- client/src/ui/login_screen.cpp | 56 +++++++++++++++++++++--- client/src/ui/login_screen.h | 14 ++++-- common/src/db/database_manager.cpp | 42 ++++++++++++++++++ common/src/db/database_manager.h | 23 ++++++++++ server/src/main.cpp | 5 ++- server/src/network_manager.cpp | 64 +++++++++++++++++++++++++++- server/src/network_manager.h | 5 ++- server/src/player.cpp | 3 +- server/src/player.h | 6 +++ 11 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 common/src/db/database_manager.cpp create mode 100644 common/src/db/database_manager.h diff --git a/.gitignore b/.gitignore index 26ef1ff..8ecf245 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ compile_commands.json assets/design_doc.org .clangd *.swp +*.db diff --git a/client/src/game_state.cpp b/client/src/game_state.cpp index 9adcc21..91777f2 100644 --- a/client/src/game_state.cpp +++ b/client/src/game_state.cpp @@ -30,7 +30,7 @@ void GameState::_init_desktop(void) { void GameState::_run_server(void) { try { - NetworkManager server; + NetworkManager server("bettola_sp.db"); server.start(SINGLE_PLAYER_PORT); /* * Server's start() method is non-blocking, but NetworkManager @@ -123,41 +123,59 @@ void GameState::update(float dt, int draw_calls, int shape_verts, int text_verts _main_menu.reset(); /* Free mem. */ if(_current_screen == Screen::LOGIN) { + /* Connect to server. */ + if(_is_single_player) { + fprintf(stdout, "Starting in single-player mode...\n"); + std::thread server_thread(&GameState::_run_server, this); + server_thread.detach(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + if(!_network->connect("127.0.0.1", SINGLE_PLAYER_PORT)) { + /* TODO: Handle connection failure. */ + } + } else { + if(!_network->connect("127.0.0.1", MULTIPLAYER_PORT)) { + /* TODO: Handle connection failure. */ + } + } _login_screen = std::make_unique(_screen_width, _screen_height); } } break; } - case Screen::LOGIN: + case Screen::LOGIN: { if(_login_screen && _login_screen->is_login_attempted()) { - /* TODO: Send credentials to server for verification. */ - printf("Login attempt: user=%s, pass=%s, host=%s\n", - _login_screen->get_username().c_str(), - _login_screen->get_password().c_str(), - _login_screen->get_hostname().c_str()); + std::string username = _login_screen->get_username(); + std::string password = _login_screen->get_password(); - _current_screen = Screen::BOOTING; - _login_screen.reset(); /* Free mem. */ - _boot_sequence = std::make_unique(); + if(_login_screen->is_new_account_mode()) { + std::string hostname = _login_screen->get_hostname(); + std::string msg = "C_ACC::" + username + "::" + password + "::" + hostname; + _network->send(msg); + } else { + std::string msg = "LOGIN::" + username + "::" + password; + _network->send(msg); + } + _login_screen->clear_login_attempt(); /* Try to spam my server now b.tch! */ } - break; + + /* Check for server response to our login attempt. */ + std::string server_msg; + while(_network->poll_message(server_msg)) { + if(server_msg == "LOGIN_SUCCESS" || server_msg == "C_ACC_SUCCESS") { + _current_screen = Screen::BOOTING; + _login_screen.reset(); /* Free mem. */ + _boot_sequence = std::make_unique(); + break; + } else if(server_msg == "LOGIN_FAIL") { + _login_screen->set_error_message("Invalid username or password."); + } else if(server_msg == "C_ACC_FAIL") { + _login_screen->set_error_message("Username already exists."); + } + } + } break; case Screen::BOOTING: { if(!_boot_sequence) break; /* Shouldn't happen. */ if(_boot_sequence->is_finished()) { - /* Connect to server. */ - if(_is_single_player) { - fprintf(stdout, "Starting in single-player mode...\n"); - std::thread server_thread(&GameState::_run_server, this); - server_thread.detach(); - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - if(!_network->connect("127.0.0.1", SINGLE_PLAYER_PORT)) { - /* TODO: Handle connection failure. */ - } - } else { - if(!_network->connect("127.0.0.1", MULTIPLAYER_PORT)) { - /* TODO: Handle connection failure. */ - } - } _current_screen = Screen::DESKTOP; _init_desktop(); _boot_sequence.reset(); /* Free mem. */ diff --git a/client/src/ui/login_screen.cpp b/client/src/ui/login_screen.cpp index 57eb939..f48dcb0 100644 --- a/client/src/ui/login_screen.cpp +++ b/client/src/ui/login_screen.cpp @@ -5,8 +5,18 @@ #include "ui/ui_renderer.h" #include "ui/login_screen.h" -LoginScreen::LoginScreen(int screen_width, int screen_height) - : _screen_width(screen_width), _screen_height(screen_height) {} +LoginScreen::LoginScreen(int screen_width, int screen_height) : + _screen_width(screen_width), + _screen_height(screen_height) { + + _is_new_account = false; /* Default to login. */ + + /* Position tabs relative to the form. */ + const int form_center_x = _screen_width / 2 - 25; + const int tabs_y = (_screen_height / 2 - 100) + 200; + _login_tab_rect = { form_center_x-90, tabs_y, 80, 30 }; + _create_account_tab_rect = { form_center_x+10, tabs_y, 150, 30 }; +} LoginScreen::~LoginScreen(void) {} @@ -18,10 +28,13 @@ void LoginScreen::render(const RenderContext& context) const { UIRenderer* ui_renderer = context.ui_renderer; /* Colours. */ + const Color bg_color = { 0.06f, 0.07f, 0.09f }; const Color text_color = { 0.8f, 0.8f, 0.8f }; const Color inactive_box_color = { 0.1f, 0.1f, 0.15f }; const Color active_box_color = { 0.15f, 0.15f, 0.2f }; const Color warning_color = { 0.7f, 0.3f, 0.3f }; + const Color active_tab_color = { 0.9f, 0.9f, 0.9f }; + const Color inactive_tab_color = { 0.4f, 0.5f, 0.6f }; /* Layout. */ const int box_width = 300; @@ -36,11 +49,28 @@ void LoginScreen::render(const RenderContext& context) const { ui_renderer->begin_shapes(); ui_renderer->begin_text(); + /* Draw background. */ + ui_renderer->draw_rect(0, 0, _screen_width, _screen_height, bg_color); + + /* Draw mode-switch tabs. */ + ui_renderer->render_text("[Login]", _login_tab_rect.x, _login_tab_rect.y + 20, + _is_new_account ? inactive_tab_color : active_tab_color); + ui_renderer->render_text("[Create Account]", _create_account_tab_rect.x, + _create_account_tab_rect.y + 20, + _is_new_account ? active_tab_color : inactive_tab_color); + /* Draw title. */ const char* title = _is_new_account ? "Create Account" : "Login"; float title_width = ui_renderer->get_text_renderer()->get_text_width(title, 1.0f); ui_renderer->render_text(title, (_screen_width-title_width)/2, start_y, text_color); + /* Draw error message if it exists. */ + if(!_error_message.empty()) { + float error_width = ui_renderer->get_text_renderer()->get_text_width(_error_message.c_str(), 1.0f); + ui_renderer->render_text(_error_message.c_str(), (_screen_width-error_width)/2, + start_y+20, warning_color); + } + /* Draw input boxes and labels. */ /* Username */ ui_renderer->render_text("Username", username_rect.x, username_rect.y-5, text_color); @@ -99,6 +129,10 @@ void LoginScreen::render(const RenderContext& context) const { ui_renderer->flush_text(); } +void LoginScreen::set_error_message(const std::string& msg) { + _error_message = msg; +} + void LoginScreen::handle_event(const SDL_Event* event) { if(event->type == SDL_EVENT_TEXT_INPUT) { /* Append character to active input string. */ @@ -139,11 +173,23 @@ void LoginScreen::handle_event(const SDL_Event* event) { const Rect password_rect = { center_x, start_y+100, box_width, 30 }; const Rect hostname_rect = { center_x, start_y+160, box_width, 30 }; - if(mouse_y >= username_rect.y && mouse_y <= username_rect.y + username_rect.h) + auto is_inside = [&](const Rect& rect) { + return mouse_x >= rect.x && mouse_x <= rect.x + rect.w && + mouse_y >= rect.y && mouse_y <= rect.y + rect.h; + }; + + if(is_inside(_login_tab_rect)) { + _is_new_account = false; + _username_input.clear(); _password_input.clear(); _hostname_input.clear(); + } else if(is_inside(_create_account_tab_rect)) { + _is_new_account = true; + _username_input.clear(); _password_input.clear(); _hostname_input.clear(); + } else if(is_inside(username_rect)) { _active_field = 0; - else if(mouse_y >= password_rect.y && mouse_y <= password_rect.y + password_rect.h) + } else if(is_inside(password_rect)) { _active_field = 1; - else if(_is_new_account && (mouse_y >= hostname_rect.y && mouse_y <= hostname_rect.y + hostname_rect.h)) + } else if(_is_new_account && is_inside(hostname_rect)) { _active_field = 2; + } } } diff --git a/client/src/ui/login_screen.h b/client/src/ui/login_screen.h index d4d5c1a..5bafd49 100644 --- a/client/src/ui/login_screen.h +++ b/client/src/ui/login_screen.h @@ -15,15 +15,19 @@ public: void handle_event(const SDL_Event* event); bool is_login_attempted(void) const { return _login_attempted; } - std::string get_username(void) const { return _username_input; } - std::string get_password(void) const { return _password_input; } - std::string get_hostname(void) const { return _hostname_input; } + std::string get_username(void) const { return _username_input; } + std::string get_password(void) const { return _password_input; } + std::string get_hostname(void) const { return _hostname_input; } + bool is_new_account_mode(void) const { return _is_new_account; } + void clear_login_attempt(void) { _login_attempted = false;} + void set_error_message(const std::string& msg); private: /* UI State. */ std::string _username_input; std::string _password_input; std::string _hostname_input; + std::string _error_message; bool _login_attempted = false; bool _is_new_account = true; @@ -32,4 +36,8 @@ private: /* Screen dimensions. */ int _screen_width; int _screen_height; + + /* Clickable tabs for mode switching. */ + Rect _create_account_tab_rect; + Rect _login_tab_rect; }; diff --git a/common/src/db/database_manager.cpp b/common/src/db/database_manager.cpp new file mode 100644 index 0000000..7149e64 --- /dev/null +++ b/common/src/db/database_manager.cpp @@ -0,0 +1,42 @@ +#include "database_manager.h" + +DatabaseManager::DatabaseManager(const std::string& db_path) : _db(db_path) { + /* db is opened in the construtor's init list. */ +} + +DatabaseManager::~DatabaseManager(void) { + /* db is auto closed when _db goes out of scope. */ +} + +void DatabaseManager::init(void) { + _db << "CREATE TABLE IF NOT EXISTS players(" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "username TEXT NOT NULL UNIQUE," + "password TEXT NOT NULL," + "hostname TEXT NOT NULL" + ");"; +} + +bool DatabaseManager::create_player(const std::string& username, const std::string& password, + const std::string& hostname) { + try { + _db << "INSERT INTO players (username, password, hostname) VALUES (?, ?, ?);" + << username + << password + << hostname; + } catch(const std::exception& e) { + /* Fail if the username exists. */ + return false; + } + return true; +} + +bool DatabaseManager::auth_player(const std::string& username, const std::string& password) { + bool authed = false; + _db << "SELECT id FROM players WHERE username = ? AND password = ?;" + << username + << password + >> [&](long long id) { authed = true; }; + + return authed; +} diff --git a/common/src/db/database_manager.h b/common/src/db/database_manager.h new file mode 100644 index 0000000..bc75611 --- /dev/null +++ b/common/src/db/database_manager.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "db.h" + +class DatabaseManager { +public: + DatabaseManager(const std::string& db_path); + ~DatabaseManager(void); + + void init(void); + + /* Return true on success, false if user already exists. */ + bool create_player(const std::string& username, const std::string& password, + const std::string& hostname); + + /* Return true if creds are valid. */ + bool auth_player(const std::string& username, const std::string& password); + +private: + sqlite::database _db; +}; diff --git a/server/src/main.cpp b/server/src/main.cpp index ad1788d..47c3b6f 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -6,11 +6,12 @@ #include "network_manager.h" #include "net/constants.h" -int main(int argc, char** argv) { +const std::string DB_FILE = "bettola.db"; +int main(int argc, char** argv) { try { /* We'll keep main thread alive while server runs in background. */ - NetworkManager server; + NetworkManager server(DB_FILE); server.start(MULTIPLAYER_PORT); while(true) { std::this_thread::sleep_for(std::chrono::seconds(1)); diff --git a/server/src/network_manager.cpp b/server/src/network_manager.cpp index 80c40ce..ccc1009 100644 --- a/server/src/network_manager.cpp +++ b/server/src/network_manager.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include "network_manager.h" +#include "db/database_manager.h" #include "asio/error_code.hpp" #include "asio/ip/tcp.hpp" @@ -13,7 +15,9 @@ #include "net/tcp_connection.h" #include "vfs.h" -NetworkManager::NetworkManager(void) : _acceptor(_io_context) { +NetworkManager::NetworkManager(const std::string& db_path) : + _db_manager(std::make_unique(db_path)), + _acceptor(_io_context) { /* World setup. */ _world_machines["8.8.8.8"] = _machine_manager.create_machine(1000, "dns.google", "npc"); _world_machines["10.0.2.15"] = _machine_manager.create_machine(1001, "corp.internal", "npc"); @@ -22,6 +26,8 @@ NetworkManager::NetworkManager(void) : _acceptor(_io_context) { _world_machines["8.8.8.8"]->services[80] = "HTTPD v2.4"; _world_machines["10.0.2.15"]->services[21] = "FTPd v3.0"; + _db_manager->init(); + fprintf(stderr, "Created world with %zu networks\n", _world_machines.size()); } @@ -99,6 +105,17 @@ void NetworkManager::start_accept(void) { }); } +static std::vector split_message(const std::string& s, const std::string& delimiter) { + std::vector tokens; + size_t start = 0, end = 0; + while((end = s.find(delimiter, start)) != std::string::npos) { + tokens.push_back(s.substr(start, end-start)); + start = end + delimiter.length(); + } + tokens.push_back(s.substr(start)); + return tokens; +} + void NetworkManager::on_message(std::shared_ptr connection, const std::string& message) { Player* player = _players[connection->get_id()].get(); @@ -107,6 +124,51 @@ void NetworkManager::on_message(std::shared_ptr connection, return; } + if(player->state == PlayerState::AUTHENTICATING) { + if(message.rfind("C_ACC::", 0) == 0) { + std::string payload = message.substr(7); + auto parts = split_message(payload, "::"); + if(parts.size() == 3) { + if(_db_manager->create_player(parts[0], parts[1], parts[2])) { + /* + * TODO: When creating a player, also create their machine and save it. + * for now, they will juse use a temp machine on auth. + */ + player->state = PlayerState::ACTIVE; + connection->send("C_ACC_SUCCESS"); + /* send initial prompt. */ + std::string prompt = "\n" + get_full_path(player->cmd_processor->get_current_dir()); + connection->send(prompt); + } else { + connection->send("C_ACC_FAIL"); + } + } + } else if(message.rfind("LOGIN::", 0) == 0) { + std::string payload = message.substr(7); + auto parts = split_message(payload, "::"); + if(parts.size() == 2) { + if(_db_manager->auth_player(parts[0], parts[1])) { + /* + * TODO: Load the player's machine from the database. + * For now, just auth them and use a temp machine. + */ + player->state = PlayerState::ACTIVE; + connection->send("LOGIN_SUCCESS"); + /* Send initial prompt. */ + std::string prompt = "\n" + get_full_path(player->cmd_processor->get_current_dir()); + connection->send(prompt); + } else { + connection->send("LOGIN_FAIL"); + } + } + } else { + /* Ignore all other messages while authing. */ + connection->send("ERR: Not authenticated.\n"); + } + return; + } + /* === PLAYER BECOMES ACTIVE HERE === */ + /* Check for "special" message prefixes. */ if(message.rfind("WRITEF::", 0) == 0) { /* Message format: WRITEF::/path/to/file::content. */ diff --git a/server/src/network_manager.h b/server/src/network_manager.h index c0e5a0e..b0d0c57 100644 --- a/server/src/network_manager.h +++ b/server/src/network_manager.h @@ -12,9 +12,11 @@ #include "player.h" #include "machine_manager.h" +class DatabaseManager; + class NetworkManager { public: - NetworkManager(); + NetworkManager(const std::string& db_path); ~NetworkManager(void); void start(uint16_t port); @@ -36,4 +38,5 @@ private: std::map _world_machines; /* For NPC's. */ MachineManager _machine_manager; + std::unique_ptr _db_manager; }; diff --git a/server/src/player.cpp b/server/src/player.cpp index 31b8dff..6a909a5 100644 --- a/server/src/player.cpp +++ b/server/src/player.cpp @@ -4,7 +4,8 @@ Player::Player(uint32_t new_id, Machine* home_machine, std::map& world_machines) : - id(new_id) { + id(new_id), + state(PlayerState::AUTHENTICATING) { cmd_processor = new CommandProcessor(home_machine, world_machines); } diff --git a/server/src/player.h b/server/src/player.h index 3900875..5320281 100644 --- a/server/src/player.h +++ b/server/src/player.h @@ -7,11 +7,17 @@ #include "command_processor.h" #include "machine.h" +enum class PlayerState { + AUTHENTICATING, + ACTIVE +}; + class Player { public: Player(uint32_t id, Machine* home_machine, std::map& world_machines); ~Player(void); uint32_t id; + PlayerState state; CommandProcessor* cmd_processor; /* Manages the VFS state for the remote session. */ };