Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saving and loading game settings from a config #639

Open
wants to merge 18 commits into
base: release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
include config.mk

cflags := -Isrc/brogue -Isrc/platform -Isrc/variants -std=c99 \
cflags := -Isrc/brogue -Isrc/platform -Isrc/variants -Isrc/cjson -std=c99 \
-Wall -Wpedantic -Werror=implicit -Wno-parentheses -Wno-unused-result \
-Wformat -Werror=format-security -Wformat-overflow=0 -Wmissing-prototypes
libs := -lm
cppflags := -DDATADIR=$(DATADIR)

sources := $(wildcard src/brogue/*.c) $(wildcard src/variants/*.c) $(addprefix src/platform/,main.c platformdependent.c null-platform.c)
sources := $(wildcard src/brogue/*.c) $(wildcard src/cjson/*.c) $(wildcard src/variants/*.c) $(addprefix src/platform/,main.c platformdependent.c null-platform.c)
objects :=

ifeq ($(SYSTEM),WINDOWS)
Expand Down
1 change: 1 addition & 0 deletions changes/saved-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Game settings are saved to a configuration file, persisting between game launches. The saved settings include game variant (vanilla or rapid), graphical mode, replay speed, color effects, stealth range visibility, wizard mode, and easy mode.
255 changes: 255 additions & 0 deletions src/brogue/Config.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
#include "../cjson/cJSON.h"
#include "Rogue.h"
#include "GlobalsBase.h"

typedef struct configParams {
short playbackDelayPerTurn;
short gameVariant;
short graphicsMode;
boolean displayStealthRangeMode;
boolean trueColorMode;
boolean wizard;
boolean easyMode;
Comment on lines +9 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional suggested naming:

stealthRangeEnabled
colorEffectsEnabled
wizardModeEnabled
easyModeEnabled

} configParams;

typedef enum {
INT_TYPE,
BOOLEAN_TYPE,
ENUM_STRING // a json string that needs to be mapped to an Enum
} fieldType;

typedef struct {
int min;
int max;
} intRange;

typedef union {
const char** enumMappings;
intRange intRange;
} fieldValidator;

typedef struct {
const char* name;
void* paramPointer; // a pointer to a field of configParams struct
fieldType type;
fieldValidator validator;
} configField;

static const char* JSON_FILENAME = "brogue_config.json";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional. How about brogue.config ?


// maps json strings to the gameVariant Enum
static const char* variantMappings[] = {"brogue", "rapid", NULL};

// maps json strings to the graphicsModes Enum
static const char* graphicsModeMappings[] = {"text", "tiles", "hybrid", NULL};

static intRange playbackDelayRange = {MIN_PLAYBACK_DELAY, MAX_PLAYBACK_DELAY};

static configParams createDefaultConfig() {
configParams config;

config.playbackDelayPerTurn = DEFAULT_PLAYBACK_DELAY;
config.gameVariant = VARIANT_BROGUE;
config.graphicsMode = TEXT_GRAPHICS;
config.displayStealthRangeMode = false;
config.trueColorMode = false;
config.easyMode = false;
config.wizard = false;

return config;
}

static char* loadConfigFile() {
FILE* jsonFile = fopen(JSON_FILENAME, "r");

if (!jsonFile) {
return NULL;
}

fseek(jsonFile, 0, SEEK_END);
long fileSize = ftell(jsonFile);
fseek(jsonFile, 0, SEEK_SET);

char* buffer = (char*)malloc(fileSize + 1);

if (!buffer) {
fclose(jsonFile);
return NULL;
}

fread(buffer, 1, fileSize, jsonFile);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should probably check the number of bytes written by fread, and return an error if it's less than fileSize

fclose(jsonFile);

buffer[fileSize] = '\0';

return buffer;
}

static configField* getFieldEntries(configParams* config) {
int numFields = 7;

configField fieldDescriptors[] = {
{"gameVariant", &(config->gameVariant), ENUM_STRING, {.enumMappings = variantMappings}},
{"graphicsMode", &(config->graphicsMode), ENUM_STRING, {.enumMappings = graphicsModeMappings}},
{"playbackDelayPerTurn", &(config->playbackDelayPerTurn), INT_TYPE, {.intRange = playbackDelayRange}},
{"displayStealthRangeMode", &(config->displayStealthRangeMode), BOOLEAN_TYPE},
{"trueColorMode", &(config->trueColorMode), BOOLEAN_TYPE},
{"wizard", &(config->wizard), BOOLEAN_TYPE},
{"easyMode", &(config->easyMode), BOOLEAN_TYPE},
{NULL}};

configField* entries = calloc(numFields + 1, sizeof(configField));

for (int i = 0; i < numFields; i++) {
entries[i] = fieldDescriptors[i];
}

return entries;
}

static short mapStringToEnum(const char* inputString, const char** mappings) {
for (short i = 0; mappings[i]; i++) {
if (strcmp(inputString, mappings[i]) == 0) {
return i;
}
}
return -1;
}

static void parseConfigValues(const char* jsonString, configParams* config) {
if (!jsonString || !config) {
return; // Invalid input
}

cJSON* root = cJSON_Parse(jsonString);

if (!root) {
return; // JSON parsing error
}

configField* entries = getFieldEntries(config);

for (int i = 0; entries[i].name; i++) {
cJSON* jsonField = cJSON_GetObjectItem(root, entries[i].name);

if (jsonField) {
switch (entries[i].type) {
case INT_TYPE:
if (cJSON_IsNumber(jsonField)) {
short value = jsonField->valueint;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check for overflow/underflow, since jsonField->valueint is an int (typically 32 bits) but value is a short (typically 16 bits).

intRange valueRange = entries[i].validator.intRange;

if (value >= valueRange.min && value <= valueRange.max) {
*((short*)entries[i].paramPointer) = value;
}
}
break;

case BOOLEAN_TYPE:
if (cJSON_IsBool(jsonField)) {
*((boolean*)entries[i].paramPointer) = jsonField->valueint;
Copy link
Contributor

@Nathan-Fenner Nathan-Fenner Dec 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading the documentation for cJson correctly, I believe you need to check cJSON_IsTrue(...) instead of (...)->valueint, which doesn't look to be set for booleans.

}
break;

case ENUM_STRING:
if (cJSON_IsString(jsonField)) {
const char* modeString = jsonField->valuestring;
short mode = mapStringToEnum(modeString, entries[i].validator.enumMappings);

if (mode != -1) {
*((short*)entries[i].paramPointer) = mode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if all of the enum types are definitely short-sized. It would be nice to have some better type-safety here, maybe a constructor function for enum struct field definitions that explicitly only accepts short*?

}
}
break;

default:
break;
}
}
}

free(entries);
cJSON_Delete(root);
}

static char* createJsonString(configParams* config) {
cJSON* root = cJSON_CreateObject();

configField* entries = getFieldEntries(config);

for (int i = 0; entries[i].name; i++) {
switch (entries[i].type) {
case INT_TYPE: {
short short_value = *((short*)entries[i].paramPointer);
cJSON_AddNumberToObject(root, entries[i].name, short_value);
break;
}
case BOOLEAN_TYPE: {
boolean bool_value = *((boolean*)entries[i].paramPointer);
cJSON_AddBoolToObject(root, entries[i].name, bool_value);
break;
}
case ENUM_STRING: {
short enum_value = *((short*)entries[i].paramPointer);
const char* string_value = entries[i].validator.enumMappings[enum_value];
cJSON_AddStringToObject(root, entries[i].name, string_value);
break;
}

default:
break;
}
}

char* jsonString = cJSON_Print(root);

free(entries);
cJSON_Delete(root);

return jsonString;
}

void readFromConfig(enum graphicsModes* initialGraphics) {
char* jsonString = loadConfigFile();

configParams config = createDefaultConfig();

parseConfigValues(jsonString, &config);

rogue.wizard = config.wizard;
rogue.easyMode = config.easyMode;
rogue.displayStealthRangeMode = config.displayStealthRangeMode;
rogue.trueColorMode = config.trueColorMode;
rogue.playbackDelayPerTurn = config.playbackDelayPerTurn;

gameVariant = config.gameVariant;
*initialGraphics = config.graphicsMode;

free(jsonString);
}

void writeIntoConfig() {
configParams config;

FILE* file = fopen(JSON_FILENAME, "w");

if (!file) {
return;
}

config.wizard = rogue.wizard;
config.easyMode = rogue.easyMode;
config.displayStealthRangeMode = rogue.displayStealthRangeMode;
config.trueColorMode = rogue.trueColorMode;
config.playbackDelayPerTurn = rogue.playbackDelayPerTurn;

config.gameVariant = gameVariant;
config.graphicsMode = graphicsMode;

char* jsonString = createJsonString(&config);

fprintf(file, "%s", jsonString);

fclose(file);
free(jsonString);
}
5 changes: 2 additions & 3 deletions src/brogue/Recordings.c
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,6 @@ void initRecording() {

if (rogue.playbackMode) {
lengthOfPlaybackFile = 100000; // so recall functions don't freak out
rogue.playbackDelayPerTurn = DEFAULT_PLAYBACK_DELAY;
rogue.playbackDelayThisTurn = rogue.playbackDelayPerTurn;
rogue.playbackPaused = false;

Expand Down Expand Up @@ -879,7 +878,7 @@ boolean executePlaybackInput(rogueEvent *recordingInput) {
switch (key) {
case UP_ARROW:
case UP_KEY:
newDelay = max(1, min(rogue.playbackDelayPerTurn * 2/3, rogue.playbackDelayPerTurn - 1));
newDelay = max(MIN_PLAYBACK_DELAY, min(rogue.playbackDelayPerTurn * 2/3, rogue.playbackDelayPerTurn - 1));
if (newDelay != rogue.playbackDelayPerTurn) {
flashTemporaryAlert(" Faster ", 300);
}
Expand All @@ -888,7 +887,7 @@ boolean executePlaybackInput(rogueEvent *recordingInput) {
return true;
case DOWN_ARROW:
case DOWN_KEY:
newDelay = min(3000, max(rogue.playbackDelayPerTurn * 3/2, rogue.playbackDelayPerTurn + 1));
newDelay = min(MAX_PLAYBACK_DELAY, max(rogue.playbackDelayPerTurn * 3/2, rogue.playbackDelayPerTurn + 1));
if (newDelay != rogue.playbackDelayPerTurn) {
flashTemporaryAlert(" Slower ", 300);
}
Expand Down
7 changes: 7 additions & 0 deletions src/brogue/Rogue.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ typedef struct windowpos {
#define COLOR_ESCAPE 25
#define COLOR_VALUE_INTERCEPT 25

#define MIN_PLAYBACK_DELAY 1
#define MAX_PLAYBACK_DELAY 3000


// variants supported in this code base
enum gameVariant {
VARIANT_BROGUE,
Expand Down Expand Up @@ -3455,6 +3459,9 @@ extern "C" {

void dijkstraScan(short **distanceMap, short **costMap, boolean useDiagonals);

void readFromConfig(enum graphicsModes* initialGraphics);
void writeIntoConfig(void);

#if defined __cplusplus
}
#endif
Expand Down
4 changes: 3 additions & 1 deletion src/brogue/RogueMain.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ void initializeRogue(uint64_t seed) {
item *theItem;
boolean playingback, playbackFF, playbackPaused, wizard, easy, displayStealthRangeMode;
boolean trueColorMode;
short oldRNG;
short oldRNG, playbackDelay;
char currentGamePath[BROGUE_FILENAME_MAX];

playingback = rogue.playbackMode; // the only animals that need to go on the ark
Expand All @@ -197,6 +197,7 @@ void initializeRogue(uint64_t seed) {
easy = rogue.easyMode;
displayStealthRangeMode = rogue.displayStealthRangeMode;
trueColorMode = rogue.trueColorMode;
playbackDelay = rogue.playbackDelayPerTurn;

strcpy(currentGamePath, rogue.currentGamePath);

Expand All @@ -212,6 +213,7 @@ void initializeRogue(uint64_t seed) {
rogue.easyMode = easy;
rogue.displayStealthRangeMode = displayStealthRangeMode;
rogue.trueColorMode = trueColorMode;
rogue.playbackDelayPerTurn = playbackDelay;

rogue.gameHasEnded = false;
rogue.gameInProgress = true;
Expand Down
Loading