diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dec2a6..143c752 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,12 @@ cmake_minimum_required(VERSION 3.16) -project(bettola CXX) +project(bettola CXX C) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# === Sol2 === include(FetchContent) FetchContent_Declare( sol2 @@ -14,6 +15,7 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(sol2) +# === Asio === FetchContent_Declare( asio GIT_REPOSITORY https://github.com/chriskohlhoff/asio @@ -21,6 +23,33 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(asio) +# === SQLite === +# Supress the developer warning for using Populate with declared content. +# We need it because sqlite zip isn't a CMAKE project. +cmake_policy(SET CMP0169 OLD) +FetchContent_Declare( + sqlite_source + URL https://sqlite.org/2025/sqlite-amalgamation-3500400.zip + URL_HASH SHA256=1d3049dd0f830a025a53105fc79fd2ab9431aea99e137809d064d8ee8356b032 + DOWNLOAD_EXTRACT_TIMESTAMP true +) +FetchContent_GetProperties(sqlite_source) +if(NOT sqlite_source_POPULATED) + FetchContent_Populate(sqlite_source) + add_library(sqlite STATIC "${sqlite_source_SOURCE_DIR}/sqlite3.c") + target_include_directories(sqlite PUBLIC "${sqlite_source_SOURCE_DIR}") +endif() +# Revert policy to default. +cmake_policy(SET CMP0169 NEW) + +# === sqlite_modern_cpp (SQLite wrapper) === +FetchContent_Declare( + sqlite_modern_cpp + GIT_REPOSITORY https://github.com/SqliteModernCpp/sqlite_modern_cpp.git + GIT_TAG v3.2 +) +FetchContent_MakeAvailable(sqlite_modern_cpp) + add_subdirectory(common) add_subdirectory(client) add_subdirectory(server) diff --git a/client/src/game_state.cpp b/client/src/game_state.cpp index 52a3c69..9adcc21 100644 --- a/client/src/game_state.cpp +++ b/client/src/game_state.cpp @@ -13,6 +13,7 @@ #include "ui/i_window_content.h" #include "ui/ui_window.h" #include "ui/editor.h" +#include "ui/login_screen.h" #include #include #include @@ -91,6 +92,11 @@ void GameState::handle_event(SDL_Event* event, int screen_width, int screen_heig _main_menu->handle_event(event); } break; + case Screen::LOGIN: + if(_login_screen) { + _login_screen->handle_event(event); + } + break; case Screen::BOOTING: /* TODO: */ break; @@ -116,12 +122,25 @@ void GameState::update(float dt, int draw_calls, int shape_verts, int text_verts _current_screen = next_screen; _main_menu.reset(); /* Free mem. */ - if(_current_screen == Screen::BOOTING) { - _boot_sequence = std::make_unique(); + if(_current_screen == Screen::LOGIN) { + _login_screen = std::make_unique(_screen_width, _screen_height); } } break; } + 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()); + + _current_screen = Screen::BOOTING; + _login_screen.reset(); /* Free mem. */ + _boot_sequence = std::make_unique(); + } + break; case Screen::BOOTING: { if(!_boot_sequence) break; /* Shouldn't happen. */ if(_boot_sequence->is_finished()) { @@ -219,6 +238,11 @@ void GameState::render(const RenderContext& context) { _main_menu->render(context.ui_renderer); } break; + case Screen::LOGIN: + if(_login_screen) { + _login_screen->render(context); + } + break; case Screen::BOOTING: if(_boot_sequence) { _boot_sequence->render(context.ui_renderer); diff --git a/client/src/game_state.h b/client/src/game_state.h index bc3a0c9..fdb5a64 100644 --- a/client/src/game_state.h +++ b/client/src/game_state.h @@ -7,6 +7,7 @@ class DebugOverlay; class ClientNetwork; class Desktop; +class LoginScreen; class BootSequence; class MainMenu; class ShapeRenderer; @@ -14,6 +15,7 @@ class TextRenderer; union SDL_Event; enum class Screen { + LOGIN, MAIN_MENU, BOOTING, DESKTOP @@ -34,6 +36,7 @@ private: std::unique_ptr _network; std::unique_ptr _desktop; std::unique_ptr _boot_sequence; + std::unique_ptr _login_screen; std::unique_ptr _main_menu; std::unique_ptr _debug_overlay; bool _show_debug_overlay; diff --git a/client/src/main.cpp b/client/src/main.cpp index d45237b..c68f4af 100644 --- a/client/src/main.cpp +++ b/client/src/main.cpp @@ -6,6 +6,8 @@ #include #include +#include "db/db.h" + #include "gfx/shape_renderer.h" #include "gfx/txt_renderer.h" #include "game_state.h" diff --git a/client/src/ui/login_screen.cpp b/client/src/ui/login_screen.cpp new file mode 100644 index 0000000..57eb939 --- /dev/null +++ b/client/src/ui/login_screen.cpp @@ -0,0 +1,149 @@ +#include +#include + +#include "gfx/types.h" +#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(void) {} + +void LoginScreen::update(float dt) { + /* TODO: */ +} + +void LoginScreen::render(const RenderContext& context) const { + UIRenderer* ui_renderer = context.ui_renderer; + + /* Colours. */ + 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 }; + + /* Layout. */ + const int box_width = 300; + const int box_height = 30; + const int center_x = (_screen_width - box_width) / 2; + const int start_y = _screen_height / 2 - 100; + + const Rect username_rect = { center_x, start_y+40, box_width, box_height }; + const Rect password_rect = { center_x, start_y+100, box_width, box_height }; + const Rect hostname_rect = { center_x, start_y+160, box_width, box_height }; + + ui_renderer->begin_shapes(); + ui_renderer->begin_text(); + + /* 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 input boxes and labels. */ + /* Username */ + ui_renderer->render_text("Username", username_rect.x, username_rect.y-5, text_color); + ui_renderer->draw_rect(username_rect.x, username_rect.y, username_rect.w, username_rect.h, + _active_field == 0 ? active_box_color : inactive_box_color); + ui_renderer->render_text(_username_input.c_str(), username_rect.x+10, username_rect.y+20, text_color); + if(_active_field == 0 && context.show_cursor) { + float cursor_x = username_rect.x + 10 + + ui_renderer->get_text_renderer()->get_text_width(_username_input.c_str(), 1.0f); + ui_renderer->draw_rect((int)cursor_x, username_rect.y+5, 2, 20, text_color); + } + + /* Password. */ + ui_renderer->render_text("Password", password_rect.x, password_rect.y-5, text_color); + ui_renderer->draw_rect(password_rect.x, password_rect.y, password_rect.w, password_rect.h, + _active_field == 1 ? active_box_color : inactive_box_color); + ui_renderer->render_text(_password_input.c_str(), password_rect.x+10, password_rect.y+20, text_color); + if(_active_field == 1 && context.show_cursor) { + float cursor_x = password_rect.x + 10 + + ui_renderer->get_text_renderer()->get_text_width(_password_input.c_str(), 1.0f); + ui_renderer->draw_rect((int)cursor_x, password_rect.y+5, 2, 20, text_color); + } + + /* Hostname (only for new account). */ + if(_is_new_account) { + ui_renderer->render_text("Hostname", hostname_rect.x, hostname_rect.y-5, text_color); + ui_renderer->draw_rect(hostname_rect.x, hostname_rect.y, hostname_rect.w, hostname_rect.h, + _active_field == 2 ? active_box_color : inactive_box_color); + ui_renderer->render_text(_hostname_input.c_str(), hostname_rect.x+10, hostname_rect.y+20, text_color); + if(_active_field == 2 && context.show_cursor) { + float cursor_x = hostname_rect.x + 10 + + ui_renderer->get_text_renderer()->get_text_width(_hostname_input.c_str(), 1.0f); + ui_renderer->draw_rect((int)cursor_x, hostname_rect.y+5, 2, 20, text_color); + } + } + + /* Draw the security warning. */ + const char* warning_title = "Security Warning"; + float warning_title_width = ui_renderer->get_text_renderer()->get_text_width(warning_title, 1.0f); + ui_renderer->render_text(warning_title, (_screen_width-warning_title_width)/2, + _screen_height-120, warning_color); + const std::vector warning_lines = { + "Passwords in Bettola are stored in plain text by design.", + "Players, can, and will attempt to obtain your password.", + "DO NOT use a password you have used for any other game or service.", + "Use a unique, throwaway password for this game only!" + }; + for(size_t i = 0; i < warning_lines.size(); ++i) { + const std::string& line = warning_lines[i]; + float line_width = ui_renderer->get_text_renderer()->get_text_width(line.c_str(), 1.0f); + ui_renderer->render_text(line.c_str(), (_screen_width-line_width)/2, + _screen_height-100+(i*20), warning_color); + } + + ui_renderer->flush_shapes(); + ui_renderer->flush_text(); +} + +void LoginScreen::handle_event(const SDL_Event* event) { + if(event->type == SDL_EVENT_TEXT_INPUT) { + /* Append character to active input string. */ + switch(_active_field) { + case 0: _username_input += event->text.text; break; + case 1: _password_input += event->text.text; break; + case 2: if(_is_new_account) { _hostname_input += event->text.text; } break; + } + } else if(event->type == SDL_EVENT_KEY_DOWN) { + if(event->key.key == SDLK_BACKSPACE) { + /* Handle backspace. */ + switch(_active_field) { + case 0: if(!_username_input.empty()) { _username_input.pop_back(); } break; + case 1: if(!_password_input.empty()) { _password_input.pop_back(); } break; + case 2: if(_is_new_account && !_hostname_input.empty()) { + _hostname_input.pop_back(); + } + break; + } + } else if(event->key.key == SDLK_TAB) { + /* Tab to switch fields. */ + int num_fields = _is_new_account ? 3 : 2; + _active_field = (_active_field+1) % num_fields; + } else if(event->key.key == SDLK_RETURN) { + _login_attempted = true; + } + } else if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + /* Handle mouse clicks. */ + int mouse_x = event->button.x; + int mouse_y = event->button.y; + + /* Recalculate rects to check for clicks. */ + const int box_width = 300; + const int start_y = _screen_height / 2 - 100; + const int center_x = (_screen_width - box_width) / 2; + + const Rect username_rect = { center_x, start_y+40, box_width, 30 }; + 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) + _active_field = 0; + else if(mouse_y >= password_rect.y && mouse_y <= password_rect.y + password_rect.h) + _active_field = 1; + else if(_is_new_account && (mouse_y >= hostname_rect.y && mouse_y <= hostname_rect.y + hostname_rect.h)) + _active_field = 2; + } +} diff --git a/client/src/ui/login_screen.h b/client/src/ui/login_screen.h new file mode 100644 index 0000000..d4d5c1a --- /dev/null +++ b/client/src/ui/login_screen.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "gfx/types.h" + +class LoginScreen { +public: + LoginScreen(int screen_width, int screen_height); + ~LoginScreen(void); + + void update(float dt); + void render(const RenderContext& context) const; + 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; } + +private: + /* UI State. */ + std::string _username_input; + std::string _password_input; + std::string _hostname_input; + + bool _login_attempted = false; + bool _is_new_account = true; + int _active_field = 0; /* 0: username, 1: password, 2: hostname. */ + + /* Screen dimensions. */ + int _screen_width; + int _screen_height; +}; diff --git a/client/src/ui/main_menu.cpp b/client/src/ui/main_menu.cpp index ec99566..82a5c9c 100644 --- a/client/src/ui/main_menu.cpp +++ b/client/src/ui/main_menu.cpp @@ -42,14 +42,14 @@ MainMenu::MainMenu(int screen_width, int screen_height) : _buttons.push_back({ "Single-Player", { center_x, center_y + 30, button_width, button_height }, - Screen::BOOTING, /* This will trigger the booting screen. */ + Screen::LOGIN, /* This will trigger the login screen. */ true, false }); _buttons.push_back({ "Online", { center_x, center_y - 30, button_width, button_height }, - Screen::BOOTING, + Screen::LOGIN, false, false }); diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 1d572b8..ab9347c 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -6,9 +6,11 @@ add_library(bettola ${BETTOLA_SOURCES} ) -target_link_libraries(bettola PUBLIC ${LUA_LIBRARIES} sol2) +target_link_libraries(bettola PUBLIC ${LUA_LIBRARIES} sol2 sqlite) target_include_directories(bettola PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ${LUA_INCLUDE_DIR} - ${asio_SOURCE_DIR}/asio/include) + ${asio_SOURCE_DIR}/asio/include + ${sqlite_modern_cpp_SOURCE_DIR}/hdr +) diff --git a/common/src/db/db.h b/common/src/db/db.h new file mode 100644 index 0000000..adf6173 --- /dev/null +++ b/common/src/db/db.h @@ -0,0 +1,3 @@ +#pragma once + +#include