Add Lua 5.4 scripting integration for dynamic game loading

- Integrated Lua 5.4 engine (32-bit mode for embedded ARM)
- Created LuaGame wrapper class implementing Game interface
- Added C++ bindings exposing renderer, game state, and input to Lua
- Implemented SD card loader for automatic .lua game discovery
- Updated GameLauncher to support std::function for lambda captures
- Made Game class members public for Lua bindings access
- Added example Lua games: counter, snake, bouncing ball
- Included comprehensive API documentation

Games can now be written as .lua text files on SD card and loaded
without recompilation. Build size: 747KB UF2, Lua VM uses ~50-80KB RAM.
This commit is contained in:
Adolfo Reyna
2026-02-07 11:56:03 -05:00
parent c8af4f6638
commit e6e4eca188
74 changed files with 29098 additions and 13 deletions

164
games/lua_game_loader.cpp Normal file
View File

@@ -0,0 +1,164 @@
// ============================================================================
// LUA GAME LOADER - IMPLEMENTATION
// ============================================================================
// Discovers Lua scripts on SD card and integrates with game launcher
#include "lua_game_loader.h"
#include "lua_game.h"
#include <stdio.h>
#include <string.h>
#include <vector>
extern "C" {
#include "ff.h"
}
// Structure to hold script path for factory closure
struct LuaGameFactoryData {
char script_path[256];
};
static std::vector<LuaGameFactoryData*> factory_data_list;
// Factory wrapper that captures script path
static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui,
InputManager* input_manager, void* user_data) {
LuaGameFactoryData* data = (LuaGameFactoryData*)user_data;
return new LuaGame(data->script_path, width, height, renderer, gui, input_manager);
}
bool LuaGameLoader::parse_metadata(const char* script_path, char* name, char* description) {
FIL fil;
FRESULT fr;
// Default name from filename
const char* filename = strrchr(script_path, '/');
if (filename) {
filename++;
} else {
filename = script_path;
}
// Remove .lua extension for default name
strncpy(name, filename, 63);
name[63] = '\0';
char* ext = strstr(name, ".lua");
if (ext) *ext = '\0';
// Default empty description
description[0] = '\0';
// Try to open file and parse metadata comments
fr = f_open(&fil, script_path, FA_READ);
if (fr != FR_OK) {
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];
UINT bytes_read;
fr = f_read(&fil, buffer, sizeof(buffer) - 1, &bytes_read);
f_close(&fil);
if (fr != FR_OK) {
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) {
DIR dir;
FILINFO fno;
FRESULT fr;
int count = 0;
printf("LuaGameLoader: Scanning /games directory for .lua scripts...\n");
// Open /games directory
fr = f_opendir(&dir, "/games");
if (fr != FR_OK) {
printf("LuaGameLoader: Could not open /games directory (error %d)\n", fr);
printf("LuaGameLoader: Make sure SD card is mounted and /games exists\n");
return 0;
}
// Scan for .lua files
while (true) {
fr = f_readdir(&dir, &fno);
if (fr != FR_OK || fno.fname[0] == 0) break; // End of directory
// Skip directories
if (fno.fattrib & AM_DIR) continue;
// Check for .lua extension
size_t len = strlen(fno.fname);
if (len < 5 || strcmp(fno.fname + len - 4, ".lua") != 0) continue;
// Build full path
char script_path[256];
snprintf(script_path, sizeof(script_path), "/games/%s", fno.fname);
// Parse metadata
char name[64];
char description[128];
parse_metadata(script_path, name, description);
printf("LuaGameLoader: Found %s - '%s'\n", fno.fname, name);
// Create factory data (persistent for game lifetime)
LuaGameFactoryData* data = new LuaGameFactoryData();
strncpy(data->script_path, script_path, 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++;
}
f_closedir(&dir);
printf("LuaGameLoader: Registered %d Lua games\n", count);
return count;
}