[Refactor] Migrate networking from SDL_net to Asio.
the SDL3_net implementation was causing blocking behaviour and was difficult to debug and has bad docs due to not being released. Caused crashes all over. so moved to Asio. This thing took so damn long as this also had it's issues! - All networking now uses Asio's async callback model. - TcpConnection class encapsulates logic for a single client-server connection, managing socket and message framing. - Implmented thread-safe queues for handling incoming and outgoing messages between the network thread and the main application. - Refactored ClientNetwork and NetworkManager, the primary client and server networking classes have been rewritten to use the new asio-based architecture. - Player objects on the server are not managed by std::unique_ptr to ensure proper lifetime management and prevent memleaks. - VFSManager is now a single instance on the server, passed by reference to new players, avoding redundant script loading for every connection. - Resolved server crash that occured immediately upon client connection. This was traced to an object lifetime issue within Asio's async handlers which was fixed by simplifying the send operation.
This commit is contained in:
		
							parent
							
								
									388c6429cf
								
							
						
					
					
						commit
						6272800a22
					
				@ -1,6 +1,8 @@
 | 
			
		||||
cmake_minimum_required(VERSION 3.16)
 | 
			
		||||
project(bettola CXX)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_CXX_STANDARD 17)
 | 
			
		||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
 | 
			
		||||
 | 
			
		||||
@ -13,16 +15,11 @@ FetchContent_Declare(
 | 
			
		||||
FetchContent_MakeAvailable(sol2)
 | 
			
		||||
 | 
			
		||||
FetchContent_Declare(
 | 
			
		||||
  SDL_net
 | 
			
		||||
  GIT_REPOSITORY https://github.com/libsdl-org/SDL_net.git
 | 
			
		||||
  # NOTE: Using 'main' because FetchContent in this environment appears to be
 | 
			
		||||
  # doing a shallow clone, which fails when checking out a specific tag/commit.
 | 
			
		||||
  # This is not ideal for reproducibility, but unblocks development.
 | 
			
		||||
  # If SDL_net breaks their main branch, you'll have to compile it from source:
 | 
			
		||||
  # https://github.com/libsdl-org/SDL_net
 | 
			
		||||
  GIT_TAG main
 | 
			
		||||
  asio
 | 
			
		||||
  GIT_REPOSITORY https://github.com/chriskohlhoff/asio
 | 
			
		||||
  git_TAG asio-1-36-0
 | 
			
		||||
)
 | 
			
		||||
FetchContent_MakeAvailable(SDL_net)
 | 
			
		||||
FetchContent_MakeAvailable(asio)
 | 
			
		||||
 | 
			
		||||
add_subdirectory(common)
 | 
			
		||||
add_subdirectory(client)
 | 
			
		||||
 | 
			
		||||
@ -9,10 +9,8 @@ find_package(GLEW REQUIRED)
 | 
			
		||||
find_package(OpenGL REQUIRED)
 | 
			
		||||
find_package(Freetype REQUIRED)
 | 
			
		||||
 | 
			
		||||
target_link_libraries(bettolac PRIVATE bettola SDL3::SDL3 SDL3_net
 | 
			
		||||
  GLEW::glew OpenGL::GL Freetype::Freetype)
 | 
			
		||||
 | 
			
		||||
target_link_directories(bettolac PRIVATE ${sdl_net_BINARY_DIR})
 | 
			
		||||
target_link_libraries(bettolac PRIVATE bettola SDL3::SDL3 ${GLEW_LIBRARIES}
 | 
			
		||||
  ${OPENGL_LIBRARIES} ${FREETYPE_LIBRARIES})
 | 
			
		||||
 | 
			
		||||
target_include_directories(bettolac PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src
 | 
			
		||||
  ${sdl_net_SOURCE_DIR}/include)
 | 
			
		||||
  ${GLEW_INCLUDE_DIRS} ${OPENGL_INCLUDE_DIR} ${FREETYPE_INCLUDE_DIRS})
 | 
			
		||||
 | 
			
		||||
@ -1,86 +1,90 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "SDL3_net/SDL_net.h"
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include "net/tcp_connection.h"
 | 
			
		||||
 | 
			
		||||
#include "client_network.h"
 | 
			
		||||
 | 
			
		||||
ClientNetwork::ClientNetwork(void) {
 | 
			
		||||
  if(!NET_Init()) {
 | 
			
		||||
    printf("Error: NET_Init: %s\n", SDL_GetError());
 | 
			
		||||
  }
 | 
			
		||||
  _socket = nullptr;
 | 
			
		||||
}
 | 
			
		||||
ClientNetwork::ClientNetwork(void) : _io_context() {}
 | 
			
		||||
 | 
			
		||||
