Newer
Older
ESP32-RetroPlay / main / tasks / display_task.c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_check.h"

#include "driver/i2c_master.h"

#include "ssd1306.h"
#include "font8x8_basic.h"
#include "display_task.h" // Now includes game_framework.h indirectly

#define TAG "DISPLAY_TASK"

#define I2C_SDA_GPIO 19
#define I2C_SCL_GPIO 18
#define OLED_I2C_ADDRESS 0x3C
#define I2C_PORT I2C_NUM_1
#define I2C_SPEED_HZ 400000

// OLED_WIDTH, OLED_HEIGHT, SCORE_DISPLAY_HEIGHT etc. are now from game_framework.h

#define N_DISPLAY_LINES (OLED_HEIGHT / 8)
#define MAX_LINE_LENGTH (OLED_WIDTH / 8) + 1

SSD1306_t dev;
SemaphoreHandle_t xDisplayMutex = NULL;
static char display_lines[N_DISPLAY_LINES][MAX_LINE_LENGTH];
static bool dirty_lines[N_DISPLAY_LINES] = { true }; // Track which lines changed

// Define the display event queue (its definition)
QueueHandle_t display_event_queue = NULL;
// Store the global button input queue for use in game over screen
static QueueHandle_t s_button_input_queue = NULL;


typedef enum {
    DISPLAY_STATE_MENU, // State for displaying the game selection menu
    DISPLAY_STATE_GAME_RUNNING,
    DISPLAY_STATE_GAME_OVER,
    DISPLAY_STATE_OTHER_INFO
} display_state_t;

// Initialize to MENU state so it starts by displaying the menu
static display_state_t s_current_display_state = DISPLAY_STATE_MENU;
static int s_game_over_score = 0;
static int s_displayed_score = -1; // Tracks the score currently drawn on the display

// Flag to indicate a state change, triggering a full screen clear and redraw
static bool s_state_changed = true;

// Pointer to the currently active game instance
static const game_t *s_current_game = NULL;

static esp_err_t display_i2c_ng_init(void) {
    i2c_master_bus_config_t bus_config = {
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .i2c_port = I2C_PORT,
        .scl_io_num = I2C_SCL_GPIO,
        .sda_io_num = I2C_SDA_GPIO,
        .flags = {
            .enable_internal_pullup = true,
        }
    };

    ESP_RETURN_ON_ERROR(i2c_new_master_bus(&bus_config, &dev._i2c_bus_handle), TAG, "Failed to create I2C bus");

    i2c_device_config_t dev_cfg = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,
        .device_address = OLED_I2C_ADDRESS,
        .scl_speed_hz = I2C_SPEED_HZ,
    };

    ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(dev._i2c_bus_handle, &dev_cfg, &dev._i2c_dev_handle), TAG, "Failed to add I2C OLED device");

    return ESP_OK;
}

