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:
297
games/lua_bindings.cpp
Normal file
297
games/lua_bindings.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
// ============================================================================
|
||||
// LUA BINDINGS - IMPLEMENTATION
|
||||
// ============================================================================
|
||||
// Exposes C++ rendering and input APIs to Lua scripts
|
||||
|
||||
#include "lua_bindings.h"
|
||||
#include "lua_game.h"
|
||||
#include "../display/low_level_render.h"
|
||||
#include "../display/low_level_gui.h"
|
||||
#include "../lib/input_manager.h"
|
||||
#include <stdio.h>
|
||||
|
||||
// Registry key for LuaGame pointer
|
||||
static const char* LUAGAME_REGISTRY_KEY = "LUAGAME_PTR";
|
||||
|
||||
// Helper: Get LuaGame instance from registry
|
||||
static LuaGame* get_game(lua_State* L) {
|
||||
lua_pushstring(L, LUAGAME_REGISTRY_KEY);
|
||||
lua_gettable(L, LUA_REGISTRYINDEX);
|
||||
LuaGame* game = (LuaGame*)lua_touserdata(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return game;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDERER BINDINGS
|
||||
// ============================================================================
|
||||
|
||||
// renderer.clear(white)
|
||||
static int lua_renderer_clear(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
bool white = lua_toboolean(L, 1);
|
||||
game->renderer->clear_buffer();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renderer.pixel(x, y, on)
|
||||
static int lua_renderer_pixel(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
int x = luaL_checkinteger(L, 1);
|
||||
int y = luaL_checkinteger(L, 2);
|
||||
bool on = lua_toboolean(L, 3);
|
||||
|
||||
game->renderer->set_pixel(x, y, on);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renderer.rect(x, y, w, h, on, filled)
|
||||
static int lua_renderer_rect(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
int x = luaL_checkinteger(L, 1);
|
||||
int y = luaL_checkinteger(L, 2);
|
||||
int w = luaL_checkinteger(L, 3);
|
||||
int h = luaL_checkinteger(L, 4);
|
||||
bool on = lua_toboolean(L, 5);
|
||||
bool filled = lua_isnone(L, 6) ? false : lua_toboolean(L, 6);
|
||||
|
||||
if (filled) {
|
||||
game->renderer->draw_filled_rectangle(x, y, w, h, on, 1);
|
||||
} else {
|
||||
game->renderer->draw_rectangle(x, y, w, h, on, 1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renderer.circle(x, y, radius, on, filled)
|
||||
static int lua_renderer_circle(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
int x = luaL_checkinteger(L, 1);
|
||||
int y = luaL_checkinteger(L, 2);
|
||||
int radius = luaL_checkinteger(L, 3);
|
||||
bool on = lua_toboolean(L, 4);
|
||||
bool filled = lua_isnone(L, 5) ? false : lua_toboolean(L, 5);
|
||||
|
||||
if (filled) {
|
||||
game->renderer->draw_filled_circle(x, y, radius, on);
|
||||
} else {
|
||||
game->renderer->draw_circle(x, y, radius, on);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renderer.line(x0, y0, x1, y1, on, width)
|
||||
static int lua_renderer_line(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
int x0 = luaL_checkinteger(L, 1);
|
||||
int y0 = luaL_checkinteger(L, 2);
|
||||
int x1 = luaL_checkinteger(L, 3);
|
||||
int y1 = luaL_checkinteger(L, 4);
|
||||
bool on = lua_toboolean(L, 5);
|
||||
int width = lua_isnone(L, 6) ? 1 : luaL_checkinteger(L, 6);
|
||||
|
||||
game->renderer->draw_line(x0, y0, x1, y1, on, width);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renderer.text(x, y, text, on)
|
||||
static int lua_renderer_text(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
int x = luaL_checkinteger(L, 1);
|
||||
int y = luaL_checkinteger(L, 2);
|
||||
const char* text = luaL_checkstring(L, 3);
|
||||
bool on = lua_toboolean(L, 4);
|
||||
|
||||
game->renderer->set_text_color(on);
|
||||
game->renderer->draw_string(x, y, text, true);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renderer.triangle(x0, y0, x1, y1, x2, y2, on, filled)
|
||||
static int lua_renderer_triangle(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
int x0 = luaL_checkinteger(L, 1);
|
||||
int y0 = luaL_checkinteger(L, 2);
|
||||
int x1 = luaL_checkinteger(L, 3);
|
||||
int y1 = luaL_checkinteger(L, 4);
|
||||
int x2 = luaL_checkinteger(L, 5);
|
||||
int y2 = luaL_checkinteger(L, 6);
|
||||
bool on = lua_toboolean(L, 7);
|
||||
bool filled = lua_isnone(L, 8) ? false : lua_toboolean(L, 8);
|
||||
|
||||
if (filled) {
|
||||
game->renderer->draw_filled_triangle(x0, y0, x1, y1, x2, y2, on);
|
||||
} else {
|
||||
game->renderer->draw_triangle(x0, y0, x1, y1, x2, y2, on);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAME STATE BINDINGS
|
||||
// ============================================================================
|
||||
|
||||
// Global table to store persistent state variables
|
||||
// game.vars[key] = value
|
||||
|
||||
// game.width() - get display width
|
||||
static int lua_game_width(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
lua_pushinteger(L, game->width);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// game.height() - get display height
|
||||
static int lua_game_height(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
lua_pushinteger(L, game->height);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// game.exit() - request exit to launcher
|
||||
static int lua_game_exit(lua_State* L) {
|
||||
LuaGame* game = get_game(L);
|
||||
if (!game) return 0;
|
||||
|
||||
// Set exit flag (will be checked in wants_to_exit())
|
||||
lua_pushstring(L, "__exit_requested");
|
||||
lua_pushboolean(L, true);
|
||||
lua_settable(L, LUA_REGISTRYINDEX);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INPUT TYPE CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
// Set up input type constants as global table
|
||||
static void register_input_constants(lua_State* L) {
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, "NONE");
|
||||
lua_pushinteger(L, 0);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "TOUCH_DOWN");
|
||||
lua_pushinteger(L, 1);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "TOUCH_MOVE");
|
||||
lua_pushinteger(L, 2);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "TOUCH_UP");
|
||||
lua_pushinteger(L, 3);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "BUTTON_0");
|
||||
lua_pushinteger(L, 4);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "BUTTON_1");
|
||||
lua_pushinteger(L, 5);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "GESTURE");
|
||||
lua_pushinteger(L, 6);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_setglobal(L, "INPUT");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN REGISTRATION FUNCTION
|
||||
// ============================================================================
|
||||
|
||||
void lua_bindings_register(lua_State* L, LuaGame* game) {
|
||||
// Store game pointer in registry
|
||||
lua_pushstring(L, LUAGAME_REGISTRY_KEY);
|
||||
lua_pushlightuserdata(L, game);
|
||||
lua_settable(L, LUA_REGISTRYINDEX);
|
||||
|
||||
// Create renderer table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, "clear");
|
||||
lua_pushcfunction(L, lua_renderer_clear);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "pixel");
|
||||
lua_pushcfunction(L, lua_renderer_pixel);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "rect");
|
||||
lua_pushcfunction(L, lua_renderer_rect);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "circle");
|
||||
lua_pushcfunction(L, lua_renderer_circle);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "line");
|
||||
lua_pushcfunction(L, lua_renderer_line);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "text");
|
||||
lua_pushcfunction(L, lua_renderer_text);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "triangle");
|
||||
lua_pushcfunction(L, lua_renderer_triangle);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_setglobal(L, "renderer");
|
||||
|
||||
// Create game table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, "width");
|
||||
lua_pushcfunction(L, lua_game_width);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "height");
|
||||
lua_pushcfunction(L, lua_game_height);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "exit");
|
||||
lua_pushcfunction(L, lua_game_exit);
|
||||
lua_settable(L, -3);
|
||||
|
||||
// Create empty vars table for persistent state
|
||||
lua_pushstring(L, "vars");
|
||||
lua_newtable(L);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_setglobal(L, "game");
|
||||
|
||||
// Register input type constants
|
||||
register_input_constants(L);
|
||||
|
||||
printf("Lua bindings registered\n");
|
||||
}
|
||||
23
games/lua_bindings.h
Normal file
23
games/lua_bindings.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// ============================================================================
|
||||
// LUA BINDINGS - HEADER
|
||||
// ============================================================================
|
||||
// Exposes C++ game API to Lua scripts
|
||||
|
||||
#ifndef LUA_BINDINGS_H
|
||||
#define LUA_BINDINGS_H
|
||||
|
||||
extern "C" {
|
||||
#include "lua.h"
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
class LuaGame;
|
||||
|
||||
/**
|
||||
* @brief Register all game API functions with Lua state
|
||||
* @param L Lua state
|
||||
* @param game Pointer to LuaGame instance (stored as light userdata)
|
||||
*/
|
||||
void lua_bindings_register(lua_State* L, LuaGame* game);
|
||||
|
||||
#endif // LUA_BINDINGS_H
|
||||
233
games/lua_examples/API.md
Normal file
233
games/lua_examples/API.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Lua Game API Documentation
|
||||
|
||||
This document describes the API available to Lua game scripts on the Pico game console.
|
||||
|
||||
## Script Structure
|
||||
|
||||
Every Lua game must define three functions:
|
||||
|
||||
```lua
|
||||
function init()
|
||||
-- Initialize game state
|
||||
end
|
||||
|
||||
function update(event)
|
||||
-- Process input and update game logic
|
||||
-- Return true if redraw needed, false otherwise
|
||||
return needs_redraw
|
||||
end
|
||||
|
||||
function draw()
|
||||
-- Render the game to screen
|
||||
end
|
||||
```
|
||||
|
||||
## Metadata Comments
|
||||
|
||||
Add metadata at the top of your script:
|
||||
|
||||
```lua
|
||||
-- NAME: My Awesome Game
|
||||
-- DESC: A fun game about squares
|
||||
```
|
||||
|
||||
## Global Tables
|
||||
|
||||
### `game` - Game State and Info
|
||||
|
||||
- `game.width()` - Get display width in pixels
|
||||
- `game.height()` - Get display height in pixels
|
||||
- `game.exit()` - Request exit to game launcher
|
||||
- `game.vars` - Table for persistent state variables
|
||||
|
||||
Example:
|
||||
```lua
|
||||
function init()
|
||||
game.vars.score = 0
|
||||
game.vars.player_x = 100
|
||||
game.vars.state = 0
|
||||
end
|
||||
```
|
||||
|
||||
### `renderer` - Drawing Functions
|
||||
|
||||
All coordinates are in pixels. Color is boolean: `true` = black/on, `false` = white/off.
|
||||
|
||||
#### Basic Shapes
|
||||
|
||||
- `renderer.clear(white)` - Clear screen to white (true) or black (false)
|
||||
- `renderer.pixel(x, y, on)` - Set single pixel
|
||||
- `renderer.line(x0, y0, x1, y1, on, width)` - Draw line (width optional, default 1)
|
||||
- `renderer.rect(x, y, w, h, on, filled)` - Draw rectangle (filled optional, default false)
|
||||
- `renderer.circle(x, y, radius, on, filled)` - Draw circle (filled optional, default false)
|
||||
- `renderer.triangle(x0, y0, x1, y1, x2, y2, on, filled)` - Draw triangle
|
||||
|
||||
#### Text
|
||||
|
||||
- `renderer.text(x, y, text, on)` - Draw text string
|
||||
|
||||
Example:
|
||||
```lua
|
||||
function draw()
|
||||
renderer.clear(false) -- Clear to black
|
||||
renderer.rect(50, 50, 100, 80, true, true) -- Filled white rectangle
|
||||
renderer.circle(100, 100, 30, true, false) -- White circle outline
|
||||
renderer.text(10, 10, "Score: " .. tostring(game.vars.score), true)
|
||||
end
|
||||
```
|
||||
|
||||
### `INPUT` - Input Event Types
|
||||
|
||||
Constants for checking `event.type`:
|
||||
|
||||
- `INPUT.NONE` - No input
|
||||
- `INPUT.TOUCH_DOWN` - Touch/click started
|
||||
- `INPUT.TOUCH_MOVE` - Touch/drag in progress
|
||||
- `INPUT.TOUCH_UP` - Touch/click released
|
||||
- `INPUT.BUTTON_0` - Physical button 0 pressed
|
||||
- `INPUT.BUTTON_1` - Physical button 1 pressed
|
||||
- `INPUT.GESTURE` - Gesture detected
|
||||
|
||||
## Input Event Structure
|
||||
|
||||
The `event` parameter passed to `update()` has these fields:
|
||||
|
||||
- `event.type` - Event type (see INPUT constants)
|
||||
- `event.x` - X coordinate (for touch events)
|
||||
- `event.y` - Y coordinate (for touch events)
|
||||
- `event.button_id` - Button identifier (for button events)
|
||||
- `event.valid` - Boolean, true if event is valid
|
||||
|
||||
Example:
|
||||
```lua
|
||||
function update(event)
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
print("Touch at: " .. event.x .. ", " .. event.y)
|
||||
game.vars.last_touch_x = event.x
|
||||
game.vars.last_touch_y = event.y
|
||||
return true -- Request redraw
|
||||
end
|
||||
return false
|
||||
end
|
||||
```
|
||||
|
||||
## State Machine Pattern
|
||||
|
||||
Use `game.vars` to implement state machines:
|
||||
|
||||
```lua
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
end
|
||||
|
||||
function update(event)
|
||||
if game.vars.state == STATE_MENU then
|
||||
-- Handle menu input
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
elseif game.vars.state == STATE_PLAYING then
|
||||
-- Handle gameplay input
|
||||
elseif game.vars.state == STATE_GAME_OVER then
|
||||
-- Handle game over screen
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
if game.vars.state == STATE_MENU then
|
||||
-- Draw menu
|
||||
elseif game.vars.state == STATE_PLAYING then
|
||||
-- Draw game
|
||||
elseif game.vars.state == STATE_GAME_OVER then
|
||||
-- Draw game over screen
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Standard Lua Libraries
|
||||
|
||||
Available libraries:
|
||||
- `math` - Math functions (`math.sin`, `math.random`, etc.)
|
||||
- `string` - String manipulation
|
||||
- `table` - Table operations (`table.insert`, `table.remove`, etc.)
|
||||
- `coroutine` - Coroutines for advanced flow control
|
||||
|
||||
**Not available** (embedded environment):
|
||||
- `io` - File I/O (use SD card APIs in C++ if needed)
|
||||
- `os` - Operating system functions
|
||||
- `debug` - Debugging functions
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Keep it Simple** - Lua is slower than C++. Keep game logic simple for smooth performance.
|
||||
|
||||
2. **Use Local Variables** - Local variables are faster than globals:
|
||||
```lua
|
||||
local function move_player() -- Local function
|
||||
local speed = 5 -- Local variable
|
||||
game.vars.player_x = game.vars.player_x + speed
|
||||
end
|
||||
```
|
||||
|
||||
3. **Minimize Allocations** - Reuse tables instead of creating new ones:
|
||||
```lua
|
||||
-- Good: Reuse existing table
|
||||
game.vars.snake[1].x = new_x
|
||||
|
||||
-- Bad: Creates garbage
|
||||
game.vars.snake[1] = {x = new_x, y = new_y}
|
||||
```
|
||||
|
||||
4. **Efficient Drawing** - Only redraw when needed. Return `false` from `update()` if nothing changed.
|
||||
|
||||
5. **Frame Limiting** - For animation, use frame counters:
|
||||
```lua
|
||||
function update(event)
|
||||
game.vars.frame = (game.vars.frame or 0) + 1
|
||||
if game.vars.frame % 5 == 0 then -- Every 5 frames
|
||||
-- Update animation
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
```
|
||||
|
||||
## Example: Complete Minimal Game
|
||||
|
||||
```lua
|
||||
-- NAME: Click Counter
|
||||
-- DESC: Count your clicks
|
||||
|
||||
function init()
|
||||
game.vars.count = 0
|
||||
end
|
||||
|
||||
function update(event)
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
game.vars.count = game.vars.count + 1
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false)
|
||||
local text = "Clicks: " .. tostring(game.vars.count)
|
||||
renderer.text(20, 20, text, true)
|
||||
end
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
1. Save your `.lua` file to SD card in `/games/` directory
|
||||
2. Eject SD card and insert into Pico console
|
||||
3. Power on - your game will appear in the launcher menu
|
||||
4. Select and play!
|
||||
|
||||
No recompilation needed - edit scripts directly on SD card for rapid iteration.
|
||||
81
games/lua_examples/ball.lua
Normal file
81
games/lua_examples/ball.lua
Normal file
@@ -0,0 +1,81 @@
|
||||
-- NAME: Bouncing Ball
|
||||
-- DESC: Physics demo with state management
|
||||
|
||||
-- States
|
||||
local STATE_PAUSED = 0
|
||||
local STATE_RUNNING = 1
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_PAUSED
|
||||
game.vars.ball_x = game.width() / 2
|
||||
game.vars.ball_y = game.height() / 2
|
||||
game.vars.vel_x = 3
|
||||
game.vars.vel_y = 2
|
||||
game.vars.radius = 10
|
||||
game.vars.frame_count = 0
|
||||
|
||||
print("Bouncing Ball initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
-- Toggle pause on tap
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
if game.vars.state == STATE_PAUSED then
|
||||
game.vars.state = STATE_RUNNING
|
||||
else
|
||||
game.vars.state = STATE_PAUSED
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Update physics if running
|
||||
if game.vars.state == STATE_RUNNING then
|
||||
-- Move ball
|
||||
game.vars.ball_x = game.vars.ball_x + game.vars.vel_x
|
||||
game.vars.ball_y = game.vars.ball_y + game.vars.vel_y
|
||||
|
||||
-- Bounce off walls
|
||||
if game.vars.ball_x - game.vars.radius < 0 or game.vars.ball_x + game.vars.radius > game.width() then
|
||||
game.vars.vel_x = -game.vars.vel_x
|
||||
game.vars.ball_x = math.max(game.vars.radius, math.min(game.width() - game.vars.radius, game.vars.ball_x))
|
||||
end
|
||||
|
||||
if game.vars.ball_y - game.vars.radius < 0 or game.vars.ball_y + game.vars.radius > game.height() then
|
||||
game.vars.vel_y = -game.vars.vel_y
|
||||
game.vars.ball_y = math.max(game.vars.radius, math.min(game.height() - game.vars.radius, game.vars.ball_y))
|
||||
end
|
||||
|
||||
game.vars.frame_count = game.vars.frame_count + 1
|
||||
return true -- Always redraw when running
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false)
|
||||
|
||||
-- Draw ball
|
||||
renderer.circle(game.vars.ball_x, game.vars.ball_y, game.vars.radius, true, true)
|
||||
|
||||
-- Draw trail (previous positions)
|
||||
local trail_radius = game.vars.radius - 2
|
||||
if trail_radius > 2 then
|
||||
renderer.circle(game.vars.ball_x - game.vars.vel_x,
|
||||
game.vars.ball_y - game.vars.vel_y,
|
||||
trail_radius, true, false)
|
||||
end
|
||||
|
||||
-- Draw status
|
||||
if game.vars.state == STATE_PAUSED then
|
||||
renderer.text(10, 10, "PAUSED - Tap to start", true)
|
||||
else
|
||||
renderer.text(10, 10, "Frames: " .. tostring(game.vars.frame_count), true)
|
||||
renderer.text(10, 25, "Tap to pause", true)
|
||||
end
|
||||
|
||||
-- Draw velocity vector
|
||||
local arrow_x = game.vars.ball_x + game.vars.vel_x * 3
|
||||
local arrow_y = game.vars.ball_y + game.vars.vel_y * 3
|
||||
renderer.line(game.vars.ball_x, game.vars.ball_y, arrow_x, arrow_y, true, 2)
|
||||
end
|
||||
49
games/lua_examples/counter.lua
Normal file
49
games/lua_examples/counter.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
-- NAME: Touch Counter
|
||||
-- DESC: Simple tap counter demo
|
||||
|
||||
-- Initialize game state
|
||||
function init()
|
||||
game.vars.count = 0
|
||||
game.vars.last_x = 0
|
||||
game.vars.last_y = 0
|
||||
print("Counter initialized")
|
||||
end
|
||||
|
||||
-- Update game logic based on input
|
||||
function update(event)
|
||||
-- Check if touch/button pressed
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.count = game.vars.count + 1
|
||||
game.vars.last_x = event.x
|
||||
game.vars.last_y = event.y
|
||||
print("Count: " .. game.vars.count)
|
||||
return true -- Request redraw
|
||||
end
|
||||
|
||||
return false -- No redraw needed
|
||||
end
|
||||
|
||||
-- Draw the game
|
||||
function draw()
|
||||
-- Clear screen
|
||||
renderer.clear(true)
|
||||
|
||||
-- Draw title
|
||||
renderer.text(20, 20, "Touch Counter", true)
|
||||
|
||||
-- Draw count (centered)
|
||||
local count_text = "Count: " .. tostring(game.vars.count)
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 10, count_text, true)
|
||||
|
||||
-- Draw last touch position
|
||||
if game.vars.count > 0 then
|
||||
local pos_text = "Last: (" .. tostring(game.vars.last_x) .. ", " .. tostring(game.vars.last_y) .. ")"
|
||||
renderer.text(20, game.height() - 30, pos_text, true)
|
||||
|
||||
-- Draw marker at last touch
|
||||
renderer.circle(game.vars.last_x, game.vars.last_y, 5, true, false)
|
||||
end
|
||||
|
||||
-- Draw instructions
|
||||
renderer.text(20, 50, "Tap screen to increment", true)
|
||||
end
|
||||
216
games/lua_examples/snake.lua
Normal file
216
games/lua_examples/snake.lua
Normal file
@@ -0,0 +1,216 @@
|
||||
-- NAME: Snake Game
|
||||
-- DESC: Classic snake with state machine
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
-- Game constants
|
||||
local CELL_SIZE = 10
|
||||
local GRID_W = 40
|
||||
local GRID_H = 28
|
||||
|
||||
-- Initialize game
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
game.vars.high_score = 0
|
||||
|
||||
-- Snake as array of {x, y} positions
|
||||
game.vars.snake = {}
|
||||
game.vars.snake[1] = {x = 20, y = 14}
|
||||
game.vars.snake[2] = {x = 19, y = 14}
|
||||
game.vars.snake[3] = {x = 18, y = 14}
|
||||
|
||||
game.vars.dir_x = 1
|
||||
game.vars.dir_y = 0
|
||||
|
||||
game.vars.food_x = 30
|
||||
game.vars.food_y = 14
|
||||
|
||||
game.vars.frame_count = 0
|
||||
game.vars.move_speed = 10 -- Frames between moves
|
||||
|
||||
print("Snake Game initialized")
|
||||
end
|
||||
|
||||
-- Update game logic
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
-- State: MENU
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_PLAYING
|
||||
game.vars.score = 0
|
||||
init_snake()
|
||||
spawn_food()
|
||||
return true
|
||||
end
|
||||
|
||||
-- State: PLAYING
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle input for direction change
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
local head = game.vars.snake[1]
|
||||
local head_screen_x = head.x * CELL_SIZE
|
||||
local head_screen_y = head.y * CELL_SIZE
|
||||
|
||||
local dx = event.x - head_screen_x
|
||||
local dy = event.y - head_screen_y
|
||||
|
||||
-- Change direction based on touch relative to head
|
||||
if math.abs(dx) > math.abs(dy) then
|
||||
if dx > 0 and game.vars.dir_x ~= -1 then
|
||||
game.vars.dir_x = 1
|
||||
game.vars.dir_y = 0
|
||||
elseif dx < 0 and game.vars.dir_x ~= 1 then
|
||||
game.vars.dir_x = -1
|
||||
game.vars.dir_y = 0
|
||||
end
|
||||
else
|
||||
if dy > 0 and game.vars.dir_y ~= -1 then
|
||||
game.vars.dir_x = 0
|
||||
game.vars.dir_y = 1
|
||||
elseif dy < 0 and game.vars.dir_y ~= 1 then
|
||||
game.vars.dir_x = 0
|
||||
game.vars.dir_y = -1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Move snake every N frames
|
||||
game.vars.frame_count = game.vars.frame_count + 1
|
||||
if game.vars.frame_count >= game.vars.move_speed then
|
||||
game.vars.frame_count = 0
|
||||
move_snake()
|
||||
return true
|
||||
end
|
||||
|
||||
-- State: GAME_OVER
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Draw game
|
||||
function draw()
|
||||
renderer.clear(false)
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
-- Draw: MENU
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "SNAKE", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
|
||||
|
||||
if game.vars.high_score > 0 then
|
||||
local hs_text = "High: " .. tostring(game.vars.high_score)
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2 + 20, hs_text, true)
|
||||
end
|
||||
|
||||
-- Draw: PLAYING
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Draw snake
|
||||
for i = 1, #game.vars.snake do
|
||||
local seg = game.vars.snake[i]
|
||||
local filled = (i == 1) -- Head filled, body outline
|
||||
renderer.rect(seg.x * CELL_SIZE, seg.y * CELL_SIZE, CELL_SIZE, CELL_SIZE, true, filled)
|
||||
end
|
||||
|
||||
-- Draw food
|
||||
renderer.circle(game.vars.food_x * CELL_SIZE + CELL_SIZE / 2,
|
||||
game.vars.food_y * CELL_SIZE + CELL_SIZE / 2,
|
||||
CELL_SIZE / 2, true, true)
|
||||
|
||||
-- Draw score
|
||||
local score_text = "Score: " .. tostring(game.vars.score)
|
||||
renderer.text(5, 5, score_text, true)
|
||||
|
||||
-- Draw: GAME_OVER
|
||||
elseif state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
|
||||
|
||||
local score_text = "Score: " .. tostring(game.vars.score)
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2, score_text, true)
|
||||
|
||||
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true)
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: Initialize snake
|
||||
function init_snake()
|
||||
game.vars.snake = {}
|
||||
game.vars.snake[1] = {x = 20, y = 14}
|
||||
game.vars.snake[2] = {x = 19, y = 14}
|
||||
game.vars.snake[3] = {x = 18, y = 14}
|
||||
game.vars.dir_x = 1
|
||||
game.vars.dir_y = 0
|
||||
end
|
||||
|
||||
-- Helper: Spawn food at random position
|
||||
function spawn_food()
|
||||
game.vars.food_x = math.random(0, GRID_W - 1)
|
||||
game.vars.food_y = math.random(0, GRID_H - 1)
|
||||
end
|
||||
|
||||
-- Helper: Move snake
|
||||
function move_snake()
|
||||
local head = game.vars.snake[1]
|
||||
local new_head = {
|
||||
x = head.x + game.vars.dir_x,
|
||||
y = head.y + game.vars.dir_y
|
||||
}
|
||||
|
||||
-- Check wall collision
|
||||
if new_head.x < 0 or new_head.x >= GRID_W or new_head.y < 0 or new_head.y >= GRID_H then
|
||||
game_over()
|
||||
return
|
||||
end
|
||||
|
||||
-- Check self collision
|
||||
for i = 1, #game.vars.snake do
|
||||
local seg = game.vars.snake[i]
|
||||
if new_head.x == seg.x and new_head.y == seg.y then
|
||||
game_over()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Check food collision
|
||||
local ate_food = false
|
||||
if new_head.x == game.vars.food_x and new_head.y == game.vars.food_y then
|
||||
ate_food = true
|
||||
game.vars.score = game.vars.score + 10
|
||||
spawn_food()
|
||||
|
||||
-- Increase speed slightly
|
||||
if game.vars.move_speed > 3 then
|
||||
game.vars.move_speed = game.vars.move_speed - 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Move snake
|
||||
table.insert(game.vars.snake, 1, new_head)
|
||||
|
||||
if not ate_food then
|
||||
table.remove(game.vars.snake) -- Remove tail
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: Game over
|
||||
function game_over()
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
|
||||
if game.vars.score > game.vars.high_score then
|
||||
game.vars.high_score = game.vars.score
|
||||
end
|
||||
|
||||
print("Game Over! Score: " .. game.vars.score)
|
||||
end
|
||||
214
games/lua_game.cpp
Normal file
214
games/lua_game.cpp
Normal file
@@ -0,0 +1,214 @@
|
||||
// ============================================================================
|
||||
// LUA GAME WRAPPER - IMPLEMENTATION
|
||||
// ============================================================================
|
||||
// Manages Lua VM lifecycle and script execution
|
||||
|
||||
#include "lua_game.h"
|
||||
#include "lua_bindings.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
extern "C" {
|
||||
#include "ff.h" // FatFS for SD card access
|
||||
}
|
||||
|
||||
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() {
|
||||
FIL fil;
|
||||
FRESULT fr;
|
||||
|
||||
// Open Lua script from SD card
|
||||
fr = f_open(&fil, script_path.c_str(), FA_READ);
|
||||
if (fr != FR_OK) {
|
||||
error_message = "Failed to open file (FatFS error: ";
|
||||
error_message += std::to_string((int)fr);
|
||||
error_message += ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
FSIZE_t file_size = f_size(&fil);
|
||||
if (file_size == 0 || file_size > 64 * 1024) { // Limit to 64KB
|
||||
f_close(&fil);
|
||||
error_message = "Script file size invalid (0 or > 64KB)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate buffer for script
|
||||
char* script_buffer = new char[file_size + 1];
|
||||
if (!script_buffer) {
|
||||
f_close(&fil);
|
||||
error_message = "Failed to allocate memory for script";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read script into buffer
|
||||
UINT bytes_read;
|
||||
fr = f_read(&fil, script_buffer, file_size, &bytes_read);
|
||||
f_close(&fil);
|
||||
|
||||
if (fr != FR_OK || bytes_read != file_size) {
|
||||
delete[] script_buffer;
|
||||
error_message = "Failed to read script file";
|
||||
return false;
|
||||
}
|
||||
|
||||
script_buffer[file_size] = '\0';
|
||||
|
||||
// Load script into Lua
|
||||
int result = luaL_loadbuffer(L, script_buffer, file_size, script_path.c_str());
|
||||
delete[] script_buffer;
|
||||
|
||||
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
|
||||
}
|
||||
109
games/lua_game.h
Normal file
109
games/lua_game.h
Normal file
@@ -0,0 +1,109 @@
|
||||
// ============================================================================
|
||||
// LUA GAME WRAPPER - HEADER
|
||||
// ============================================================================
|
||||
// Allows Lua scripts to implement the Game interface
|
||||
|
||||
#ifndef LUA_GAME_H
|
||||
#define LUA_GAME_H
|
||||
|
||||
#include "game.h"
|
||||
#include <string>
|
||||
|
||||
extern "C" {
|
||||
#include "lua.h"
|
||||
#include "lualib.h"
|
||||
#include "lauxlib.h"
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game implementation that runs Lua scripts
|
||||
*
|
||||
* Loads a .lua file from SD card and calls its init(), update(), and draw()
|
||||
* functions. The Lua script has access to drawing primitives, input events,
|
||||
* and persistent state variables.
|
||||
*/
|
||||
class LuaGame : public Game {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Lua Game
|
||||
* @param script_path Path to .lua file on SD card (e.g., "/games/pong.lua")
|
||||
* @param width Display width in pixels
|
||||
* @param height Display height in pixels
|
||||
* @param renderer Pointer to low-level rendering interface
|
||||
* @param gui Pointer to GUI drawing primitives
|
||||
* @param input_manager Pointer to input manager
|
||||
*/
|
||||
LuaGame(const char* script_path, uint16_t width, uint16_t height,
|
||||
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager);
|
||||
|
||||
/**
|
||||
* @brief Destructor - clean up Lua state
|
||||
*/
|
||||
~LuaGame() override;
|
||||
|
||||
/**
|
||||
* @brief Initialize game - calls Lua init() function
|
||||
*/
|
||||
void init() override;
|
||||
|
||||
/**
|
||||
* @brief Update game logic - calls Lua update(event) function
|
||||
* @param event Input event from InputManager
|
||||
* @return true if screen redraw is needed
|
||||
*/
|
||||
bool update(const InputEvent& event) override;
|
||||
|
||||
/**
|
||||
* @brief Draw the game - calls Lua draw() function
|
||||
*/
|
||||
void draw() override;
|
||||
|
||||
/**
|
||||
* @brief Check if game wants to exit
|
||||
* @return true if Lua script requested exit
|
||||
*/
|
||||
bool wants_to_exit() const override;
|
||||
|
||||
/**
|
||||
* @brief Get Lua state for bindings access
|
||||
*/
|
||||
lua_State* get_lua_state() { return L; }
|
||||
|
||||
/**
|
||||
* @brief Check if Lua script loaded successfully
|
||||
*/
|
||||
bool is_loaded() const { return loaded; }
|
||||
|
||||
/**
|
||||
* @brief Get last error message if load failed
|
||||
*/
|
||||
const char* get_error() const { return error_message.c_str(); }
|
||||
|
||||
private:
|
||||
lua_State* L;
|
||||
std::string script_path;
|
||||
std::string error_message;
|
||||
bool loaded;
|
||||
|
||||
/**
|
||||
* @brief Load and execute Lua script file from SD card
|
||||
* @return true if successful, false on error
|
||||
*/
|
||||
bool load_script();
|
||||
|
||||
/**
|
||||
* @brief Call a Lua function safely with error handling
|
||||
* @param func_name Name of Lua function to call
|
||||
* @param nargs Number of arguments on stack
|
||||
* @param nresults Number of expected return values
|
||||
* @return true if successful, false on error
|
||||
*/
|
||||
bool call_lua_function(const char* func_name, int nargs = 0, int nresults = 0);
|
||||
|
||||
/**
|
||||
* @brief Report Lua error to console and store message
|
||||
*/
|
||||
void report_error(const char* context);
|
||||
};
|
||||
|
||||
#endif // LUA_GAME_H
|
||||
164
games/lua_game_loader.cpp
Normal file
164
games/lua_game_loader.cpp
Normal 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;
|
||||
}
|
||||
40
games/lua_game_loader.h
Normal file
40
games/lua_game_loader.h
Normal file
@@ -0,0 +1,40 @@
|
||||
// ============================================================================
|
||||
// LUA GAME LOADER - HEADER
|
||||
// ============================================================================
|
||||
// Scans SD card for .lua game scripts and registers them with GameLauncher
|
||||
|
||||
#ifndef LUA_GAME_LOADER_H
|
||||
#define LUA_GAME_LOADER_H
|
||||
|
||||
#include "../lib/game_launcher.h"
|
||||
|
||||
/**
|
||||
* @brief Lua Game Loader - discovers and registers Lua games from SD card
|
||||
*/
|
||||
class LuaGameLoader {
|
||||
public:
|
||||
/**
|
||||
* @brief Scan SD card /games directory and register all .lua files
|
||||
* @param launcher GameLauncher to register games with
|
||||
* @return Number of Lua games found and registered
|
||||
*/
|
||||
static int register_all_games(GameLauncher* launcher);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Parse metadata from Lua script comments
|
||||
* @param script_path Path to .lua file
|
||||
* @param name Output: game name (default: filename)
|
||||
* @param description Output: game description (default: empty)
|
||||
* @return true if file could be read
|
||||
*/
|
||||
static bool parse_metadata(const char* script_path, char* name, char* description);
|
||||
|
||||
/**
|
||||
* @brief Factory function for creating LuaGame instances
|
||||
*/
|
||||
static Game* create_lua_game(const char* script_path, uint16_t width, uint16_t height,
|
||||
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager);
|
||||
};
|
||||
|
||||
#endif // LUA_GAME_LOADER_H
|
||||
Reference in New Issue
Block a user