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
This commit is contained in:
Adolfo Reyna
2026-02-07 12:14:33 -05:00
parent e6e4eca188
commit 285dffc32e
10 changed files with 447 additions and 51 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -1,25 +1,33 @@
// Copy of game_launcher.h for emulator build
#include <stdint.h>
#include <vector>
#include <functional>
#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<Game*(uint16_t, uint16_t, LowLevelRenderer*, LowLevelGUI*, InputManager*)> 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<Game*(uint16_t, uint16_t, LowLevelRenderer*, LowLevelGUI*, InputManager*)> 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<GameEntry> 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;

View File

@@ -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

View File

@@ -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] &&

View File

@@ -5,8 +5,8 @@
#include <vector>
// Add missing method implementations for emulator linkage
bool LowLevelDisplaySFML::pollEvent(sf::Event& event) {
return window.pollEvent(event);
std::optional<sf::Event> 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<sf::Uint8> pixels(width * height * 4, 0);
std::vector<std::uint8_t> 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();
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <SFML/Graphics.hpp>
#include <optional>
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<sf::Event> pollEvent();
void close();
private:
int width, height;
sf::RenderWindow window;
sf::Texture texture;
sf::Sprite sprite;
std::optional<sf::Sprite> sprite;
std::vector<uint8_t> framebuffer;
};

View File

@@ -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 <stdio.h>
#include <string.h>
#include <fstream>
#include <sstream>
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
}

View File

@@ -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 <stdio.h>
#include <string.h>
#include <vector>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
// Structure to hold script path for factory closure
struct LuaGameFactoryData {
char script_path[256];
};
static std::vector<LuaGameFactoryData*> 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;
}

View File

@@ -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 <SFML/Window.hpp>
#include <SFML/Graphics.hpp>
@@ -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<sf::Event::Closed>()) {
display.close();
running = false;
} else if (sfEvent.type == sf::Event::MouseButtonPressed) {
} else if (const auto* mousePressed = sfEvent->getIf<sf::Event::MouseButtonPressed>()) {
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<sf::Event::KeyPressed>()) {
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,7 +109,7 @@ int main() {
}
}
if (needs_redraw) {
// Always redraw every frame for emulator
renderer.clear_buffer();
if (launcher.is_game_selected()) {
current_game = launcher.get_selected_game();
@@ -113,8 +119,9 @@ int main() {
}
display.draw_buffer(framebuffer.data());
display.refresh();
needs_redraw = false;
}
// Small delay to prevent busy-waiting (60 FPS)
sf::sleep(sf::milliseconds(16));
}
return 0;
}