From 285dffc32ea11827e13b2fec77fb558d2849766e Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Sat, 7 Feb 2026 12:14:33 -0500 Subject: [PATCH] Add Lua scripting support to desktop emulator - Created emulator-specific lua_game_emulator.cpp using filesystem instead of FatFS - Created lua_game_loader_emulator.cpp to scan games/lua_examples directory - Updated CMakeLists.txt to include Lua 5.4 engine and bindings - Updated to SFML 3.0 API compatibility (event handling, sprite initialization) - Updated Game class to use public members for Lua bindings - Updated GameLauncher to use std::function for lambda captures - Added continuous 60 FPS rendering for smooth display - Emulator now loads and runs all three example Lua games --- emulator/CMakeLists.txt | 30 +++- emulator/game.h | 3 +- emulator/game_launcher.h | 13 +- emulator/input_manager.cpp | 4 +- emulator/input_manager.h | 12 +- emulator/low_level_display_sfml.cpp | 28 ++-- emulator/low_level_display_sfml.h | 5 +- emulator/lua_game_emulator.cpp | 193 ++++++++++++++++++++++++++ emulator/lua_game_loader_emulator.cpp | 159 +++++++++++++++++++++ emulator/main.cpp | 51 ++++--- 10 files changed, 447 insertions(+), 51 deletions(-) create mode 100644 emulator/lua_game_emulator.cpp create mode 100644 emulator/lua_game_loader_emulator.cpp diff --git a/emulator/CMakeLists.txt b/emulator/CMakeLists.txt index b323bf9..3154523 100644 --- a/emulator/CMakeLists.txt +++ b/emulator/CMakeLists.txt @@ -3,17 +3,41 @@ project(basic1_emulator) set(CMAKE_CXX_STANDARD 17) -find_package(SFML 2.5 COMPONENTS graphics window system REQUIRED) +find_package(SFML 3.0 COMPONENTS Graphics Window System REQUIRED) + +# Lua source files +file(GLOB LUA_SOURCES "../lib/lua/*.c") +list(FILTER LUA_SOURCES EXCLUDE REGEX "lua\\.c$|luac\\.c$|loslib\\.c$|liolib\\.c$") + +# Game source files +set(GAME_SOURCES + ../games/lua_bindings.cpp + ../games/demo_game.cpp + ../games/tic_tac_toe.cpp + ../games/monopoly/monopoly_game.cpp + ../games/monopoly/player.c + ../lib/game_launcher.cpp + ../display/low_level_render.cpp + ../display/low_level_gui.cpp +) # Add source files set(SOURCES main.cpp low_level_display_sfml.cpp + lua_game_emulator.cpp + lua_game_loader_emulator.cpp + input_manager.cpp + ${GAME_SOURCES} + ${LUA_SOURCES} # Add more emulator-specific sources here ) add_executable(basic1_emulator ${SOURCES}) -target_include_directories(basic1_emulator PRIVATE ../display ../fonts ../games .) +# Define LUA_32BITS for 32-bit embedded mode +target_compile_definitions(basic1_emulator PRIVATE LUA_32BITS=1) -target_link_libraries(basic1_emulator sfml-graphics sfml-window sfml-system) +target_include_directories(basic1_emulator PRIVATE . .. ../display ../fonts ../games ../lib ../lib/lua) + +target_link_libraries(basic1_emulator SFML::Graphics SFML::Window SFML::System) diff --git a/emulator/game.h b/emulator/game.h index efab3e9..ab4a72c 100644 --- a/emulator/game.h +++ b/emulator/game.h @@ -15,7 +15,8 @@ public: virtual bool update(const InputEvent& event) = 0; virtual void draw() = 0; virtual bool wants_to_exit() const { return false; } -protected: + + // Public members for Lua bindings access uint16_t width; uint16_t height; LowLevelRenderer* renderer; diff --git a/emulator/game_launcher.h b/emulator/game_launcher.h index e7d00dd..a5c4f28 100644 --- a/emulator/game_launcher.h +++ b/emulator/game_launcher.h @@ -1,25 +1,33 @@ // Copy of game_launcher.h for emulator build #include #include +#include #include "input_event.h" #include "game.h" + class LowLevelRenderer; class LowLevelGUI; class InputManager; + struct GameEntry { const char* name; const char* description; - Game* (*factory)(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager); + std::function factory; }; + class GameLauncher { public: GameLauncher(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager); - void register_game(const char* name, const char* description, Game* (*factory)(uint16_t, uint16_t, LowLevelRenderer*, LowLevelGUI*, InputManager*)); + + void register_game(const char* name, const char* description, + std::function factory); + void draw(); bool update(const InputEvent& event); Game* get_selected_game(); void reset(); bool is_game_selected() const { return selected_game != nullptr; } + private: uint16_t width; uint16_t height; @@ -29,6 +37,7 @@ private: std::vector games; int selected_index; Game* selected_game; + static const int MENU_Y_START = 60; static const int MENU_ITEM_HEIGHT = 40; static const int MENU_PADDING = 10; diff --git a/emulator/input_manager.cpp b/emulator/input_manager.cpp index 4c3a4cd..f60d159 100644 --- a/emulator/input_manager.cpp +++ b/emulator/input_manager.cpp @@ -1,3 +1,5 @@ // Emulator stub for InputManager implementation #include "input_manager.h" -// No implementation needed for stub + +// Methods are all defined inline in the header +// This file exists just to ensure the class has a compilation unit diff --git a/emulator/input_manager.h b/emulator/input_manager.h index 7e9fa2d..bbe4639 100644 --- a/emulator/input_manager.h +++ b/emulator/input_manager.h @@ -7,21 +7,21 @@ // Minimal stub for emulator build class InputManager { public: - bool has_buttons() const { return false; } - bool has_touch() const { return false; } + inline bool has_buttons() const { return false; } + inline bool has_touch() const { return false; } - void get_virtual_button_regions(int* a_rect, int* b_rect) const { + inline void get_virtual_button_regions(int* a_rect, int* b_rect) const { for (int i = 0; i < 4; i++) { a_rect[i] = v_button_a[i]; b_rect[i] = v_button_b[i]; } } - void set_virtual_button_regions(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) { + inline void set_virtual_button_regions(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) { v_button_a[0] = ax; v_button_a[1] = ay; v_button_a[2] = aw; v_button_a[3] = ah; v_button_b[0] = bx; v_button_b[1] = by; v_button_b[2] = bw; v_button_b[3] = bh; v_buttons_active = true; } - void clear_virtual_button_regions() { + inline void clear_virtual_button_regions() { v_buttons_active = false; for (int i = 0; i < 4; i++) { v_button_a[i] = 0; @@ -29,7 +29,7 @@ public: } } - bool check_virtual_buttons(int16_t x, int16_t y, InputType& out_type) const { + inline bool check_virtual_buttons(int16_t x, int16_t y, InputType& out_type) const { if (!v_buttons_active) return false; if (x >= v_button_a[0] && x <= v_button_a[0] + v_button_a[2] && diff --git a/emulator/low_level_display_sfml.cpp b/emulator/low_level_display_sfml.cpp index cb67a04..dd3c15f 100644 --- a/emulator/low_level_display_sfml.cpp +++ b/emulator/low_level_display_sfml.cpp @@ -5,8 +5,8 @@ #include // Add missing method implementations for emulator linkage -bool LowLevelDisplaySFML::pollEvent(sf::Event& event) { - return window.pollEvent(event); +std::optional LowLevelDisplaySFML::pollEvent() { + return window.pollEvent(); } void LowLevelDisplaySFML::close() { @@ -14,18 +14,22 @@ void LowLevelDisplaySFML::close() { } LowLevelDisplaySFML::LowLevelDisplaySFML(int w, int h) - : width(w), height(h), window(sf::VideoMode(w, h), "basic1 Emulator"), framebuffer((w * h + 7) / 8, 0) {} + : width(w), height(h), + window(sf::VideoMode({(unsigned)w, (unsigned)h}), "basic1 Emulator"), + framebuffer((w * h + 7) / 8, 0) {} bool LowLevelDisplaySFML::init() { - texture.create(width, height); - sprite.setTexture(texture); + if (!texture.resize({(unsigned)width, (unsigned)height})) { + return false; + } + sprite.emplace(texture); return window.isOpen(); } void LowLevelDisplaySFML::draw_buffer(const uint8_t* bit_buffer) { // Convert 1-bit buffer to 8-bit grayscale (or RGBA) for SFML // Each bit in bit_buffer represents a pixel (0=black, 1=white) - std::vector pixels(width * height * 4, 0); + std::vector pixels(width * height * 4, 0); for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int bit_index = y * width + x; @@ -33,7 +37,7 @@ void LowLevelDisplaySFML::draw_buffer(const uint8_t* bit_buffer) { int bit_offset = 7 - (bit_index % 8); bool on = (bit_buffer[byte_index] >> bit_offset) & 0x1; int idx = (y * width + x) * 4; - sf::Uint8 color = on ? 255 : 0; + std::uint8_t color = on ? 255 : 0; pixels[idx + 0] = color; // R pixels[idx + 1] = color; // G pixels[idx + 2] = color; // B @@ -44,14 +48,10 @@ void LowLevelDisplaySFML::draw_buffer(const uint8_t* bit_buffer) { } void LowLevelDisplaySFML::refresh() { - sf::Event event; - while (window.pollEvent(event)) { - if (event.type == sf::Event::Closed) - window.close(); - // TODO: Handle mouse/keyboard input here - } window.clear(sf::Color::Black); - window.draw(sprite); + if (sprite) { + window.draw(*sprite); + } window.display(); } diff --git a/emulator/low_level_display_sfml.h b/emulator/low_level_display_sfml.h index 6d065a6..47fe661 100644 --- a/emulator/low_level_display_sfml.h +++ b/emulator/low_level_display_sfml.h @@ -1,5 +1,6 @@ #pragma once #include +#include class LowLevelDisplaySFML { public: @@ -8,12 +9,12 @@ public: void draw_buffer(const uint8_t* bit_buffer); void refresh(); bool isOpen() const; - bool pollEvent(sf::Event& event); + std::optional pollEvent(); void close(); private: int width, height; sf::RenderWindow window; sf::Texture texture; - sf::Sprite sprite; + std::optional sprite; std::vector framebuffer; }; diff --git a/emulator/lua_game_emulator.cpp b/emulator/lua_game_emulator.cpp new file mode 100644 index 0000000..62515eb --- /dev/null +++ b/emulator/lua_game_emulator.cpp @@ -0,0 +1,193 @@ +// ============================================================================ +// LUA GAME WRAPPER - EMULATOR IMPLEMENTATION +// ============================================================================ +// Manages Lua VM lifecycle and script execution for desktop emulator + +#include "../games/lua_game.h" +#include "../games/lua_bindings.h" +#include +#include +#include +#include + +LuaGame::LuaGame(const char* script_path, uint16_t width, uint16_t height, + LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager) + : Game(width, height, renderer, gui, input_manager), + L(nullptr), + script_path(script_path), + loaded(false) { + + // Create new Lua state + L = luaL_newstate(); + if (!L) { + error_message = "Failed to create Lua state"; + printf("LuaGame: %s\n", error_message.c_str()); + return; + } + + // Open standard Lua libraries (math, string, table, coroutine) + luaL_openlibs(L); + + // Register game API bindings + lua_bindings_register(L, this); + + // Load the script + loaded = load_script(); + + if (!loaded) { + printf("LuaGame: Failed to load %s: %s\n", script_path, error_message.c_str()); + } else { + printf("LuaGame: Successfully loaded %s\n", script_path); + } +} + +LuaGame::~LuaGame() { + if (L) { + lua_close(L); + L = nullptr; + } +} + +bool LuaGame::load_script() { + // Open Lua script from filesystem (emulator) + std::ifstream file(script_path); + if (!file.is_open()) { + error_message = "Failed to open file: " + script_path; + return false; + } + + // Read entire file into string + std::stringstream buffer; + buffer << file.rdbuf(); + std::string script_content = buffer.str(); + file.close(); + + if (script_content.empty()) { + error_message = "Script file is empty"; + return false; + } + + if (script_content.size() > 64 * 1024) { // Limit to 64KB + error_message = "Script file too large (> 64KB)"; + return false; + } + + // Load script into Lua + int result = luaL_loadbuffer(L, script_content.c_str(), script_content.size(), script_path.c_str()); + + if (result != LUA_OK) { + report_error("load script"); + return false; + } + + // Execute script (loads functions into global namespace) + result = lua_pcall(L, 0, 0, 0); + if (result != LUA_OK) { + report_error("execute script"); + return false; + } + + return true; +} + +void LuaGame::init() { + if (!loaded) return; + + // Call Lua init() function if it exists + lua_getglobal(L, "init"); + if (lua_isfunction(L, -1)) { + call_lua_function("init", 0, 0); + } else { + lua_pop(L, 1); // Pop non-function value + printf("LuaGame: Warning - no init() function found\n"); + } +} + +bool LuaGame::update(const InputEvent& event) { + if (!loaded) return false; + + // Call Lua update(event) function if it exists + lua_getglobal(L, "update"); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 1); + return false; // No update function, no redraw needed + } + + // Push event table to Lua + lua_newtable(L); + + lua_pushstring(L, "type"); + lua_pushinteger(L, (int)event.type); + lua_settable(L, -3); + + lua_pushstring(L, "x"); + lua_pushinteger(L, event.x); + lua_settable(L, -3); + + lua_pushstring(L, "y"); + lua_pushinteger(L, event.y); + lua_settable(L, -3); + + lua_pushstring(L, "button_id"); + lua_pushinteger(L, event.button_id); + lua_settable(L, -3); + + lua_pushstring(L, "valid"); + lua_pushboolean(L, event.valid); + lua_settable(L, -3); + + // Call update(event) with 1 arg, expecting 1 result (needs_redraw) + if (!call_lua_function("update", 1, 1)) { + return false; + } + + // Get return value (needs redraw?) + bool needs_redraw = lua_toboolean(L, -1); + lua_pop(L, 1); + + return needs_redraw; +} + +void LuaGame::draw() { + if (!loaded) return; + + // Call Lua draw() function if it exists + lua_getglobal(L, "draw"); + if (lua_isfunction(L, -1)) { + call_lua_function("draw", 0, 0); + } else { + lua_pop(L, 1); + } +} + +bool LuaGame::wants_to_exit() const { + if (!L) return false; + + // Check if Lua script requested exit + lua_pushstring(L, "__exit_requested"); + lua_gettable(L, LUA_REGISTRYINDEX); + bool exit = lua_toboolean(L, -1); + lua_pop(L, 1); + + return exit; +} + +bool LuaGame::call_lua_function(const char* func_name, int nargs, int nresults) { + int result = lua_pcall(L, nargs, nresults, 0); + if (result != LUA_OK) { + report_error(func_name); + return false; + } + return true; +} + +void LuaGame::report_error(const char* context) { + const char* msg = lua_tostring(L, -1); + if (msg) { + error_message = context; + error_message += ": "; + error_message += msg; + printf("LuaGame Error [%s]: %s\n", context, msg); + } + lua_pop(L, 1); // Pop error message +} diff --git a/emulator/lua_game_loader_emulator.cpp b/emulator/lua_game_loader_emulator.cpp new file mode 100644 index 0000000..78f8017 --- /dev/null +++ b/emulator/lua_game_loader_emulator.cpp @@ -0,0 +1,159 @@ +// ============================================================================ +// LUA GAME LOADER - EMULATOR IMPLEMENTATION +// ============================================================================ +// Discovers Lua scripts from filesystem and integrates with game launcher + +#include "../games/lua_game_loader.h" +#include "../games/lua_game.h" +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// Structure to hold script path for factory closure +struct LuaGameFactoryData { + char script_path[256]; +}; + +static std::vector factory_data_list; + +bool LuaGameLoader::parse_metadata(const char* script_path, char* name, char* description) { + // Default name from filename + fs::path path(script_path); + std::string filename = path.stem().string(); // Get filename without extension + strncpy(name, filename.c_str(), 63); + name[63] = '\0'; + + // Default empty description + description[0] = '\0'; + + // Try to open file and parse metadata comments + std::ifstream file(script_path); + if (!file.is_open()) { + printf("LuaGameLoader: Warning - could not open %s for metadata\n", script_path); + return false; + } + + // Read first 512 bytes to look for metadata comments + char buffer[512]; + file.read(buffer, sizeof(buffer) - 1); + std::streamsize bytes_read = file.gcount(); + file.close(); + + if (bytes_read == 0) { + return false; + } + + buffer[bytes_read] = '\0'; + + // Parse metadata comments: -- NAME: Game Name + char* line = buffer; + while (line && (line - buffer) < bytes_read) { + char* next_line = strchr(line, '\n'); + if (next_line) *next_line = '\0'; + + // Check for -- NAME: + if (strncmp(line, "-- NAME:", 8) == 0) { + const char* value = line + 8; + while (*value == ' ') value++; // Skip spaces + strncpy(name, value, 63); + name[63] = '\0'; + } + // Check for -- DESC: + else if (strncmp(line, "-- DESC:", 8) == 0) { + const char* value = line + 8; + while (*value == ' ') value++; + strncpy(description, value, 127); + description[127] = '\0'; + } + + if (next_line) { + line = next_line + 1; + } else { + break; + } + } + + return true; +} + +int LuaGameLoader::register_all_games(GameLauncher* launcher) { + int count = 0; + + printf("LuaGameLoader: Scanning games/lua_examples directory for .lua scripts...\n"); + + // Path to lua examples relative to emulator binary + const char* search_paths[] = { + "../games/lua_examples", + "games/lua_examples", + "./lua_examples" + }; + + fs::path games_dir; + bool found_dir = false; + + // Try to find the lua_examples directory + for (const char* search_path : search_paths) { + if (fs::exists(search_path) && fs::is_directory(search_path)) { + games_dir = fs::path(search_path); + found_dir = true; + break; + } + } + + if (!found_dir) { + printf("LuaGameLoader: Could not find games/lua_examples directory\n"); + printf("LuaGameLoader: Tried: ../games/lua_examples, games/lua_examples, ./lua_examples\n"); + return 0; + } + + printf("LuaGameLoader: Found directory: %s\n", games_dir.string().c_str()); + + // Scan for .lua files + try { + for (const auto& entry : fs::directory_iterator(games_dir)) { + if (!entry.is_regular_file()) continue; + + // Check for .lua extension + if (entry.path().extension() != ".lua") continue; + + std::string script_path = entry.path().string(); + + // Parse metadata + char name[64]; + char description[128]; + parse_metadata(script_path.c_str(), name, description); + + printf("LuaGameLoader: Found %s - '%s'\n", entry.path().filename().string().c_str(), name); + + // Create factory data (persistent for game lifetime) + LuaGameFactoryData* data = new LuaGameFactoryData(); + strncpy(data->script_path, script_path.c_str(), sizeof(data->script_path) - 1); + data->script_path[sizeof(data->script_path) - 1] = '\0'; + factory_data_list.push_back(data); + + // Register with launcher - using lambda factory pattern + launcher->register_game( + name, + description[0] ? description : "Lua Script", + [data](uint16_t width, uint16_t height, LowLevelRenderer* renderer, + LowLevelGUI* gui, InputManager* input_manager) -> Game* { + return new LuaGame(data->script_path, width, height, renderer, gui, input_manager); + } + ); + + count++; + } + } catch (const fs::filesystem_error& e) { + printf("LuaGameLoader: Error scanning directory: %s\n", e.what()); + return count; + } + + printf("LuaGameLoader: Registered %d Lua games\n", count); + return count; +} diff --git a/emulator/main.cpp b/emulator/main.cpp index f44873b..71072cc 100644 --- a/emulator/main.cpp +++ b/emulator/main.cpp @@ -2,10 +2,11 @@ #include "low_level_display_sfml.h" #include "../display/low_level_render.h" #include "../display/low_level_gui.h" -#include "game_launcher.h" +#include "../lib/game_launcher.h" #include "../games/demo_game.h" #include "../games/tic_tac_toe.h" #include "../games/monopoly/monopoly_game.h" +#include "../games/lua_game_loader.h" #include "input_manager.h" #include #include @@ -34,6 +35,11 @@ int main() { // Create GameLauncher GameLauncher launcher(WIDTH, HEIGHT, &renderer, &gui, &input_manager); + + // Register Lua games from lua_examples directory + LuaGameLoader::register_all_games(&launcher); + + // Register built-in C++ games launcher.register_game("Tic-Tac-Toe", "Classic 2-player game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { return new TicTacToeGame(w, h, r, g, im); @@ -54,15 +60,15 @@ int main() { while (display.isOpen() && running) { // Handle SFML events and translate to InputEvent InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false}; - sf::Event sfEvent; - while (display.pollEvent(sfEvent)) { - if (sfEvent.type == sf::Event::Closed) { + + while (const auto sfEvent = display.pollEvent()) { + if (const auto* closed = sfEvent->getIf()) { display.close(); running = false; - } else if (sfEvent.type == sf::Event::MouseButtonPressed) { + } else if (const auto* mousePressed = sfEvent->getIf()) { event.type = INPUT_TOUCH_DOWN; - event.x = sfEvent.mouseButton.x; - event.y = sfEvent.mouseButton.y; + event.x = mousePressed->position.x; + event.y = mousePressed->position.y; event.valid = true; // Check for virtual buttons @@ -70,14 +76,14 @@ int main() { if (input_manager.check_virtual_buttons(event.x, event.y, virtual_type)) { event.type = virtual_type; } - } else if (sfEvent.type == sf::Event::KeyPressed) { - if (sfEvent.key.code == sf::Keyboard::Space) { + } else if (const auto* keyPressed = sfEvent->getIf()) { + if (keyPressed->code == sf::Keyboard::Key::Space) { event.type = INPUT_BUTTON_0; event.valid = true; - } else if (sfEvent.key.code == sf::Keyboard::Enter) { + } else if (keyPressed->code == sf::Keyboard::Key::Enter) { event.type = INPUT_BUTTON_1; event.valid = true; - } else if (sfEvent.key.code == sf::Keyboard::Escape) { + } else if (keyPressed->code == sf::Keyboard::Key::Escape) { // Simulate long-press exit if (launcher.is_game_selected()) { launcher.reset(); @@ -103,18 +109,19 @@ int main() { } } - if (needs_redraw) { - renderer.clear_buffer(); - if (launcher.is_game_selected()) { - current_game = launcher.get_selected_game(); - current_game->draw(); - } else { - launcher.draw(); - } - display.draw_buffer(framebuffer.data()); - display.refresh(); - needs_redraw = false; + // Always redraw every frame for emulator + renderer.clear_buffer(); + if (launcher.is_game_selected()) { + current_game = launcher.get_selected_game(); + current_game->draw(); + } else { + launcher.draw(); } + display.draw_buffer(framebuffer.data()); + display.refresh(); + + // Small delay to prevent busy-waiting (60 FPS) + sf::sleep(sf::milliseconds(16)); } return 0; }