feat(gamebook): library tile picker on boot
Context: The gamebook played a single hard-coded book. We want the master to boot into a library of several stories, shown as tiles, and let the player pick one with the pad. Approach: Turn the gamebook component into a two-mode machine (LIBRARY / STORY). On boot it loads /sdcard/gamebook/library.json and shows the stories as a tile grid; selecting one loads that book's <id>.json and plays it; finishing a story returns to the library. Changes: - display_ui: new library view — a 2x3 grid of title tiles with the selected one highlighted (display_ui_library_show/hide); it wins the view selector while open. - gamebook.c: LIBRARY/STORY modes; load library.json + per-book <id>.json from the SD; grid navigation (up/down +/-2, left/right +/-1, click opens); story ending click returns to the library. - main.c: gamebook_init only installs the pad hook; gamebook_start() (which opens the library) is now called after the SD + media_manager are up, so the boot picker actually finds the SD pack. Impact: The Freenove boots straight into a story picker and runs any book from the SD library, fully offline. Hardware-validated: boots into a 6-story library.
This commit was merged in pull request #38.
This commit is contained in:
@@ -258,6 +258,17 @@ static volatile bool s_gamebook_open = false;
|
||||
static char s_gb_title_buf[64];
|
||||
static char s_gb_body_buf[512];
|
||||
static char s_gb_menu_buf[160];
|
||||
|
||||
// ── Gamebook library (tile grid) ────────────────────────────────────────────
|
||||
#define LIB_TILES 6
|
||||
static lv_obj_t *s_scr_library;
|
||||
static lv_obj_t *s_lib_tile[LIB_TILES];
|
||||
static lv_obj_t *s_lib_label[LIB_TILES];
|
||||
static volatile bool s_library_open = false;
|
||||
static char s_lib_title_buf[LIB_TILES][48];
|
||||
static int s_lib_count_buf = 0;
|
||||
static int s_lib_sel_buf = 0;
|
||||
|
||||
static volatile bool s_browser_reload = false;
|
||||
|
||||
// ─── Intro — faithful port of the original cracktro ──────────────────────────
|
||||
@@ -771,6 +782,43 @@ scroller:
|
||||
lv_obj_set_pos(s_intro_scroll, sx, sy);
|
||||
}
|
||||
|
||||
static void build_library_screen(void) {
|
||||
s_scr_library = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_scr_library, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(s_scr_library, LV_OPA_COVER, 0);
|
||||
lv_obj_clear_flag(s_scr_library, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t *hdr = lv_label_create(s_scr_library);
|
||||
lv_obj_set_style_text_color(hdr, lv_color_hex(COL_CODE), 0);
|
||||
lv_obj_set_style_text_font(hdr, &lv_font_montserrat_14, 0);
|
||||
lv_label_set_text(hdr, "Choisis ton histoire");
|
||||
lv_obj_align(hdr, LV_ALIGN_TOP_MID, 0, 4);
|
||||
|
||||
for (int i = 0; i < LIB_TILES; i++) {
|
||||
int col = i % 2, row = i / 2;
|
||||
lv_obj_t *t = lv_obj_create(s_scr_library);
|
||||
lv_obj_set_size(t, 228, 84);
|
||||
lv_obj_set_pos(t, 8 + col * 236, 26 + row * 94);
|
||||
lv_obj_set_style_radius(t, 8, 0);
|
||||
lv_obj_set_style_bg_color(t, lv_color_hex(0x101820), 0);
|
||||
lv_obj_set_style_bg_opa(t, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(t, 2, 0);
|
||||
lv_obj_set_style_border_color(t, lv_color_hex(0x445566), 0);
|
||||
lv_obj_set_style_pad_all(t, 6, 0);
|
||||
lv_obj_clear_flag(t, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_t *l = lv_label_create(t);
|
||||
lv_obj_set_style_text_color(l, lv_color_hex(COL_VALUE), 0);
|
||||
lv_obj_set_style_text_font(l, &lv_font_montserrat_14, 0);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, 208);
|
||||
lv_obj_set_style_text_align(l, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_label_set_text(l, "");
|
||||
lv_obj_center(l);
|
||||
s_lib_tile[i] = t;
|
||||
s_lib_label[i] = l;
|
||||
}
|
||||
}
|
||||
|
||||
static void build_gamebook_screen(void) {
|
||||
s_scr_gamebook = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_scr_gamebook, lv_color_hex(0x000000), 0);
|
||||
@@ -907,11 +955,30 @@ static void apply_status(const display_status_t *s) {
|
||||
lv_label_set_text(s_gb_body, s_gb_body_buf);
|
||||
lv_label_set_text(s_gb_menu, s_gb_menu_buf);
|
||||
}
|
||||
// Library grid: refresh tile titles + highlight the selected one.
|
||||
if (s_library_open) {
|
||||
for (int i = 0; i < LIB_TILES; i++) {
|
||||
if (i < s_lib_count_buf) {
|
||||
lv_label_set_text(s_lib_label[i], s_lib_title_buf[i]);
|
||||
lv_obj_clear_flag(s_lib_tile[i], LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(s_lib_tile[i], LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
bool on = (i == s_lib_sel_buf);
|
||||
lv_obj_set_style_border_color(s_lib_tile[i],
|
||||
lv_color_hex(on ? COL_CODE : 0x445566), 0);
|
||||
lv_obj_set_style_border_width(s_lib_tile[i], on ? 4 : 2, 0);
|
||||
lv_obj_set_style_bg_color(s_lib_tile[i],
|
||||
lv_color_hex(on ? 0x223040 : 0x101820), 0);
|
||||
}
|
||||
}
|
||||
|
||||
extern volatile uint8_t g_dui_view_override;
|
||||
lv_obj_t *want;
|
||||
if (!s_intro_done) {
|
||||
want = s_scr_intro; // boot intro plays out first
|
||||
} else if (s_library_open) {
|
||||
want = s_scr_library; // story picker owns the screen
|
||||
} else if (s_gamebook_open) {
|
||||
want = s_scr_gamebook; // gamebook owns the screen while running
|
||||
} else if (s_browser_open) {
|
||||
@@ -1022,6 +1089,7 @@ static void display_task(void *arg) {
|
||||
build_scene_screen();
|
||||
build_shell_screen();
|
||||
build_gamebook_screen();
|
||||
build_library_screen();
|
||||
build_browser_screen();
|
||||
build_intro_screen();
|
||||
|
||||
@@ -1225,6 +1293,28 @@ extern "C" void display_ui_gamebook_hide(void) {
|
||||
s_dirty = true;
|
||||
}
|
||||
|
||||
extern "C" void display_ui_library_show(const char *const *titles, int count,
|
||||
int sel) {
|
||||
if (count > LIB_TILES) count = LIB_TILES;
|
||||
if (count < 0) count = 0;
|
||||
if (s_mutex) xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||
s_lib_count_buf = count;
|
||||
s_lib_sel_buf = (count > 0) ? ((sel % count + count) % count) : 0;
|
||||
for (int i = 0; i < LIB_TILES; i++) {
|
||||
snprintf(s_lib_title_buf[i], sizeof(s_lib_title_buf[i]), "%s",
|
||||
(i < count && titles && titles[i]) ? titles[i] : "");
|
||||
}
|
||||
s_library_open = true;
|
||||
s_gamebook_open = false; // library and a running story are exclusive
|
||||
s_dirty = true;
|
||||
if (s_mutex) xSemaphoreGive(s_mutex);
|
||||
}
|
||||
|
||||
extern "C" void display_ui_library_hide(void) {
|
||||
s_library_open = false;
|
||||
s_dirty = true;
|
||||
}
|
||||
|
||||
extern "C" void display_ui_handle_key(uint8_t key) {
|
||||
static uint8_t s_brightness = 100;
|
||||
|
||||
|
||||
@@ -108,6 +108,23 @@ void display_ui_gamebook_show(const char *title, const char *body,
|
||||
/** @brief Leave the gamebook view and return to the normal scene/status flow. */
|
||||
void display_ui_gamebook_hide(void);
|
||||
|
||||
/**
|
||||
* @brief Show the gamebook library as a grid of up to 6 tiles.
|
||||
*
|
||||
* Each tile shows a story title; the tile at index `sel` is highlighted. The
|
||||
* gamebook owns the pad and calls this on every cursor move. Forces the
|
||||
* library view until a story is loaded or display_ui_library_hide(). Titles
|
||||
* should be ASCII.
|
||||
*
|
||||
* @param titles array of `count` story titles
|
||||
* @param count number of stories (clamped to 6)
|
||||
* @param sel highlighted tile index
|
||||
*/
|
||||
void display_ui_library_show(const char *const *titles, int count, int sel);
|
||||
|
||||
/** @brief Leave the library view. */
|
||||
void display_ui_library_hide(void);
|
||||
|
||||
/**
|
||||
* @brief Pop the pending shell-app launch request, if any.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// gamebook.c — see gamebook.h. Standalone "livre dont vous êtes le héros".
|
||||
// gamebook.c — see gamebook.h. Standalone gamebook player + library for the
|
||||
// Freenove master. On boot it shows the library (a grid of story tiles);
|
||||
// picking one loads and plays that book; finishing returns to the library.
|
||||
#include "gamebook.h"
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -14,34 +16,103 @@
|
||||
static const char *TAG = "gamebook";
|
||||
|
||||
#define GAMEBOOK_DIR "/sdcard/gamebook"
|
||||
#define GAMEBOOK_JSON GAMEBOOK_DIR "/gamebook.json"
|
||||
#define GAMEBOOK_MAX (32 * 1024) /* JSON sanity cap */
|
||||
#define LIBRARY_JSON GAMEBOOK_DIR "/library.json"
|
||||
#define JSON_MAX (64 * 1024) /* per-file JSON sanity cap */
|
||||
#define LIB_MAX 6 /* matches the display tile grid */
|
||||
|
||||
/* 5-way pad codes (mirror display_ui): 1=SELECT 2=DOWN 3=MENU 4=L/R 5=UP. */
|
||||
#define KEY_MENU 3
|
||||
typedef enum { GB_OFF, GB_LIBRARY, GB_STORY } gb_mode_t;
|
||||
|
||||
static cJSON *s_root = NULL; /* owns the parsed JSON while active */
|
||||
static cJSON *s_passages = NULL; /* borrowed: s_root->"passages" */
|
||||
// Library state
|
||||
static cJSON *s_lib_root = NULL; /* owns library.json while loaded */
|
||||
static cJSON *s_lib = NULL; /* borrowed: root->"library" array */
|
||||
static int s_lib_n = 0;
|
||||
static int s_lib_sel = 0;
|
||||
|
||||
// Story state
|
||||
static cJSON *s_book = NULL; /* owns the current <book>.json */
|
||||
static cJSON *s_passages = NULL; /* borrowed: book->"passages" */
|
||||
static char s_title[48] = {0};
|
||||
static char s_current[48] = {0};
|
||||
static int s_sel = 0; /* highlighted choice index */
|
||||
static volatile bool s_active = false;
|
||||
static int s_sel = 0; /* highlighted choice index */
|
||||
|
||||
bool gamebook_active(void) { return s_active; }
|
||||
static volatile gb_mode_t s_mode = GB_OFF;
|
||||
|
||||
static const cJSON *cur_passage(void)
|
||||
bool gamebook_active(void) { return s_mode != GB_OFF; }
|
||||
|
||||
/* Read a whole JSON file from the SD into a cJSON tree (caller frees). */
|
||||
static cJSON *load_json(const char *path)
|
||||
{
|
||||
return cJSON_GetObjectItem(s_passages, s_current);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) { ESP_LOGW(TAG, "open %s failed", path); return NULL; }
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
rewind(f);
|
||||
if (sz <= 0 || sz > JSON_MAX) { fclose(f); ESP_LOGW(TAG, "%s size %ld", path, sz); return NULL; }
|
||||
char *buf = malloc((size_t)sz + 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
size_t rd = fread(buf, 1, (size_t)sz, f);
|
||||
fclose(f);
|
||||
buf[rd] = '\0';
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
if (!root) ESP_LOGW(TAG, "malformed JSON: %s", path);
|
||||
return root;
|
||||
}
|
||||
|
||||
// ── Library ──────────────────────────────────────────────────────────────────
|
||||
|
||||
static void render_library(void)
|
||||
{
|
||||
const char *titles[LIB_MAX] = {0};
|
||||
int n = (s_lib_n < LIB_MAX) ? s_lib_n : LIB_MAX;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cJSON *t = cJSON_GetObjectItem(cJSON_GetArrayItem(s_lib, i), "title");
|
||||
titles[i] = cJSON_IsString(t) ? t->valuestring : "?";
|
||||
}
|
||||
display_ui_library_show(titles, n, s_lib_sel);
|
||||
}
|
||||
|
||||
static void free_story(void)
|
||||
{
|
||||
media_manager_stop();
|
||||
if (s_book) { cJSON_Delete(s_book); s_book = NULL; }
|
||||
s_passages = NULL;
|
||||
s_current[0] = '\0';
|
||||
}
|
||||
|
||||
static esp_err_t open_library(void)
|
||||
{
|
||||
free_story();
|
||||
if (s_lib_root) { cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL; }
|
||||
|
||||
s_lib_root = load_json(LIBRARY_JSON);
|
||||
if (!s_lib_root) return ESP_ERR_NOT_FOUND;
|
||||
s_lib = cJSON_GetObjectItem(s_lib_root, "library");
|
||||
if (!cJSON_IsArray(s_lib) || cJSON_GetArraySize(s_lib) == 0) {
|
||||
cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL;
|
||||
ESP_LOGW(TAG, "library.json has no stories");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
s_lib_n = cJSON_GetArraySize(s_lib);
|
||||
s_lib_sel = 0;
|
||||
s_mode = GB_LIBRARY;
|
||||
display_ui_gamebook_hide();
|
||||
render_library();
|
||||
ESP_LOGI(TAG, "library open (%d stories)", s_lib_n);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ── Story ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
static const cJSON *cur_passage(void) { return cJSON_GetObjectItem(s_passages, s_current); }
|
||||
|
||||
static int cur_choice_count(void)
|
||||
{
|
||||
const cJSON *ch = cJSON_GetObjectItem(cur_passage(), "choices");
|
||||
return cJSON_IsArray(ch) ? cJSON_GetArraySize(ch) : 0;
|
||||
}
|
||||
|
||||
/* (Re)draw the current page: title + wrapped text + choice list with a "> "
|
||||
* cursor on s_sel. Does NOT touch audio — call on every cursor move. */
|
||||
/* (Re)draw the current page: title + wrapped text + choice list with a cursor. */
|
||||
static void render_page(void)
|
||||
{
|
||||
const cJSON *p = cur_passage();
|
||||
@@ -53,7 +124,7 @@ static void render_page(void)
|
||||
|
||||
char menu[256];
|
||||
if (n == 0) {
|
||||
snprintf(menu, sizeof(menu), "~ Fin ~ (clic = recommencer)");
|
||||
snprintf(menu, sizeof(menu), "~ Fin ~ (clic = bibliotheque)");
|
||||
} else {
|
||||
size_t off = 0;
|
||||
for (int i = 0; i < n && off < sizeof(menu); i++) {
|
||||
@@ -70,14 +141,10 @@ static void render_page(void)
|
||||
menu);
|
||||
}
|
||||
|
||||
/* Enter a passage: reset the cursor, draw the page, play its WAV from the SD. */
|
||||
static void enter_passage(const char *pid)
|
||||
{
|
||||
const cJSON *p = cJSON_GetObjectItem(s_passages, pid);
|
||||
if (!cJSON_IsObject(p)) {
|
||||
ESP_LOGW(TAG, "passage '%s' not found", pid);
|
||||
return;
|
||||
}
|
||||
if (!cJSON_IsObject(p)) { ESP_LOGW(TAG, "passage '%s' not found", pid); return; }
|
||||
snprintf(s_current, sizeof(s_current), "%s", pid);
|
||||
s_sel = 0;
|
||||
render_page();
|
||||
@@ -86,38 +153,79 @@ static void enter_passage(const char *pid)
|
||||
if (cJSON_IsString(wav) && wav->valuestring[0]) {
|
||||
char path[96];
|
||||
snprintf(path, sizeof(path), "%s/%s", GAMEBOOK_DIR, wav->valuestring);
|
||||
media_manager_stop(); /* a choice skips the narration */
|
||||
media_manager_stop(); /* a choice skips the narration */
|
||||
media_manager_play(path);
|
||||
}
|
||||
ESP_LOGI(TAG, "passage '%s' (%d choices)", pid, cur_choice_count());
|
||||
}
|
||||
|
||||
/* Key hook (we own the 5-way pad while active). D-pad navigates the choice
|
||||
* list, click (SELECT) confirms. Ladder codes: 1=click 2=down 3/4=left/right
|
||||
* 5=up. up/left → previous, down/right → next. On an ending (no choices) the
|
||||
* click restarts the book. Returns true to consume the key. */
|
||||
static bool gamebook_key_hook(uint8_t key)
|
||||
/* Load story #idx from the library and play it. */
|
||||
static void load_book(int idx)
|
||||
{
|
||||
if (!s_active) return false;
|
||||
const cJSON *e = cJSON_GetArrayItem(s_lib, idx);
|
||||
const cJSON *file = cJSON_GetObjectItem(e, "book");
|
||||
if (!cJSON_IsString(file)) { ESP_LOGW(TAG, "library[%d] has no book", idx); return; }
|
||||
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "%s/%s", GAMEBOOK_DIR, file->valuestring);
|
||||
cJSON *book = load_json(path);
|
||||
if (!book) return;
|
||||
const cJSON *passages = cJSON_GetObjectItem(book, "passages");
|
||||
const cJSON *start = cJSON_GetObjectItem(book, "start");
|
||||
const cJSON *title = cJSON_GetObjectItem(book, "title");
|
||||
if (!cJSON_IsObject(passages) || !cJSON_IsString(start)) {
|
||||
ESP_LOGW(TAG, "%s missing passages/start", path);
|
||||
cJSON_Delete(book);
|
||||
return;
|
||||
}
|
||||
free_story();
|
||||
s_book = book;
|
||||
s_passages = (cJSON *)passages;
|
||||
snprintf(s_title, sizeof(s_title), "%s",
|
||||
cJSON_IsString(title) ? title->valuestring : "");
|
||||
s_mode = GB_STORY;
|
||||
display_ui_library_hide();
|
||||
ESP_LOGI(TAG, "load book \"%s\" @ '%s'", s_title, start->valuestring);
|
||||
enter_passage(start->valuestring);
|
||||
}
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/* Library grid nav: 2 columns. up/down = ±2 (row), left/right = ±1 (col),
|
||||
* click = open the highlighted story. Codes: 1=click 2=down 3=left 4=right
|
||||
* 5=up (5-way ladder). */
|
||||
static bool library_key(uint8_t key)
|
||||
{
|
||||
int s = s_lib_sel;
|
||||
switch (key) {
|
||||
case 5: s -= 2; break; /* up */
|
||||
case 2: s += 2; break; /* down */
|
||||
case 3: s -= 1; break; /* left */
|
||||
case 4: s += 1; break; /* right */
|
||||
case 1: /* click → open */
|
||||
load_book(s_lib_sel);
|
||||
return true;
|
||||
default: return true;
|
||||
}
|
||||
if (s >= 0 && s < s_lib_n) { s_lib_sel = s; render_library(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Story nav: D-pad moves the choice cursor, click confirms. Ending + click
|
||||
* returns to the library. */
|
||||
static bool story_key(uint8_t key)
|
||||
{
|
||||
int n = cur_choice_count();
|
||||
if (n == 0) { /* ending page */
|
||||
if (key == 1) { /* click → restart */
|
||||
const cJSON *start = cJSON_GetObjectItem(s_root, "start");
|
||||
if (cJSON_IsString(start)) enter_passage(start->valuestring);
|
||||
}
|
||||
if (n == 0) { /* ending → back to library on click */
|
||||
if (key == 1) open_library();
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 5: case 3: /* up / left → previous */
|
||||
s_sel = (s_sel - 1 + n) % n; render_page(); break;
|
||||
case 2: case 4: /* down / right → next */
|
||||
s_sel = (s_sel + 1) % n; render_page(); break;
|
||||
case 1: { /* click → confirm */
|
||||
case 5: case 3: s_sel = (s_sel - 1 + n) % n; render_page(); break; /* up/left */
|
||||
case 2: case 4: s_sel = (s_sel + 1) % n; render_page(); break; /* down/right */
|
||||
case 1: { /* click → confirm */
|
||||
const cJSON *choices = cJSON_GetObjectItem(cur_passage(), "choices");
|
||||
const cJSON *g = cJSON_GetObjectItem(cJSON_GetArrayItem(choices, s_sel),
|
||||
"goto");
|
||||
const cJSON *g = cJSON_GetObjectItem(cJSON_GetArrayItem(choices, s_sel), "goto");
|
||||
if (cJSON_IsString(g)) {
|
||||
ESP_LOGI(TAG, "select #%d -> '%s'", s_sel, g->valuestring);
|
||||
enter_passage(g->valuestring);
|
||||
@@ -129,60 +237,38 @@ static bool gamebook_key_hook(uint8_t key)
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool gamebook_key_hook(uint8_t key)
|
||||
{
|
||||
switch (s_mode) {
|
||||
case GB_LIBRARY: return library_key(key);
|
||||
case GB_STORY: return story_key(key);
|
||||
default: return false; /* not active → let the shell have it */
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
esp_err_t gamebook_start(void)
|
||||
{
|
||||
gamebook_stop(); /* clean any previous run */
|
||||
|
||||
FILE *f = fopen(GAMEBOOK_JSON, "rb");
|
||||
if (!f) {
|
||||
ESP_LOGW(TAG, "open %s failed — SD card / pack missing?", GAMEBOOK_JSON);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
rewind(f);
|
||||
if (sz <= 0 || sz > GAMEBOOK_MAX) { fclose(f); return ESP_ERR_INVALID_SIZE; }
|
||||
char *buf = malloc((size_t)sz + 1);
|
||||
if (!buf) { fclose(f); return ESP_ERR_NO_MEM; }
|
||||
size_t rd = fread(buf, 1, (size_t)sz, f);
|
||||
fclose(f);
|
||||
buf[rd] = '\0';
|
||||
|
||||
s_root = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
if (!s_root) { ESP_LOGW(TAG, "malformed gamebook.json"); return ESP_ERR_INVALID_ARG; }
|
||||
|
||||
s_passages = cJSON_GetObjectItem(s_root, "passages");
|
||||
const cJSON *start = cJSON_GetObjectItem(s_root, "start");
|
||||
const cJSON *title = cJSON_GetObjectItem(s_root, "title");
|
||||
if (!cJSON_IsObject(s_passages) || !cJSON_IsString(start)) {
|
||||
ESP_LOGW(TAG, "gamebook.json missing passages/start");
|
||||
cJSON_Delete(s_root); s_root = NULL; s_passages = NULL;
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
snprintf(s_title, sizeof(s_title), "%s",
|
||||
cJSON_IsString(title) ? title->valuestring : "");
|
||||
|
||||
s_active = true;
|
||||
ESP_LOGI(TAG, "start \"%s\" @ '%s'", s_title, start->valuestring);
|
||||
enter_passage(start->valuestring);
|
||||
return ESP_OK;
|
||||
return open_library();
|
||||
}
|
||||
|
||||
void gamebook_stop(void)
|
||||
{
|
||||
if (!s_active && !s_root) return;
|
||||
s_active = false;
|
||||
media_manager_stop();
|
||||
if (s_mode == GB_OFF) return;
|
||||
s_mode = GB_OFF;
|
||||
free_story();
|
||||
if (s_lib_root) { cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL; }
|
||||
display_ui_gamebook_hide();
|
||||
if (s_root) { cJSON_Delete(s_root); s_root = NULL; }
|
||||
s_passages = NULL;
|
||||
s_current[0] = '\0';
|
||||
display_ui_library_hide();
|
||||
ESP_LOGI(TAG, "stopped");
|
||||
}
|
||||
|
||||
void gamebook_init(void)
|
||||
{
|
||||
/* Only register the pad hook here — the SD and media_manager aren't up yet
|
||||
* at this point in boot. main.c calls gamebook_start() once they are, to
|
||||
* boot into the library. */
|
||||
display_ui_set_key_hook(gamebook_key_hook);
|
||||
ESP_LOGI(TAG, "ready (key hook installed)");
|
||||
ESP_LOGI(TAG, "key hook installed");
|
||||
}
|
||||
|
||||
@@ -523,6 +523,13 @@ void app_main(void) {
|
||||
ESP_LOGI(TAG, "media smoke play -> %s",
|
||||
esp_err_to_name(play_err));
|
||||
|
||||
// Boot into the gamebook library (the 6-story tile picker) when the
|
||||
// SD pack is present. Best-effort: no /sdcard/gamebook/library.json
|
||||
// -> the normal shell stays in charge.
|
||||
if (gamebook_start() == ESP_OK) {
|
||||
ESP_LOGI(TAG, "gamebook library up (boot picker)");
|
||||
}
|
||||
|
||||
// Slice 4: bring up the ported npc_engine. Cue table is empty
|
||||
// at this stage — wiring the scenario IR-driven cue catalog is
|
||||
// a follow-up slice. The engine still boots, accepts ticks
|
||||
|
||||
Reference in New Issue
Block a user