#include "settings.h" #define get_object(id) gtk_builder_get_object(builder, id) #define builder_get(type, id) type(get_object(id)) gchar* settings_file_path; GKeyFile *key_file; static void print_config_error(GError *error) { if (error == NULL) return; if (!g_error_matches(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND) && !g_error_matches(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) { g_warning("Config error: %s", error->message); } } void _print_config(config_t *config, GLogLevelFlags log_level) { #define EXPAND_GROUP(name, members) { \ g_log(G_LOG_DOMAIN, log_level, "[%s]", #name); \ struct config_ ## name ## _t *group = &config->name; \ members \ } #define EXPAND_GROUP_MEMBER(member, key_type, default_value) \ g_log(G_LOG_DOMAIN, log_level, "%s="FORMAT_FOR_KEY_TYPE(key_type)"", #member, group->member); EXPAND_CONFIG #undef EXPAND_GROUP #undef EXPAND_GROUP_MEMBER } // Duplicates the input and converts the first character to uppercase gchar* ascii_ucfirst(gchar *str) { gchar *out = g_strdup(str); out[0] = g_ascii_toupper(str[0]); return out; } void load_config_from_key_file(config_t *config, GKeyFile *key_file) { g_message("Loading config from key file"); GError *error = NULL; #define EXPAND_GROUP(name_arg, members) { \ struct config_ ## name_arg ## _t *group = &config->name_arg; \ g_autofree gchar *group_name = ascii_ucfirst(#name_arg); \ members \ } #define EXPAND_GROUP_MEMBER(member, key_type, default_value) \ group->member = g_key_file_get_##key_type(key_file, group_name, #member, &error); \ if (error != NULL) { \ group->member = default_value; \ print_config_error(error); \ g_clear_error(&error); \ } EXPAND_CONFIG if (config->emulation.rewind_duration > 600) { g_warning("Setting Emulation.rewind_duration too high might affect performance."); } #undef EXPAND_GROUP #undef EXPAND_GROUP_MEMBER } void print_config(config_t *config) { _print_config(config, G_LOG_LEVEL_MESSAGE); } void save_config_to_key_file(config_t *config, GKeyFile *key_file) { g_message("Saving config to key file"); GError *error = NULL; gchar *group_name; #define EXPAND_GROUP(name_arg, members) { \ struct config_ ## name_arg ## _t *group = &config->name_arg; \ g_autofree gchar *group_name = ascii_ucfirst(#name_arg); \ members \ } #define EXPAND_GROUP_MEMBER_IF_0(member, key_type, default_value) \ g_key_file_set_##key_type(key_file, group_name, #member, group->member); #define EXPAND_GROUP_MEMBER_IF_1(member, key_type, default_value) \ if (group->member != NULL) { \ g_key_file_set_##key_type(key_file, group_name, #member, group->member); \ } \ else if (g_key_file_has_key(key_file, group_name, #member, &error)) { \ if (error != NULL) { \ g_warning("%s", error->message); \ g_clear_error(&error); \ } \ g_key_file_remove_key(key_file, group_name, #member, &error); \ if (error != NULL) { \ g_warning("%s", error->message); \ g_clear_error(&error); \ } \ } #define EXPAND_GROUP_MEMBER_IF_EVAL(y, member, key_type, default_value) EXPAND_GROUP_MEMBER_IF_ ## y(member, key_type, default_value) #define EXPAND_GROUP_MEMBER_IF(member, key_type, is_pointer, default_value) EXPAND_GROUP_MEMBER_IF_EVAL(is_pointer, member, key_type, default_value) #define EXPAND_GROUP_MEMBER(member, key_type, default_value) EXPAND_GROUP_MEMBER_IF(member, key_type, GTYPE_IS_POINTER(key_type), default_value) EXPAND_CONFIG #undef EXPAND_GROUP #undef EXPAND_GROUP_MEMBER #undef EXPAND_GROUP_MEMBER_IF #undef EXPAND_GROUP_MEMBER_IF_EVAL #undef EXPAND_GROUP_MEMBER_IF_0 #undef EXPAND_GROUP_MEMBER_IF_1 // Save config to disk if (!g_key_file_save_to_file(key_file, settings_file_path, &error)) { g_warning ("Failed to save %s: %s", settings_file_path, error->message); g_error_free(error); return; } } void on_preferences_realize(GtkWidget *w, gpointer builder_ptr) { GtkWindow *preferences = GTK_WINDOW(w); GtkBuilder *builder = (GtkBuilder *) builder_ptr; GApplication *app = G_APPLICATION(gtk_builder_get_application(builder)); update_boot_rom_selector(builder); // Hook up the static preferences gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "rewind_duration_selector"), g_strdup_printf("%i", config.emulation.rewind_duration)); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "dmg_revision_selector"), config.emulation.dmg_revision_name); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "sgb_revision_selector"), config.emulation.sgb_revision_name); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "cgb_revision_selector"), config.emulation.cgb_revision_name); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "shader_selector"), config.video.shader); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "color_correction_selector"), config.video.color_correction_id); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "frame_blending_selector"), config.video.frame_blending_mode); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "display_border_selector"), config.video.display_border_mode); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "monochrome_palette_selector"), config.video.monochrome_palette_id); gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "integer_scaling_toggle"), config.video.use_integer_scaling); gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "aspect_ratio_toggle"), config.video.keep_aspect_ratio); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "highpass_filter_selector"), config.audio.high_pass_filter_id); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "sample_rate_selector"), g_strdup_printf("%i", config.audio.sample_rate)); gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "analog_speed_controls_toggle"), config.controls.analog_speed_controls); gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "rumble_mode_selector"), config.controls.rumble_mode); } void init_settings(GApplication *app, gchar *path, GDateTime **modification_date, GtkWindow *preferences) { free_settings(); key_file = g_key_file_new(); if (path != NULL) { settings_file_path = path; } else { settings_file_path = g_build_filename(g_get_user_config_dir(), SETTINGS_FILE, NULL); } load_settings(app, modification_date); } int load_settings(GApplication *app, GDateTime **modification_date) { GError *error = NULL; g_message("Trying to load settings from %s", settings_file_path); g_autoptr(GFile) file = g_file_new_for_path(settings_file_path); g_autoptr(GFileInfo) file_info = g_file_query_info(file, "time::*", G_FILE_QUERY_INFO_NONE, NULL, NULL); *modification_date = g_file_info_get_modification_date_time(file_info); if (!g_key_file_load_from_file(key_file, settings_file_path, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error)) { if (error->domain == G_FILE_ERROR) { g_warning("Unable to load %s: %s", settings_file_path, error->message); } else if (error->domain == G_KEY_FILE_ERROR) { g_warning("Failed to parse %s: %s", settings_file_path, error->message); } g_error_free(error); } load_config_from_key_file(&config, key_file); _print_config(&config, G_LOG_LEVEL_DEBUG); // Update GAction states g_action_group_change_action_state(G_ACTION_GROUP(app), "toggle_mute", g_variant_new_boolean(config.audio.muted)); return 0; } void save_settings(GtkWindow *main_window, GDateTime *saved_modification_date) { GError *error = NULL; g_message("Trying to save settings to %s", settings_file_path); g_autoptr(GFile) file = g_file_new_for_path(settings_file_path); g_autoptr(GFileInfo) file_info = g_file_query_info(file, "time::*", G_FILE_QUERY_INFO_NONE, NULL, NULL); GDateTime *modification_date = g_file_info_get_modification_date_time(file_info); if (!g_date_time_equal(saved_modification_date, modification_date)) { GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new( main_window, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "It looks like the configuration has changed on disk. Overwrite?" )); gtk_window_set_title(GTK_WINDOW(dialog), "SameBoy"); gint result = gtk_dialog_run(GTK_DIALOG(dialog)); switch (result) { case GTK_RESPONSE_YES: save_config_to_key_file(&config, key_file); break; default: // Action has been canceled break; } } else { save_config_to_key_file(&config, key_file); } } void free_settings(void) { if (key_file != NULL) { g_key_file_free(key_file); key_file = NULL; } } void update_boot_rom_selector(GtkBuilder *builder) { GtkComboBoxText *combo_box = builder_get(GTK_COMBO_BOX_TEXT, "boot_rom_selector"); gtk_combo_box_text_remove_all(combo_box); gtk_combo_box_text_append(combo_box, "auto", "Use Built-in Boot ROMs"); if (config.emulation.boot_rom_path != NULL && !g_str_equal(config.emulation.boot_rom_path, "auto") && !g_str_equal(config.emulation.boot_rom_path, "other")) { gtk_combo_box_text_append(combo_box, config.emulation.boot_rom_path, config.emulation.boot_rom_path); gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo_box), config.emulation.boot_rom_path); } else { gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo_box), "auto"); } gtk_combo_box_text_append_text(combo_box, ""); gtk_combo_box_text_append(combo_box, "other", "Other"); } GB_color_correction_mode_t get_color_correction_mode(void) { if (config.video.color_correction_id == NULL) goto default_value; if (g_strcmp0(config.video.color_correction_id, "disabled") == 0) { return GB_COLOR_CORRECTION_DISABLED; } else if (g_strcmp0(config.video.color_correction_id, "correct_color_curves") == 0) { return GB_COLOR_CORRECTION_CORRECT_CURVES; } else if (g_strcmp0(config.video.color_correction_id, "emulate_hardware") == 0) { return GB_COLOR_CORRECTION_EMULATE_HARDWARE; } else if (g_strcmp0(config.video.color_correction_id, "preserve_brightness") == 0) { return GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS; } else if (g_strcmp0(config.video.color_correction_id, "reduce_contrast") == 0) { return GB_COLOR_CORRECTION_REDUCE_CONTRAST; } // This should not happen g_warning("Unknown color correction mode: %s\nFalling back to “Emulate Hardware”", config.video.color_correction_id); default_value: return GB_COLOR_CORRECTION_EMULATE_HARDWARE; } void set_color_correction_mode(GB_color_correction_mode_t mode) { switch (mode) { case GB_COLOR_CORRECTION_DISABLED: config.video.color_correction_id = "disabled"; break; case GB_COLOR_CORRECTION_CORRECT_CURVES: config.video.color_correction_id = "correct_color_curves"; break; case GB_COLOR_CORRECTION_EMULATE_HARDWARE: config.video.color_correction_id = "emulate_hardware"; break; case GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS: config.video.color_correction_id = "preserve_brightness"; break; case GB_COLOR_CORRECTION_REDUCE_CONTRAST: config.video.color_correction_id = "reduce_contrast"; break; } } GB_frame_blending_mode_t get_frame_blending_mode(void) { if (config.video.frame_blending_mode == NULL) goto default_value; if (g_strcmp0(config.video.frame_blending_mode, "disabled") == 0) { return GB_FRAME_BLENDING_MODE_DISABLED; } else if (g_strcmp0(config.video.frame_blending_mode, "simple") == 0) { return GB_FRAME_BLENDING_MODE_SIMPLE; } else if (g_strcmp0(config.video.frame_blending_mode, "accurate") == 0) { return GB_FRAME_BLENDING_MODE_ACCURATE; } // This should not happen g_warning("Unknown frame blending mode: %s\nFalling back to “Disabled”", config.video.frame_blending_mode); default_value: return GB_FRAME_BLENDING_MODE_DISABLED; } void set_frame_blending_mode(GB_frame_blending_mode_t mode) { switch (mode) { case GB_FRAME_BLENDING_MODE_DISABLED: config.video.frame_blending_mode = "disabled"; break; case GB_FRAME_BLENDING_MODE_SIMPLE: config.video.frame_blending_mode = "simple"; break; case GB_FRAME_BLENDING_MODE_ACCURATE: case GB_FRAME_BLENDING_MODE_ACCURATE_ODD: config.video.frame_blending_mode = "accurate"; break; } } GB_border_mode_t get_display_border_mode(void) { if (config.video.display_border_mode == NULL) goto default_value; if (g_strcmp0(config.video.display_border_mode, "never") == 0) { return GB_BORDER_NEVER; } else if (g_strcmp0(config.video.display_border_mode, "sgb_only") == 0) { return GB_BORDER_SGB; } else if (g_strcmp0(config.video.display_border_mode, "always") == 0) { return GB_BORDER_ALWAYS; } // This should not happen g_warning("Unknown SGB border mode: %s\nFalling back to “Never”", config.video.display_border_mode); default_value: return GB_BORDER_NEVER; } void set_display_border_mode(GB_border_mode_t mode) { switch (mode) { case GB_BORDER_NEVER: config.video.display_border_mode = "never"; break; case GB_BORDER_SGB: config.video.display_border_mode = "sgb_only"; break; case GB_BORDER_ALWAYS: config.video.display_border_mode = "always"; break; } } const GB_palette_t* get_monochrome_palette(void) { if (config.video.monochrome_palette_id == NULL) goto default_value; if (g_strcmp0(config.video.monochrome_palette_id, "greyscale") == 0) { return &GB_PALETTE_GREY; } else if (g_strcmp0(config.video.monochrome_palette_id, "lime") == 0) { return &GB_PALETTE_DMG; } else if (g_strcmp0(config.video.monochrome_palette_id, "olive") == 0) { return &GB_PALETTE_MGB; } else if (g_strcmp0(config.video.monochrome_palette_id, "teal") == 0) { return &GB_PALETTE_GBL; } // This should not happen g_warning("Unknown monochrome palette: %s\nFalling back to “Greyscale”", config.video.monochrome_palette_id); default_value: return &GB_PALETTE_GREY; } void set_monochrome_palette(const GB_palette_t *mode) { g_message("%p | %p | %p | %p | %p", mode, &GB_PALETTE_GREY, &GB_PALETTE_DMG, &GB_PALETTE_MGB, &GB_PALETTE_GBL); if (mode == &GB_PALETTE_GREY) { config.video.monochrome_palette_id = "greyscale"; } else if (mode == &GB_PALETTE_DMG) { config.video.monochrome_palette_id = "lime"; } else if (mode == &GB_PALETTE_MGB) { config.video.monochrome_palette_id = "olive"; } else if (mode == &GB_PALETTE_GBL) { config.video.monochrome_palette_id = "teal"; } } GB_highpass_mode_t get_highpass_mode(void) { if (config.audio.high_pass_filter_id == NULL) goto default_value; if (g_strcmp0(config.audio.high_pass_filter_id, "disabled") == 0) { return GB_HIGHPASS_OFF; } else if (g_strcmp0(config.audio.high_pass_filter_id, "emulate_hardware") == 0) { return GB_HIGHPASS_ACCURATE; } else if (g_strcmp0(config.audio.high_pass_filter_id, "preserve_waveform") == 0) { return GB_HIGHPASS_REMOVE_DC_OFFSET; } // This should not happen g_warning("Unknown highpass mode: %s\nFalling back to “Accurate”", config.audio.high_pass_filter_id); default_value: return GB_HIGHPASS_ACCURATE; } void set_highpass_mode(GB_highpass_mode_t mode) { switch (mode) { case GB_HIGHPASS_OFF: config.audio.high_pass_filter_id = "disabled"; break; case GB_HIGHPASS_MAX: g_warning("GB_HIGHPASS_MAX is not a valid highpass mode, falling back to “Accurate”."); case GB_HIGHPASS_ACCURATE: config.audio.high_pass_filter_id = "emulate_hardware"; break; case GB_HIGHPASS_REMOVE_DC_OFFSET: config.audio.high_pass_filter_id = "preserve_waveform"; break; } } const GB_rumble_mode_t get_rumble_mode(void) { if (config.controls.rumble_mode == NULL) goto default_value; if (g_strcmp0(config.controls.rumble_mode, "never") == 0) { return GB_RUMBLE_DISABLED; } else if (g_strcmp0(config.controls.rumble_mode, "rumble_cartridges") == 0) { return GB_RUMBLE_CARTRIDGE_ONLY; } else if (g_strcmp0(config.controls.rumble_mode, "always") == 0) { return GB_RUMBLE_ALL_GAMES; } // This should not happen g_warning("Unknown highpass mode: %s\nFalling back to “Never”", config.controls.rumble_mode); default_value: return GB_RUMBLE_DISABLED; } void set_rumble_mode(GB_rumble_mode_t mode) { switch (mode) { case GB_RUMBLE_DISABLED: config.controls.rumble_mode = "never"; break; case GB_RUMBLE_CARTRIDGE_ONLY: config.controls.rumble_mode = "rumble_cartridges"; break; case GB_RUMBLE_ALL_GAMES: config.controls.rumble_mode = "always"; break; } } GB_model_t get_model(void) { if (g_strcmp0(config.emulation.model, "DMG") == 0) { return get_dmg_model(); } else if (g_strcmp0(config.emulation.model, "MGB") == 0) { g_warning("Emulation of MGBs is unsupported, falling back to DMG."); return get_dmg_model(); } else if (g_strcmp0(config.emulation.model, "AGB") == 0) { return GB_MODEL_AGB; } else if (g_strcmp0(config.emulation.model, "SGB") == 0) { return get_sgb_model(); } return get_cgb_model(); } void set_model(GB_model_t model) { switch (model & GB_MODEL_FAMILY_MASK) { case GB_MODEL_DMG_FAMILY: if (model & GB_MODEL_SGB) { config.emulation.model = "SGB"; } else { config.emulation.model = "DMG"; } break; case GB_MODEL_MGB_FAMILY: config.emulation.model = "MGB"; break; case GB_MODEL_CGB_FAMILY: if (model & GB_MODEL_AGB) { config.emulation.model = "AGB"; } else { config.emulation.model = "CGB"; } break; } } GB_model_t get_dmg_model(void) { if (config.emulation.dmg_revision_name == NULL) goto default_value; // TODO: Synchronize with GB_model_t (Core/gb.h) if (g_strcmp0(config.emulation.dmg_revision_name, "DMG_CPU_B") == 0) { return GB_MODEL_DMG_B; } default_value: return GB_MODEL_DMG_B; } GB_model_t get_sgb_model(void) { if (config.emulation.sgb_revision_name == NULL) goto default_value; // TODO: Synchronize with GB_model_t (Core/gb.h) if (g_strcmp0(config.emulation.sgb_revision_name, "SGB1_NTSC") == 0) { return GB_MODEL_SGB_NTSC; } else if (g_strcmp0(config.emulation.sgb_revision_name, "SGB1_PAL") == 0) { return GB_MODEL_SGB_PAL; } else if (g_strcmp0(config.emulation.sgb_revision_name, "SGB2") == 0) { return GB_MODEL_SGB2; } default_value: return GB_MODEL_SGB2; } GB_model_t get_cgb_model(void) { if (config.emulation.cgb_revision_name == NULL) goto default_value; // TODO: Synchronize with GB_model_t (Core/gb.h) if (g_strcmp0(config.emulation.cgb_revision_name, "CPU_CGB_C") == 0) { return GB_MODEL_CGB_C; } else if (g_strcmp0(config.emulation.cgb_revision_name, "CPU_CGB_E") == 0) { return GB_MODEL_CGB_E; } default_value: return GB_MODEL_CGB_E; }