sm64coopdx/src/pc/configfile.c
2025-10-29 18:44:49 +01:00

908 lines
42 KiB
C

// configfile.c - handles loading and saving the configuration options
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include "platform.h"
#include "configfile.h"
#include "cliopts.h"
#include "gfx/gfx_screen_config.h"
#include "gfx/gfx_window_manager_api.h"
#include "controller/controller_api.h"
#include "fs/fs.h"
#include "mods/mods.h"
#include "network/ban_list.h"
#include "crash_handler.h"
#include "network/moderator_list.h"
#include "debuglog.h"
#include "djui/djui_hud_utils.h"
#include "game/save_file.h"
#include "pc/network/network_player.h"
#include "pc/pc_main.h"
#define ARRAY_LEN(arr) (sizeof(arr) / sizeof(arr[0]))
enum ConfigOptionType {
CONFIG_TYPE_BOOL,
CONFIG_TYPE_UINT,
CONFIG_TYPE_FLOAT,
CONFIG_TYPE_BIND,
CONFIG_TYPE_STRING,
CONFIG_TYPE_U64,
CONFIG_TYPE_COLOR,
};
struct ConfigOption {
const char *name;
enum ConfigOptionType type;
union {
bool *boolValue;
unsigned int *uintValue;
float* floatValue;
char* stringValue;
u64* u64Value;
u8 (*colorValue)[3];
};
int maxStringLength;
};
struct FunctionConfigOption {
const char *name;
void (*read)(char**, int);
void (*write)(FILE*);
};
/*
*Config options and default values
*/
char configSaveNames[4][MAX_SAVE_NAME_STRING] = {
"SM64",
"SM64",
"SM64",
"SM64"
};
// Video/audio stuff
ConfigWindow configWindow = {
.x = WAPI_WIN_CENTERPOS,
.y = WAPI_WIN_CENTERPOS,
.w = DESIRED_SCREEN_WIDTH,
.h = DESIRED_SCREEN_HEIGHT,
.vsync = 1,
.reset = false,
.fullscreen = false,
.exiting_fullscreen = false,
.settings_changed = false,
.msaa = 0,
};
ConfigStick configStick = { 0 };
// display settings
unsigned int configFiltering = 2; // 0 = Nearest, 1 = Bilinear, 2 = Trilinear
bool configShowFPS = false;
bool configShowPing = false;
enum RefreshRateMode configFramerateMode = RRM_AUTO;
unsigned int configFrameLimit = 60;
unsigned int configInterpolationMode = 1;
unsigned int configDrawDistance = 4;
// sound settings
unsigned int configMasterVolume = 80; // 0 - MAX_VOLUME
unsigned int configMusicVolume = MAX_VOLUME;
unsigned int configSfxVolume = MAX_VOLUME;
unsigned int configEnvVolume = MAX_VOLUME;
bool configFadeoutDistantSounds = false;
bool configMuteFocusLoss = false;
// control binds
unsigned int configKeyA[MAX_BINDS] = { 0x0026, 0x1000, 0x1103 };
unsigned int configKeyB[MAX_BINDS] = { 0x0033, 0x1001, 0x1101 };
unsigned int configKeyX[MAX_BINDS] = { 0x0017, 0x1002, VK_INVALID };
unsigned int configKeyY[MAX_BINDS] = { 0x0032, 0x1003, VK_INVALID };
unsigned int configKeyStart[MAX_BINDS] = { 0x0039, 0x1006, VK_INVALID };
unsigned int configKeyL[MAX_BINDS] = { 0x002A, 0x1009, 0x1104 };
unsigned int configKeyR[MAX_BINDS] = { 0x0036, 0x100A, 0x101B };
unsigned int configKeyZ[MAX_BINDS] = { 0x0025, 0x1007, 0x101A };
unsigned int configKeyCUp[MAX_BINDS] = { 0x0148, VK_INVALID, VK_INVALID };
unsigned int configKeyCDown[MAX_BINDS] = { 0x0150, VK_INVALID, VK_INVALID };
unsigned int configKeyCLeft[MAX_BINDS] = { 0x014B, VK_INVALID, VK_INVALID };
unsigned int configKeyCRight[MAX_BINDS] = { 0x014D, VK_INVALID, VK_INVALID };
unsigned int configKeyStickUp[MAX_BINDS] = { 0x0011, VK_INVALID, VK_INVALID };
unsigned int configKeyStickDown[MAX_BINDS] = { 0x001F, VK_INVALID, VK_INVALID };
unsigned int configKeyStickLeft[MAX_BINDS] = { 0x001E, VK_INVALID, VK_INVALID };
unsigned int configKeyStickRight[MAX_BINDS] = { 0x0020, VK_INVALID, VK_INVALID };
unsigned int configKeyChat[MAX_BINDS] = { 0x001C, VK_INVALID, VK_INVALID };
unsigned int configKeyPlayerList[MAX_BINDS] = { 0x000F, 0x1004, VK_INVALID };
unsigned int configKeyDUp[MAX_BINDS] = { 0x0147, 0x100b, VK_INVALID };
unsigned int configKeyDDown[MAX_BINDS] = { 0x014f, 0x100c, VK_INVALID };
unsigned int configKeyDLeft[MAX_BINDS] = { 0x0153, 0x100d, VK_INVALID };
unsigned int configKeyDRight[MAX_BINDS] = { 0x0151, 0x100e, VK_INVALID };
unsigned int configKeyConsole[MAX_BINDS] = { 0x0029, 0x003B, VK_INVALID };
unsigned int configKeyPrevPage[MAX_BINDS] = { 0x0016, VK_INVALID, VK_INVALID };
unsigned int configKeyNextPage[MAX_BINDS] = { 0x0018, VK_INVALID, VK_INVALID };
unsigned int configKeyDisconnect[MAX_BINDS] = { VK_INVALID, VK_INVALID, VK_INVALID };
unsigned int configStickDeadzone = 16;
unsigned int configRumbleStrength = 50;
unsigned int configGamepadNumber = 0;
bool configBackgroundGamepad = true;
bool configDisableGamepads = false;
bool configUseStandardKeyBindingsChat = false;
bool configSmoothScrolling = false;
// free camera settings
bool configEnableFreeCamera = false;
bool configFreeCameraAnalog = false;
bool configFreeCameraLCentering = false;
bool configFreeCameraDPadBehavior = false;
bool configFreeCameraHasCollision = true;
bool configFreeCameraMouse = false;
unsigned int configFreeCameraXSens = 50;
unsigned int configFreeCameraYSens = 50;
unsigned int configFreeCameraAggr = 0;
unsigned int configFreeCameraPan = 0;
unsigned int configFreeCameraDegrade = 50; // 0 - 100%
// romhack camera settings
unsigned int configEnableRomhackCamera = 0; // 0 for automatic, 1 for force on, 2 for force off
bool configRomhackCameraBowserFights = false;
bool configRomhackCameraHasCollision = true;
bool configRomhackCameraHasCentering = false;
bool configRomhackCameraDPadBehavior = false;
bool configRomhackCameraSlowFall = true;
// common camera settings
bool configCameraInvertX = false;
bool configCameraInvertY = true;
bool configCameraToxicGas = true;
// debug
bool configLuaProfiler = false;
bool configDebugPrint = false;
bool configDebugInfo = false;
bool configDebugError = false;
#ifdef DEVELOPMENT
bool configCtxProfiler = false;
#endif
// player settings
char configPlayerName[MAX_CONFIG_STRING] = "";
unsigned int configPlayerModel = 0;
struct PlayerPalette configPlayerPalette = { { { 0x00, 0x00, 0xff }, { 0xff, 0x00, 0x00 }, { 0xff, 0xff, 0xff }, { 0x72, 0x1c, 0x0e }, { 0x73, 0x06, 0x00 }, { 0xfe, 0xc1, 0x79 }, { 0xff, 0x00, 0x00 }, { 0xff, 0x00, 0x00 } } };
// coop settings
unsigned int configAmountOfPlayers = MAX_PLAYERS;
bool configBubbleDeath = true;
unsigned int configHostPort = DEFAULT_PORT;
unsigned int configHostSaveSlot = 1;
char configJoinIp[MAX_CONFIG_STRING] = "";
unsigned int configJoinPort = DEFAULT_PORT;
unsigned int configNetworkSystem = 0;
unsigned int configPlayerInteraction = 1;
unsigned int configPlayerKnockbackStrength = 25;
unsigned int configStayInLevelAfterStar = 0;
bool configNametags = true;
bool configModDevMode = false;
unsigned int configBouncyLevelBounds = 0;
bool configSkipIntro = 0;
bool configPauseAnywhere = false;
bool configMenuStaffRoll = false;
unsigned int configMenuLevel = 0;
unsigned int configMenuSound = 0;
bool configMenuRandom = false;
bool configMenuDemos = false;
bool configDisablePopups = false;
char configLanguage[MAX_CONFIG_STRING] = "";
bool configForce4By3 = false;
bool configDynosLocalPlayerModelOnly = false;
unsigned int configPvpType = PLAYER_PVP_CLASSIC;
// CoopNet settings
char configCoopNetIp[MAX_CONFIG_STRING] = DEFAULT_COOPNET_IP;
unsigned int configCoopNetPort = DEFAULT_COOPNET_PORT;
char configPassword[MAX_CONFIG_STRING] = "";
char configDestId[MAX_CONFIG_STRING] = "0";
// DJUI settings
unsigned int configDjuiTheme = DJUI_THEME_DARK;
#ifdef HANDHELD
bool configDjuiThemeCenter = false;
#else
bool configDjuiThemeCenter = true;
#endif
bool configDjuiThemeGradients = true;
unsigned int configDjuiThemeFont = FONT_NORMAL;
unsigned int configDjuiScale = 0;
unsigned int configDjuiChatAlpha = 255;
unsigned int configDjuiBaseAlpha = 127;
unsigned int configDjuiChatSize = 100;
// other
unsigned int configRulesVersion = 0;
bool configCompressOnStartup = false;
bool configSkipPackGeneration = false;
// secrets
bool configExCoopTheme = false;
static const struct ConfigOption options[] = {
// window settings
{.name = "fullscreen", .type = CONFIG_TYPE_BOOL, .boolValue = &configWindow.fullscreen},
{.name = "window_x", .type = CONFIG_TYPE_UINT, .uintValue = &configWindow.x},
{.name = "window_y", .type = CONFIG_TYPE_UINT, .uintValue = &configWindow.y},
{.name = "window_w", .type = CONFIG_TYPE_UINT, .uintValue = &configWindow.w},
{.name = "window_h", .type = CONFIG_TYPE_UINT, .uintValue = &configWindow.h},
{.name = "vsync", .type = CONFIG_TYPE_BOOL, .boolValue = &configWindow.vsync},
{.name = "msaa", .type = CONFIG_TYPE_UINT, .uintValue = &configWindow.msaa},
// display settings
{.name = "texture_filtering", .type = CONFIG_TYPE_UINT, .uintValue = &configFiltering},
{.name = "show_fps", .type = CONFIG_TYPE_BOOL, .boolValue = &configShowFPS},
{.name = "show_ping", .type = CONFIG_TYPE_BOOL, .boolValue = &configShowPing},
{.name = "framerate_mode", .type = CONFIG_TYPE_UINT, .uintValue = &configFramerateMode},
{.name = "frame_limit", .type = CONFIG_TYPE_UINT, .uintValue = &configFrameLimit},
{.name = "interpolation_mode", .type = CONFIG_TYPE_UINT, .uintValue = &configInterpolationMode},
{.name = "coop_draw_distance", .type = CONFIG_TYPE_UINT, .uintValue = &configDrawDistance},
// sound settings
{.name = "master_volume", .type = CONFIG_TYPE_UINT, .uintValue = &configMasterVolume},
{.name = "music_volume", .type = CONFIG_TYPE_UINT, .uintValue = &configMusicVolume},
{.name = "sfx_volume", .type = CONFIG_TYPE_UINT, .uintValue = &configSfxVolume},
{.name = "env_volume", .type = CONFIG_TYPE_UINT, .uintValue = &configEnvVolume},
{.name = "fade_distant_sounds", .type = CONFIG_TYPE_BOOL, .boolValue = &configFadeoutDistantSounds},
{.name = "mute_focus_loss", .type = CONFIG_TYPE_BOOL, .boolValue = &configMuteFocusLoss},
// control binds
{.name = "key_a", .type = CONFIG_TYPE_BIND, .uintValue = configKeyA},
{.name = "key_b", .type = CONFIG_TYPE_BIND, .uintValue = configKeyB},
{.name = "key_x", .type = CONFIG_TYPE_BIND, .uintValue = configKeyX},
{.name = "key_y", .type = CONFIG_TYPE_BIND, .uintValue = configKeyY},
{.name = "key_start", .type = CONFIG_TYPE_BIND, .uintValue = configKeyStart},
{.name = "key_l", .type = CONFIG_TYPE_BIND, .uintValue = configKeyL},
{.name = "key_r", .type = CONFIG_TYPE_BIND, .uintValue = configKeyR},
{.name = "key_z", .type = CONFIG_TYPE_BIND, .uintValue = configKeyZ},
{.name = "key_cup", .type = CONFIG_TYPE_BIND, .uintValue = configKeyCUp},
{.name = "key_cdown", .type = CONFIG_TYPE_BIND, .uintValue = configKeyCDown},
{.name = "key_cleft", .type = CONFIG_TYPE_BIND, .uintValue = configKeyCLeft},
{.name = "key_cright", .type = CONFIG_TYPE_BIND, .uintValue = configKeyCRight},
{.name = "key_stickup", .type = CONFIG_TYPE_BIND, .uintValue = configKeyStickUp},
{.name = "key_stickdown", .type = CONFIG_TYPE_BIND, .uintValue = configKeyStickDown},
{.name = "key_stickleft", .type = CONFIG_TYPE_BIND, .uintValue = configKeyStickLeft},
{.name = "key_stickright", .type = CONFIG_TYPE_BIND, .uintValue = configKeyStickRight},
{.name = "key_chat", .type = CONFIG_TYPE_BIND, .uintValue = configKeyChat},
{.name = "key_playerlist", .type = CONFIG_TYPE_BIND, .uintValue = configKeyPlayerList},
{.name = "key_dup", .type = CONFIG_TYPE_BIND, .uintValue = configKeyDUp},
{.name = "key_ddown", .type = CONFIG_TYPE_BIND, .uintValue = configKeyDDown},
{.name = "key_dleft", .type = CONFIG_TYPE_BIND, .uintValue = configKeyDLeft},
{.name = "key_dright", .type = CONFIG_TYPE_BIND, .uintValue = configKeyDRight},
{.name = "key_console", .type = CONFIG_TYPE_BIND, .uintValue = configKeyConsole},
{.name = "key_prev", .type = CONFIG_TYPE_BIND, .uintValue = configKeyPrevPage},
{.name = "key_next", .type = CONFIG_TYPE_BIND, .uintValue = configKeyNextPage},
{.name = "key_disconnect", .type = CONFIG_TYPE_BIND, .uintValue = configKeyDisconnect},
{.name = "stick_deadzone", .type = CONFIG_TYPE_UINT, .uintValue = &configStickDeadzone},
{.name = "rumble_strength", .type = CONFIG_TYPE_UINT, .uintValue = &configRumbleStrength},
{.name = "gamepad_number", .type = CONFIG_TYPE_UINT, .uintValue = &configGamepadNumber},
{.name = "background_gamepad", .type = CONFIG_TYPE_UINT, .boolValue = &configBackgroundGamepad},
#ifndef HANDHELD
{.name = "disable_gamepads", .type = CONFIG_TYPE_BOOL, .boolValue = &configDisableGamepads},
#endif
{.name = "use_standard_key_bindings_chat", .type = CONFIG_TYPE_BOOL, .boolValue = &configUseStandardKeyBindingsChat},
{.name = "smooth_scrolling", .type = CONFIG_TYPE_BOOL, .boolValue = &configSmoothScrolling},
{.name = "stick_rotate_left", .type = CONFIG_TYPE_BOOL, .boolValue = &configStick.rotateLeft},
{.name = "stick_invert_left_x", .type = CONFIG_TYPE_BOOL, .boolValue = &configStick.invertLeftX},
{.name = "stick_invert_left_y", .type = CONFIG_TYPE_BOOL, .boolValue = &configStick.invertLeftY},
{.name = "stick_rotate_right", .type = CONFIG_TYPE_BOOL, .boolValue = &configStick.rotateRight},
{.name = "stick_invert_right_x", .type = CONFIG_TYPE_BOOL, .boolValue = &configStick.invertRightX},
{.name = "stick_invert_right_y", .type = CONFIG_TYPE_BOOL, .boolValue = &configStick.invertRightY},
// free camera settings
{.name = "bettercam_enable", .type = CONFIG_TYPE_BOOL, .boolValue = &configEnableFreeCamera},
{.name = "bettercam_analog", .type = CONFIG_TYPE_BOOL, .boolValue = &configFreeCameraAnalog},
{.name = "bettercam_centering", .type = CONFIG_TYPE_BOOL, .boolValue = &configFreeCameraLCentering},
{.name = "bettercam_dpad", .type = CONFIG_TYPE_BOOL, .boolValue = &configFreeCameraDPadBehavior},
{.name = "bettercam_collision", .type = CONFIG_TYPE_BOOL, .boolValue = &configFreeCameraHasCollision},
{.name = "bettercam_mouse_look", .type = CONFIG_TYPE_BOOL, .boolValue = &configFreeCameraMouse},
{.name = "bettercam_xsens", .type = CONFIG_TYPE_UINT, .uintValue = &configFreeCameraXSens},
{.name = "bettercam_ysens", .type = CONFIG_TYPE_UINT, .uintValue = &configFreeCameraYSens},
{.name = "bettercam_aggression", .type = CONFIG_TYPE_UINT, .uintValue = &configFreeCameraAggr},
{.name = "bettercam_pan_level", .type = CONFIG_TYPE_UINT, .uintValue = &configFreeCameraPan},
{.name = "bettercam_degrade", .type = CONFIG_TYPE_UINT, .uintValue = &configFreeCameraDegrade},
// romhack camera settings
{.name = "romhackcam_enable", .type = CONFIG_TYPE_UINT, .uintValue = &configEnableRomhackCamera},
{.name = "romhackcam_bowser", .type = CONFIG_TYPE_BOOL, .boolValue = &configRomhackCameraBowserFights},
{.name = "romhackcam_collision", .type = CONFIG_TYPE_BOOL, .boolValue = &configRomhackCameraHasCollision},
{.name = "romhackcam_centering", .type = CONFIG_TYPE_BOOL, .boolValue = &configRomhackCameraHasCentering},
{.name = "romhackcam_dpad", .type = CONFIG_TYPE_BOOL, .boolValue = &configRomhackCameraDPadBehavior},
{.name = "romhackcam_slowfall", .type = CONFIG_TYPE_BOOL, .boolValue = &configRomhackCameraSlowFall},
// common camera settings
{.name = "bettercam_invertx", .type = CONFIG_TYPE_BOOL, .boolValue = &configCameraInvertX},
{.name = "bettercam_inverty", .type = CONFIG_TYPE_BOOL, .boolValue = &configCameraInvertY},
{.name = "romhackcam_toxic_gas", .type = CONFIG_TYPE_BOOL, .boolValue = &configCameraToxicGas},
// debug
{.name = "debug_offset", .type = CONFIG_TYPE_U64, .u64Value = &gPcDebug.bhvOffset},
{.name = "debug_tags", .type = CONFIG_TYPE_U64, .u64Value = gPcDebug.tags},
{.name = "lua_profiler", .type = CONFIG_TYPE_BOOL, .boolValue = &configLuaProfiler},
{.name = "debug_print", .type = CONFIG_TYPE_BOOL, .boolValue = &configDebugPrint},
{.name = "debug_info", .type = CONFIG_TYPE_BOOL, .boolValue = &configDebugInfo},
{.name = "debug_error", .type = CONFIG_TYPE_BOOL, .boolValue = &configDebugError},
#ifdef DEVELOPMENT
{.name = "ctx_profiler", .type = CONFIG_TYPE_BOOL, .boolValue = &configCtxProfiler},
#endif
// player settings
{.name = "coop_player_name", .type = CONFIG_TYPE_STRING, .stringValue = (char*)&configPlayerName, .maxStringLength = MAX_CONFIG_STRING},
{.name = "coop_player_model", .type = CONFIG_TYPE_UINT, .uintValue = &configPlayerModel},
{.name = "coop_player_palette_pants", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[PANTS]},
{.name = "coop_player_palette_shirt", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[SHIRT]},
{.name = "coop_player_palette_gloves", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[GLOVES]},
{.name = "coop_player_palette_shoes", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[SHOES]},
{.name = "coop_player_palette_hair", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[HAIR]},
{.name = "coop_player_palette_skin", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[SKIN]},
{.name = "coop_player_palette_cap", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[CAP]},
{.name = "coop_player_palette_emblem", .type = CONFIG_TYPE_COLOR, .colorValue = &configPlayerPalette.parts[EMBLEM]},
// coop settings
{.name = "amount_of_players", .type = CONFIG_TYPE_UINT, .uintValue = &configAmountOfPlayers},
{.name = "bubble_death", .type = CONFIG_TYPE_BOOL, .boolValue = &configBubbleDeath},
{.name = "coop_host_port", .type = CONFIG_TYPE_UINT, .uintValue = &configHostPort},
{.name = "coop_host_save_slot", .type = CONFIG_TYPE_UINT, .uintValue = &configHostSaveSlot},
{.name = "coop_join_ip", .type = CONFIG_TYPE_STRING, .stringValue = (char*)&configJoinIp, .maxStringLength = MAX_CONFIG_STRING},
{.name = "coop_join_port", .type = CONFIG_TYPE_UINT, .uintValue = &configJoinPort},
{.name = "coop_network_system", .type = CONFIG_TYPE_UINT, .uintValue = &configNetworkSystem},
{.name = "coop_player_interaction", .type = CONFIG_TYPE_UINT, .uintValue = &configPlayerInteraction},
{.name = "coop_player_knockback_strength", .type = CONFIG_TYPE_UINT, .uintValue = &configPlayerKnockbackStrength},
{.name = "coop_stay_in_level_after_star", .type = CONFIG_TYPE_UINT, .uintValue = &configStayInLevelAfterStar},
{.name = "coop_nametags", .type = CONFIG_TYPE_BOOL, .boolValue = &configNametags},
{.name = "coop_mod_dev_mode", .type = CONFIG_TYPE_BOOL, .boolValue = &configModDevMode},
{.name = "coop_bouncy_bounds", .type = CONFIG_TYPE_UINT, .uintValue = &configBouncyLevelBounds},
{.name = "skip_intro", .type = CONFIG_TYPE_BOOL, .boolValue = &configSkipIntro},
{.name = "pause_anywhere", .type = CONFIG_TYPE_BOOL, .boolValue = &configPauseAnywhere},
{.name = "coop_menu_staff_roll", .type = CONFIG_TYPE_BOOL, .boolValue = &configMenuStaffRoll},
{.name = "coop_menu_level", .type = CONFIG_TYPE_UINT, .uintValue = &configMenuLevel},
{.name = "coop_menu_sound", .type = CONFIG_TYPE_UINT, .uintValue = &configMenuSound},
{.name = "coop_menu_random", .type = CONFIG_TYPE_BOOL, .boolValue = &configMenuRandom},
{.name = "player_pvp_mode", .type = CONFIG_TYPE_UINT, .uintValue = &configPvpType},
// {.name = "coop_menu_demos", .type = CONFIG_TYPE_BOOL, .boolValue = &configMenuDemos},
{.name = "disable_popups", .type = CONFIG_TYPE_BOOL, .boolValue = &configDisablePopups},
{.name = "language", .type = CONFIG_TYPE_STRING, .stringValue = (char*)&configLanguage, .maxStringLength = MAX_CONFIG_STRING},
{.name = "force_4by3", .type = CONFIG_TYPE_BOOL, .boolValue = &configForce4By3},
{.name = "dynos_local_player_model_only", .type = CONFIG_TYPE_BOOL, .boolValue = &configDynosLocalPlayerModelOnly},
// CoopNet settings
{.name = "coopnet_ip", .type = CONFIG_TYPE_STRING, .stringValue = (char*)&configCoopNetIp, .maxStringLength = MAX_CONFIG_STRING},
{.name = "coopnet_port", .type = CONFIG_TYPE_UINT, .uintValue = &configCoopNetPort},
{.name = "coopnet_password", .type = CONFIG_TYPE_STRING, .stringValue = (char*)&configPassword, .maxStringLength = MAX_CONFIG_STRING},
{.name = "coopnet_dest", .type = CONFIG_TYPE_STRING, .stringValue = (char*)&configDestId, .maxStringLength = MAX_CONFIG_STRING},
// DJUI settings
{.name = "djui_theme", .type = CONFIG_TYPE_UINT, .uintValue = &configDjuiTheme},
{.name = "djui_theme_center", .type = CONFIG_TYPE_BOOL, .boolValue = &configDjuiThemeCenter},
{.name = "djui_theme_gradients", .type = CONFIG_TYPE_BOOL, .boolValue = &configDjuiThemeGradients},
{.name = "djui_theme_font", .type = CONFIG_TYPE_UINT, .uintValue = &configDjuiThemeFont},
{.name = "djui_scale", .type = CONFIG_TYPE_UINT, .uintValue = &configDjuiScale},
{.name = "djui_chat_alpha", .type = CONFIG_TYPE_UINT, .uintValue = &configDjuiChatAlpha},
{.name = "djui_base_alpha", .type = CONFIG_TYPE_UINT, .uintValue = &configDjuiBaseAlpha},
{.name = "djui_chat_size", .type = CONFIG_TYPE_UINT, .uintValue = &configDjuiChatSize},
// other
{.name = "rules_version", .type = CONFIG_TYPE_UINT, .uintValue = &configRulesVersion},
{.name = "compress_on_startup", .type = CONFIG_TYPE_BOOL, .boolValue = &configCompressOnStartup},
{.name = "skip_pack_generation", .type = CONFIG_TYPE_BOOL, .boolValue = &configSkipPackGeneration},
};
struct SecretConfigOption {
const char *name;
enum ConfigOptionType type;
union {
bool *boolValue;
unsigned int *uintValue;
float* floatValue;
char* stringValue;
u64* u64Value;
u8 (*colorValue)[3];
};
int maxStringLength;
bool inConfig;
};
static struct SecretConfigOption secret_options[] = {
{.name = "ex_coop_theme", .type = CONFIG_TYPE_BOOL, .boolValue = &configExCoopTheme},
};
// FunctionConfigOption functions
struct QueuedFile {
char* path;
struct QueuedFile *next;
};
static struct QueuedFile *sQueuedEnableModsHead = NULL;
void enable_queued_mods(void) {
while (sQueuedEnableModsHead) {
struct QueuedFile *next = sQueuedEnableModsHead->next;
mods_enable(sQueuedEnableModsHead->path);
free(sQueuedEnableModsHead->path);
free(sQueuedEnableModsHead);
sQueuedEnableModsHead = next;
}
}
static void enable_mod_read(char** tokens, UNUSED int numTokens) {
if (gCLIOpts.disableMods) { return; }
char combined[256] = { 0 };
for (int i = 1; i < numTokens; i++) {
if (i != 1) { strncat(combined, " ", 255); }
strncat(combined, tokens[i], 255);
}
struct QueuedFile* queued = malloc(sizeof(struct QueuedFile));
queued->path = strdup(combined);
queued->next = NULL;
if (!sQueuedEnableModsHead) {
sQueuedEnableModsHead = queued;
} else {
struct QueuedFile* tail = sQueuedEnableModsHead;
while (tail->next) { tail = tail->next; }
tail->next = queued;
}
}
static void enable_mod(char* mod) {
struct QueuedFile* queued = malloc(sizeof(struct QueuedFile));
queued->path = mod;
queued->next = NULL;
if (!sQueuedEnableModsHead) {
sQueuedEnableModsHead = queued;
} else {
struct QueuedFile* tail = sQueuedEnableModsHead;
while (tail->next) { tail = tail->next; }
tail->next = queued;
}
}
static void enable_mod_write(FILE* file) {
for (unsigned int i = 0; i < gLocalMods.entryCount; i++) {
struct Mod* mod = gLocalMods.entries[i];
if (mod == NULL) { continue; }
if (!mod->enabled) { continue; }
fprintf(file, "%s %s\n", "enable-mod:", mod->relativePath);
}
}
static void ban_read(char** tokens, UNUSED int numTokens) {
ban_list_add(tokens[1], true);
}
static void ban_write(FILE* file) {
for (unsigned int i = 0; i < gBanCount; i++) {
if (gBanAddresses == NULL) { break; }
if (gBanAddresses[i] == NULL) { continue; }
if (!gBanPerm[i]) { continue; }
fprintf(file, "%s %s\n", "ban:", gBanAddresses[i]);
}
}
static void moderator_read(char** tokens, UNUSED int numTokens) {
moderator_list_add(tokens[1], true);
}
static void moderator_write(FILE* file) {
for (unsigned int i = 0; i < gModeratorCount; i++) {
if (gModeratorAddresses == NULL) { break; }
if (gModeratorAddresses[i] == NULL) { continue; }
if (!gModerator[i]) { continue; }
fprintf(file, "%s %s\n", "moderator:", gModeratorAddresses[i]);
}
}
static struct QueuedFile *sQueuedEnableDynosPacksHead = NULL;
void enable_queued_dynos_packs(void) {
while (sQueuedEnableDynosPacksHead) {
int packCount = dynos_pack_get_count();
const char *path = sQueuedEnableDynosPacksHead->path;
for (int i = 0; i < packCount; i++) {
const char* pack = dynos_pack_get_name(i);
if (!strcmp(path, pack)) {
dynos_pack_set_enabled(i, true);
break;
}
}
struct QueuedFile *next = sQueuedEnableDynosPacksHead->next;
free(sQueuedEnableDynosPacksHead->path);
free(sQueuedEnableDynosPacksHead);
sQueuedEnableDynosPacksHead = next;
}
}
static void dynos_pack_read(char** tokens, int numTokens) {
if (numTokens < 2) { return; }
if (strcmp(tokens[numTokens-1], "false") == 0) { return; } // Only accept enabled packs. Default is disabled. (old coop config compatibility)
char fullPackName[256] = { 0 };
for (int i = 1; i < numTokens; i++) {
if (i == numTokens - 1 && strcmp(tokens[i], "true") == 0) { break; } // old coop config compatibility
if (i != 1) { strncat(fullPackName, " ", 255); }
strncat(fullPackName, tokens[i], 255);
}
struct QueuedFile* queued = malloc(sizeof(struct QueuedFile));
queued->path = strdup(fullPackName);
queued->next = NULL;
if (!sQueuedEnableDynosPacksHead) {
sQueuedEnableDynosPacksHead = queued;
} else {
struct QueuedFile* tail = sQueuedEnableDynosPacksHead;
while (tail->next) { tail = tail->next; }
tail->next = queued;
}
}
static void dynos_pack_write(FILE* file) {
int packCount = dynos_pack_get_count();
for (int i = 0; i < packCount; i++) {
if (dynos_pack_get_enabled(i)) {
const char* pack = dynos_pack_get_name(i);
fprintf(file, "%s %s\n", "dynos-pack:", pack);
}
}
}
static void save_name_read(char** tokens, int numTokens) {
if (numTokens < 2) { return; }
char fullSaveName[MAX_SAVE_NAME_STRING] = { 0 };
int index = 0;
for (int i = 1; i < numTokens; i++) {
if (i == 1) {
index = atoi(tokens[i]);
} else {
if (i > 2) {
strncat(fullSaveName, " ", MAX_SAVE_NAME_STRING - 1);
}
strncat(fullSaveName, tokens[i], MAX_SAVE_NAME_STRING - 1);
}
}
snprintf(configSaveNames[index], MAX_SAVE_NAME_STRING, "%s", fullSaveName);
}
static void save_name_write(FILE* file) {
for (int i = 0; i < NUM_SAVE_FILES; i++) {
fprintf(file, "%s %d %s\n", "save-name:", i, configSaveNames[i]);
}
}
static const struct FunctionConfigOption functionOptions[] = {
{ .name = "enable-mod:", .read = enable_mod_read, .write = enable_mod_write },
{ .name = "ban:", .read = ban_read, .write = ban_write },
{ .name = "moderator:", .read = moderator_read, .write = moderator_write },
{ .name = "dynos-pack:", .read = dynos_pack_read, .write = dynos_pack_write },
{ .name = "save-name:", .read = save_name_read, .write = save_name_write }
};
// Reads an entire line from a file (excluding the newline character) and returns an allocated string
// Returns NULL if no lines could be read from the file
static char *read_file_line(fs_file_t *file, bool* error) {
char *buffer;
size_t bufferSize = 8;
size_t offset = 0; // offset in buffer to write
*error = false;
buffer = malloc(bufferSize);
buffer[0] = '\0';
while (1) {
// Read a line from the file
if (fs_readline(file, buffer + offset, bufferSize - offset) == NULL) {
free(buffer);
return NULL; // Nothing could be read.
}
offset = strlen(buffer);
if (offset <= 0) {
LOG_ERROR("Configfile offset <= 0");
*error = true;
return NULL;
}
// If a newline was found, remove the trailing newline and exit
if (buffer[offset - 1] == '\n') {
buffer[offset - 1] = '\0';
break;
}
if (fs_eof(file)) // EOF was reached
break;
// If no newline or EOF was reached, then the whole line wasn't read.
bufferSize *= 2; // Increase buffer size
buffer = realloc(buffer, bufferSize);
assert(buffer != NULL);
}
return buffer;
}
// Returns the position of the first non-whitespace character
static char *skip_whitespace(char *str) {
while (isspace(*str))
str++;
return str;
}
// NULL-terminates the current whitespace-delimited word, and returns a pointer to the next word
static char *word_split(char *str) {
// Precondition: str must not point to whitespace
assert(!isspace(*str));
// Find either the next whitespace char or end of string
while (!isspace(*str) && *str != '\0')
str++;
if (*str == '\0') // End of string
return str;
// Terminate current word
*(str++) = '\0';
// Skip whitespace to next word
return skip_whitespace(str);
}
// Splits a string into words, and stores the words into the 'tokens' array
// 'maxTokens' is the length of the 'tokens' array
// Returns the number of tokens parsed
static unsigned int tokenize_string(char *str, int maxTokens, char **tokens) {
int count = 0;
str = skip_whitespace(str);
while (str[0] != '\0' && count < maxTokens) {
tokens[count] = str;
str = word_split(str);
count++;
}
return count;
}
// Gets the config file path and caches it
const char *configfile_name(void) {
return (gCLIOpts.configFile[0]) ? gCLIOpts.configFile : CONFIGFILE_DEFAULT;
}
const char *configfile_backup_name(void) {
return CONFIGFILE_BACKUP;
}
// Loads the config file specified by 'filename'
static void configfile_load_internal(const char *filename, bool* error) {
fs_file_t *file;
char *line;
unsigned int temp;
*error = false;
#ifdef DEVELOPMENT
printf("Loading configuration from '%s'\n", filename);
#endif
file = fs_open(filename);
if (file == NULL) {
// Create a new config file and save defaults
printf("Config file '%s' not found. Creating it.\n", filename);
configfile_save(filename);
return;
}
// Go through each line in the file
while ((line = read_file_line(file, error)) != NULL && !*error) {
char *p = line;
char *tokens[20];
int numTokens;
// skip whitespace
while (isspace(*p))
p++;
// skip comment or empty line
if (!*p || *p == '#') {
free(line);
continue;
}
numTokens = tokenize_string(p, sizeof(tokens) / sizeof(tokens[0]), tokens);
if (numTokens != 0) {
if (numTokens >= 2) {
const struct ConfigOption *option = NULL;
// find functionOption
for (unsigned int i = 0; i < ARRAY_LEN(functionOptions); i++) {
if (strcmp(tokens[0], functionOptions[i].name) == 0) {
functionOptions[i].read(tokens, numTokens);
goto NEXT_OPTION;
}
}
// find option
for (unsigned int i = 0; i < ARRAY_LEN(options); i++) {
if (strcmp(tokens[0], options[i].name) == 0) {
option = &options[i];
break;
}
}
// secret options
for (unsigned int i = 0; i < ARRAY_LEN(secret_options); i++) {
if (strcmp(tokens[0], secret_options[i].name) == 0) {
secret_options[i].inConfig = true;
option = (const struct ConfigOption *) &secret_options[i];
break;
}
}
if (option == NULL) {
#ifdef DEVELOPMENT
printf("unknown option '%s'\n", tokens[0]);
#endif
} else {
switch (option->type) {
case CONFIG_TYPE_BOOL:
if (strcmp(tokens[1], "true") == 0)
*option->boolValue = true;
else
*option->boolValue = false;
break;
case CONFIG_TYPE_UINT:
sscanf(tokens[1], "%u", option->uintValue);
break;
case CONFIG_TYPE_BIND:
for (int i = 0; i < MAX_BINDS && i < numTokens - 1; ++i)
sscanf(tokens[i + 1], "%x", option->uintValue + i);
break;
case CONFIG_TYPE_FLOAT:
sscanf(tokens[1], "%f", option->floatValue);
break;
case CONFIG_TYPE_STRING:
memset(option->stringValue, '\0', option->maxStringLength);
snprintf(option->stringValue, option->maxStringLength, "%s", tokens[1]);
break;
case CONFIG_TYPE_U64:
sscanf(tokens[1], "%llu", option->u64Value);
break;
case CONFIG_TYPE_COLOR:
for (int i = 0; i < 3 && i < numTokens - 1; ++i) {
sscanf(tokens[i + 1], "%x", &temp);
(*option->colorValue)[i] = temp;
}
break;
default:
LOG_ERROR("Configfile read bad type '%d': %s", (int)option->type, line);
goto NEXT_OPTION;
}
#ifdef DEVELOPMENT
printf("option: '%s', value:", tokens[0]);
for (int i = 1; i < numTokens; ++i) printf(" '%s'", tokens[i]);
printf("\n");
#endif
}
} else {
#ifdef DEVELOPMENT
printf("error: expected value\n");
#endif
}
}
NEXT_OPTION:
free(line);
line = NULL;
}
if (line) {
free(line);
}
fs_close(file);
if (configFramerateMode < 0 || configFramerateMode > RRM_MAX) { configFramerateMode = 0; }
if (configFrameLimit < 30) { configFrameLimit = 30; }
if (configFrameLimit > 3000) { configFrameLimit = 3000; }
gMasterVolume = (f32)configMasterVolume / 127.0f;
if (configPlayerModel >= CT_MAX) { configPlayerModel = 0; }
if (configDjuiTheme >= DJUI_THEME_MAX) { configDjuiTheme = 0; }
if (configDjuiScale >= 5) { configDjuiScale = 0; }
if (configDjuiChatAlpha > 255) { configDjuiChatAlpha = 255; }
if (configDjuiBaseAlpha > 255) { configDjuiBaseAlpha = 255; }
if (configDjuiChatSize > 200) { configDjuiChatSize = 200; }
if (gCLIOpts.fullscreen == 1) {
configWindow.fullscreen = true;
} else if (gCLIOpts.fullscreen == 2) {
configWindow.fullscreen = false;
}
if (gCLIOpts.width != 0) { configWindow.w = gCLIOpts.width; }
if (gCLIOpts.height != 0) { configWindow.h = gCLIOpts.height; }
if (gCLIOpts.playerName[0]) { snprintf(configPlayerName, MAX_CONFIG_STRING, "%s", gCLIOpts.playerName); }
if (!network_player_name_valid(configPlayerName)) {
snprintf(configPlayerName, MAX_CONFIG_STRING, "Player");
}
for (int i = 0; i < gCLIOpts.enabledModsCount; i++) {
enable_mod(gCLIOpts.enableMods[i]);
}
free(gCLIOpts.enableMods);
if (gCLIOpts.playerCount != 0) {
configAmountOfPlayers = MIN(gCLIOpts.playerCount, MAX_PLAYERS);
}
#ifndef COOPNET
configNetworkSystem = NS_SOCKET;
#endif
}
void configfile_load(void) {
bool configReadError = false;
#ifdef DEVELOPMENT
configfile_load_internal(configfile_name(), &configReadError);
#else
configfile_load_internal(configfile_name(), &configReadError);
if (configReadError) {
configfile_load_internal(configfile_backup_name(), &configReadError);
} else {
configfile_save(configfile_backup_name());
}
#endif
}
static void configfile_save_option(FILE *file, const struct ConfigOption *option, bool isSecret) {
if (isSecret) {
const struct SecretConfigOption *secret_option = (const struct SecretConfigOption *) option;
if (!secret_option->inConfig) { return; }
}
switch (option->type) {
case CONFIG_TYPE_BOOL:
fprintf(file, "%s %s\n", option->name, *option->boolValue ? "true" : "false");
break;
case CONFIG_TYPE_UINT:
fprintf(file, "%s %u\n", option->name, *option->uintValue);
break;
case CONFIG_TYPE_FLOAT:
fprintf(file, "%s %f\n", option->name, *option->floatValue);
break;
case CONFIG_TYPE_BIND:
fprintf(file, "%s ", option->name);
for (int i = 0; i < MAX_BINDS; ++i)
fprintf(file, "%04x ", option->uintValue[i]);
fprintf(file, "\n");
break;
case CONFIG_TYPE_STRING:
fprintf(file, "%s %s\n", option->name, option->stringValue);
break;
case CONFIG_TYPE_U64:
fprintf(file, "%s %llu\n", option->name, *option->u64Value);
break;
case CONFIG_TYPE_COLOR:
fprintf(file, "%s %02x %02x %02x\n", option->name, (*option->colorValue)[0], (*option->colorValue)[1], (*option->colorValue)[2]);
break;
default:
LOG_ERROR("Configfile wrote bad type '%d': %s", (int)option->type, option->name);
break;
}
}
// Writes the config file to 'filename'
void configfile_save(const char *filename) {
FILE *file;
file = fopen(fs_get_write_path(filename), "w");
if (file == NULL) {
// error
return;
}
printf("Saving configuration to '%s'\n", filename);
for (unsigned int i = 0; i < ARRAY_LEN(options); i++) {
const struct ConfigOption *option = &options[i];
configfile_save_option(file, option, false);
}
for (unsigned int i = 0; i < ARRAY_LEN(secret_options); i++) {
const struct ConfigOption *option = (const struct ConfigOption *) &secret_options[i];
configfile_save_option(file, option, true);
}
// save function options
for (unsigned int i = 0; i < ARRAY_LEN(functionOptions); i++) {
functionOptions[i].write(file);
}
fclose(file);
}