Implements a complete serial upload workflow that allows uploading and immediately testing Lua games via USB serial connection. New Components: - SerialUploader: Receives files via serial, writes to SD card - upload_game.py: Python tool for sending files from host computer - Protocol: Text-based with base64 encoding for reliability Key Features: - Uploads file to /games folder on SD card - Overwrites existing files (FA_CREATE_ALWAYS) - Auto-launches uploaded game immediately - Proper memory cleanup (prevents Lua state conflicts) SD Card Fixes: - Fixed SPI speed management (12.5MHz for SD, 32MHz for display) - Fixed SD write protocol (poll for data response token) - Added speed switching wrappers around all FatFS operations - Cleaned up excessive debug output Game Launcher Improvements: - Added clear_games() to prevent duplicate registrations - Added cleanup in select_game_by_name() to delete old instances - Added exact match priority in game selection - LuaGameLoader now has clear_factory_data() for memory cleanup Integration: - Added serial_uploader to CMakeLists.txt - Integrated into main loop in basic1.cpp - Re-scans games after upload to pick up new files Documentation: - UPLOAD_TOOL.md: Usage instructions - sd_card_best_practices.md: Critical lessons learned Known Issues: - Game launch after upload occasionally causes freeze (needs investigation) - Display may not refresh properly after upload Usage: python upload_game.py games/lua_examples/2048.lua /dev/tty.usbmodem101 Co-Authored-By: Claude <noreply@anthropic.com>
294 lines
11 KiB
C++
294 lines
11 KiB
C++
// ============================================================================
|
|
// GAME LAUNCHER IMPLEMENTATION
|
|
// ============================================================================
|
|
// Menu system for selecting and launching games
|
|
|
|
#include "game_launcher.h"
|
|
#include "input_manager.h"
|
|
#include "display/low_level_render.h"
|
|
#include "display/low_level_gui.h"
|
|
#include <stdio.h>
|
|
#include <cstring>
|
|
|
|
GameLauncher::GameLauncher(uint16_t width, uint16_t height,
|
|
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager)
|
|
: width(width), height(height), renderer(renderer), gui(gui), input_manager(input_manager),
|
|
selected_index(0), selected_game(nullptr), current_page(0) {
|
|
}
|
|
|
|
void GameLauncher::register_game(const char* name, const char* description,
|
|
std::function<Game*(uint16_t, uint16_t, LowLevelRenderer*, LowLevelGUI*, InputManager*)> factory) {
|
|
GameEntry entry;
|
|
entry.name = name;
|
|
entry.description = description;
|
|
entry.factory = factory;
|
|
games.push_back(entry);
|
|
|
|
printf("Registered game: %s - %s\n", name, description);
|
|
}
|
|
|
|
void GameLauncher::draw() {
|
|
// Draw main window
|
|
LowLevelWindow* window = gui->draw_new_window(10, 10, width - 20, height - 20, "Game Launcher");
|
|
|
|
renderer->set_font(&font_5x5_obj);
|
|
|
|
// Draw title with page indicator
|
|
int total_pages = get_total_pages();
|
|
char title[64];
|
|
snprintf(title, sizeof(title), "Select a Game: (Page %d/%d)", current_page + 1, total_pages);
|
|
renderer->draw_string_scaled(30, 40, title, 2);
|
|
|
|
// Get games for current page
|
|
int page_start = get_page_start_index();
|
|
int page_end = get_page_end_index();
|
|
|
|
// Draw game list with GUI buttons for current page only
|
|
for (int i = page_start; i < page_end && i < (int)games.size(); i++) {
|
|
int item_index = i - page_start;
|
|
int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT);
|
|
|
|
// Draw button (pressed/highlighted if selected)
|
|
bool is_selected = (i == selected_index);
|
|
gui->draw_button(window, 20, y, games[i].name, is_selected, true);
|
|
|
|
// Draw description below button
|
|
renderer->set_font(&font_5x5_obj); // Restore small font for description
|
|
renderer->set_text_color(true); // Normal text color
|
|
renderer->draw_string_scaled(50, y + 36, games[i].description, 1);
|
|
}
|
|
|
|
// Draw navigation buttons at bottom (only if multiple pages)
|
|
if (total_pages > 1) {
|
|
int button_y = height - 65;
|
|
|
|
// Previous page button
|
|
bool has_prev = (current_page > 0);
|
|
gui->draw_button(window, 30, button_y, "< PREV", false, true);
|
|
|
|
// Next page button
|
|
bool has_next = (current_page + 1 < total_pages);
|
|
gui->draw_button(window, 200, button_y, "NEXT >", false, true);
|
|
|
|
// Draw instructions
|
|
renderer->set_font(&font_5x5_obj);
|
|
renderer->draw_string_scaled(30, height - 25, "Touch buttons or KEY0/KEY1", 1);
|
|
} else {
|
|
// Single page - just show select instruction
|
|
renderer->set_font(&font_5x5_obj);
|
|
renderer->draw_string_scaled(30, height - 35, "KEY0: Navigate | KEY1: Select | Touch to play", 1);
|
|
}
|
|
}
|
|
|
|
|
|
bool GameLauncher::update(const InputEvent& event) {
|
|
bool needs_refresh = false;
|
|
int total_pages = get_total_pages();
|
|
|
|
switch (event.type) {
|
|
case INPUT_TOUCH_DOWN: {
|
|
printf("Touch at (%d,%d) in launcher\n", event.x, event.y);
|
|
|
|
// Check if touch is on navigation buttons (if multiple pages)
|
|
if (total_pages > 1) {
|
|
// Previous button: x [30-180], y [235-275]
|
|
if (event.x >= PREV_BUTTON_X && event.x < PREV_BUTTON_X + BUTTON_WIDTH &&
|
|
event.y >= NAV_BUTTON_Y && event.y < NAV_BUTTON_Y + BUTTON_HEIGHT) {
|
|
if (current_page > 0) {
|
|
current_page--;
|
|
selected_index = get_page_start_index();
|
|
needs_refresh = true;
|
|
printf("Navigated to previous page: %d\n", current_page);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Next button: x [200-350], y [235-275]
|
|
if (event.x >= NEXT_BUTTON_X && event.x < NEXT_BUTTON_X + BUTTON_WIDTH &&
|
|
event.y >= NAV_BUTTON_Y && event.y < NAV_BUTTON_Y + BUTTON_HEIGHT) {
|
|
if (current_page + 1 < total_pages) {
|
|
current_page++;
|
|
selected_index = get_page_start_index();
|
|
needs_refresh = true;
|
|
printf("Navigated to next page: %d\n", current_page);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check if touch is on a game entry for current page
|
|
int page_start = get_page_start_index();
|
|
int page_end = get_page_end_index();
|
|
|
|
for (int i = page_start; i < page_end && i < (int)games.size(); i++) {
|
|
int item_index = i - page_start;
|
|
int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT);
|
|
|
|
// Touch area is the entire menu item
|
|
if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) {
|
|
// Game selected - create instance
|
|
printf("Selected game: %s\n", games[i].name);
|
|
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
|
|
if (selected_game) {
|
|
selected_game->init();
|
|
return true; // Signal game selected
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case INPUT_BUTTON_0: {
|
|
// Navigate within current page or switch pages
|
|
int page_start = get_page_start_index();
|
|
int page_end = get_page_end_index();
|
|
|
|
// If multiple games on current page, navigate within page first
|
|
if (page_end - page_start > 1) {
|
|
// Move within current page
|
|
int old_index = selected_index;
|
|
selected_index++;
|
|
if (selected_index >= page_end) {
|
|
// Moving to next page
|
|
if (current_page + 1 < total_pages) {
|
|
current_page++;
|
|
selected_index = get_page_start_index();
|
|
} else {
|
|
// Wrap to first page
|
|
current_page = 0;
|
|
selected_index = 0;
|
|
}
|
|
} else if (selected_index < page_start) {
|
|
selected_index = page_start;
|
|
}
|
|
} else {
|
|
// Single game on page, move to next page
|
|
if (current_page + 1 < total_pages) {
|
|
current_page++;
|
|
selected_index = get_page_start_index();
|
|
} else {
|
|
// Wrap to first page
|
|
current_page = 0;
|
|
selected_index = 0;
|
|
}
|
|
}
|
|
needs_refresh = true;
|
|
printf("Menu selection: %d (%s), Page: %d\n", selected_index, games[selected_index].name, current_page);
|
|
break;
|
|
}
|
|
|
|
case INPUT_BUTTON_1: {
|
|
// Select current game
|
|
if (selected_index >= 0 && selected_index < (int)games.size()) {
|
|
printf("Selected game: %s\n", games[selected_index].name);
|
|
selected_game = games[selected_index].factory(width, height, renderer, gui, input_manager);
|
|
if (selected_game) {
|
|
selected_game->init();
|
|
return true; // Signal game selected
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return needs_refresh;
|
|
}
|
|
|
|
Game* GameLauncher::get_selected_game() {
|
|
return selected_game;
|
|
}
|
|
|
|
void GameLauncher::reset() {
|
|
// Clean up current game if any
|
|
if (selected_game) {
|
|
delete selected_game;
|
|
selected_game = nullptr;
|
|
}
|
|
selected_index = 0;
|
|
current_page = 0;
|
|
printf("Launcher reset - returning to menu\n");
|
|
}
|
|
|
|
void GameLauncher::clear_games() {
|
|
// Clean up currently selected game first
|
|
if (selected_game) {
|
|
delete selected_game;
|
|
selected_game = nullptr;
|
|
}
|
|
|
|
// Clear the games vector
|
|
games.clear();
|
|
selected_index = 0;
|
|
current_page = 0;
|
|
printf("Launcher: Cleared all registered games\n");
|
|
}
|
|
|
|
bool GameLauncher::select_game_by_name(const char* name) {
|
|
// Clean up old game first if one exists
|
|
if (selected_game) {
|
|
printf("Cleaning up previous game...\n");
|
|
delete selected_game;
|
|
selected_game = nullptr;
|
|
}
|
|
|
|
// First pass: search for exact match (prefer exact matches)
|
|
for (size_t i = 0; i < games.size(); i++) {
|
|
if (strcmp(games[i].name, name) == 0) {
|
|
printf("Found exact match: %s\n", games[i].name);
|
|
|
|
// Create and initialize the game
|
|
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
|
|
if (selected_game) {
|
|
printf("Game instance created, initializing...\n");
|
|
selected_game->init();
|
|
selected_index = i;
|
|
current_page = i / GAMES_PER_PAGE;
|
|
return true;
|
|
} else {
|
|
printf("Failed to create game instance\n");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second pass: search in reverse order for partial match (newest files first)
|
|
for (int i = games.size() - 1; i >= 0; i--) {
|
|
if (strstr(games[i].name, name) != nullptr) {
|
|
printf("Found partial match: %s (searching for: %s)\n", games[i].name, name);
|
|
|
|
// Create and initialize the game
|
|
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
|
|
if (selected_game) {
|
|
printf("Game instance created, initializing...\n");
|
|
selected_game->init();
|
|
selected_index = i;
|
|
current_page = i / GAMES_PER_PAGE;
|
|
return true;
|
|
} else {
|
|
printf("Failed to create game instance\n");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
printf("Game not found: %s\n", name);
|
|
return false;
|
|
}
|
|
|
|
int GameLauncher::get_total_pages() const {
|
|
if (games.empty()) return 1;
|
|
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;
|
|
}
|
|
|
|
int GameLauncher::get_page_start_index() const {
|
|
return current_page * GAMES_PER_PAGE;
|
|
}
|
|
|
|
int GameLauncher::get_page_end_index() const {
|
|
return (current_page + 1) * GAMES_PER_PAGE;
|
|
}
|