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
#include "game/jump_bird_game_input_queue.h" // Still needed for input queue, but could be abstracted further

#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

typedef enum {
    DISPLAY_STATE_INITIAL_MESSAGE,
    DISPLAY_STATE_GAME_RUNNING,
    DISPLAY_STATE_GAME_OVER,
    DISPLAY_STATE_OTHER_INFO
} display_state_t;

static display_state_t s_current_display_state = DISPLAY_STATE_INITIAL_MESSAGE;
static int s_game_over_score = 0;
static int s_displayed_score = -1; // Tracks the score currently drawn on the display

// 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);

    while (1) {
        if (xSemaphoreTake(xDisplayMutex, portMAX_DELAY) == pdTRUE) {
            switch (s_current_display_state) {
            case DISPLAY_STATE_INITIAL_MESSAGE: {
                bool jump_signal_peek = false;
                // Check if s_current_game is valid before trying to access its input queue
                if (s_current_game && s_current_game->get_input_queue && s_current_game->get_input_queue() != NULL &&
                    xQueuePeek(s_current_game->get_input_queue(), &jump_signal_peek, 0) == pdTRUE) {
                    if (jump_signal_peek) {
                        bool jump_signal_start = false;
                        if (xQueueReceive(s_current_game->get_input_queue(), &jump_signal_start, 0) == pdTRUE) {
                            if (jump_signal_start) {
                                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);
                                }
                                // No full screen clear here, game drawing will handle its area
                                s_displayed_score = -1; // Reset score display tracker for new game
                            }
                        }
                    }
                }

                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);
                        }
                        ssd1306_clear_screen(&dev, false); // Keep this clear for game over screen
                        // Mark all lines dirty for the game over screen
                        for (int i = 0; i < N_DISPLAY_LINES; i++) {
                            dirty_lines[i] = true;
                        }
                        s_displayed_score = -1; // Reset score display tracker
                    }
                } 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;
                    ssd1306_clear_screen(&dev, false); // Keep this clear for game over screen
                    for (int i = 0; i < N_DISPLAY_LINES; i++) {
                        dirty_lines[i] = true;
                    }
                    s_displayed_score = -1; // Reset score display tracker
                }
                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,
                    "                  ",
                    "   Press button   ",
                    "   to Restart     ",
                    NULL, NULL
                };

                for (int i = 0; i < N_DISPLAY_LINES; i++) {
                    if (game_over_messages[i] && dirty_lines[i]) {
                        ssd1306_display_text(&dev, i, game_over_messages[i], strlen(game_over_messages[i]), false);
                        dirty_lines[i] = false;
                    }
                }

                bool jump_signal_restart = false;
                // Check if s_current_game is valid before trying to access its input queue
                if (s_current_game && s_current_game->get_input_queue && s_current_game->get_input_queue() != NULL &&
                    xQueueReceive(s_current_game->get_input_queue(), &jump_signal_restart, 0) == pdTRUE) {
                    if (jump_signal_restart) {
                        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);
                        }
                        // No full screen clear here, game drawing will handle its area
                        s_displayed_score = -1; // Reset score display tracker for new game
                    }
                }
                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] : "";
            if (strncmp(display_lines[i], new_line, MAX_LINE_LENGTH) != 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 to accept a game instance
esp_err_t start_display_task(const game_t *game_instance) {
    if (game_instance == NULL) {
        ESP_LOGE(TAG, "Game instance provided to start_display_task is NULL!");
        return ESP_FAIL;
    }
    s_current_game = game_instance; // Store the game instance

    xDisplayMutex = xSemaphoreCreateMutex();
    if (xDisplayMutex == NULL) return ESP_FAIL;
    xSemaphoreGive(xDisplayMutex);

    // Initialize the display with "Score: 0"
    const char *initial_lines[] = {"Score: 0"};
    display_update_text(initial_lines, 1);

    if (xTaskCreate(display_manager_task, "Display_Manager_Task", 4096, NULL, 5, NULL) != pdPASS) {
        vSemaphoreDelete(xDisplayMutex);
        return ESP_FAIL;
    }
    return ESP_OK;
}