diff --git a/basic1.cpp b/basic1.cpp index b2caf45..51324fc 100644 --- a/basic1.cpp +++ b/basic1.cpp @@ -58,6 +58,7 @@ extern "C" { #include "monopoly_game.h" #include "lua_game_loader.h" #include "serial_uploader.h" +#include "scene_stack.h" // Binary info for RP2350 - ensures proper boot image structure @@ -161,15 +162,17 @@ struct GameConfig { // ============================================================================ // Display dimming settings -#define DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim -#define SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep -#define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds +#define DEFAULT_DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim +#define DEFAULT_SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep +#define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds // Display dimming state static uint32_t last_interaction_time = 0; // Last time user interacted static bool is_idle_2min_triggered = false; // Flag for 2min trigger static bool is_idle_10min_triggered = false; // Flag for 10min trigger static volatile bool dim_check_flag = false; // Flag set by timer to check dimming +static uint32_t dim_timeout_ms = DEFAULT_DIM_TIMEOUT_MS; +static uint32_t sleep_timeout_ms = DEFAULT_SLEEP_TIMEOUT_MS; /** * @brief Update last interaction time and notify display driver @@ -222,14 +225,14 @@ static inline void check_and_apply_dimming(LowLevelDisplay* display) { uint32_t current_time = to_ms_since_boot(get_absolute_time()); uint32_t elapsed = current_time - last_interaction_time; - // Check for 10 minute timeout (Sleep) - if (!is_idle_10min_triggered && elapsed >= SLEEP_TIMEOUT_MS) { + // Check sleep timeout (if enabled) + if (sleep_timeout_ms > 0 && !is_idle_10min_triggered && elapsed >= sleep_timeout_ms) { display->on_idle_10min(); is_idle_10min_triggered = true; is_idle_2min_triggered = true; // Implicitly triggered } - // Check for 2 minute timeout (Dim) - else if (!is_idle_2min_triggered && elapsed >= DIM_TIMEOUT_MS) { + // Check dim timeout (if enabled) + else if (dim_timeout_ms > 0 && !is_idle_2min_triggered && elapsed >= dim_timeout_ms) { display->on_idle_2min(); is_idle_2min_triggered = true; } @@ -267,6 +270,92 @@ static inline bool has_pending_wake_source() { return false; } +struct SleepOption { + const char* label; + uint32_t sleep_ms; +}; + +static constexpr SleepOption kSleepOptions[] = { + {"Never", 0}, + {"30 sec", 30 * 1000}, + {"1 min", 60 * 1000}, + {"2 min", 2 * 60 * 1000}, + {"5 min", 5 * 60 * 1000}, + {"10 min", 10 * 60 * 1000} +}; +static constexpr int kSleepOptionCount = sizeof(kSleepOptions) / sizeof(kSleepOptions[0]); + +static inline void apply_sleep_option(int option_index) { + if (option_index < 0 || option_index >= kSleepOptionCount) { + return; + } + + sleep_timeout_ms = kSleepOptions[option_index].sleep_ms; + if (sleep_timeout_ms == 0) { + dim_timeout_ms = 0; + return; + } + + uint32_t candidate_dim = sleep_timeout_ms / 5; + if (candidate_dim < 30 * 1000) { + candidate_dim = 30 * 1000; + } + if (candidate_dim > 2 * 60 * 1000) { + candidate_dim = 2 * 60 * 1000; + } + if (candidate_dim >= sleep_timeout_ms && sleep_timeout_ms > 5000) { + candidate_dim = sleep_timeout_ms - 5000; + } + dim_timeout_ms = candidate_dim; +} + +static inline bool in_rect(int16_t x, int16_t y, int rx, int ry, int rw, int rh) { + return x >= rx && x < (rx + rw) && y >= ry && y < (ry + rh); +} + +static inline bool is_top_right_start(int16_t x, int16_t y, int width, int height) { + return x >= (width * 3 / 4) && y <= (height / 4); +} + +static inline bool is_open_menu_swipe(int16_t sx, int16_t sy, int16_t ex, int16_t ey, int width, int height) { + if (!is_top_right_start(sx, sy, width, height)) { + return false; + } + + const bool released_near_center = in_rect(ex, ey, width / 4, height / 4, width / 2, height / 2); + const int dx = ex - sx; + const int dy = ey - sy; + const bool moved_left_and_down = (dx <= -(width / 5)) && (dy >= (height / 10)); + + printf("Swipe from (%d, %d) to (%d, %d) - released_near_center=%d, moved_left_and_down=%d\n", + sx, sy, ex, ey, released_near_center, moved_left_and_down); + + return released_near_center && moved_left_and_down; +} + +static void draw_in_game_menu(LowLevelRenderer* renderer, LowLevelGUI* gui, int width, int height, const char* sleep_label) { + const int menu_w = width - 40; + const int menu_h = 190; + const int menu_x = 20; + const int menu_y = (height - menu_h) / 2; + const int row_h = 34; + const int row_x = menu_x + 16; + const int row_w = menu_w - 32; + const int row_start_y = menu_y + 48; + + renderer->draw_filled_rectangle(0, 0, width, height, false, 1); + renderer->set_font(&font_5x5_obj); + renderer->set_text_color(true); + + LowLevelWindow* win = gui->draw_new_window(menu_x, menu_y, menu_w, menu_h, "Game Menu"); + gui->draw_button(win, row_x, row_start_y + (0 * row_h), "Restart Game", false, true); + gui->draw_button(win, row_x, row_start_y + (1 * row_h), "Back to Game Selection", false, true); + + char sleep_label_text[64]; + snprintf(sleep_label_text, sizeof(sleep_label_text), "Auto Sleep: %s", sleep_label); + gui->draw_button(win, row_x, row_start_y + (2 * row_h), sleep_label_text, false, true); +} + /** * @brief Returns true when the currently selected game needs frame ticks. */ @@ -574,12 +663,24 @@ int main() // Set up repeating alarm to periodically check dimming status // This wakes the CPU from __wfi() every DIM_CHECK_INTERVAL_MS add_alarm_in_ms(DIM_CHECK_INTERVAL_MS, dim_check_alarm_callback, nullptr, true); + + int sleep_option_index = 5; // Default: 10 min + apply_sleep_option(sleep_option_index); if (display->get_type() == DISPLAY_TYPE_ST7796) { - printf("Power saving: Dim at %d min, Sleep at %d min\n", - DIM_TIMEOUT_MS / 60000, SLEEP_TIMEOUT_MS / 60000); + if (sleep_timeout_ms == 0) { + printf("Power saving: auto sleep disabled\n"); + } else { + printf("Power saving: Dim at %lus, Sleep at %lus\n", + (unsigned long)(dim_timeout_ms / 1000), + (unsigned long)(sleep_timeout_ms / 1000)); + } } else { - printf("Power saving: Sleep at %d min\n", SLEEP_TIMEOUT_MS / 60000); + if (sleep_timeout_ms == 0) { + printf("Power saving: auto sleep disabled\n"); + } else { + printf("Power saving: Sleep at %lus\n", (unsigned long)(sleep_timeout_ms / 1000)); + } } printf("Dimming check timer set to %d seconds\n", DIM_CHECK_INTERVAL_MS / 1000); @@ -596,6 +697,12 @@ int main() bool needs_refresh = false; // Track if screen needs redraw bool dirty_rect_opt_state = (display->get_type() == DISPLAY_TYPE_ST7796); + SceneStack scene_stack; + bool swipe_candidate_active = false; + int16_t swipe_start_x = 0; + int16_t swipe_start_y = 0; + int16_t swipe_last_x = 0; + int16_t swipe_last_y = 0; while (1) { // 0. Process serial uploads (for rapid game iteration) @@ -609,18 +716,21 @@ int main() // A new game was uploaded and launched - trigger redraw needs_refresh = true; current_game = launcher.get_selected_game(); + scene_stack.clear_to_launcher(); + scene_stack.push(SceneId::GAME); // Note: game is already initialized by select_game_by_name() } } // Determine if we should sleep or stay awake for updates bool stay_awake = false; + if (needs_refresh) stay_awake = true; if (pending_refresh) stay_awake = true; if (serial_uploader.wants_to_launch_game()) stay_awake = true; // Don't sleep while waiting to launch if (touch_event_down) stay_awake = true; // Keep sampling while finger is down if (last_touch_time != 0) stay_awake = true; // Keep sampling during active touch session - if (game_wants_frame_updates(launcher)) stay_awake = true; + if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) stay_awake = true; if (!stay_awake) { // Sleep until interrupt wakes us up (very power efficient!) @@ -660,50 +770,11 @@ int main() input.button_id, input.pressure); } - if (launcher.is_game_selected()) { - // In game mode - process game input - current_game = launcher.get_selected_game(); - needs_refresh = current_game->update(input); - - // Check if game wants to exit - if (current_game->wants_to_exit()) { - printf("Game requested exit - returning to launcher\n"); - launcher.reset(); - needs_refresh = true; - // Force full clear for clean transition - // display->clear(false); // Unsafe while Core 1 might be busy - if (display->get_type() == DISPLAY_TYPE_EPAPER) { - LowLevelDisplayEPaper* epaper = static_cast(display); - epaper->full_refresh(); - } - } - - // Check if player wants to exit (hold for 2+ seconds or special gesture) - // For now, we'll add a simple long-press detection - if (input.type == INPUT_TOUCH_DOWN) { - // Record start time on first touch - if (game_start_time == 0) { - game_start_time = to_ms_since_boot(get_absolute_time()); - } - } else if (input.type == INPUT_TOUCH_UP) { - uint32_t now = to_ms_since_boot(get_absolute_time()); - if (game_start_time > 0 && (now - game_start_time) > 10000) { - // Long press detected - return to menu - printf("Long press detected - returning to launcher\n"); - launcher.reset(); - needs_refresh = true; - // Force full clear for clean transition - // display->clear(false); // Unsafe while Core 1 might be busy - if (display->get_type() == DISPLAY_TYPE_EPAPER) { - LowLevelDisplayEPaper* epaper = static_cast(display); - epaper->full_refresh(); - } - } - game_start_time = 0; - } - } else { - // In launcher mode - process menu input - + SceneId scene = scene_stack.current(); + + if (scene == SceneId::LAUNCHER) { + swipe_candidate_active = false; + // Wait for any active display refresh to finish before potentially loading a game (SD Card I/O) // This prevents SPI bus conflicts between Core 0 (SD Card) and Core 1 (Display) while (is_refresh_in_progress()) { @@ -714,18 +785,133 @@ int main() if (game_selected) { printf("Game launched successfully\n"); game_start_time = 0; - // Force full clear for clean transition to game - // display->clear(false); // Unsafe while Core 1 might be busy - // if (display->get_type() == DISPLAY_TYPE_EPAPER) { - // LowLevelDisplayEPaper* epaper = static_cast(display); - // epaper->full_refresh(); - // } + scene_stack.push(SceneId::GAME); } needs_refresh = true; + } else if (scene == SceneId::GAME) { + current_game = launcher.get_selected_game(); + if (!current_game) { + scene_stack.clear_to_launcher(); + needs_refresh = true; + } else { + bool consumed_by_scene = false; + + // Swipe gesture candidate for opening menu (evaluated on touch release). + if (input.type == INPUT_TOUCH_DOWN && + is_top_right_start(input.x, input.y, V_WIDTH, V_HEIGHT)) { + swipe_candidate_active = true; + swipe_start_x = input.x; + swipe_start_y = input.y; + swipe_last_x = input.x; + swipe_last_y = input.y; + consumed_by_scene = true; + } + + if (swipe_candidate_active && (input.type == INPUT_TOUCH_MOVE || input.type == INPUT_TOUCH_UP)) { + consumed_by_scene = true; + if (input.type == INPUT_TOUCH_MOVE) { + swipe_last_x = input.x; + swipe_last_y = input.y; + } + if (input.type == INPUT_TOUCH_UP) { + if (is_open_menu_swipe(swipe_start_x, swipe_start_y, swipe_last_x, swipe_last_y, V_WIDTH, V_HEIGHT)) { + scene_stack.push(SceneId::IN_GAME_MENU); + needs_refresh = true; + printf("Opened in-game menu\n"); + } + swipe_candidate_active = false; + } + } + + if (!consumed_by_scene) { + needs_refresh = current_game->update(input) || needs_refresh; + + if (current_game->wants_to_exit()) { + printf("Game requested exit - returning to launcher\n"); + swipe_candidate_active = false; + launcher.reset(); + scene_stack.clear_to_launcher(); + needs_refresh = true; + if (display->get_type() == DISPLAY_TYPE_EPAPER) { + LowLevelDisplayEPaper* epaper = static_cast(display); + epaper->full_refresh(); + } + } + + if (input.type == INPUT_TOUCH_DOWN) { + if (game_start_time == 0) { + game_start_time = to_ms_since_boot(get_absolute_time()); + } + } else if (input.type == INPUT_TOUCH_UP) { + uint32_t now = to_ms_since_boot(get_absolute_time()); + if (game_start_time > 0 && (now - game_start_time) > 10000) { + printf("Long press detected - returning to launcher\n"); + swipe_candidate_active = false; + launcher.reset(); + scene_stack.clear_to_launcher(); + needs_refresh = true; + if (display->get_type() == DISPLAY_TYPE_EPAPER) { + LowLevelDisplayEPaper* epaper = static_cast(display); + epaper->full_refresh(); + } + } + game_start_time = 0; + } + } + } + } else if (scene == SceneId::IN_GAME_MENU) { + current_game = launcher.get_selected_game(); + if (!current_game) { + launcher.return_to_menu(); + scene_stack.clear_to_launcher(); + needs_refresh = true; + } else { + const int menu_w = V_WIDTH - 40; + const int menu_h = 190; + const int menu_x = 20; + const int menu_y = (V_HEIGHT - menu_h) / 2; + const int row_h = 34; + const int row_x = menu_x + 16; + const int row_w = menu_w - 32; + const int row_start_y = menu_y + 48; + + // Menu tap handling on TOUCH_DOWN because TOUCH_UP coordinates can be unreliable (often 0,0). + if (input.type == INPUT_TOUCH_DOWN) { + if (in_rect(input.x, input.y, row_x, row_start_y + (0 * row_h), row_w, row_h)) { + if (launcher.restart_selected_game()) { + scene_stack.pop(); // Back to GAME + needs_refresh = true; + printf("Restarted current game from global menu\n"); + } + } else if (in_rect(input.x, input.y, row_x, row_start_y + (1 * row_h), row_w, row_h)) { + swipe_candidate_active = false; + launcher.return_to_menu(); + scene_stack.clear_to_launcher(); + needs_refresh = true; + printf("Returned to game launcher from global menu\n"); + if (display->get_type() == DISPLAY_TYPE_EPAPER) { + LowLevelDisplayEPaper* epaper = static_cast(display); + epaper->full_refresh(); + } + } else if (in_rect(input.x, input.y, row_x, row_start_y + (2 * row_h), row_w, row_h)) { + sleep_option_index = (sleep_option_index + 1) % kSleepOptionCount; + apply_sleep_option(sleep_option_index); + needs_refresh = true; + if (sleep_timeout_ms == 0) { + printf("Auto sleep disabled\n"); + } else { + printf("Auto sleep set to %s\n", kSleepOptions[sleep_option_index].label); + } + } else if (!in_rect(input.x, input.y, menu_x, menu_y, menu_w, menu_h)) { + scene_stack.pop(); // Close menu, resume game scene + needs_refresh = true; + } + } + } } } - if (game_wants_frame_updates(launcher)) { + if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) { // No input, but check if game wants continuous updates current_game = launcher.get_selected_game(); if (current_game) { @@ -750,7 +936,7 @@ int main() // Update dirty rectangle optimization based on continuous updates if (display->get_type() == DISPLAY_TYPE_ST7796) { bool wants_opt = false; - if (launcher.is_game_selected()) { + if (scene_stack.is(SceneId::GAME)) { Game* g = launcher.get_selected_game(); if (g && g->wants_frame_updates()) { wants_opt = true; @@ -766,18 +952,48 @@ int main() // Clear buffer and redraw entire UI with updated state memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); + // Reset renderer state so one scene/game cannot leak clip/text/font settings + // into subsequent scenes (e.g. Lua games using clip rects). + renderer.reset_clip_rect(); + renderer.set_text_color(true); + renderer.set_font(&font_homespun_obj); - if (launcher.is_game_selected()) { - current_game = launcher.get_selected_game(); - current_game->draw(); - } else { + if (scene_stack.is(SceneId::LAUNCHER)) { launcher.draw(); + } else if (scene_stack.is(SceneId::GAME)) { + current_game = launcher.get_selected_game(); + if (current_game) { + current_game->draw(); + } else { + launcher.draw(); + scene_stack.clear_to_launcher(); + } + } else if (scene_stack.is(SceneId::IN_GAME_MENU)) { + current_game = launcher.get_selected_game(); + if (current_game) { + current_game->draw(); + draw_in_game_menu(&renderer, &gui, V_WIDTH, V_HEIGHT, kSleepOptions[sleep_option_index].label); + } else { + launcher.draw(); + scene_stack.clear_to_launcher(); + } } - // Request async refresh (non-blocking - handled by Core 1) - bool refresh_started = refresh_screen_async(bit_buffer, display); + // TFT fix: + // Run refresh synchronously on Core 0 for ST7796/ST7789. + // We observed intermittent frozen screens with async Core 1 refresh + // after SD/Lua activity even though scene/game logic continued. + // E-paper keeps async refresh because its refresh latency is high. + bool refresh_started = false; + if (display->get_type() == DISPLAY_TYPE_ST7796 || display->get_type() == DISPLAY_TYPE_ST7789) { + refresh_screen(bit_buffer, display); + refresh_started = true; + } else { + refresh_started = refresh_screen_async(bit_buffer, display); + } if (refresh_started) { + needs_refresh = false; pending_refresh = false; // Refresh queued successfully last_frame_time = current_time; // Update frame time } else { diff --git a/lib/game_launcher.cpp b/lib/game_launcher.cpp index b203e0a..6533216 100644 --- a/lib/game_launcher.cpp +++ b/lib/game_launcher.cpp @@ -129,6 +129,7 @@ bool GameLauncher::update(const InputEvent& event) { 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_index = i; selected_game = games[i].factory(width, height, renderer, gui, input_manager); if (selected_game) { selected_game->init(); @@ -298,6 +299,30 @@ bool GameLauncher::select_game_by_name(const char* name) { return false; } +bool GameLauncher::restart_selected_game() { + if (selected_index < 0 || selected_index >= (int)games.size()) { + return false; + } + + if (selected_game) { + delete selected_game; + selected_game = nullptr; + } + + selected_game = games[selected_index].factory(width, height, renderer, gui, input_manager); + if (!selected_game) { + return false; + } + + selected_game->init(); + current_page = selected_index / GAMES_PER_PAGE; + return true; +} + +void GameLauncher::return_to_menu() { + reset(); +} + int GameLauncher::get_total_pages() const { if (games.empty()) return 1; return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE; diff --git a/lib/game_launcher.h b/lib/game_launcher.h index c625a03..ee91a53 100644 --- a/lib/game_launcher.h +++ b/lib/game_launcher.h @@ -97,6 +97,17 @@ public: */ bool select_game_by_name(const char* name); + /** + * @brief Restart the currently selected game. + * @return true if restart succeeded, false otherwise + */ + bool restart_selected_game(); + + /** + * @brief Exit current game and return to launcher menu. + */ + void return_to_menu(); + private: uint16_t width; uint16_t height; diff --git a/lib/scene_stack.h b/lib/scene_stack.h new file mode 100644 index 0000000..b440882 --- /dev/null +++ b/lib/scene_stack.h @@ -0,0 +1,45 @@ +#ifndef SCENE_STACK_H +#define SCENE_STACK_H + +#include + +enum class SceneId { + LAUNCHER = 0, + GAME, + IN_GAME_MENU +}; + +class SceneStack { +public: + SceneStack() { + stack.push_back(SceneId::LAUNCHER); + } + + SceneId current() const { + return stack.empty() ? SceneId::LAUNCHER : stack.back(); + } + + bool is(SceneId scene) const { + return current() == scene; + } + + void push(SceneId scene) { + stack.push_back(scene); + } + + void pop() { + if (stack.size() > 1) { + stack.pop_back(); + } + } + + void clear_to_launcher() { + stack.clear(); + stack.push_back(SceneId::LAUNCHER); + } + +private: + std::vector stack; +}; + +#endif // SCENE_STACK_H