Game Config
Source: src/game_config.c · 250 lines · 12 functions.
Module entry points
These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.
How to read these diagrams
Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.
These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.
Functions in this file
selectedGame() · source line 44
static const game_config_game_t *selectedGame(void)
- Exit this function
return &games[selected_game];
Read original function source
static const game_config_game_t *selectedGame(void)
{
return &games[selected_game];
}readInteger() · source line 49
static int readInteger(const char *filename, const char *key, int fallback, int minimum, int maximum)
- Execute
char path[48]; - Execute
const int written = snprintf(path, sizeof(path), "%s/%s", CONFIG_ROOT, filename); - Decision
written < 0 || (size_t)written >= sizeof(path)True- Exit this function
return fallback;
FalseContinue after this decision.
- Execute
FILE *file = fopen(path, "rb"); - Decision
file == NULLTrue- Exit this function
return fallback;
FalseContinue after this decision.
- Execute
char line[CONFIG_LINE_CHARS]; - Execute
int value = fallback; - WHILE loop
fgets(line, sizeof(line), file) != NULLRepeat body while the loop condition permits; then continue below.
- Execute
char found_key[32]; - Execute
int candidate; - Decision
sscanf(line, " %31[^= ] = %d", found_key, &candidate) != 2True- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
strcmp(found_key, key) != 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
candidate >= minimum && candidate <= maximumTrue- Execute
value = candidate;
FalseContinue after this decision.
- Exit enclosing loop or switch
break;
- Execute
fclose(file); - Exit this function
return value;
Read original function source
static int readInteger(const char *filename, const char *key, int fallback, int minimum, int maximum)
{
char path[48];
const int written = snprintf(path, sizeof(path), "%s/%s", CONFIG_ROOT, filename);
if (written < 0 || (size_t)written >= sizeof(path)) return fallback;
FILE *file = fopen(path, "rb");
if (file == NULL) return fallback;
char line[CONFIG_LINE_CHARS];
int value = fallback;
while (fgets(line, sizeof(line), file) != NULL) {
char found_key[32];
int candidate;
if (sscanf(line, " %31[^= ] = %d", found_key, &candidate) != 2) continue;
if (strcmp(found_key, key) != 0) continue;
if (candidate >= minimum && candidate <= maximum) value = candidate;
break;
}
fclose(file);
return value;
}writeInvadersConfig() · source line 71
static bool writeInvadersConfig(uint8_t lives)
- Execute
const char *final_path = CONFIG_ROOT "/invaders.cfg"; - Execute
const char *temporary_path = CONFIG_ROOT "/invaders.tmp"; - Execute
FILE *file = fopen(temporary_path, "wb"); - Decision
file == NULLTrue- Exit this function
return false;
FalseContinue after this decision.
- Execute
bool written = fprintf(file, "# Mini Arcade Invaders configuration\n" "# Remove this file to restore the built-in defaults.\n" "lives=%u\n", (unsigned)lives) > 0; - Decision
fclose(file) != 0True- Execute
written = false;
FalseContinue after this decision.
- Decision
!writtenTrue- Execute
remove(temporary_path); - Exit this function
return false;
FalseContinue after this decision.
- Decision
rename(temporary_path, final_path) == 0True- Exit this function
return true;
FalseContinue after this decision.
- Execute
remove(temporary_path); - Exit this function
return false;
Read original function source
static bool writeInvadersConfig(uint8_t lives)
{
const char *final_path = CONFIG_ROOT "/invaders.cfg";
const char *temporary_path = CONFIG_ROOT "/invaders.tmp";
FILE *file = fopen(temporary_path, "wb");
if (file == NULL) return false;
bool written = fprintf(file, "# Mini Arcade Invaders configuration\n"
"# Remove this file to restore the built-in defaults.\n"
"lives=%u\n", (unsigned)lives) > 0;
if (fclose(file) != 0) written = false;
if (!written) {
remove(temporary_path);
return false;
}
if (rename(temporary_path, final_path) == 0) return true;
remove(temporary_path);
return false;
}gameConfigInvadersStartingLives() · source line 90
uint8_t gameConfigInvadersStartingLives(void)
Calls into this documentation:
- Exit this function
return (uint8_t)readInteger("invaders.cfg", "lives", 3, 1, 9);
Read original function source
uint8_t gameConfigInvadersStartingLives(void)
{
return (uint8_t)readInteger("invaders.cfg", "lives", 3, 1, 9);
}startGameConfig() · source line 95
void startGameConfig(uint32_t now_ms)
Calls into this documentation:
- Execute
(void)now_ms; - Execute
active = true; - Execute
view = GAME_CONFIG_GAMES; - Execute
selected_game = selected_option = 0U; - Execute
waiting_for_centre = true; - Execute
repeating = false; - Execute
held_direction = 0; - Execute
invaders_lives = gameConfigInvadersStartingLives();
Read original function source
void startGameConfig(uint32_t now_ms)
{
(void)now_ms;
active = true;
view = GAME_CONFIG_GAMES;
selected_game = selected_option = 0U;
waiting_for_centre = true;
repeating = false;
held_direction = 0;
invaders_lives = gameConfigInvadersStartingLives();
}stopGameConfig() · source line 107
void stopGameConfig(void)
- Execute
active = false; - Execute
held_direction = 0;
Read original function source
void stopGameConfig(void)
{
active = false;
held_direction = 0;
}gameConfigAction() · source line 113
void gameConfigAction(bool increase, bool decrease, uint32_t now_ms)
Calls into this documentation:
- Execute
(void)now_ms; - Decision
!active || (!increase && !decrease)True- Exit this function
return;
FalseContinue after this decision.
- Decision
view == GAME_CONFIG_GAMESTrue- Execute
view = GAME_CONFIG_OPTIONS; - Execute
selected_option = 0U; - Execute
invaders_lives = gameConfigInvadersStartingLives(); - Exit this function
return;
FalseContinue after this decision.
- Decision
!selectedGame()->configuredTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
selected_option == 0UTrue- Execute
int next = invaders_lives + (increase ? 1 : -1); - Decision
next < 1True- Execute
next = 1;
FalseContinue after this decision.
- Decision
next > 9True- Execute
next = 9;
FalseContinue after this decision.
- Decision
(uint8_t)next == invaders_livesTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
writeInvadersConfig((uint8_t)next)True- Execute
invaders_lives = (uint8_t)next;
FalseContinue after this decision.
False- Decision
remove(CONFIG_ROOT "/invaders.cfg") == 0 || errno == ENOENTTrue- Execute
invaders_lives = 3U;
FalseContinue after this decision.
Read original function source
void gameConfigAction(bool increase, bool decrease, uint32_t now_ms)
{
(void)now_ms;
if (!active || (!increase && !decrease)) return;
if (view == GAME_CONFIG_GAMES) {
view = GAME_CONFIG_OPTIONS;
selected_option = 0U;
invaders_lives = gameConfigInvadersStartingLives();
return;
}
if (!selectedGame()->configured) return;
if (selected_option == 0U) {
int next = invaders_lives + (increase ? 1 : -1);
if (next < 1) next = 1;
if (next > 9) next = 9;
if ((uint8_t)next == invaders_lives) return;
if (writeInvadersConfig((uint8_t)next)) invaders_lives = (uint8_t)next;
} else if (remove(CONFIG_ROOT "/invaders.cfg") == 0 || errno == ENOENT) {
invaders_lives = 3U;
}
}moveGameSelection() · source line 135
static void moveGameSelection(int horizontal_direction, int vertical_direction)
- Execute
const unsigned count = sizeof(games) / sizeof(games[0]); - Execute
const unsigned page_count = (count + GAME_CONFIG_PER_PAGE - 1U) / GAME_CONFIG_PER_PAGE; - Execute
unsigned page = selected_game / GAME_CONFIG_PER_PAGE; - Execute
unsigned column = (selected_game % GAME_CONFIG_PER_PAGE) / GAME_CONFIG_ROWS; - Execute
unsigned row = selected_game % GAME_CONFIG_ROWS; - Execute
unsigned page_items = count - page * GAME_CONFIG_PER_PAGE; - Decision
page_items > GAME_CONFIG_PER_PAGETrue- Execute
page_items = GAME_CONFIG_PER_PAGE;
FalseContinue after this decision.
- Execute
unsigned rows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U; - Decision
rows > GAME_CONFIG_ROWSTrue- Execute
rows = GAME_CONFIG_ROWS;
FalseContinue after this decision.
- Decision
horizontal_direction != 0True- Execute
const unsigned other_column = column == 0U ? 1U : 0U; - Execute
unsigned other_rows = page_items > other_column * GAME_CONFIG_ROWS ? page_items - other_column * GAME_CONFIG_ROWS : 0U; - Decision
other_rows > GAME_CONFIG_ROWSTrue- Execute
other_rows = GAME_CONFIG_ROWS;
FalseContinue after this decision.
- Decision
other_rows > 0UTrue- Execute
column = other_column; - Decision
row >= other_rowsTrue- Execute
row = other_rows - 1U;
FalseContinue after this decision.
FalseContinue after this decision.
False- Decision
vertical_direction != 0True- Decision
(vertical_direction < 0 && row > 0U) || (vertical_direction > 0 && row + 1U < rows)True- Execute
row = vertical_direction < 0 ? row - 1U : row + 1U;
False- DO / WHILE loop
rows == 0URun body, then test condition; repeat while true.
- Execute
page = vertical_direction < 0 ? (page + page_count - 1U) % page_count : (page + 1U) % page_count; - Execute
page_items = count - page * GAME_CONFIG_PER_PAGE; - Decision
page_items > GAME_CONFIG_PER_PAGETrue- Execute
page_items = GAME_CONFIG_PER_PAGE;
FalseContinue after this decision.
- Execute
rows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U; - Decision
rows > GAME_CONFIG_ROWSTrue- Execute
rows = GAME_CONFIG_ROWS;
FalseContinue after this decision.
- Execute
row = vertical_direction < 0 ? rows - 1U : 0U;
FalseContinue after this decision.
- Execute
selected_game = page * GAME_CONFIG_PER_PAGE + column * GAME_CONFIG_ROWS + row;
Read original function source
static void moveGameSelection(int horizontal_direction, int vertical_direction)
{
const unsigned count = sizeof(games) / sizeof(games[0]);
const unsigned page_count = (count + GAME_CONFIG_PER_PAGE - 1U) / GAME_CONFIG_PER_PAGE;
unsigned page = selected_game / GAME_CONFIG_PER_PAGE;
unsigned column = (selected_game % GAME_CONFIG_PER_PAGE) / GAME_CONFIG_ROWS;
unsigned row = selected_game % GAME_CONFIG_ROWS;
unsigned page_items = count - page * GAME_CONFIG_PER_PAGE;
if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
unsigned rows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U;
if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;
if (horizontal_direction != 0) {
const unsigned other_column = column == 0U ? 1U : 0U;
unsigned other_rows = page_items > other_column * GAME_CONFIG_ROWS ?
page_items - other_column * GAME_CONFIG_ROWS : 0U;
if (other_rows > GAME_CONFIG_ROWS) other_rows = GAME_CONFIG_ROWS;
if (other_rows > 0U) {
column = other_column;
if (row >= other_rows) row = other_rows - 1U;
}
} else if (vertical_direction != 0) {
if ((vertical_direction < 0 && row > 0U) ||
(vertical_direction > 0 && row + 1U < rows)) {
row = vertical_direction < 0 ? row - 1U : row + 1U;
} else {
do {
page = vertical_direction < 0 ? (page + page_count - 1U) % page_count :
(page + 1U) % page_count;
page_items = count - page * GAME_CONFIG_PER_PAGE;
if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
rows = page_items > column * GAME_CONFIG_ROWS ?
page_items - column * GAME_CONFIG_ROWS : 0U;
if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;
} while (rows == 0U);
row = vertical_direction < 0 ? rows - 1U : 0U;
}
}
selected_game = page * GAME_CONFIG_PER_PAGE + column * GAME_CONFIG_ROWS + row;
}updateGameConfig() · source line 176
void updateGameConfig(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
Calls into this documentation:
- Decision
!activeTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
!joystick_validTrue- Execute
waiting_for_centre = true; - Execute
held_direction = 0; - Exit this function
return;
FalseContinue after this decision.
- Execute
const int x_magnitude = x_offset < 0 ? -x_offset : x_offset; - Execute
const int magnitude = y_offset < 0 ? -y_offset : y_offset; - Decision
x_magnitude <= JOYSTICK_RELEASE && magnitude <= JOYSTICK_RELEASETrue- Execute
waiting_for_centre = false; - Execute
held_direction = 0; - Execute
repeating = false; - Exit this function
return;
FalseContinue after this decision.
- Decision
waiting_for_centre || (x_magnitude < JOYSTICK_ENTER && magnitude < JOYSTICK_ENTER)True- Exit this function
return;
FalseContinue after this decision.
- Execute
const bool horizontal = x_magnitude >= magnitude; - Execute
const int direction = horizontal ? (x_offset > 0 ? 1 : -1) : (y_offset > 0 ? -1 : 1); - Execute
const int held = horizontal ? (direction > 0 ? 1 : 2) : (direction < 0 ? 3 : 4); - Decision
held == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)True- Exit this function
return;
FalseContinue after this decision.
- Execute
repeating = held == held_direction; - Execute
held_direction = held; - Execute
last_move = now_ms; - Decision
view == GAME_CONFIG_GAMESTrue- Execute
moveGameSelection(horizontal ? direction : 0, horizontal ? 0 : direction);
False- Decision
selectedGame()->configuredTrue- Execute
selected_option = selected_option == 0U ? 1U : 0U;
FalseContinue after this decision.
Read original function source
void updateGameConfig(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
if (!active) return;
if (!joystick_valid) { waiting_for_centre = true; held_direction = 0; return; }
const int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
const int magnitude = y_offset < 0 ? -y_offset : y_offset;
if (x_magnitude <= JOYSTICK_RELEASE && magnitude <= JOYSTICK_RELEASE) {
waiting_for_centre = false;
held_direction = 0;
repeating = false;
return;
}
if (waiting_for_centre || (x_magnitude < JOYSTICK_ENTER && magnitude < JOYSTICK_ENTER)) return;
const bool horizontal = x_magnitude >= magnitude;
const int direction = horizontal ? (x_offset > 0 ? 1 : -1) : (y_offset > 0 ? -1 : 1);
const int held = horizontal ? (direction > 0 ? 1 : 2) : (direction < 0 ? 3 : 4);
if (held == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)) return;
repeating = held == held_direction;
held_direction = held;
last_move = now_ms;
if (view == GAME_CONFIG_GAMES) {
moveGameSelection(horizontal ? direction : 0, horizontal ? 0 : direction);
} else if (selectedGame()->configured) {
selected_option = selected_option == 0U ? 1U : 0U;
}
}drawGameList() · source line 203
static void drawGameList(uint16_t *frame)
Calls into this documentation:
- Execute
const unsigned count = sizeof(games) / sizeof(games[0]); - Execute
const unsigned first = selected_game / GAME_CONFIG_PER_PAGE * GAME_CONFIG_PER_PAGE; - Execute
clear(frame, COLOR_BLACK); - Execute
draw_text_centred(frame, 160, 4, "GAME CONFIG", 2, COLOR_CYAN); - FOR loop
unsigned index = first; index < count && index < first + GAME_CONFIG_PER_PAGE; ++indexRepeat body while the loop condition permits; then continue below.
- Execute
const unsigned position = index - first; - Execute
const int x = position < GAME_CONFIG_ROWS ? 8 : 166; - Execute
const int y = 39 + (int)(position % GAME_CONFIG_ROWS) * 24; - Execute
draw_text(frame, x, y, games[index].name, 1, index == selected_game ? COLOR_CYAN : COLOR_YELLOW);
- Execute
draw_text_centred(frame, 160, 157, "STICK SELECT A/K OPEN BOOT BACK", 1, COLOR_LIGHTGREY);
Read original function source
static void drawGameList(uint16_t *frame)
{
const unsigned count = sizeof(games) / sizeof(games[0]);
const unsigned first = selected_game / GAME_CONFIG_PER_PAGE * GAME_CONFIG_PER_PAGE;
clear(frame, COLOR_BLACK);
draw_text_centred(frame, 160, 4, "GAME CONFIG", 2, COLOR_CYAN);
for (unsigned index = first; index < count && index < first + GAME_CONFIG_PER_PAGE; ++index) {
const unsigned position = index - first;
const int x = position < GAME_CONFIG_ROWS ? 8 : 166;
const int y = 39 + (int)(position % GAME_CONFIG_ROWS) * 24;
draw_text(frame, x, y, games[index].name, 1,
index == selected_game ? COLOR_CYAN : COLOR_YELLOW);
}
draw_text_centred(frame, 160, 157, "STICK SELECT A/K OPEN BOOT BACK", 1, COLOR_LIGHTGREY);
}drawGameOptions() · source line 219
static void drawGameOptions(uint16_t *frame)
Calls into this documentation:
- Execute
clear(frame, COLOR_BLACK); - Execute
draw_text_centred(frame, 160, 4, selectedGame()->name, 2, COLOR_CYAN); - Decision
!selectedGame()->configuredTrue- Execute
draw_text_centred(frame, 160, 66, "NO SETTINGS YET", 2, COLOR_YELLOW); - Execute
draw_text_centred(frame, 160, 100, "BUILT-IN DEFAULTS ACTIVE", 1, COLOR_LIGHTGREY); - Execute
draw_text_centred(frame, 160, 157, "BOOT BACK", 1, COLOR_LIGHTGREY); - Exit this function
return;
FalseContinue after this decision.
- Execute
char value[24]; - Execute
snprintf(value, sizeof(value), "%u", (unsigned)invaders_lives); - Execute
draw_text(frame, 16, 43, "STARTING LIVES", 1, selected_option == 0U ? COLOR_CYAN : COLOR_YELLOW); - Execute
draw_text(frame, 258, 43, value, 1, selected_option == 0U ? COLOR_CYAN : COLOR_WHITE); - Execute
draw_text(frame, 16, 73, "RESTORE DEFAULTS", 1, selected_option == 1U ? COLOR_CYAN : COLOR_YELLOW); - Execute
draw_text_centred(frame, 160, 119, "A INCREASE K DECREASE", 1, COLOR_GREEN); - Execute
draw_text_centred(frame, 160, 133, "A/K RESTORES DEFAULTS", 1, COLOR_GREEN); - Execute
draw_text_centred(frame, 160, 157, "UP/DOWN SELECT BOOT BACK", 1, COLOR_LIGHTGREY);
Read original function source
static void drawGameOptions(uint16_t *frame)
{
clear(frame, COLOR_BLACK);
draw_text_centred(frame, 160, 4, selectedGame()->name, 2, COLOR_CYAN);
if (!selectedGame()->configured) {
draw_text_centred(frame, 160, 66, "NO SETTINGS YET", 2, COLOR_YELLOW);
draw_text_centred(frame, 160, 100, "BUILT-IN DEFAULTS ACTIVE", 1, COLOR_LIGHTGREY);
draw_text_centred(frame, 160, 157, "BOOT BACK", 1, COLOR_LIGHTGREY);
return;
}
char value[24];
snprintf(value, sizeof(value), "%u", (unsigned)invaders_lives);
draw_text(frame, 16, 43, "STARTING LIVES", 1,
selected_option == 0U ? COLOR_CYAN : COLOR_YELLOW);
draw_text(frame, 258, 43, value, 1, selected_option == 0U ? COLOR_CYAN : COLOR_WHITE);
draw_text(frame, 16, 73, "RESTORE DEFAULTS", 1,
selected_option == 1U ? COLOR_CYAN : COLOR_YELLOW);
draw_text_centred(frame, 160, 119, "A INCREASE K DECREASE", 1, COLOR_GREEN);
draw_text_centred(frame, 160, 133, "A/K RESTORES DEFAULTS", 1, COLOR_GREEN);
draw_text_centred(frame, 160, 157, "UP/DOWN SELECT BOOT BACK", 1, COLOR_LIGHTGREY);
}drawGameConfig() · source line 241
void drawGameConfig(uint16_t *frame)
Calls into this documentation:
- Decision
!active || frame == NULLTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
view == GAME_CONFIG_GAMESTrue- Execute
drawGameList(frame);
False- Execute
drawGameOptions(frame);
Read original function source
void drawGameConfig(uint16_t *frame)
{
if (!active || frame == NULL) return;
if (view == GAME_CONFIG_GAMES) drawGameList(frame);
else drawGameOptions(frame);
}Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "game_config.h"
#include "game_colors.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
#define CONFIG_ROOT "/spiffs"
#define CONFIG_LINE_CHARS 64
#define GAME_CONFIG_ROWS 4U
#define GAME_CONFIG_PER_PAGE (GAME_CONFIG_ROWS * 2U)
#define JOYSTICK_ENTER 600
#define JOYSTICK_RELEASE 350
#define REPEAT_START_MS 400U
#define REPEAT_MS 150U
typedef struct {
const char *name;
const char *filename;
bool configured;
} game_config_game_t;
typedef enum { GAME_CONFIG_GAMES, GAME_CONFIG_OPTIONS } game_config_view_t;
static const game_config_game_t games[] = {
{"CHECKERS", "checkers.cfg", false}, {"OTHELLO", "othello.cfg", false},
{"CONNECT FOUR", "connect_four.cfg", false}, {"BATTLESHIPS", "battleships.cfg", false},
{"ASTEROIDS", "asteroids.cfg", false}, {"TEMPEST", "tempest.cfg", false},
{"INVADERS", "invaders.cfg", true}, {"YAHTZEE", "yahtzee.cfg", false},
{"BLACKJACK", "blackjack.cfg", false}, {"PIG", "pig.cfg", false},
{"MISSILE COMMAND", "missile_command.cfg", false}, {"SCRAMBLE", "scramble.cfg", false},
{"DEFENDER", "defender.cfg", false}, {"GALAGA", "galaga.cfg", false},
{"DUNGEON", "dungeon.cfg", false},
};
static bool active, waiting_for_centre, repeating;
static game_config_view_t view;
static unsigned selected_game, selected_option;
static int held_direction;
static uint32_t last_move;
static uint8_t invaders_lives;
static const game_config_game_t *selectedGame(void)
{
return &games[selected_game];
}
static int readInteger(const char *filename, const char *key, int fallback, int minimum, int maximum)
{
char path[48];
const int written = snprintf(path, sizeof(path), "%s/%s", CONFIG_ROOT, filename);
if (written < 0 || (size_t)written >= sizeof(path)) return fallback;
FILE *file = fopen(path, "rb");
if (file == NULL) return fallback;
char line[CONFIG_LINE_CHARS];
int value = fallback;
while (fgets(line, sizeof(line), file) != NULL) {
char found_key[32];
int candidate;
if (sscanf(line, " %31[^= ] = %d", found_key, &candidate) != 2) continue;
if (strcmp(found_key, key) != 0) continue;
if (candidate >= minimum && candidate <= maximum) value = candidate;
break;
}
fclose(file);
return value;
}
static bool writeInvadersConfig(uint8_t lives)
{
const char *final_path = CONFIG_ROOT "/invaders.cfg";
const char *temporary_path = CONFIG_ROOT "/invaders.tmp";
FILE *file = fopen(temporary_path, "wb");
if (file == NULL) return false;
bool written = fprintf(file, "# Mini Arcade Invaders configuration\n"
"# Remove this file to restore the built-in defaults.\n"
"lives=%u\n", (unsigned)lives) > 0;
if (fclose(file) != 0) written = false;
if (!written) {
remove(temporary_path);
return false;
}
if (rename(temporary_path, final_path) == 0) return true;
remove(temporary_path);
return false;
}
uint8_t gameConfigInvadersStartingLives(void)
{
return (uint8_t)readInteger("invaders.cfg", "lives", 3, 1, 9);
}
void startGameConfig(uint32_t now_ms)
{
(void)now_ms;
active = true;
view = GAME_CONFIG_GAMES;
selected_game = selected_option = 0U;
waiting_for_centre = true;
repeating = false;
held_direction = 0;
invaders_lives = gameConfigInvadersStartingLives();
}
void stopGameConfig(void)
{
active = false;
held_direction = 0;
}
void gameConfigAction(bool increase, bool decrease, uint32_t now_ms)
{
(void)now_ms;
if (!active || (!increase && !decrease)) return;
if (view == GAME_CONFIG_GAMES) {
view = GAME_CONFIG_OPTIONS;
selected_option = 0U;
invaders_lives = gameConfigInvadersStartingLives();
return;
}
if (!selectedGame()->configured) return;
if (selected_option == 0U) {
int next = invaders_lives + (increase ? 1 : -1);
if (next < 1) next = 1;
if (next > 9) next = 9;
if ((uint8_t)next == invaders_lives) return;
if (writeInvadersConfig((uint8_t)next)) invaders_lives = (uint8_t)next;
} else if (remove(CONFIG_ROOT "/invaders.cfg") == 0 || errno == ENOENT) {
invaders_lives = 3U;
}
}
static void moveGameSelection(int horizontal_direction, int vertical_direction)
{
const unsigned count = sizeof(games) / sizeof(games[0]);
const unsigned page_count = (count + GAME_CONFIG_PER_PAGE - 1U) / GAME_CONFIG_PER_PAGE;
unsigned page = selected_game / GAME_CONFIG_PER_PAGE;
unsigned column = (selected_game % GAME_CONFIG_PER_PAGE) / GAME_CONFIG_ROWS;
unsigned row = selected_game % GAME_CONFIG_ROWS;
unsigned page_items = count - page * GAME_CONFIG_PER_PAGE;
if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
unsigned rows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U;
if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;
if (horizontal_direction != 0) {
const unsigned other_column = column == 0U ? 1U : 0U;
unsigned other_rows = page_items > other_column * GAME_CONFIG_ROWS ?
page_items - other_column * GAME_CONFIG_ROWS : 0U;
if (other_rows > GAME_CONFIG_ROWS) other_rows = GAME_CONFIG_ROWS;
if (other_rows > 0U) {
column = other_column;
if (row >= other_rows) row = other_rows - 1U;
}
} else if (vertical_direction != 0) {
if ((vertical_direction < 0 && row > 0U) ||
(vertical_direction > 0 && row + 1U < rows)) {
row = vertical_direction < 0 ? row - 1U : row + 1U;
} else {
do {
page = vertical_direction < 0 ? (page + page_count - 1U) % page_count :
(page + 1U) % page_count;
page_items = count - page * GAME_CONFIG_PER_PAGE;
if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
rows = page_items > column * GAME_CONFIG_ROWS ?
page_items - column * GAME_CONFIG_ROWS : 0U;
if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;
} while (rows == 0U);
row = vertical_direction < 0 ? rows - 1U : 0U;
}
}
selected_game = page * GAME_CONFIG_PER_PAGE + column * GAME_CONFIG_ROWS + row;
}
void updateGameConfig(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
if (!active) return;
if (!joystick_valid) { waiting_for_centre = true; held_direction = 0; return; }
const int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
const int magnitude = y_offset < 0 ? -y_offset : y_offset;
if (x_magnitude <= JOYSTICK_RELEASE && magnitude <= JOYSTICK_RELEASE) {
waiting_for_centre = false;
held_direction = 0;
repeating = false;
return;
}
if (waiting_for_centre || (x_magnitude < JOYSTICK_ENTER && magnitude < JOYSTICK_ENTER)) return;
const bool horizontal = x_magnitude >= magnitude;
const int direction = horizontal ? (x_offset > 0 ? 1 : -1) : (y_offset > 0 ? -1 : 1);
const int held = horizontal ? (direction > 0 ? 1 : 2) : (direction < 0 ? 3 : 4);
if (held == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)) return;
repeating = held == held_direction;
held_direction = held;
last_move = now_ms;
if (view == GAME_CONFIG_GAMES) {
moveGameSelection(horizontal ? direction : 0, horizontal ? 0 : direction);
} else if (selectedGame()->configured) {
selected_option = selected_option == 0U ? 1U : 0U;
}
}
static void drawGameList(uint16_t *frame)
{
const unsigned count = sizeof(games) / sizeof(games[0]);
const unsigned first = selected_game / GAME_CONFIG_PER_PAGE * GAME_CONFIG_PER_PAGE;
clear(frame, COLOR_BLACK);
drawPet2001TextCentredTransparent(frame, 160, 4, "GAME CONFIG", COLOR_CYAN, 2);
for (unsigned index = first; index < count && index < first + GAME_CONFIG_PER_PAGE; ++index) {
const unsigned position = index - first;
const int x = position < GAME_CONFIG_ROWS ? 8 : 166;
const int y = 39 + (int)(position % GAME_CONFIG_ROWS) * 24;
drawPet2001TextTransparent(frame, x, y, games[index].name,
index == selected_game ? COLOR_CYAN : COLOR_YELLOW, 1);
}
drawPet2001TextCentredTransparent(frame, 160, 157,
"STICK SELECT A/K OPEN BOOT BACK", COLOR_LIGHTGREY, 1);
}
static void drawGameOptions(uint16_t *frame)
{
clear(frame, COLOR_BLACK);
drawPet2001TextCentredTransparent(frame, 160, 4, selectedGame()->name, COLOR_CYAN, 2);
if (!selectedGame()->configured) {
drawPet2001TextCentredTransparent(frame, 160, 66, "NO SETTINGS YET", COLOR_YELLOW, 2);
drawPet2001TextCentredTransparent(frame, 160, 100,
"BUILT-IN DEFAULTS ACTIVE", COLOR_LIGHTGREY, 1);
drawPet2001TextCentredTransparent(frame, 160, 157, "BOOT BACK", COLOR_LIGHTGREY, 1);
return;
}
char value[24];
snprintf(value, sizeof(value), "%u", (unsigned)invaders_lives);
drawPet2001TextTransparent(frame, 16, 43, "STARTING LIVES",
selected_option == 0U ? COLOR_CYAN : COLOR_YELLOW, 1);
drawPet2001TextTransparent(frame, 258, 43, value,
selected_option == 0U ? COLOR_CYAN : COLOR_WHITE, 1);
drawPet2001TextTransparent(frame, 16, 73, "RESTORE DEFAULTS",
selected_option == 1U ? COLOR_CYAN : COLOR_YELLOW, 1);
drawPet2001TextCentredTransparent(frame, 160, 119, "A INCREASE K DECREASE", COLOR_GREEN, 1);
drawPet2001TextCentredTransparent(frame, 160, 133, "A/K RESTORES DEFAULTS", COLOR_GREEN, 1);
drawPet2001TextCentredTransparent(frame, 160, 157, "UP/DOWN SELECT BOOT BACK", COLOR_LIGHTGREY, 1);
}
void drawGameConfig(uint16_t *frame)
{
if (!active || frame == NULL) return;
if (view == GAME_CONFIG_GAMES) drawGameList(frame);
else drawGameOptions(frame);
}