taisei/src/config.c

467 lines
11 KiB
C
Raw Normal View History

/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
2017-09-12 03:28:15 +02:00
* Copyright (c) 2011-2017, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2017, Andrei Alexeyev <akari@alienslab.net>.
*/
#include <string.h>
#include "config.h"
#include "global.h"
#include "version.h"
2017-02-17 17:03:49 +01:00
static bool config_initialized = false;
CONFIGDEFS_EXPORT ConfigEntry configdefs[] = {
#define CONFIGDEF(type,entryname,default,ufield) {type, entryname, {.ufield = default}, NULL},
2017-02-17 17:03:49 +01:00
#define CONFIGDEF_KEYBINDING(id,entryname,default) CONFIGDEF(CONFIG_TYPE_KEYBINDING, entryname, default, i)
#define CONFIGDEF_GPKEYBINDING(id,entryname,default) CONFIGDEF(CONFIG_TYPE_GPKEYBINDING, entryname, default, i)
2017-02-17 17:03:49 +01:00
#define CONFIGDEF_INT(id,entryname,default) CONFIGDEF(CONFIG_TYPE_INT, entryname, default, i)
#define CONFIGDEF_FLOAT(id,entryname,default) CONFIGDEF(CONFIG_TYPE_FLOAT, entryname, default, f)
#define CONFIGDEF_STRING(id,entryname,default) CONFIGDEF(CONFIG_TYPE_STRING, entryname, NULL, s)
CONFIGDEFS
{0}
#undef CONFIGDEF
#undef CONFIGDEF_KEYBINDING
#undef CONFIGDEF_GPKEYBINDING
#undef CONFIGDEF_INT
#undef CONFIGDEF_FLOAT
#undef CONFIGDEF_STRING
};
typedef struct ConfigEntryList {
struct ConfigEntryList *next;
struct ConfigEntryList *prev;
ConfigEntry entry;
} ConfigEntryList;
static ConfigEntryList *unknowndefs = NULL;
2017-02-17 17:03:49 +01:00
void config_init(void) {
if(config_initialized) {
return;
}
2017-02-17 17:03:49 +01:00
#define CONFIGDEF_KEYBINDING(id,entryname,default)
#define CONFIGDEF_GPKEYBINDING(id,entryname,default)
#define CONFIGDEF_INT(id,entryname,default)
#define CONFIGDEF_FLOAT(id,entryname,default)
#define CONFIGDEF_STRING(id,entryname,default) stralloc(&configdefs[CONFIG_##id].val.s, default);
2017-02-17 17:03:49 +01:00
CONFIGDEFS
2017-02-17 17:03:49 +01:00
#undef CONFIGDEF_KEYBINDING
#undef CONFIGDEF_GPKEYBINDING
#undef CONFIGDEF_INT
#undef CONFIGDEF_FLOAT
#undef CONFIGDEF_STRING
2017-02-17 17:03:49 +01:00
config_initialized = true;
}
static void config_delete_unknown_entries(void);
void config_shutdown(void) {
2017-02-17 17:03:49 +01:00
for(ConfigEntry *e = configdefs; e->name; ++e) {
if(e->type == CONFIG_TYPE_STRING) {
2017-02-17 17:03:49 +01:00
free(e->val.s);
e->val.s = NULL;
}
}
config_delete_unknown_entries();
2017-02-17 17:03:49 +01:00
}
2017-02-17 17:03:49 +01:00
void config_reset(void) {
#define CONFIGDEF(id,default,ufield) configdefs[CONFIG_##id].val.ufield = default;
#define CONFIGDEF_KEYBINDING(id,entryname,default) CONFIGDEF(id, default, i)
#define CONFIGDEF_GPKEYBINDING(id,entryname,default) CONFIGDEF(id, default, i)
#define CONFIGDEF_INT(id,entryname,default) CONFIGDEF(id, default, i)
#define CONFIGDEF_FLOAT(id,entryname,default) CONFIGDEF(id, default, f)
#define CONFIGDEF_STRING(id,entryname,default) stralloc(&configdefs[CONFIG_##id].val.s, default);
CONFIGDEFS
#undef CONFIGDEF
#undef CONFIGDEF_KEYBINDING
#undef CONFIGDEF_GPKEYBINDING
#undef CONFIGDEF_INT
#undef CONFIGDEF_FLOAT
#undef CONFIGDEF_STRING
}
2017-02-17 17:03:49 +01:00
#ifndef CONFIG_RAWACCESS
ConfigEntry* config_get(ConfigIndex idx) {
assert(idx >= 0 && idx < CONFIGIDX_NUM);
return configdefs + idx;
}
#endif
static ConfigEntry* config_find_entry(const char *name) {
2017-02-17 17:03:49 +01:00
ConfigEntry *e = configdefs;
do if(!strcmp(e->name, name)) return e; while((++e)->name);
return NULL;
}
2017-02-17 17:03:49 +01:00
KeyIndex config_key_from_scancode(int scan) {
for(int i = CONFIG_KEY_FIRST; i <= CONFIG_KEY_LAST; ++i) {
if(configdefs[i].val.i == scan) {
return CFGIDX_TO_KEYIDX(i);
}
}
return -1;
}
2017-02-17 17:03:49 +01:00
GamepadKeyIndex config_gamepad_key_from_gamepad_button(int btn) {
for(int i = CONFIG_GAMEPAD_KEY_FIRST; i <= CONFIG_GAMEPAD_KEY_LAST; ++i) {
if(configdefs[i].val.i == btn) {
return CFGIDX_TO_GPKEYIDX(i);
}
}
2012-08-15 02:41:21 +02:00
return -1;
}
2017-02-17 17:03:49 +01:00
KeyIndex config_key_from_gamepad_key(GamepadKeyIndex gpkey) {
#define CONFIGDEF_GPKEYBINDING(id,entryname,default) case GAMEPAD_##id: return id;
2012-08-15 02:41:21 +02:00
switch(gpkey) {
2017-02-17 17:03:49 +01:00
GPKEYDEFS
default: return -1;
2012-08-15 02:41:21 +02:00
}
2017-02-17 17:03:49 +01:00
#undef CONFIGDEF_GPKEYBINDING
}
2017-02-17 17:03:49 +01:00
GamepadKeyIndex config_gamepad_key_from_key(KeyIndex key) {
#define CONFIGDEF_GPKEYBINDING(id,entryname,default) case id: return GAMEPAD_##id;
switch(key) {
GPKEYDEFS
default: return -1;
}
#undef CONFIGDEF_GPKEYBINDING
2012-08-15 02:41:21 +02:00
}
2017-02-17 17:03:49 +01:00
KeyIndex config_key_from_gamepad_button(int btn) {
return config_key_from_gamepad_key(config_gamepad_key_from_gamepad_button(btn));
2012-08-15 02:41:21 +02:00
}
static void config_set_val(ConfigIndex idx, ConfigValue v) {
ConfigEntry *e = config_get(idx);
ConfigCallback callback = e->callback;
bool difference = true;
switch(e->type) {
case CONFIG_TYPE_INT:
case CONFIG_TYPE_KEYBINDING:
case CONFIG_TYPE_GPKEYBINDING:
difference = e->val.i != v.i;
break;
case CONFIG_TYPE_FLOAT:
difference = e->val.f != v.f;
break;
case CONFIG_TYPE_STRING:
difference = strcmp(e->val.s, v.s);
break;
}
if(!difference) {
return;
}
if(callback) {
e->callback = NULL;
callback(idx, v);
e->callback = callback;
return;
}
Implemented a simple and consistent logging subsystem The goal of this change is mainly to clean up Taisei's codebase and improve its console output. I've been frustrated by files littered with inconsistent printf/fprintf/warnx/errx calls for a long time, and now I actually did something about it. All the above functions are now considered deprecated and result in a compile-time warning when used. Instead, the following macros should be used: log_debug(format, ...) log_info(format, ...) log_warn(format, ...) log_err(format, ...) As you can see, all of them have the same printf-like interface. But they have different functionality and purpose: log_debug is intended for very verbose and specific information. It does nothing in release builds, much like assert(), so don't use expressions with side-effects in its arguments. log_info is for various status updates that are expected during normal operation of the program. log_warn is for non-critical failures or other things that may be worth investigating, but don't inherently render the program non-functional. log_err is for when the only choice is to give up. Like errx, it also terminates the program. Unlike errx, it actually calls abort(), which means the cleanup functions are not ran -- but on the other hand, you get a debuggable backtrace. However, if you're trying to catch programming errors, consider using assert() instead. All of them produce output that contains a timestamp, the log level identifier, the calling function's name, and the formatted message. The newline at the end of the format string is not required -- no, it is actually *prohibited*. The logging system will take care of the line breaks by itself, don't litter the code with that shit. Internally, the logging system is based on the SDL_RWops abstraction, and may have multiple, configurable destinations. This makes it easily extensible. Currently, log_debug and log_info are set to write to stdout, log_warn and log_err to stderr, and all of them also to the file log.txt in the Taisei config directory. Consequently, the nasty freopen hacks we used to make Taisei write to log files on Windows are no longer needed -- which is a very good thing, considering they probably would break if the configdir path contains UTF-8 characters. SDL_RWFromFile does not suffer this limitation. As an added bonus, it's also thread-safe. Note about printf and fprintf: in very few cases, the logging system is not a good substitute for these functions. That is, when you care about writing exactly to stdout/stderr and about exactly how the output looks. However, I insist on keeping the deprecation warnings on them to not tempt anyone to use them for logging/debugging out of habit and/or laziness. For this reason, I've added a tsfprintf function to util.c. It is functionally identical to fprintf, except it returns void. Yes, the name is deliberately ugly. Avoid using it if possible, but if you must, only use it to write to stdout or stderr. Do not write to actual files with it, use SDL_RWops.
2017-03-13 03:51:58 +01:00
#define PRINTVAL(t) log_debug("%s:" #t " = %" #t, e->name, e->val.t);
switch(e->type) {
case CONFIG_TYPE_INT:
case CONFIG_TYPE_KEYBINDING:
case CONFIG_TYPE_GPKEYBINDING:
e->val.i = v.i;
PRINTVAL(i)
break;
case CONFIG_TYPE_FLOAT:
e->val.f = v.f;
PRINTVAL(f)
break;
case CONFIG_TYPE_STRING:
stralloc(&e->val.s, v.s);
PRINTVAL(s)
break;
}
#undef PRINTVAL
}
int config_set_int(ConfigIndex idx, int val) {
ConfigValue v = {.i = val};
config_set_val(idx, v);
return config_get_int(idx);
}
double config_set_float(ConfigIndex idx, double val) {
ConfigValue v = {.f = val};
config_set_val(idx, v);
return config_get_float(idx);
}
char* config_set_str(ConfigIndex idx, const char *val) {
ConfigValue v = {.s = (char*)val};
config_set_val(idx, v);
return config_get_str(idx);
}
void config_set_callback(ConfigIndex idx, ConfigCallback callback) {
ConfigEntry *e = config_get(idx);
assert(e->callback == NULL);
e->callback = callback;
}
static ConfigEntry* config_get_unknown_entry(const char *name) {
ConfigEntry *e;
ConfigEntryList *l;
for(l = unknowndefs; l; l = l->next) {
if(!strcmp(name, l->entry.name)) {
return &l->entry;
}
}
2017-11-21 15:45:01 +01:00
l = (ConfigEntryList*)list_push((List**)&unknowndefs, malloc(sizeof(ConfigEntryList)));
e = &l->entry;
memset(e, 0, sizeof(ConfigEntry));
stralloc(&e->name, name);
e->type = CONFIG_TYPE_STRING;
return e;
}
static void config_set_unknown(const char *name, const char *val) {
stralloc(&config_get_unknown_entry(name)->val.s, val);
}
2017-11-21 15:45:01 +01:00
static void* config_delete_unknown_entry(List **list, List *lentry, void *arg) {
ConfigEntry *e = &((ConfigEntryList*)lentry)->entry;
free(e->name);
free(e->val.s);
2017-11-21 15:45:01 +01:00
free(list_unlink(list, lentry));
2017-11-21 15:45:01 +01:00
return NULL;
}
static void config_delete_unknown_entries(void) {
2017-11-21 15:45:01 +01:00
list_foreach((List**)&unknowndefs, config_delete_unknown_entry, NULL);
}
2017-04-18 21:48:18 +02:00
void config_save(void) {
SDL_RWops *out = vfs_open(CONFIG_FILE, VFS_MODE_WRITE);
ConfigEntry *e = configdefs;
if(!out) {
log_warn("VFS error: %s", vfs_get_error());
return;
}
SDL_RWprintf(out, "# Generated by %s %s\n", TAISEI_VERSION_FULL, TAISEI_VERSION_BUILD_TYPE);
do switch(e->type) {
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_INT:
2017-03-12 22:40:19 +01:00
SDL_RWprintf(out, "%s = %i\n", e->name, e->val.i);
break;
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_KEYBINDING:
2017-03-12 22:40:19 +01:00
SDL_RWprintf(out, "%s = %s\n", e->name, SDL_GetScancodeName(e->val.i));
break;
case CONFIG_TYPE_GPKEYBINDING:
SDL_RWprintf(out, "%s = %s\n", e->name, SDL_GameControllerGetStringForButton(e->val.i));
break;
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_STRING:
2017-03-12 22:40:19 +01:00
SDL_RWprintf(out, "%s = %s\n", e->name, e->val.s);
break;
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_FLOAT:
2017-03-12 22:40:19 +01:00
SDL_RWprintf(out, "%s = %f\n", e->name, e->val.f);
2017-02-17 17:03:49 +01:00
break;
} while((++e)->name);
if(unknowndefs) {
2017-03-12 22:40:19 +01:00
SDL_RWprintf(out, "# The following options were not recognized by taisei\n");
for(ConfigEntryList *l = unknowndefs; l; l = l->next) {
e = &l->entry;
2017-03-12 22:40:19 +01:00
SDL_RWprintf(out, "%s = %s\n", e->name, e->val.s);
}
}
2017-03-12 22:40:19 +01:00
SDL_RWclose(out);
2017-04-18 21:48:18 +02:00
char *sp = vfs_repr(CONFIG_FILE, true);
2017-04-18 21:48:18 +02:00
log_info("Saved config '%s'", sp);
free(sp);
}
#define INTOF(s) ((int)strtol(s, NULL, 10))
#define FLOATOF(s) ((float)strtod(s, NULL))
typedef struct ConfigParseState {
int first_entry;
} ConfigParseState;
static void config_set(const char *key, const char *val, void *data) {
2017-02-17 17:03:49 +01:00
ConfigEntry *e = config_find_entry(key);
ConfigParseState *state = data;
if(!e) {
Implemented a simple and consistent logging subsystem The goal of this change is mainly to clean up Taisei's codebase and improve its console output. I've been frustrated by files littered with inconsistent printf/fprintf/warnx/errx calls for a long time, and now I actually did something about it. All the above functions are now considered deprecated and result in a compile-time warning when used. Instead, the following macros should be used: log_debug(format, ...) log_info(format, ...) log_warn(format, ...) log_err(format, ...) As you can see, all of them have the same printf-like interface. But they have different functionality and purpose: log_debug is intended for very verbose and specific information. It does nothing in release builds, much like assert(), so don't use expressions with side-effects in its arguments. log_info is for various status updates that are expected during normal operation of the program. log_warn is for non-critical failures or other things that may be worth investigating, but don't inherently render the program non-functional. log_err is for when the only choice is to give up. Like errx, it also terminates the program. Unlike errx, it actually calls abort(), which means the cleanup functions are not ran -- but on the other hand, you get a debuggable backtrace. However, if you're trying to catch programming errors, consider using assert() instead. All of them produce output that contains a timestamp, the log level identifier, the calling function's name, and the formatted message. The newline at the end of the format string is not required -- no, it is actually *prohibited*. The logging system will take care of the line breaks by itself, don't litter the code with that shit. Internally, the logging system is based on the SDL_RWops abstraction, and may have multiple, configurable destinations. This makes it easily extensible. Currently, log_debug and log_info are set to write to stdout, log_warn and log_err to stderr, and all of them also to the file log.txt in the Taisei config directory. Consequently, the nasty freopen hacks we used to make Taisei write to log files on Windows are no longer needed -- which is a very good thing, considering they probably would break if the configdir path contains UTF-8 characters. SDL_RWFromFile does not suffer this limitation. As an added bonus, it's also thread-safe. Note about printf and fprintf: in very few cases, the logging system is not a good substitute for these functions. That is, when you care about writing exactly to stdout/stderr and about exactly how the output looks. However, I insist on keeping the deprecation warnings on them to not tempt anyone to use them for logging/debugging out of habit and/or laziness. For this reason, I've added a tsfprintf function to util.c. It is functionally identical to fprintf, except it returns void. Yes, the name is deliberately ugly. Avoid using it if possible, but if you must, only use it to write to stdout or stderr. Do not write to actual files with it, use SDL_RWops.
2017-03-13 03:51:58 +01:00
log_warn("Unknown setting '%s'", key);
config_set_unknown(key, val);
if(state->first_entry < 0) {
state->first_entry = CONFIGIDX_NUM;
}
return;
}
if(state->first_entry < 0) {
state->first_entry = (intptr_t)(e - configdefs);
}
switch(e->type) {
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_INT:
e->val.i = INTOF(val);
break;
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_KEYBINDING: {
SDL_Scancode scan = SDL_GetScancodeFromName(val);
if(scan == SDL_SCANCODE_UNKNOWN) {
Implemented a simple and consistent logging subsystem The goal of this change is mainly to clean up Taisei's codebase and improve its console output. I've been frustrated by files littered with inconsistent printf/fprintf/warnx/errx calls for a long time, and now I actually did something about it. All the above functions are now considered deprecated and result in a compile-time warning when used. Instead, the following macros should be used: log_debug(format, ...) log_info(format, ...) log_warn(format, ...) log_err(format, ...) As you can see, all of them have the same printf-like interface. But they have different functionality and purpose: log_debug is intended for very verbose and specific information. It does nothing in release builds, much like assert(), so don't use expressions with side-effects in its arguments. log_info is for various status updates that are expected during normal operation of the program. log_warn is for non-critical failures or other things that may be worth investigating, but don't inherently render the program non-functional. log_err is for when the only choice is to give up. Like errx, it also terminates the program. Unlike errx, it actually calls abort(), which means the cleanup functions are not ran -- but on the other hand, you get a debuggable backtrace. However, if you're trying to catch programming errors, consider using assert() instead. All of them produce output that contains a timestamp, the log level identifier, the calling function's name, and the formatted message. The newline at the end of the format string is not required -- no, it is actually *prohibited*. The logging system will take care of the line breaks by itself, don't litter the code with that shit. Internally, the logging system is based on the SDL_RWops abstraction, and may have multiple, configurable destinations. This makes it easily extensible. Currently, log_debug and log_info are set to write to stdout, log_warn and log_err to stderr, and all of them also to the file log.txt in the Taisei config directory. Consequently, the nasty freopen hacks we used to make Taisei write to log files on Windows are no longer needed -- which is a very good thing, considering they probably would break if the configdir path contains UTF-8 characters. SDL_RWFromFile does not suffer this limitation. As an added bonus, it's also thread-safe. Note about printf and fprintf: in very few cases, the logging system is not a good substitute for these functions. That is, when you care about writing exactly to stdout/stderr and about exactly how the output looks. However, I insist on keeping the deprecation warnings on them to not tempt anyone to use them for logging/debugging out of habit and/or laziness. For this reason, I've added a tsfprintf function to util.c. It is functionally identical to fprintf, except it returns void. Yes, the name is deliberately ugly. Avoid using it if possible, but if you must, only use it to write to stdout or stderr. Do not write to actual files with it, use SDL_RWops.
2017-03-13 03:51:58 +01:00
log_warn("Unknown key '%s'", val);
} else {
2017-02-17 17:03:49 +01:00
e->val.i = scan;
}
break;
}
case CONFIG_TYPE_GPKEYBINDING: {
SDL_GameControllerButton btn = SDL_GameControllerGetButtonFromString(val);
if(btn == SDL_CONTROLLER_BUTTON_INVALID) {
log_warn("Unknown gamepad key '%s'", val);
} else {
e->val.i = btn;
}
break;
}
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_STRING:
stralloc(&e->val.s, val);
break;
2017-02-17 17:03:49 +01:00
case CONFIG_TYPE_FLOAT:
e->val.f = FLOATOF(val);
break;
}
}
#undef INTOF
#undef FLOATOF
typedef void (*ConfigUpgradeFunc)(void);
static void config_upgrade_1(void) {
// disable vsync by default
config_set_int(CONFIG_VSYNC, 0);
// this version also changes meaning of the vsync value
// previously it was: 0 = on, 1 = off, 2 = adaptive, because lachs0r doesn't know how my absolutely genius options menu works.
// now it is: 0 = off, 1 = on, 2 = adaptive, as it should be.
}
static ConfigUpgradeFunc config_upgrades[] = {
/*
To bump the config version and add an upgrade state, simply append an upgrade function to this array.
NEVER reorder these entries, remove them, etc, ONLY append.
If a stage is no longer needed (nothing useful needs to be done, e.g. the later stages would override it),
then replace the function pointer with NULL.
*/
config_upgrade_1,
};
static void config_apply_upgrades(int start) {
int num_upgrades = sizeof(config_upgrades) / sizeof(ConfigUpgradeFunc);
if(start > num_upgrades) {
log_warn("Config seems to be from a newer version of Taisei (config version %i, ours is %i)", start, num_upgrades);
return;
}
for(int upgrade = start; upgrade < num_upgrades; ++upgrade) {
if(config_upgrades[upgrade]) {
log_info("Upgrading config to version %i", upgrade + 1);
config_upgrades[upgrade]();
} else {
log_debug("Upgrade to version %i is a no-op", upgrade + 1);
}
}
}
2017-04-18 21:48:18 +02:00
void config_load(void) {
2017-02-17 17:03:49 +01:00
config_init();
2017-04-18 21:48:18 +02:00
char *config_path = vfs_repr(CONFIG_FILE, true);
SDL_RWops *config_file = vfs_open(CONFIG_FILE, VFS_MODE_READ);
2017-04-18 21:48:18 +02:00
if(config_file) {
log_info("Loading configuration from %s", config_path);
ConfigParseState *state = malloc(sizeof(ConfigParseState));
state->first_entry = -1;
if(!parse_keyvalue_stream_cb(config_file, config_set, state)) {
log_warn("Errors occured while parsing the configuration file");
}
if(state->first_entry != CONFIG_VERSION) {
// config file was likely written by an old version of taisei that is unaware of the upgrade mechanism.
config_set_int(CONFIG_VERSION, 0);
}
free(state);
SDL_RWclose(config_file);
config_apply_upgrades(config_get_int(CONFIG_VERSION));
} else {
VFSInfo i = vfs_query(CONFIG_FILE);
if(i.error) {
log_warn("VFS error: %s", vfs_get_error());
} else if(i.exists) {
log_warn("Config file %s is not readable", config_path);
}
}
free(config_path);
// set config version to the latest
config_set_int(CONFIG_VERSION, sizeof(config_upgrades) / sizeof(ConfigUpgradeFunc));
}