102 Patches: Detours to the Rescue
C reference for DttR maintainers and modders.
Loading...
Searching...
No Matches
io.c
Go to the documentation of this file.
1#include "config_internal.h"
2#include "sds.h"
3#include "yyjson.h"
4#include <dttr_log.h>
5
6#include <float.h>
7#include <limits.h>
8#include <math.h>
9#include <stdarg.h>
10#include <stdint.h>
11#include <string.h>
12
13#include "dttr_errors.h"
14
15static sds errors;
16
17static void errors_clear() {
18 if (errors) {
19 sdsclear(errors);
20 } else {
21 errors = sdsempty();
22 }
23}
24
25static void errors_addf(const char *fmt, ...) {
26 if (!errors) {
27 return;
28 }
29
30 va_list args;
31 va_start(args, fmt);
32 errors = sdscatvprintf(errors, fmt, args);
33 va_end(args);
34 errors = sdscat(errors, "\n");
35}
36
37static bool errors_show() {
38 if (!errors || sdslen(errors) < 1) {
39 return false;
40 }
41
42 DTTR_LOG_ERROR("Configuration Error: %s", errors);
43 return true;
44}
45
46const char *DTTR_Config_LastError() {
47 return errors && sdslen(errors) > 0 ? errors : NULL;
48}
49
51 const char *section,
52 const char *key,
53 const char *value
54) {
55 if (section) {
56 errors_addf("%s.%s: invalid value \"%s\"", section, key, value);
57 return;
58 }
59
60 errors_addf("%s: invalid value \"%s\"", key, value);
61}
62
63static void config_apply_buttons(DTTR_Config *config, yyjson_val *buttons) {
64 if (!yyjson_is_obj(buttons)) {
65 return;
66 }
67
69 yyjson_val *key;
70 yyjson_val *val;
71 size_t idx;
72 size_t max;
73
74 yyjson_obj_foreach(buttons, idx, max, key, val) {
75 const char *key_str = yyjson_get_str(key);
76
77 int source = -1;
78 if (!config_parse_gamepad_source(key_str, &source)) {
79 errors_addf("gamepad.buttons.%s: unknown SDL input", key_str);
80 continue;
81 }
82
83 const char *value = yyjson_get_str(val);
84 if (!value) {
85 errors_addf("gamepad.buttons.%s: expected string value", key_str);
86 continue;
87 }
88
89 int action = DTTR_GAMEPAD_MAPPING_NONE;
90 if (!config_parse_game_action(value, &action)) {
91 errors_addf("gamepad.buttons.%s: invalid action \"%s\"", key_str, value);
92 continue;
93 }
94
95 config->gamepad_button_map[source] = action;
96 }
97}
98
99static void config_apply_disabled_mods(DTTR_Config *config, yyjson_val *disabled_mods) {
100 if (!yyjson_is_arr(disabled_mods)) {
101 return;
102 }
103
104 config->disabled_mod_count = 0;
105
106 yyjson_val *val;
107 size_t idx;
108 size_t max;
109 yyjson_arr_foreach(disabled_mods, idx, max, val) {
110 const char *mod_filename = yyjson_get_str(val);
111 if (!mod_filename || !mod_filename[0]) {
112 errors_addf("modding.disabled_mods[%zu]: expected DLL filename", idx);
113 continue;
114 }
115
116 if (!DTTR_Config_SetModEnabled(config, mod_filename, false)) {
118 "modding.disabled_mods[%zu]: could not store \"%s\"",
119 idx,
120 mod_filename
121 );
122 }
123 }
124}
125
126static bool validate_schema_major_version(yyjson_val *root, const char *filename) {
127 yyjson_val *version = yyjson_obj_get(root, "schema_major_version");
128 if (!version) {
129 errors_addf("%s: missing required schema_major_version", filename);
130 errors_show();
131 return false;
132 }
133
134 if (!yyjson_is_int(version)) {
135 errors_addf("%s.schema_major_version: expected integer value", filename);
136 errors_show();
137 return false;
138 }
139
140 const int64_t parsed_version = yyjson_get_int(version);
141 if (parsed_version < INT_MIN || parsed_version > INT_MAX) {
142 errors_addf("%s.schema_major_version: integer value out of range", filename);
143 errors_show();
144 return false;
145 }
146
147 const int schema_major_version = (int)parsed_version;
148 if (schema_major_version == DTTR_CONFIG_SCHEMA_MAJOR_VERSION) {
149 return true;
150 }
151
153 "%s.schema_major_version: unsupported major version %d (expected %d)",
154 filename,
155 schema_major_version,
157 );
158 errors_show();
159 return false;
160}
161
163 DTTR_Config *config,
164 const DTTR_ConfigFieldSpec *spec,
165 yyjson_val *val
166) {
167 if (!config || !spec || !val) {
168 return false;
169 }
170
171 char *const field = ((char *)config) + spec->offset;
172 switch (spec->value_type) {
173 case CONFIG_BOOL:
174 if (!yyjson_is_bool(val)) {
175 return false;
176 }
177
178 *(bool *)field = yyjson_get_bool(val);
179 return true;
180
181 case CONFIG_INT:
182 if (!yyjson_is_int(val)) {
183 return false;
184 }
185
186 const int64_t parsed = yyjson_get_int(val);
187 if (parsed < INT_MIN || parsed > INT_MAX) {
188 return false;
189 }
190
191 *(int *)field = (int)parsed;
192 return true;
193
194 case CONFIG_FLOAT: {
195 if (!yyjson_is_num(val)) {
196 return false;
197 }
198
199 const double parsed = yyjson_get_num(val);
200 if (!isfinite(parsed) || parsed < -(double)FLT_MAX || parsed > (double)FLT_MAX) {
201 return false;
202 }
203
204 *(float *)field = (float)parsed;
205 return true;
206 }
207
208 case CONFIG_STRING:
209 if (yyjson_is_null(val)) {
210 return config_apply_entry(config, spec->section, spec->key, "");
211 }
212
213 if (!yyjson_is_str(val)) {
214 return false;
215 }
216
217 return config_apply_entry(config, spec->section, spec->key, yyjson_get_str(val));
218
219 default:
220 if (!yyjson_is_str(val)) {
221 return false;
222 }
223
224 return config_apply_entry(config, spec->section, spec->key, yyjson_get_str(val));
225 }
226}
227
228static void apply_section(yyjson_val *obj, const char *section) {
229 if (!yyjson_is_obj(obj)) {
230 return;
231 }
232
233 yyjson_val *key;
234 yyjson_val *val;
235 size_t idx;
236 size_t max;
237
238 yyjson_obj_foreach(obj, idx, max, key, val) {
239 if (yyjson_is_obj(val)) {
240 continue;
241 }
242
243 const char *key_str = yyjson_get_str(key);
244 const DTTR_ConfigFieldSpec *spec = config_schema_find(section, key_str);
245 if (!spec) {
246 if (!yyjson_is_arr(val)) {
247 errors_add_invalid_value(section, key_str, yyjson_get_type_desc(val));
248 }
249
250 continue;
251 }
252
253 if (!config_apply_json_value(&dttr_config, spec, val)) {
254 errors_add_invalid_value(section, key_str, yyjson_get_type_desc(val));
255 }
256 }
257}
258
259bool DTTR_Config_Load(const char *filename) {
260 errors_clear();
261
262 if (!filename || !filename[0]) {
263 errors_addf("Load failed: empty filename");
264 errors_show();
266 return false;
267 }
268
270
271 yyjson_read_err err;
272 yyjson_doc *doc = yyjson_read_file(filename, 0, NULL, &err);
273
274 if (!doc) {
275 if (err.code == YYJSON_READ_ERROR_FILE_OPEN) {
276 DTTR_LOG_WARN("File '%s' not found. Creating it from defaults.", filename);
277 if (!DTTR_Config_Save(filename, &dttr_config)) {
279 "Could not create default config %s; continuing with built-in "
280 "defaults",
281 filename
282 );
283 }
284
285 return true;
286 }
287
288 if (err.code == YYJSON_READ_ERROR_EMPTY_CONTENT) {
289 DTTR_LOG_WARN("File '%s' is empty. Using defaults.", filename);
290 return true;
291 }
292
293 DTTR_LOG_ERROR("JSON parse failed: %s at position %zu", err.msg, err.pos);
294 errors_addf("Failed to parse %s (%s at position %zu)", filename, err.msg, err.pos);
295 errors_show();
296 return false;
297 }
298
299 yyjson_val *root = yyjson_doc_get_root(doc);
300 if (!validate_schema_major_version(root, filename)) {
301 yyjson_doc_free(doc);
302 return false;
303 }
304
305 apply_section(root, NULL);
306 apply_section(yyjson_obj_get(root, "graphics"), "graphics");
307 apply_section(yyjson_obj_get(root, "audio"), "audio");
308 yyjson_val *modding = yyjson_obj_get(root, "modding");
309 apply_section(modding, "modding");
310 config_apply_disabled_mods(&dttr_config, yyjson_obj_get(modding, "disabled_mods"));
311
312 yyjson_val *gamepad = yyjson_obj_get(root, "gamepad");
313 if (yyjson_is_obj(gamepad)) {
314 apply_section(gamepad, "gamepad");
315 config_apply_buttons(&dttr_config, yyjson_obj_get(gamepad, "buttons"));
316 }
317
318 yyjson_doc_free(doc);
319 return !errors_show();
320}
321
322static bool obj_add_strcpy(
323 yyjson_mut_doc *doc,
324 yyjson_mut_val *obj,
325 const char *key,
326 const char *value
327) {
328 return yyjson_mut_obj_add_strcpy(doc, obj, key, value ? value : "");
329}
330
331typedef struct {
332 yyjson_mut_val *root;
333 yyjson_mut_val *graphics;
334 yyjson_mut_val *audio;
335 yyjson_mut_val *modding;
336 yyjson_mut_val *gamepad;
338
339static yyjson_mut_val *config_object_for_section(
340 const config_json_objects *objects,
341 const char *section
342) {
343 if (config_sections_match(section, NULL)) {
344 return objects->root;
345 }
346
347 if (config_sections_match(section, "graphics")) {
348 return objects->graphics;
349 }
350
351 if (config_sections_match(section, "audio")) {
352 return objects->audio;
353 }
354
355 if (config_sections_match(section, "modding")) {
356 return objects->modding;
357 }
358
359 if (config_sections_match(section, "gamepad")) {
360 return objects->gamepad;
361 }
362
363 return NULL;
364}
365
366static const char *config_format_field_string(
367 const DTTR_Config *config,
368 const DTTR_ConfigFieldSpec *spec
369) {
370 const char *const field = ((const char *)config) + spec->offset;
371 switch (spec->value_type) {
373 return config_format_scaling_fit(*(const DTTR_ScalingMode *)field);
375 return config_format_scaling_method(*(const DTTR_ScalingMethod *)field);
377 return config_format_graphics_api(*(const DTTR_GraphicsApi *)field);
379 return config_format_present_filter(*(const SDL_GPUFilter *)field);
380 case CONFIG_LOG_LEVEL:
381 return config_format_log_level(*(const int *)field);
383 return config_format_minidump_type(*(const DTTR_MinidumpType *)field);
387 return config_format_gamepad_axis(*(const int *)field);
388 case CONFIG_STRING:
389 return field;
390 default:
391 return NULL;
392 }
393}
394
396 yyjson_mut_doc *doc,
397 const config_json_objects *objects,
398 const DTTR_Config *config,
399 const DTTR_ConfigFieldSpec *spec
400) {
401 yyjson_mut_val *obj = config_object_for_section(objects, spec->section);
402 if (!obj) {
403 return false;
404 }
405
406 const char *const field = ((const char *)config) + spec->offset;
407 switch (spec->value_type) {
408 case CONFIG_BOOL:
409 return yyjson_mut_obj_add_bool(doc, obj, spec->key, *(const bool *)field);
410 case CONFIG_INT:
411 return yyjson_mut_obj_add_int(doc, obj, spec->key, *(const int *)field);
412 case CONFIG_FLOAT:
413 return yyjson_mut_obj_add_real(doc, obj, spec->key, *(const float *)field);
414 default:
415 return obj_add_strcpy(
416 doc,
417 obj,
418 spec->key,
419 config_format_field_string(config, spec)
420 );
421 }
422}
423
425 yyjson_mut_doc *doc,
426 const config_json_objects *objects,
427 const DTTR_Config *config
428) {
429 const int count = DTTR_Config_SchemaCount();
430 for (int i = 0; i < count; i++) {
432 if (!spec || !config_add_schema_field(doc, objects, config, spec)) {
433 return false;
434 }
435 }
436
437 return true;
438}
439
441 yyjson_mut_doc *doc,
442 yyjson_mut_val *modding,
443 const DTTR_Config *config
444) {
445 yyjson_mut_val *disabled_mods = yyjson_mut_obj_add_arr(doc, modding, "disabled_mods");
446 if (!disabled_mods) {
447 return false;
448 }
449
450 for (int i = 0; i < config->disabled_mod_count; i++) {
451 if (!yyjson_mut_arr_add_strcpy(doc, disabled_mods, config->disabled_mods[i])) {
452 return false;
453 }
454 }
455
456 return true;
457}
458
460 yyjson_mut_doc *doc,
461 yyjson_mut_val *buttons,
462 const DTTR_Config *config
463) {
464 for (int i = 0; i < DTTR_GAMEPAD_SOURCE_COUNT; i++) {
465 const char *source_name = config_format_gamepad_source(i);
466 if (!source_name) {
467 continue;
468 }
469
470 if (!obj_add_strcpy(
471 doc,
472 buttons,
473 source_name,
475 )) {
476 return false;
477 }
478 }
479
480 return true;
481}
482
483bool DTTR_Config_Save(const char *filename, const DTTR_Config *config) {
484 if (!filename || !config) {
485 return false;
486 }
487
488 bool ok = false;
489 yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
490 if (!doc) {
491 return false;
492 }
493
494 yyjson_mut_val *root = yyjson_mut_obj(doc);
495 if (!root) {
496 goto done;
497 }
498
499 yyjson_mut_doc_set_root(doc, root);
500
501 config_json_objects objects = {
502 .root = root,
503 .graphics = yyjson_mut_obj_add_obj(doc, root, "graphics"),
504 .audio = yyjson_mut_obj_add_obj(doc, root, "audio"),
505 .modding = yyjson_mut_obj_add_obj(doc, root, "modding"),
506 .gamepad = yyjson_mut_obj_add_obj(doc, root, "gamepad"),
507 };
508
509 if (!objects.graphics || !objects.audio || !objects.modding || !objects.gamepad) {
510 goto done;
511 }
512
513 yyjson_mut_val *buttons = yyjson_mut_obj_add_obj(doc, objects.gamepad, "buttons");
514 if (!buttons) {
515 goto done;
516 }
517
518 ok = config_add_schema_fields(doc, &objects, config)
519 && config_add_disabled_mods(doc, objects.modding, config)
520 && config_add_gamepad_buttons(doc, buttons, config);
521
522 if (ok) {
523 yyjson_write_err err;
524 ok = yyjson_mut_write_file(
525 filename,
526 doc,
527 YYJSON_WRITE_PRETTY | YYJSON_WRITE_NEWLINE_AT_END,
528 NULL,
529 &err
530 );
531 if (!ok) {
532 DTTR_LOG_ERROR("Failed to write config %s: %s", filename, err.msg);
533 }
534 }
535
536done:
537 yyjson_mut_doc_free(doc);
538 return ok;
539}
DTTR_Graphics_COM_Direct3DDevice7 void DWORD flags DWORD count
DTTR_Graphics_COM_DirectDrawSurface7 DWORD flags void NULL
static void config_clear_button_map(int *map)
bool config_parse_game_action(const char *value, int *out_value)
Definition parse.c:322
bool config_apply_entry(DTTR_Config *config, const char *section, const char *key, const char *value)
Definition schema.c:252
const char * config_format_graphics_api(DTTR_GraphicsApi api)
const char * config_format_vertex_precision(DTTR_VertexPrecision precision)
const char * config_format_scaling_fit(DTTR_ScalingMode mode)
const DTTR_ConfigFieldSpec * config_schema_find(const char *section, const char *key)
Definition schema.c:173
const char * config_format_minidump_type(DTTR_MinidumpType type)
@ CONFIG_VERTEX_PRECISION
@ CONFIG_SCALING_FIT
@ CONFIG_MINIDUMP_TYPE
@ CONFIG_STRING
@ CONFIG_PRESENT_FILTER
@ CONFIG_GRAPHICS_API
@ CONFIG_LOG_LEVEL
@ CONFIG_INT
@ CONFIG_FLOAT
@ CONFIG_GAMEPAD_AXIS
@ CONFIG_BOOL
@ CONFIG_SCALING_METHOD
const char * config_format_present_filter(SDL_GPUFilter filter)
const char * config_format_gamepad_axis(int axis)
const char * config_format_game_action(int action)
const char * config_format_gamepad_source(int source)
static bool config_sections_match(const char *lhs, const char *rhs)
bool config_parse_gamepad_source(const char *value, int *out_value)
Definition parse.c:311
const char * config_format_scaling_method(DTTR_ScalingMethod method)
const char * config_format_log_level(int level)
DTTR_VertexPrecision
Definition dttr_config.h:43
bool DTTR_Config_SetModEnabled(DTTR_Config *config, const char *mod_filename, bool enabled)
Definition defaults.c:145
DTTR_GraphicsApi
Definition dttr_config.h:36
void DTTR_Config_SetDefaults(DTTR_Config *config)
Resets a config object to built-in defaults.
Definition defaults.c:196
DTTR_ScalingMethod
Definition dttr_config.h:20
#define DTTR_GAMEPAD_SOURCE_COUNT
Definition dttr_config.h:53
#define DTTR_GAMEPAD_MAPPING_NONE
Definition dttr_config.h:48
DTTR_MinidumpType
Definition dttr_config.h:25
int DTTR_Config_SchemaCount()
Definition schema.c:91
DTTR_ScalingMode
Definition dttr_config.h:14
DTTR_Config dttr_config
Definition defaults.c:53
const DTTR_ConfigFieldSpec * DTTR_Config_SchemaGet(int index)
Definition schema.c:95
#define DTTR_CONFIG_SCHEMA_MAJOR_VERSION
Definition dttr_config.h:55
#define DTTR_LOG_WARN(...)
Definition dttr_log.h:30
#define DTTR_LOG_ERROR(...)
Definition dttr_log.h:31
static game_data_source source
Definition game_data.c:21
static void errors_addf(const char *fmt,...)
Definition io.c:25
static bool config_add_gamepad_buttons(yyjson_mut_doc *doc, yyjson_mut_val *buttons, const DTTR_Config *config)
Definition io.c:459
static yyjson_mut_val * config_object_for_section(const config_json_objects *objects, const char *section)
Definition io.c:339
static bool config_add_schema_field(yyjson_mut_doc *doc, const config_json_objects *objects, const DTTR_Config *config, const DTTR_ConfigFieldSpec *spec)
Definition io.c:395
static void errors_add_invalid_value(const char *section, const char *key, const char *value)
Definition io.c:50
static const char * config_format_field_string(const DTTR_Config *config, const DTTR_ConfigFieldSpec *spec)
Definition io.c:366
static void config_apply_disabled_mods(DTTR_Config *config, yyjson_val *disabled_mods)
Definition io.c:99
static bool config_add_schema_fields(yyjson_mut_doc *doc, const config_json_objects *objects, const DTTR_Config *config)
Definition io.c:424
static bool obj_add_strcpy(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, const char *value)
Definition io.c:322
static void config_apply_buttons(DTTR_Config *config, yyjson_val *buttons)
Definition io.c:63
const char * DTTR_Config_LastError()
Returns details from the most recent config load failure, or NULL when none exist.
Definition io.c:46
static void errors_clear()
Definition io.c:17
static bool validate_schema_major_version(yyjson_val *root, const char *filename)
Definition io.c:126
static void apply_section(yyjson_val *obj, const char *section)
Definition io.c:228
bool DTTR_Config_Save(const char *filename, const DTTR_Config *config)
Saves config values back to a strict JSON file.
Definition io.c:483
bool DTTR_Config_Load(const char *filename)
Loads config values from a strict JSON file into the global config object.
Definition io.c:259
static bool errors_show()
Definition io.c:37
static bool config_add_disabled_mods(yyjson_mut_doc *doc, yyjson_mut_val *modding, const DTTR_Config *config)
Definition io.c:440
static bool config_apply_json_value(DTTR_Config *config, const DTTR_ConfigFieldSpec *spec, yyjson_val *val)
Definition io.c:162
static sds errors
Definition io.c:15
DTTR_ConfigValueType value_type
const char * section
int gamepad_button_map[DTTR_GAMEPAD_SOURCE_COUNT]
int disabled_mod_count
char disabled_mods[DTTR_CONFIG_DISABLED_MODS_MAX][MAX_PATH]
yyjson_mut_val * audio
Definition io.c:334
yyjson_mut_val * root
Definition io.c:332
yyjson_mut_val * modding
Definition io.c:335
yyjson_mut_val * graphics
Definition io.c:333
yyjson_mut_val * gamepad
Definition io.c:336