static void display_manager_task(void *pvParameters) {
    ESP_LOGI(TAG, "Display Manager Task started.");

    if (display_i2c_ng_init() != ESP_OK) {
        ESP_LOGE(TAG, "I2C init failed, halting task.");
        vTaskDelete(NULL);
    }

    ssd1306_init(&dev, OLED_WIDTH, OLED_HEIGHT);
    ssd1306_clear_screen(&dev, false);
    ssd1306_show_buffer(&dev);

    // Static variables to track previous button states for edge detection in game over screen
    static bool left_button_pressed_prev_go = false;
    static bool right_button_pressed_prev_go = false;

    while (1) {
        if (xSemaphoreTake(xDisplayMutex, portMAX_DELAY) == pdTRUE) {
            // Check for state change and clear screen if necessary
            if (s_state_changed) {
                ssd1306_clear_screen(&dev, false); // Clear the entire display buffer
                // Mark all lines dirty to ensure they are redrawn with new content
                for (int i = 0; i < N_DISPLAY_LINES; i++) {
                    dirty_lines[i] = true;
                }
                s_state_changed = false; // Reset flag after clearing
            }

            switch (s_current_display_state) {
            case DISPLAY_STATE_MENU: {
                // In menu state, simply display the text set by display_update_text
                for (int i = 0; i < N_DISPLAY_LINES; i++) {
                    if (dirty_lines[i] && strlen(display_lines[i]) > 0) {
                        ssd1306_display_text(&dev, i, display_lines[i], strlen(display_lines[i]), false);
                        dirty_lines[i] = false;
                    }
                }
                break;
            }
            case DISPLAY_STATE_GAME_RUNNING: {
                if (s_current_game && s_current_game->is_active && s_current_game->is_active()) {
                    if (s_current_game->handle_input) {
                        s_current_game->handle_input();
                    }
                    bool game_over = false;
                    if (s_current_game->update_game_state) {
                        game_over = s_current_game->update_game_state();
                    }
                    if (s_current_game->draw_game) {
                        s_current_game->draw_game(&dev); // This now only draws game elements
                    }

                    // Update score display only if it has changed
                    int current_game_score = 0;
                    if (s_current_game && s_current_game->get_score) {
                        current_game_score = s_current_game->get_score();
                    }

                    if (current_game_score != s_displayed_score) {
                        char score_str[MAX_LINE_LENGTH];
                        snprintf(score_str, sizeof(score_str), "Score: %d", current_game_score);
                        // Clear the entire line 0 before drawing the new score to avoid artifacts
                        ssd1306_display_text(&dev, 0, "                ", strlen("                "), false); // Clear line 0
                        ssd1306_display_text(&dev, 0, score_str, strlen(score_str), false);
                        s_displayed_score = current_game_score;
                    }

                    if (game_over) {
                        s_game_over_score = current_game_score; // Use the final score
                        s_current_display_state = DISPLAY_STATE_GAME_OVER;
                        if (s_current_game && s_current_game->set_active) {
                            s_current_game->set_active(false);
                        }
                        s_state_changed = true; // Mark state changed for next iteration (game over screen)
                        s_displayed_score = -1; // Reset score display tracker
                        // Reset button states for game over screen input
                        left_button_pressed_prev_go = false;
                        right_button_pressed_prev_go = false;
                    }
                } else {
                    // This block handles cases where the game might become inactive externally
                    // or if is_active() becomes false for other reasons.
                    if (s_current_game && s_current_game->get_score) {
                        s_game_over_score = s_current_game->get_score(); // Get final score
                    } else {
                        s_game_over_score = 0; // Default if no game or get_score not implemented
                    }
                    s_current_display_state = DISPLAY_STATE_GAME_OVER;
                    s_state_changed = true; // Mark state changed for next iteration (game over screen)
                    s_displayed_score = -1; // Reset score display tracker
                    // Reset button states for game over screen input
                    left_button_pressed_prev_go = false;
                    right_button_pressed_prev_go = false;
                }
                break;
            }
            case DISPLAY_STATE_GAME_OVER: {
                char score_display[MAX_LINE_LENGTH];
                snprintf(score_display, sizeof(score_display), "Score: %d", s_game_over_score);
                const char *game_over_messages[] = {
                    "    GAME OVER!    ",
                    "                  ",
                    score_display,
                    "                  ",
                    "  Left: RESTART   ", // Instructions for restart
                    " Right: MENU      ", // Instructions for menu
                    NULL, NULL
                };

                for (int i = 0; i < N_DISPLAY_LINES; i++) {
                    if (game_over_messages[i] && strlen(game_over_messages[i]) > 0) { // Only draw if message exists
                        ssd1306_display_text(&dev, i, game_over_messages[i], strlen(game_over_messages[i]), false);
                        dirty_lines[i] = false; // Mark as drawn
                    } else if (dirty_lines[i]) { // Clear line if it was dirty but no message
                        ssd1306_display_text(&dev, i, "                ", strlen("                "), false);
                        dirty_lines[i] = false;
                    }
                }

                // Handle input on game over screen using the global button_input_queue
                int input_signal = 0;
                if (s_button_input_queue != NULL && xQueueReceive(s_button_input_queue, &input_signal, 0) == pdTRUE) {
                    
                    bool left_button_now_pressed = (input_signal == -1);
                    bool right_button_now_pressed = (input_signal == 1);

                    bool left_press_event = left_button_now_pressed && !left_button_pressed_prev_go;
                    bool right_press_event = right_button_now_pressed && !right_button_pressed_prev_go;

                    if (left_press_event) { // Left button pressed: Restart current game
                        ESP_LOGI(TAG, "Restarting current game...");
                        s_current_display_state = DISPLAY_STATE_GAME_RUNNING;
                        if (s_current_game && s_current_game->init_game) {
                            s_current_game->init_game();
                        }
                        if (s_current_game && s_current_game->set_active) {
                            s_current_game->set_active(true);
                        }
                        s_state_changed = true; // Mark state changed for next iteration (game running)
                        s_displayed_score = -1;
                    } else if (right_press_event) { // Right button pressed: Return to main menu
                        ESP_LOGI(TAG, "Returning to main menu...");
                        s_current_display_state = DISPLAY_STATE_MENU; // Transition to menu state
                        if (s_current_game && s_current_game->set_active) {
                            s_current_game->set_active(false); // Deactivate current game
                        }
                        s_state_changed = true; // Mark state changed for next iteration (menu)
                        // Signal main.c to re-run the menu logic
                        display_event_t event = DISPLAY_EVENT_RETURN_TO_MENU;
                        if (display_event_queue != NULL) {
                            xQueueSend(display_event_queue, &event, 0);
                        } else {
                            ESP_LOGE(TAG, "Display event queue is NULL! Cannot signal main.");
                        }
                    }
                    // Update previous button states for next iteration in game over screen
                    left_button_pressed_prev_go = left_button_now_pressed;
                    right_button_pressed_prev_go = right_button_now_pressed;
                }
                break;
            }
            case DISPLAY_STATE_OTHER_INFO: {
                for (int i = 0; i < N_DISPLAY_LINES; i++) {
                    if (dirty_lines[i] && strlen(display_lines[i]) > 0) {
                        ssd1306_display_text(&dev, i, display_lines[i], strlen(display_lines[i]), false);
                        dirty_lines[i] = false;
                    }
                }
                break;
            }
            }

            ssd1306_show_buffer(&dev); // Push the entire buffer to the display
            xSemaphoreGive(xDisplayMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(5));
    }
}

void display_update_text(const char *lines[], size_t num_lines) {
    if (xDisplayMutex == NULL) return;
    if (xSemaphoreTake(xDisplayMutex, portMAX_DELAY) == pdTRUE) {
        for (size_t i = 0; i < N_DISPLAY_LINES; i++) {
            const char *new_line = (i < num_lines && lines[i]) ? lines[i] : "";
            // Only update if content changed or if it's a line that needs to be cleared
            if (strncmp(display_lines[i], new_line, MAX_LINE_LENGTH) != 0 || strlen(new_line) == 0) {
                strncpy(display_lines[i], new_line, MAX_LINE_LENGTH - 1);
                display_lines[i][MAX_LINE_LENGTH - 1] = '\0';
                dirty_lines[i] = true;
            }
        }
        xSemaphoreGive(xDisplayMutex);
    }
}

// Updated function signature: now takes the button_input_queue
esp_err_t start_display_task(QueueHandle_t button_input_queue) {
    xDisplayMutex = xSemaphoreCreateMutex();
    if (xDisplayMutex == NULL) return ESP_FAIL;
    xSemaphoreGive(xDisplayMutex);

    // Store the button input queue
    s_button_input_queue = button_input_queue;

    // Create the display event queue
    display_event_queue = xQueueCreate(1, sizeof(display_event_t));
    if (display_event_queue == NULL) {
        ESP_LOGE(TAG, "Failed to create display event queue!");
        vSemaphoreDelete(xDisplayMutex);
        return ESP_FAIL;
    }

    // The display_manager_task will start in DISPLAY_STATE_MENU and display whatever
    // display_update_text provides (which will be the menu from game_menu).
    // The s_state_changed flag is already true by default, ensuring a clear on first run.

    if (xTaskCreate(display_manager_task, "Display_Manager_Task", 4096, NULL, 5, NULL) != pdPASS) {
        vSemaphoreDelete(xDisplayMutex);
        vQueueDelete(display_event_queue); // Clean up queue on task creation failure
        return ESP_FAIL;
    }
    return ESP_OK;
}

// New function to set the active game and transition the display state
void display_set_game_active(const game_t *game_instance) {
    if (game_instance == NULL) {
        ESP_LOGE(TAG, "Attempted to set NULL game instance!");
        return;
    }
    if (xDisplayMutex == NULL || xSemaphoreTake(xDisplayMutex, portMAX_DELAY) != pdTRUE) {
        ESP_LOGE(TAG, "Failed to take display mutex for setting game active.");
        return;
    }

    s_current_game = game_instance; // Store the game instance
    s_current_display_state = DISPLAY_STATE_GAME_RUNNING; // Transition to game running state
    s_state_changed = true; // Mark state changed to trigger a full clear on next iteration

    // Initialize the game and set it active
    if (s_current_game->init_game) {
        s_current_game->init_game();
    }
    if (s_current_game->set_active) {
        s_current_game->set_active(true);
    }
    // Pass the global button input queue to the game
    if (s_current_game->set_input_queue) {
        s_current_game->set_input_queue(s_button_input_queue);
    } else {
        ESP_LOGW(TAG, "Current game does not have set_input_queue function.");
    }

    s_displayed_score = -1; // Reset score display tracker for new game
    // ssd1306_clear_screen(&dev, false); // Removed: Clearing now handled by s_state_changed flag
    // Mark all lines dirty to force a full redraw by the game (handled by s_state_changed)
    // for (int i = 0; i < N_DISPLAY_LINES; i++) {
    //     dirty_lines[i] = true;
    // }

    xSemaphoreGive(xDisplayMutex);
    ESP_LOGI(TAG, "Display transitioned to GAME_RUNNING state for new game.");
}