ClientNetwork::~ClientNetwork(void) {
 | 
			
		||||
  if(_socket) {
 | 
			
		||||
    NET_DestroyStreamSocket(_socket);
 | 
			
		||||
  }
 | 
			
		||||
  NET_Quit();
 | 
			
		||||
  disconnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ClientNetwork::connect(const char* host, int port) {
 | 
			
		||||
  NET_Address* address = NET_ResolveHostname(host);
 | 
			
		||||
  if(!address) {
 | 
			
		||||
    printf("Error: NET_ResolveHostname %s\n", SDL_GetError());
 | 
			
		||||
bool ClientNetwork::connect(const std::string& host, uint16_t port) {
 | 
			
		||||
  try {
 | 
			
		||||
    /* Resolve hostname into list of endpoints. */
 | 
			
		||||
    asio::ip::tcp::resolver resolver(_io_context);
 | 
			
		||||
    asio::ip::tcp::resolver::results_type endpoints =
 | 
			
		||||
        resolver.resolve(host, std::to_string(port));
 | 
			
		||||
 | 
			
		||||
    /* Create connection object. It will own a socket. */
 | 
			
		||||
    _connection = std::make_shared<net::TcpConnection>(_io_context,
 | 
			
		||||
                                                       asio::ip::tcp::socket(_io_context));
 | 
			
		||||
 | 
			
		||||
    /* Connect to server. */
 | 
			
		||||
    asio::connect(_connection->socket(), endpoints);
 | 
			
		||||
 | 
			
		||||
    if(!_connection->socket().is_open()) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Define callbacks for the connection. */
 | 
			
		||||
    _connection->start(
 | 
			
		||||
      [this](const std::string& msg) {
 | 
			
		||||
        /* Push new messages to the thread safe queue. */
 | 
			
		||||
        this->_incoming_messages.push_back(msg);
 | 
			
		||||
      },
 | 
			
		||||
      [this]() {
 | 
			
		||||
        /* Reset our connection on server disconnect. */
 | 
			
		||||
        fprintf(stderr, "[BettolaClient] Server disconnected.\n");
 | 
			
		||||
        this->_connection.reset();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    /* Start io_context in background thread. */
 | 
			
		||||
    _context_thread = std::thread([this]() { _io_context.run(); });
 | 
			
		||||
 | 
			
		||||
  } catch(const std::exception& e) {
 | 
			
		||||
    fprintf(stderr, "[BettolaClient] Connection exception: %s\n", e.what());
 | 
			
		||||
    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 std::string& command) {
 | 
			
		||||
  if(!_socket) return;
 | 
			
		||||
  NET_WriteToStreamSocket(_socket, command.c_str(), command.length()+1);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ClientNetwork::disconnect(void) {
 | 
			
		||||
  if(_socket) {
 | 
			
		||||
    NET_DestroyStreamSocket(_socket);
 | 
			
		||||
    _socket = nullptr;
 | 
			
		||||
  if(is_connected()) {
 | 
			
		||||
    /* Close the socket. Causes outstanding async operations
 | 
			
		||||
     * in TcpConnection to compete with an error, which in return
 | 
			
		||||
     * triggers the on_disconnect callback.
 | 
			
		||||
     */
 | 
			
		||||
    _connection->socket().close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Kill background thread. */
 | 
			
		||||
  _io_context.stop();
 | 
			
		||||
  if(_context_thread.joinable()) {
 | 
			
		||||
    _context_thread.join();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Release connection object. */
 | 
			
		||||
  _connection.reset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ClientNetwork::is_connected(void) {
 | 
			
		||||
  return _connection && _connection->socket().is_open();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ClientNetwork::send(const std::string& message) {
 | 
			
		||||
  if(is_connected()) {
 | 
			
		||||
    _connection->send(message);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string ClientNetwork::receive_response(void) {
 | 
			
		||||
  if(!_socket) return "";
 | 
			
		||||
  char buffer[2048];
 | 
			
		||||
  memset(buffer, 0, sizeof(buffer));
 | 
			
		||||
 | 
			
		||||
  void* socket_ptr = _socket;
 | 
			
		||||
  /* Wait up to 2 seconds for data to be available. */
 | 
			
		||||
  if(NET_WaitUntilInputAvailable(&socket_ptr, 1, 2000) > 0) {
 | 
			
		||||
    int bytes_received = NET_ReadFromStreamSocket(_socket, buffer, sizeof(buffer)-1);
 | 
			
		||||
    if(bytes_received > 0) {
 | 
			
		||||
      return std::string(buffer);
 | 
			
		||||
    }
 | 
			
		||||
bool ClientNetwork::poll_message(std::string& message) {
 | 
			
		||||
  if(_incoming_messages.empty()) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return ""; /* Return empty on timeout or error. */
 | 
			
		||||
  message = _incoming_messages.pop_front();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,30 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <SDL3_net/SDL_net.h>
 | 
			
		||||
#include <asio.hpp>
 | 
			
		||||
#include <memory.h>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <thread>
 | 
			
		||||
 | 
			
		||||
#include "asio/io_context.hpp"
 | 
			
		||||
#include "net/tcp_connection.h"
 | 
			
		||||
#include "net/ts_queue.h"
 | 
			
		||||
 | 
			
		||||
class ClientNetwork {
 | 
			
		||||
public:
 | 
			
		||||
  ClientNetwork(void);
 | 
			
		||||
  ~ClientNetwork(void);
 | 
			
		||||
 | 
			
		||||
  bool connect(const char* host, int port);
 | 
			
		||||
  void send_command(const std::string& data);
 | 
			
		||||
  bool connect(const std::string& host, uint16_t port);
 | 
			
		||||
  void disconnect(void);
 | 
			
		||||
  std::string receive_response(void);
 | 
			
		||||
  bool is_connected(void);
 | 
			
		||||
 | 
			
		||||
  void send(const std::string& message);
 | 
			
		||||
 | 
			
		||||
  bool poll_message(std::string& message);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
  NET_StreamSocket* _socket;
 | 
			
		||||
  asio::io_context _io_context;
 | 
			
		||||
  std::thread _context_thread;
 | 
			
		||||
  std::shared_ptr<net::TcpConnection> _connection;
 | 
			
		||||
  TsQueue<std::string> _incoming_messages;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <GL/glew.h>
 | 
			
		||||
#include <SDL3/SDL.h>
 | 
			
		||||
@ -73,15 +74,15 @@ int main(int argc, char** argv) {
 | 
			
		||||
  ShapeRenderer* shape_renderer_instance = new ShapeRenderer(SCREEN_WIDTH, SCREEN_HEIGHT);
 | 
			
		||||
 | 
			
		||||
  /* Create terminal. */
 | 
			
		||||
  Terminal* term = new Terminal();
 | 
			
		||||
  auto term = std::make_unique<Terminal>();
 | 
			
		||||
 | 
			
		||||
  /* Create UI window and pass it a terminal. */
 | 
			
		||||
  UIWindow* test_window = new UIWindow("Terminal", 100, 100, 800, 500);
 | 
			
		||||
  test_window->set_content(term);
 | 
			
		||||
  auto test_window = std::make_unique<UIWindow>("Terminal", 100, 100, 800, 500);
 | 
			
		||||
  test_window->set_content(std::move(term));
 | 
			
		||||
 | 
			
		||||
  /* Create desktop and add the window. */
 | 
			
		||||
  Desktop* desktop = new Desktop();
 | 
			
		||||
  desktop->add_window(test_window);
 | 
			
		||||
  auto desktop = std::make_unique<Desktop>();
 | 
			
		||||
  desktop->add_window(std::move(test_window));
 | 
			
		||||
 | 
			
		||||
  /* timer for cursor blink. */
 | 
			
		||||
  Uint32 last_blink_time = 0;
 | 
			
		||||
@ -93,10 +94,10 @@ int main(int argc, char** argv) {
 | 
			
		||||
    SDL_Event event;
 | 
			
		||||
    while(SDL_PollEvent(&event)) {
 | 
			
		||||
      if(event.type == SDL_EVENT_QUIT) { running = false; }
 | 
			
		||||
      desktop->handle_event(&event);
 | 
			
		||||
      desktop.get()->handle_event(&event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    desktop->update();
 | 
			
		||||
    desktop.get()->update();
 | 
			
		||||
 | 
			
		||||
    Uint32 current_time = SDL_GetTicks();
 | 
			
		||||
    if(current_time - last_blink_time > 500) { /* Every 500ms. */
 | 
			
		||||
@ -108,14 +109,14 @@ int main(int argc, char** argv) {
 | 
			
		||||
    glClearColor(0.1f, 0.1f, 0.1, 1.0f);
 | 
			
		||||
    glClear(GL_COLOR_BUFFER_BIT);
 | 
			
		||||
 | 
			
		||||
    desktop->render(shape_renderer_instance, txt_render_instance, SCREEN_HEIGHT, show_cursor);
 | 
			
		||||
    desktop.get()->render(shape_renderer_instance, txt_render_instance, SCREEN_HEIGHT, show_cursor);
 | 
			
		||||
 | 
			
		||||
    /* It's really odd to call it SwapWindow now, rather than SwapBuffer. */
 | 
			
		||||
    SDL_GL_SwapWindow(window);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Cleanup. */
 | 
			
		||||
  delete desktop;
 | 
			
		||||
  desktop.reset();
 | 
			
		||||
  delete shape_renderer_instance;
 | 
			
		||||
  delete txt_render_instance;
 | 
			
		||||
  SDL_GL_DestroyContext(context);
 | 
			
		||||
 | 
			
		||||
@ -1,41 +1,57 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <SDL3/SDL.h>
 | 
			
		||||
#include <GL/glew.h>
 | 
			
		||||
#include <SDL3/SDL_events.h>
 | 
			
		||||
 | 
			
		||||
#include "terminal.h"
 | 
			
		||||
#include "client_network.h"
 | 
			
		||||
#include "gfx/txt_renderer.h"
 | 
			
		||||
#include "terminal.h"
 | 
			
		||||
 | 
			
		||||
Terminal::Terminal(void) {
 | 
			
		||||
  /* Placeholder welcome message to history. */
 | 
			
		||||
  _history.push_back("Welcome to Bettola (local mode)");
 | 
			
		||||
  _history.push_back("Welcome to Bettola");
 | 
			
		||||
  _history.push_back("Connecting to server...");
 | 
			
		||||
 | 
			
		||||
  _network      = new ClientNetwork();
 | 
			
		||||
  _network      = std::make_unique<ClientNetwork>();
 | 
			
		||||
  _should_close = false;
 | 
			
		||||
  _scroll_offset = 0;
 | 
			
		||||
  _prompt       = "";
 | 
			
		||||
 | 
			
		||||
  /* TODO: Move network to main.cpp, or better yet, a dedicatated game state file */
 | 
			
		||||
  if(_network->connect("localhost", 8080)) {
 | 
			
		||||
  if(_network->connect("127.0.0.1", 1337)) {
 | 
			
		||||
    _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) {
 | 
			
		||||
  return _should_close;
 | 
			
		||||
}
 | 
			
		||||
Terminal::~Terminal(void) {}
 | 
			
		||||
 | 
			
		||||
Terminal::~Terminal(void) {
 | 
			
		||||
  if(_network) {
 | 
			
		||||
    delete _network;
 | 
			
		||||
void Terminal::update(void) {
 | 
			
		||||
  std::string server_msg;
 | 
			
		||||
  while(_network->poll_message(server_msg)) {
 | 
			
		||||
    /* Server will send "output\nprompt". Split them. */
 | 
			
		||||
    size_t last_newline = server_msg.find_last_of('\n');
 | 
			
		||||
    if(last_newline != std::string::npos) {
 | 
			
		||||
      _prompt = server_msg.substr(last_newline+1);
 | 
			
		||||
      std::string output = server_msg.substr(0, last_newline);
 | 
			
		||||
      if(!output.empty()) {
 | 
			
		||||
        _history.push_back(output);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      /*
 | 
			
		||||
       * If there's no newline, it might be the initial welcome message,
 | 
			
		||||
       * or some other non-standard message, just it to history.
 | 
			
		||||
       */
 | 
			
		||||
      _history.push_back(server_msg);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Terminal::close(void) { return _should_close; }
 | 
			
		||||
 | 
			
		||||
void Terminal::_on_ret_press(void) {
 | 
			
		||||
  std::string command = _input_buffer;
 | 
			
		||||
  _input_buffer.clear(); /* Clear the input buffer immediately. */
 | 
			
		||||
@ -54,15 +70,7 @@ void Terminal::_on_ret_press(void) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _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); }
 | 
			
		||||
  }
 | 
			
		||||
  _network->send(command);
 | 
			
		||||
 | 
			
		||||
  /* TODO: Ugly hack. Refactor to pass window height
 | 
			
		||||
   * We need the window height to know if we should
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <SDL3/SDL.h>
 | 
			
		||||
@ -11,6 +12,7 @@ public:
 | 
			
		||||
  Terminal(void);
 | 
			
		||||
  ~Terminal(void);
 | 
			
		||||
 | 
			
		||||
  void update(void);
 | 
			
		||||
  void handle_input(SDL_Event* event);
 | 
			
		||||
  void render(TextRenderer* renderer, int x, int y, int width, int height, bool show_cursor);
 | 
			
		||||
  void scroll(int amount, int content_height);
 | 
			
		||||
@ -24,5 +26,5 @@ private:
 | 
			
		||||
  std::vector<std::string> _history;
 | 
			
		||||
  int _scroll_offset;
 | 
			
		||||
  std::string _prompt;
 | 
			
		||||
  ClientNetwork* _network;
 | 
			
		||||
  std::unique_ptr<ClientNetwork> _network;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
#include "desktop.h"
 | 
			
		||||
#include <SDL3/SDL_events.h>
 | 
			
		||||
@ -12,14 +13,10 @@ Desktop::Desktop(void) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Desktop owns UIWindow, make sure we delete them. */
 | 
			
		||||
Desktop::~Desktop(void) {
 | 
			
		||||
  for(auto win : _windows) {
 | 
			
		||||
    delete win;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
Desktop::~Desktop(void) {}
 | 
			
		||||
 | 
			
		||||
void Desktop::add_window(UIWindow* window) {
 | 
			
		||||
  _windows.push_back(window);
 | 
			
		||||
void Desktop::add_window(std::unique_ptr<UIWindow> window) {
 | 
			
		||||
  _windows.push_back(std::move(window));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Desktop::handle_event(SDL_Event* event) {
 | 
			
		||||
@ -29,18 +26,19 @@ void Desktop::handle_event(SDL_Event* event) {
 | 
			
		||||
 | 
			
		||||
    /* Find the top-most window that was clicked. */
 | 
			
		||||
    for(int i = _windows.size()-1; i >= 0; --i) {
 | 
			
		||||
      if(_windows[i]->is_point_inside(mouse_x, mouse_y)) {
 | 
			
		||||
      if(_windows[i].get()->is_point_inside(mouse_x, mouse_y)) {
 | 
			
		||||
        /* If not focused, focus it. */
 | 
			
		||||
        if(_windows[i] != _focused_window) {
 | 
			
		||||
        if(_windows[i].get() != _focused_window) {
 | 
			
		||||
          if(_focused_window) {
 | 
			
		||||
            _focused_window->set_focused(false);
 | 
			
		||||
          }
 | 
			
		||||
          _focused_window = _windows[i];
 | 
			
		||||
          _focused_window = _windows[i].get();
 | 
			
		||||
          _focused_window->set_focused(true);
 | 
			
		||||
 | 
			
		||||
          /* Move window to the front. */
 | 
			
		||||
          auto window_to_move = std::move(_windows[i]);
 | 
			
		||||
          _windows.erase(_windows.begin() + i);
 | 
			
		||||
          _windows.push_back(_focused_window);
 | 
			
		||||
          _windows.push_back(std::move(window_to_move));
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@ -57,22 +55,25 @@ void Desktop::handle_event(SDL_Event* event) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Desktop::update(void) {
 | 
			
		||||
  /* Poll all windows for network updates. */
 | 
			
		||||
  for(auto& window : _windows) {
 | 
			
		||||
    Terminal* term = window->get_content();
 | 
			
		||||
    if(term) {
 | 
			
		||||
      term->update();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _windows.erase(std::remove_if(_windows.begin(), _windows.end(),
 | 
			
		||||
                                [](UIWindow* window) {
 | 
			
		||||
                                [](const std::unique_ptr<UIWindow>& window) {
 | 
			
		||||
                                  Terminal* term = window->get_content();
 | 
			
		||||
                                  if(term && term->close()) {
 | 
			
		||||
                                    /* Window owns the terminal content, delete window also deletes terminal. */
 | 
			
		||||
                                    delete window;
 | 
			
		||||
                                    return true; /* Remove from vector. */
 | 
			
		||||
                                  }
 | 
			
		||||
                                  return false;
 | 
			
		||||
                                  return term && term->close();
 | 
			
		||||
                                }),
 | 
			
		||||
                              _windows.end());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Desktop::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer,
 | 
			
		||||
                     int screen_height, bool show_cursor) {
 | 
			
		||||
  for(auto win: _windows) {
 | 
			
		||||
    win->render(shape_renderer, txt_renderer, screen_height, show_cursor);
 | 
			
		||||
  for(const auto& win : _windows) {
 | 
			
		||||
    win.get()->render(shape_renderer, txt_renderer, screen_height, show_cursor);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <SDL3/SDL.h>
 | 
			
		||||
 | 
			
		||||
@ -12,13 +13,13 @@ public:
 | 
			
		||||
  Desktop(void);
 | 
			
		||||
  ~Desktop(void);
 | 
			
		||||
 | 
			
		||||
  void add_window(UIWindow* window);
 | 
			
		||||
  void add_window(std::unique_ptr<UIWindow> window);
 | 
			
		||||
  void handle_event(SDL_Event* event);
 | 
			
		||||
  void update(void);
 | 
			
		||||
  void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height,
 | 
			
		||||
              bool show_cursor);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
  std::vector<UIWindow*> _windows;
 | 
			
		||||
  std::vector<std::unique_ptr<UIWindow>> _windows;
 | 
			
		||||
  UIWindow* _focused_window;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
#include "ui_window.h"
 | 
			
		||||
#include <SDL3/SDL_events.h>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include "gfx/shape_renderer.h"
 | 
			
		||||
#include "gfx/txt_renderer.h"
 | 
			
		||||
 | 
			
		||||
@ -15,14 +16,10 @@ UIWindow::UIWindow(const char* title, int x, int y, int width, int height) {
 | 
			
		||||
  _is_focused   = false; /* Not focused by default? */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
UIWindow::~UIWindow(void) {
 | 
			
		||||
  if(_content) {
 | 
			
		||||
    delete _content;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
UIWindow::~UIWindow(void) {}
 | 
			
		||||
 | 
			
		||||
void UIWindow::set_content(Terminal* term) {
 | 
			
		||||
  _content = term;
 | 
			
		||||
void UIWindow::set_content(std::unique_ptr<Terminal> term) {
 | 
			
		||||
  _content = std::move(term);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void UIWindow::set_focused(bool focused) {
 | 
			
		||||
@ -30,7 +27,7 @@ void UIWindow::set_focused(bool focused) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Terminal* UIWindow::get_content(void) {
 | 
			
		||||
  return _content;
 | 
			
		||||
  return _content.get();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool UIWindow::is_point_inside(int x, int y) {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include "gfx/shape_renderer.h"
 | 
			
		||||
@ -16,13 +17,13 @@ public:
 | 
			
		||||
  void handle_event(SDL_Event* event);
 | 
			
		||||
  void set_focused(bool focused);
 | 
			
		||||
  bool is_point_inside(int x, int y);
 | 
			
		||||
  void set_content(Terminal* term);
 | 
			
		||||
  void set_content(std::unique_ptr<Terminal> term);
 | 
			
		||||
  Terminal* get_content(void);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
  int _x, _y, _width, _height;
 | 
			
		||||
  std::string _title;
 | 
			
		||||
  Terminal* _content;
 | 
			
		||||
  std::unique_ptr<Terminal> _content;
 | 
			
		||||
  bool _is_focused; /* Managed by desktop. */
 | 
			
		||||
  bool _is_hovered; /* Send scroll events even if not focused. */
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,5 +9,6 @@ add_library(bettola
 | 
			
		||||
target_link_libraries(bettola PUBLIC ${LUA_LIBRARIES} sol2)
 | 
			
		||||
 | 
			
		||||
target_include_directories(bettola PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src
 | 
			
		||||
  ${LUA_INCLUDE_DIR})
 | 
			
		||||
  ${LUA_INCLUDE_DIR}
 | 
			
		||||
  ${asio_SOURCE_DIR}/asio/include)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								common/src/net/message.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								common/src/net/message.h
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace net {
 | 
			
		||||
 | 
			
		||||
struct Message {
 | 
			
		||||
  std::vector<uint8_t> body;
 | 
			
		||||
 | 
			
		||||
  void pack(const std::string& data) {
 | 
			
		||||
    body.assign(data.begin(), data.end());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::string unpack(void) const {
 | 
			
		||||
    return std::string(body.begin(), body.end());
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} /* namespace net. */
 | 
			
		||||
							
								
								
									
										120
									
								
								common/src/net/tcp_connection.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								common/src/net/tcp_connection.cpp
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,120 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
#include <arpa/inet.h>
 | 
			
		||||
#include "asio/error_code.hpp"
 | 
			
		||||
#include "asio/io_context.hpp"
 | 
			
		||||
#include "asio/ip/tcp.hpp"
 | 
			
		||||
#include "asio/read.hpp"
 | 
			
		||||
#include "net/message.h"
 | 
			
		||||
 | 
			
		||||
#include "tcp_connection.h"
 | 
			
		||||
 | 
			
		||||
namespace net {
 | 
			
		||||
 | 
			
		||||
TcpConnection::TcpConnection(asio::io_context& io_context, asio::ip::tcp::socket socket)
 | 
			
		||||
    : _io_context(_io_context), _socket(std::move(socket)) {}
 | 
			
		||||
 | 
			
		||||
asio::ip::tcp::socket& TcpConnection::socket(void) {
 | 
			
		||||
  return _socket;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TcpConnection::start(std::function<void(const std::string&)> on_message,
 | 
			
		||||
                          std::function<void()> on_disconnect) {
 | 
			
		||||
  _on_message     = on_message;
 | 
			
		||||
  _on_disconnect  = on_disconnect;
 | 
			
		||||
  async_read_header();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TcpConnection::send(const std::string& data) {
 | 
			
		||||
  std::lock_guard<std::mutex> lock(_write_mutex);
 | 
			
		||||
  bool was_writing = !_write_msgs.empty();
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> write_buf;
 | 
			
		||||
  uint32_t body_size = htonl(data.size()); /* Convert host to network byte order. */
 | 
			
		||||
  write_buf.resize(sizeof(body_size) + data.size());
 | 
			
		||||
  std::memcpy(write_buf.data(), &body_size, sizeof(body_size));
 | 
			
		||||
  std::memcpy(write_buf.data() + sizeof(body_size), data.data(), data.size());
 | 
			
		||||
  _write_msgs.push_back(write_buf);
 | 
			
		||||
 | 
			
		||||
  /* If we aren't in the middle of a write op, start one! */
 | 
			
		||||
  if(!was_writing) {
 | 
			
		||||
    async_write_header();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TcpConnection::set_id(uint32_t id) {
 | 
			
		||||
  _id = id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t TcpConnection::get_id(void) const {
 | 
			
		||||
  return _id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TcpConnection::async_read_header(void) {
 | 
			
		||||
  /* Ensure TcpConnection object lives as long as
 | 
			
		||||
   * async operation is outstanding.
 | 
			
		||||
   */
 | 
			
		||||
  auto self = shared_from_this();
 | 
			
		||||
  /* Create buffer for the 4-byte header. */
 | 
			
		||||
  auto header_buf = std::make_shared<std::vector<uint8_t>>(4);
 | 
			
		||||
  asio::async_read(
 | 
			
		||||
      _socket, asio::buffer(*header_buf),
 | 
			
		||||
      [this, self, header_buf](const asio::error_code& ec, size_t /*length*/) {
 | 
			
		||||
          if(!ec) {
 | 
			
		||||
            uint32_t body_size;
 | 
			
		||||
            std::memcpy(&body_size, header_buf->data(), sizeof(body_size));
 | 
			
		||||
            body_size = ntohl(body_size); /* Convert network to host byte order. */
 | 
			
		||||
            
 | 
			
		||||
            if(body_size > 0) {
 | 
			
		||||
              _read_msg.body.resize(body_size);
 | 
			
		||||
                  /* Have header, now read body. */
 | 
			
		||||
                  asio::async_read(_socket, asio::buffer(_read_msg.body.data(), _read_msg.body.size()),
 | 
			
		||||
                      [this, self](const asio::error_code& ec,
 | 
			
		||||
                                   size_t /*length*/) {
 | 
			
		||||
                          if(!ec) {
 | 
			
		||||
                              if(_on_message) {
 | 
			
		||||
                                  _on_message(_read_msg.unpack());
 | 
			
		||||
                              }
 | 
			
		||||
                              /* Wait for next message. */
 | 
			
		||||
                              async_read_header();
 | 
			
		||||
                          } else {
 | 
			
		||||
                              _socket.close();
 | 
			
		||||
                              if(_on_disconnect) _on_disconnect();
 | 
			
		||||
                          }
 | 
			
		||||
                      });
 | 
			
		||||
              } else {
 | 
			
		||||
                  /* Message has no body, we're done with this message, wait
 | 
			
		||||
                   * next.
 | 
			
		||||
                   */
 | 
			
		||||
                  async_read_header();
 | 
			
		||||
              }
 | 
			
		||||
          } else {
 | 
			
		||||
              /* Peer disconnected cleanly? */
 | 
			
		||||
              if(ec != asio::error::eof) {
 | 
			
		||||
              }
 | 
			
		||||
              _socket.close();
 | 
			
		||||
              if(_on_disconnect) _on_disconnect();
 | 
			
		||||
          }
 | 
			
		||||
      });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TcpConnection::async_write_header() {
 | 
			
		||||
  auto self = shared_from_this();
 | 
			
		||||
  asio::async_write(
 | 
			
		||||
      _socket, asio::buffer(_write_msgs.front().data(), _write_msgs.front().size()),
 | 
			
		||||
      [this, self](const asio::error_code& ec, size_t /*length*/) {
 | 
			
		||||
          if(!ec) {
 | 
			
		||||
              std::lock_guard<std::mutex> lock(_write_mutex);
 | 
			
		||||
              _write_msgs.pop_front();
 | 
			
		||||
              if(!_write_msgs.empty()) {
 | 
			
		||||
                  async_write_header();
 | 
			
		||||
              }
 | 
			
		||||
          } else {
 | 
			
		||||
              _socket.close();
 | 
			
		||||
              if(_on_disconnect) _on_disconnect();
 | 
			
		||||
          }
 | 
			
		||||
      });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} /* namespace net. */
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										47
									
								
								common/src/net/tcp_connection.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								common/src/net/tcp_connection.h
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <asio.hpp>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
 | 
			
		||||
#include "net/message.h"
 | 
			
		||||
 | 
			
		||||
namespace net {
 | 
			
		||||
 | 
			
		||||
class TcpConnection : public std::enable_shared_from_this<TcpConnection> {
 | 
			
		||||
public:
 | 
			
		||||
  TcpConnection(asio::io_context& io_context, asio::ip::tcp::socket socket);
 | 
			
		||||
 | 
			
		||||
  void start(std::function<void(const std::string&)> on_message,
 | 
			
		||||
             std::function<void()> on_disconnect);
 | 
			
		||||
 | 
			
		||||
  void send(const std::string& data);
 | 
			
		||||
  void set_id(uint32_t id);
 | 
			
		||||
  uint32_t get_id(void) const;
 | 
			
		||||
 | 
			
		||||
  asio::ip::tcp::socket& socket(void);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
  void async_read_header(void);
 | 
			
		||||
  void async_write_header(void);
 | 
			
		||||
 | 
			
		||||
  uint32_t _id = 0;
 | 
			
		||||
 | 
			
		||||
  asio::ip::tcp::socket _socket;
 | 
			
		||||
  asio::io_context&     _io_context;
 | 
			
		||||
 | 
			
		||||
  /* Incoming message buffer. */
 | 
			
		||||
  Message _read_msg;
 | 
			
		||||
 | 
			
		||||
  /* Thread-safe outgoing queue. */
 | 
			
		||||
  std::deque<std::vector<uint8_t>> _write_msgs;
 | 
			
		||||
  std::mutex _write_mutex;
 | 
			
		||||
 | 
			
		||||
  /* Callbacks. */
 | 
			
		||||
  std::function<void(const std::string&)> _on_message;
 | 
			
		||||
  std::function<void()>                   _on_disconnect;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} /* namespace net. */
 | 
			
		||||
							
								
								
									
										46
									
								
								common/src/net/ts_queue.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								common/src/net/ts_queue.h
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
 | 
			
		||||
template<typename T> class TsQueue {
 | 
			
		||||
public:
 | 
			
		||||
  TsQueue(void) = default;
 | 
			
		||||
  TsQueue(const TsQueue<T>&) = delete; /* No copying plz. */
 | 
			
		||||
 | 
			
		||||
  const T& front(void) {
 | 
			
		||||
    std::scoped_lock lock(_mutex);
 | 
			
		||||
    return _deque.front();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  T pop_front(void) {
 | 
			
		||||
    std::scoped_lock lock(_mutex);
 | 
			
		||||
    auto t = std::move(_deque.front());
 | 
			
		||||
    _deque.pop_front();
 | 
			
		||||
    return t;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void push_back(const T& item) {
 | 
			
		||||
    std::scoped_lock lock(_mutex);
 | 
			
		||||
    _deque.push_back(item);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool empty(void) {
 | 
			
		||||
    std::scoped_lock lock(_mutex);
 | 
			
		||||
    return _deque.empty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t count(void) {
 | 
			
		||||
    std::scoped_lock lock(_mutex);
 | 
			
		||||
    return _deque.size();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void clear(void) {
 | 
			
		||||
    std::scoped_lock lock(_mutex);
 | 
			
		||||
    _deque.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
  std::mutex _mutex;
 | 
			
		||||
  std::deque<T> _deque;
 | 
			
		||||
};
 | 
			
		||||
@ -4,11 +4,8 @@ add_executable(bettolas
 | 
			
		||||
  ${BETTOLAS_SOURCES}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
find_package(SDL3 REQUIRED)
 | 
			
		||||
find_package(Threads REQUIRED)
 | 
			
		||||
 | 
			
		||||
target_link_libraries(bettolas PRIVATE bettola SDL3::SDL3 SDL3_net)
 | 
			
		||||
target_link_libraries(bettolas PRIVATE bettola Threads::Threads)
 | 
			
		||||
 | 
			
		||||
target_link_directories(bettolas PRIVATE ${sdl_net_BINARY_DIR})
 | 
			
		||||
 | 
			
		||||
target_include_directories(bettolas PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src
 | 
			
		||||
  ${sdl_net_SOURCE_DIR}/include)
 | 
			
		||||
target_include_directories(bettolas PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,23 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
 | 
			
		||||
#include "network_manager.h"
 | 
			
		||||
 | 
			
		||||
int main(int argc, char** argv) {
 | 
			
		||||
  printf("=== Server starting ===\n");
 | 
			
		||||
 | 
			
		||||
  NetworkManager* net_manager = new NetworkManager();
 | 
			
		||||
  net_manager->start(); /* Our loop. */
 | 
			
		||||
  try {
 | 
			
		||||
    /* We'll keep main thread alive while server runs in background. */
 | 
			
		||||
    NetworkManager server;
 | 
			
		||||
    server.start(1337);
 | 
			
		||||
    while(true) {
 | 
			
		||||
      std::this_thread::sleep_for(std::chrono::seconds(1));
 | 
			
		||||
    }
 | 
			
		||||
  } catch(const std::exception& e) {
 | 
			
		||||
    fprintf(stderr, "Main exception: %s\n", e.what());
 | 
			
		||||
    return 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete net_manager; /* Shouldn't get here. */
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,140 +1,113 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
#include "SDL3_net/SDL_net.h"
 | 
			
		||||
#include "vfs.h"
 | 
			
		||||
#include "vfs_manager.h"
 | 
			
		||||
#include "command_processor.h"
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <functional>
 | 
			
		||||
 | 
			
		||||
#include "network_manager.h"
 | 
			
		||||
 | 
			
		||||
/* 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);
 | 
			
		||||
/* TODO: Re-implement. */
 | 
			
		||||
#include "asio/error_code.hpp"
 | 
			
		||||
#include "asio/ip/tcp.hpp"
 | 
			
		||||
#include "command_processor.h"
 | 
			
		||||
#include "player.h"
 | 
			
		||||
#include "net/tcp_connection.h"
 | 
			
		||||
#include "vfs.h"
 | 
			
		||||
 | 
			
		||||
NetworkManager::NetworkManager(void) : _acceptor(_io_context) {
 | 
			
		||||
  /* VFS setup. */
 | 
			
		||||
  _world_vfs["8.8.8.8"]   = _vfs_manager.create_vfs("npc");
 | 
			
		||||
  _world_vfs["10.0.2.15"] = _vfs_manager.create_vfs("npc");
 | 
			
		||||
  fprintf(stderr, "Created world with %zu networks\n", _world_vfs.size());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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());
 | 
			
		||||
NetworkManager::~NetworkManager(void) { stop(); }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
void NetworkManager::stop(void) {
 | 
			
		||||
  _io_context.stop();
 | 
			
		||||
  if(_context_thread.joinable()) {
 | 
			
		||||
    _context_thread.join();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
NetworkManager::~NetworkManager(void) {
 | 
			
		||||
  for(auto player : _players) {
 | 
			
		||||
    delete player;
 | 
			
		||||
  }
 | 
			
		||||
  NET_DestroyServer(_server_socket);
 | 
			
		||||
  NET_Quit();
 | 
			
		||||
}
 | 
			
		||||
void NetworkManager::start(uint16_t port) {
 | 
			
		||||
  try {
 | 
			
		||||
    asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), port);
 | 
			
		||||
    _acceptor.open(endpoint.protocol());
 | 
			
		||||
    _acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true));
 | 
			
		||||
    _acceptor.bind(endpoint);
 | 
			
		||||
    _acceptor.listen();
 | 
			
		||||
 | 
			
		||||
void NetworkManager::start(void) {
 | 
			
		||||
  printf("BettolaServer listening on port 8080...\n");
 | 
			
		||||
  while(true) {
 | 
			
		||||
    /* Check for and accept any new client connections. */
 | 
			
		||||
    _handle_new_connections();
 | 
			
		||||
    fprintf(stderr, "BettolaServer started on port %d\n", port);
 | 
			
		||||
 | 
			
		||||
    /* Check all existing clients for incoming data. */
 | 
			
		||||
    _handle_client_activity();
 | 
			
		||||
    start_accept();
 | 
			
		||||
 | 
			
		||||
    /* Let's not be burning CPU cycles. */
 | 
			
		||||
    SDL_Delay(0);
 | 
			
		||||
    /* Run io_context in its own thread so it doesn't block main. */
 | 
			
		||||
    _context_thread = std::thread([this]() { _io_context.run(); });
 | 
			
		||||
  } catch (const std::exception& e) {
 | 
			
		||||
    fprintf(stderr, "BettolaServer exception: %s\n", e.what());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
      _players.push_back(new_player);
 | 
			
		||||
      printf("Client connected. Total players: %zu\n", _players.size());
 | 
			
		||||
void NetworkManager::start_accept(void) {
 | 
			
		||||
  _acceptor.async_accept(
 | 
			
		||||
    [this](const asio::error_code& ec, asio::ip::tcp::socket socket) {
 | 
			
		||||
      if(!ec) {
 | 
			
		||||
        fprintf(stderr, "New connection from: %s\n",
 | 
			
		||||
                socket.remote_endpoint().address().to_string().c_str());
 | 
			
		||||
 | 
			
		||||
      /* 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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
        auto new_connection =
 | 
			
		||||
          std::make_shared<net::TcpConnection>(_io_context, std::move(socket));
 | 
			
		||||
 | 
			
		||||
void NetworkManager::_handle_client_activity(void) {
 | 
			
		||||
  /* 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 };
 | 
			
		||||
        /* Create a new player for this connection. */
 | 
			
		||||
        uint32_t player_id = _next_player_id++;
 | 
			
		||||
        auto new_player = std::make_unique<Player>(player_id, _vfs_manager);
 | 
			
		||||
        Player* new_player_ptr = new_player.get();
 | 
			
		||||
        _players[player_id] = std::move(new_player);
 | 
			
		||||
        new_connection->set_id(player_id);
 | 
			
		||||
 | 
			
		||||
    /* 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;
 | 
			
		||||
    }
 | 
			
		||||
        /* Callback for new connection. Capture new_connection's shared_ptr
 | 
			
		||||
         * to keep it alive and ident it.
 | 
			
		||||
         */
 | 
			
		||||
        new_connection->start(
 | 
			
		||||
            [this, new_connection](const std::string& msg) {
 | 
			
		||||
                this->on_message(new_connection, msg);
 | 
			
		||||
            },
 | 
			
		||||
            [this, new_connection]() { this->on_disconnect(new_connection); });
 | 
			
		||||
 | 
			
		||||
    if(ready > 0) {
 | 
			
		||||
      /* Socket has data. If processing fails, disconnect the client. */
 | 
			
		||||
      fprintf(stderr, "[SERVER] Socket has data, processing command...\n");
 | 
			
		||||
        /* Send initial prompt. */
 | 
			
		||||
        std::string prompt = "\n" + get_full_path(new_player_ptr->cmd_processor->get_current_dir());
 | 
			
		||||
        new_connection->send(prompt);
 | 
			
		||||
 | 
			
		||||
      if(!_process_command(player)) {
 | 
			
		||||
        fprintf(stderr, "[SERVER] _process_command failed, disconnecting client.\n");
 | 
			
		||||
        _disconnect_client(player, i);
 | 
			
		||||
        _connections.push_back(new_connection);
 | 
			
		||||
      } else {
 | 
			
		||||
        fprintf(stderr, "Accept error: %s\n", ec.message().c_str());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
      /* Continue listening for the next connection. */
 | 
			
		||||
      start_accept();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool NetworkManager::_process_command(Player* player) {
 | 
			
		||||
  /* Read the length-prefixed command from the client. */
 | 
			
		||||
  char buffer[2048];
 | 
			
		||||
  memset(buffer, 0, sizeof(buffer));
 | 
			
		||||
 | 
			
		||||
  int bytes_received = NET_ReadFromStreamSocket(player->socket, buffer, sizeof(buffer)-1);
 | 
			
		||||
  if(bytes_received <= 0) {
 | 
			
		||||
    return false; /* Error or disconnect. */
 | 
			
		||||
void NetworkManager::on_message(std::shared_ptr<net::TcpConnection> connection,
 | 
			
		||||
                                const std::string& message) {
 | 
			
		||||
  Player* player = _players[connection->get_id()].get();
 | 
			
		||||
  if(!player) {
 | 
			
		||||
    fprintf(stderr, "Error: Receiving message from unknown player.\n");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::string cmd_str(buffer);
 | 
			
		||||
  fprintf(stderr, "[SERVER] Received command: \"%s\"\n", cmd_str.c_str());
 | 
			
		||||
  fprintf(stderr, "[Player %u] Command: '%s'\n", player->id, message.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 response = player->cmd_processor->process_command(message);
 | 
			
		||||
  std::string new_prompt = get_full_path(player->cmd_processor->get_current_dir());
 | 
			
		||||
  std::string final_response = response + "\n" + new_prompt;
 | 
			
		||||
  response += "\n" + new_prompt;
 | 
			
		||||
 | 
			
		||||
  send_response_string(player->socket, final_response);
 | 
			
		||||
  return true;
 | 
			
		||||
  connection->send(response);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
  }
 | 
			
		||||
  delete player;
 | 
			
		||||
  printf("Client disconnected. Total players: %zu\n", _players.size());
 | 
			
		||||
void NetworkManager::on_disconnect(std::shared_ptr<net::TcpConnection> connection) {
 | 
			
		||||
  uint32_t player_id = connection->get_id();
 | 
			
		||||
  fprintf(stderr, "[Player %u] Disconnected.\n", player_id);
 | 
			
		||||
  _connections.erase(
 | 
			
		||||
    std::remove(_connections.begin(), _connections.end(), connection), _connections.end());
 | 
			
		||||
 | 
			
		||||
  _players.erase(player_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,14 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
#include <asio.hpp>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <memory.h>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <thread>
 | 
			
		||||
 | 
			
		||||
#include <SDL3_net/SDL_net.h>
 | 
			
		||||
#include "asio/io_context.hpp"
 | 
			
		||||
#include "net/tcp_connection.h"
 | 
			
		||||
#include "player.h"
 | 
			
		||||
#include "vfs_manager.h"
 | 
			
		||||
 | 
			
		||||
@ -13,16 +17,23 @@ public:
 | 
			
		||||
  NetworkManager();
 | 
			
		||||
  ~NetworkManager(void);
 | 
			
		||||
 | 
			
		||||
  void start(void);
 | 
			
		||||
  void start(uint16_t port);
 | 
			
		||||
  void stop(void);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
  void _handle_new_connections(void);
 | 
			
		||||
  void _handle_client_activity(void);
 | 
			
		||||
  bool _process_command(Player* player);
 | 
			
		||||
  void _disconnect_client(Player* player, int index);
 | 
			
		||||
  void start_accept(void);
 | 
			
		||||
  void on_message(std::shared_ptr<net::TcpConnection> connection,
 | 
			
		||||
                  const std::string& message);
 | 
			
		||||
  void on_disconnect(std::shared_ptr<net::TcpConnection> connection);
 | 
			
		||||
 | 
			
		||||
  asio::io_context _io_context;
 | 
			
		||||
  std::thread _context_thread;
 | 
			
		||||
  asio::ip::tcp::acceptor _acceptor;
 | 
			
		||||
 | 
			
		||||
  std::deque<std::shared_ptr<net::TcpConnection>> _connections;
 | 
			
		||||
  std::unordered_map<uint32_t, std::unique_ptr<Player>> _players;
 | 
			
		||||
  uint32_t _next_player_id = 1;
 | 
			
		||||
 | 
			
		||||
  NET_Server* _server_socket;
 | 
			
		||||
  std::vector<Player*> _players;
 | 
			
		||||
  std::map<std::string, vfs_node*> _world_vfs; /* For NPC's. */
 | 
			
		||||
  VFSManager _npc_vfs_manager;
 | 
			
		||||
  VFSManager _vfs_manager;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
#include "player.h"
 | 
			
		||||
#include "command_processor.h"
 | 
			
		||||
 | 
			
		||||
Player::Player(NET_StreamSocket* new_socket) :
 | 
			
		||||
    socket(new_socket),
 | 
			
		||||
Player::Player(uint32_t new_id, VFSManager& vfs_manager) :
 | 
			
		||||
    id(new_id),
 | 
			
		||||
    vfs_root(vfs_manager.create_vfs("player")),
 | 
			
		||||
    cmd_processor(new CommandProcessor(vfs_root)) {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <SDL3_net/SDL_net.h>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
 | 
			
		||||
#include "vfs.h"
 | 
			
		||||
#include "vfs_manager.h"
 | 
			
		||||
@ -8,12 +8,10 @@
 | 
			
		||||
 | 
			
		||||
class Player {
 | 
			
		||||
public:
 | 
			
		||||
  Player(NET_StreamSocket* socket);
 | 
			
		||||
  Player(uint32_t id, VFSManager& vfs_manager);
 | 
			
		||||
  ~Player(void);
 | 
			
		||||
 | 
			
		||||
  NET_StreamSocket* socket;
 | 
			
		||||
  VFSManager vfs_manager;
 | 
			
		||||
  uint32_t id;
 | 
			
		||||
  vfs_node* vfs_root;
 | 
			
		||||
  CommandProcessor* cmd_processor; /* Manages the VFS state for the remote session. */
 | 
			
		||||
private:
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user