Files
basic1/lib/game_launcher.cpp
Adolfo Reyna 76e3d2435e Make game name search case-insensitive
Serial uploader searches for games by filename (e.g., "tetris") but
games are registered by their metadata NAME field (e.g., "Tetris").
This caused launch failures when case didn't match.

Changed select_game_by_name() to use case-insensitive matching:
- strcasecmp() for exact match
- tolower() both strings before strstr() for partial match

Now uploading "tetris.lua" will successfully match and launch "Tetris".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 23:27:48 -05:00

313 lines
12 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>
#include <cctype>
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 (case-insensitive)
for (size_t i = 0; i < games.size(); i++) {
if (strcasecmp(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 (case-insensitive, newest files first)
for (int i = games.size() - 1; i >= 0; i--) {
// Convert both strings to lowercase for comparison
char game_name_lower[256];
char search_name_lower[256];
strncpy(game_name_lower, games[i].name, sizeof(game_name_lower) - 1);
game_name_lower[sizeof(game_name_lower) - 1] = '\0';
strncpy(search_name_lower, name, sizeof(search_name_lower) - 1);
search_name_lower[sizeof(search_name_lower) - 1] = '\0';
// Convert to lowercase
for (size_t j = 0; game_name_lower[j]; j++) {
game_name_lower[j] = tolower(game_name_lower[j]);
}
for (size_t j = 0; search_name_lower[j]; j++) {
search_name_lower[j] = tolower(search_name_lower[j]);
}
if (strstr(game_name_lower, search_name_lower) != 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;
}