#include #include "gamecontrol.h" #include "tarray.h" #include "zstring.h" #include "name.h" #include "control.h" #include "keyboard.h" #include "sc_man.h" #include "c_cvars.h" #include "gameconfigfile.h" #include "gamecvars.h" #include "build.h" #include "inputstate.h" #include "_control.h" #include "control.h" #include "m_argv.h" #include "rts.h" #include "printf.h" #include "c_bind.h" #include "v_font.h" #include "c_console.h" #include "c_dispatch.h" #include "i_specialpaths.h" #include "z_music.h" #include "statistics.h" #include "menu.h" #include "gstrings.h" #ifndef NETCODE_DISABLE #include "enet.h" #endif InputState inputState; void SetClipshapes(); int ShowStartupWindow(TArray &); void InitFileSystem(TArray&); bool gHaveNetworking; FString currentGame; FString LumpFilter; CVAR(Int, cl_defaultconfiguration, 2, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) UserConfig userConfig; void UserConfig::ProcessOptions() { // -help etc are omitted // -cfg / -setupfile refer to Build style config which are not supported. if (Args->CheckParm("-cfg") || Args->CheckParm("-setupfile")) { initprintf("Build-format config files not supported and will be ignored\n"); } auto v = Args->CheckValue("-port"); if (v) netPort = strtol(v, nullptr, 0); netServerMode = Args->CheckParm("-server"); netServerAddress = Args->CheckValue("-connect"); netPassword = Args->CheckValue("-password"); v = Args->CheckValue("-addon"); if (v) { auto val = strtol(v, nullptr, 0); static const char* const addons[] = { "DUKE3D.GRP", "DUKEDC.GRP", "NWINTER.GRP", "VACATION.GRP" }; if (val > 0 && val < 4) gamegrp = addons[val]; else initprintf("%s: Unknown Addon\n", v); } else if (Args->CheckParm("-nam")) { gamegrp = "NAM.GRP"; } else if (Args->CheckParm("-napalm")) { gamegrp = "NAPALM.GRP"; } else if (Args->CheckParm("-ww2gi")) { gamegrp = "WW2GI.GRP"; } v = Args->CheckValue("-gamegrp"); if (v) { gamegrp = v; } else { // This is to enable the use of Doom launchers. that are limited to -iwad for specifying the game's main resource. v = Args->CheckValue("-iwad"); if (v) { gamegrp = v; } } Args->CollectFiles("-rts", ".rts"); auto rts = Args->CheckValue("-rts"); if (rts) RTS_Init(rts); Args->CollectFiles("-map", ".map"); CommandMap = Args->CheckValue("-map"); static const char* defs[] = { "-def", "-h", nullptr }; Args->CollectFiles("-def", defs, ".def"); DefaultDef = Args->CheckValue("-def"); static const char* cons[] = { "-con", "-x", nullptr }; Args->CollectFiles("-con", cons, ".con"); DefaultCon = Args->CheckValue("-con"); static const char* demos[] = { "-playback", "-d", "-demo", nullptr }; Args->CollectFiles("-demo", demos, ".dmo"); CommandDemo = Args->CheckValue("-demo"); static const char* names[] = { "-pname", "-name", nullptr }; Args->CollectFiles("-name", names, ".---"); // this shouldn't collect any file names at all so use a nonsense extension CommandName = Args->CheckValue("-name"); static const char* nomos[] = { "-nomonsters", "-nodudes", nullptr }; Args->CollectFiles("-nomonsters", nomos, ".---"); // this shouldn't collect any file names at all so use a nonsense extension nomonsters = Args->CheckParm("-nomonsters"); static const char* acons[] = { "-addcon", "-mx", nullptr }; Args->CollectFiles("-addcon", acons, ".con"); AddCons.reset(Args->GatherFiles("-addcon")); static const char* adefs[] = { "-adddef", "-mh", nullptr }; Args->CollectFiles("-adddef", adefs, ".def"); AddDefs.reset(Args->GatherFiles("-adddef")); Args->CollectFiles("-art", ".art"); AddArt.reset(Args->GatherFiles("-art")); CommandIni = Args->CheckValue("-ini"); nologo = Args->CheckParm("-nologo") || Args->CheckParm("-quick"); nomusic = Args->CheckParm("-nomusic"); nosound = Args->CheckParm("-nosfx"); if (Args->CheckParm("-nosound")) nomusic = nosound = true; if (Args->CheckParm("-setup")) setupstate = 1; else if (Args->CheckParm("-nosetup")) setupstate = 0; if (Args->CheckParm("-file")) { // For file loading there's two modes: // If -file is given, all content will be processed in order and the legacy options be ignored entirely. //This allows mixing directories and GRP files in arbitrary order. Args->CollectFiles("-file", NULL); AddFiles.reset(Args->GatherFiles("-file")); } else { // Trying to emulate Build. This means to treat RFF files as lowest priority, then all GRPs and then all directories. // This is only for people depending on lauchers. Since the semantics are so crappy it is strongly recommended to // use -file instead which gives the user full control over the order in which things are added. // For single mods this is no problem but don't even think about loading more stuff consistently... static const char* grps[] = { "-g", "-grp", nullptr }; static const char* dirs[] = { "-game_dir", "-j", nullptr }; static const char* rffs[] = { "-rff", "-snd", nullptr }; static const char* twostep[] = { "-rff", "-grp", nullptr }; // Abuse the inner workings to get the files into proper order. This is not 100% accurate but should work fine for everything that doesn't intentionally fuck things up. Args->CollectFiles("-rff", rffs, ".rff"); Args->CollectFiles("-grp", grps, nullptr); Args->CollectFiles("-grp", twostep, nullptr); // The two previous calls have already brought the content in order so collecting it again gives us one list with everything. AddFilesPre.reset(Args->GatherFiles("-grp")); Args->CollectFiles("-game_dir", dirs, nullptr); AddFiles.reset(Args->GatherFiles("-game_dir")); } } //========================================================================== // // // //========================================================================== namespace Duke { ::GameInterface* CreateInterface(); } namespace Redneck { ::GameInterface* CreateInterface(); } namespace Blood { ::GameInterface* CreateInterface(); } namespace ShadowWarrior { ::GameInterface* CreateInterface(); } void CheckFrontend(int flags) { if (flags & GAMEFLAG_BLOOD) { gi = Blood::CreateInterface(); } else if (flags & GAMEFLAG_RR) { gi = Redneck::CreateInterface(); } else if (flags & GAMEFLAG_SW) { gi = ShadowWarrior::CreateInterface(); } else { gi = Duke::CreateInterface(); } } int GameMain() { // Set up the console before anything else so that it can receive text. C_InitConsole(1024, 768, true); FStringf logpath("logfile %sdemolition.log", M_GetDocumentsPath().GetChars()); C_DoCommand(logpath); #ifndef NETCODE_DISABLE gHaveNetworking = !enet_initialize(); if (!gHaveNetworking) initprintf("An error occurred while initializing ENet.\n"); #endif int r; try { r = CONFIG_Init(); } catch (const std::runtime_error & err) { wm_msgbox("Error", "%s", err.what()); return 3; } catch (const ExitEvent & exit) { // Just let the rest of the function execute. r = exit.Reason(); } G_SaveConfig(); #ifndef NETCODE_DISABLE if (gHaveNetworking) enet_deinitialize(); #endif return r; } //========================================================================== // // Try to keep all initializations of global string variables in this one place // //========================================================================== #define LOCALIZED_STRING(s) s // change to "${" s "}" later, once all text output functions can replace text macros void SetDefaultStrings() { // Hard coded texts for the episode and skill selection menus. if (g_gameType & GAMEFLAG_DUKE) { gVolumeNames[0] = LOCALIZED_STRING("L.A. Meltdown"); gVolumeNames[1] = LOCALIZED_STRING("Lunar Apocalypse"); gVolumeNames[2] = LOCALIZED_STRING("Shrapnel City"); gSkillNames[0] = LOCALIZED_STRING("Piece Of Cake"); gSkillNames[1] = LOCALIZED_STRING("Let's Rock"); gSkillNames[2] = LOCALIZED_STRING("Come Get Some"); gSkillNames[3] = LOCALIZED_STRING("Damn I'm Good"); } else if (g_gameType & GAMEFLAG_BLOOD) { gSkillNames[0] = LOCALIZED_STRING("STILL KICKING"); gSkillNames[1] = LOCALIZED_STRING("PINK ON THE INSIDE"); gSkillNames[2] = LOCALIZED_STRING("LIGHTLY BROILED"); gSkillNames[3] = LOCALIZED_STRING("WELL DONE"); gSkillNames[4] = LOCALIZED_STRING("EXTRA CRISPY"); } else if (g_gameType & GAMEFLAG_SW) { gVolumeNames[0] = LOCALIZED_STRING("Enter the Wang"); gVolumeNames[1] = LOCALIZED_STRING("Code of Honor"); gVolumeSubtitles[0] = LOCALIZED_STRING("Four levels (Shareware Version)"); gVolumeSubtitles[1] = LOCALIZED_STRING("Eighteen levels (Full Version Only)"); gSkillNames[0] = LOCALIZED_STRING("Tiny grasshopper"); gSkillNames[1] = LOCALIZED_STRING("I Have No Fear"); gSkillNames[2] = LOCALIZED_STRING("Who Wants Wang"); gSkillNames[3] = LOCALIZED_STRING("No Pain, No Gain"); } } //========================================================================== // // // //========================================================================== int CONFIG_Init() { SetClipshapes(); // This must be done before initializing any data, so doing it late in the startup process won't work. if (CONTROL_Startup(controltype_keyboardandmouse, BGetTime, 120)) { return 1; } userConfig.ProcessOptions(); G_LoadConfig(); // Startup dialog must be presented here so that everything can be set up before reading the keybinds. auto groups = GrpScan(); if (groups.Size() == 0) { // Abort if no game data found. G_SaveConfig(); I_Error("Unable to find any game data. Please verify your settings."); } decltype(groups) usedgroups; int groupno = -1; // If the user has specified a file name, let's see if we know it. // if (userConfig.gamegrp.Len()) { FString gamegrplower = "/" + userConfig.gamegrp.MakeLower(); int g = 0; for (auto& grp : groups) { auto grplower = grp.FileName.MakeLower(); grplower.Substitute("\\", "/"); if (grplower.LastIndexOf(gamegrplower) == grplower.Len() - gamegrplower.Len()) { groupno = g; break; } g++; } } if (groupno == -1 || userConfig.setupstate == 1) groupno = ShowStartupWindow(groups); if (groupno == -1) return 0; auto &group = groups[groupno]; // Now filter out the data we actually need and delete the rest. usedgroups.Push(group); auto crc = group.FileInfo.dependencyCRC; if (crc != 0) for (auto& dep : groups) { if (dep.FileInfo.CRC == crc) { usedgroups.Insert(0, dep); // Order from least dependent to most dependent, which is the loading order of data. } } groups.Reset(); FString selectedScript; FString selectedDef; for (auto& ugroup : usedgroups) { // For CONs the command line has priority, aside from that, the last one wins. For Blood this handles INIs - the rules are the same. if (ugroup.FileInfo.scriptname.IsNotEmpty()) selectedScript = ugroup.FileInfo.scriptname; if (ugroup.FileInfo.defname.IsNotEmpty()) selectedDef = ugroup.FileInfo.defname; // CVAR has priority. This also overwrites the global variable each time. Init here is lazy so this is ok. if (ugroup.FileInfo.rtsname.IsNotEmpty() && **rtsname == 0) RTS_Init(ugroup.FileInfo.rtsname); // For the game filter the last non-empty one wins. if (ugroup.FileInfo.gamefilter.IsNotEmpty()) LumpFilter = ugroup.FileInfo.gamefilter; g_gameType |= ugroup.FileInfo.flags; } if (userConfig.DefaultCon.IsEmpty()) userConfig.DefaultCon = selectedScript; if (userConfig.DefaultDef.IsEmpty()) userConfig.DefaultDef = selectedDef; // This can only happen with a custom game that does not define any filter. // In this case take the display name and strip all whitespace and invaliid path characters from it. if (LumpFilter.IsEmpty()) { LumpFilter = usedgroups.Last().FileInfo.name; LumpFilter.StripChars(".:/\\<>?\"*| \t\r\n"); } currentGame = LumpFilter; currentGame.Truncate(currentGame.IndexOf(".")); CheckFrontend(g_gameType); InitFileSystem(usedgroups); CONTROL_ClearAssignments(); CONFIG_InitMouseAndController(); CONFIG_SetGameControllerDefaultsStandard(); CONFIG_SetDefaultKeys(cl_defaultconfiguration == 1 ? "demolition/origbinds.txt" : cl_defaultconfiguration == 2 ? "demolition/leftbinds.txt" : "demolition/defbinds.txt"); G_ReadConfig(currentGame); if (!GameConfig->IsInitialized()) { CONFIG_ReadCombatMacros(); } if (userConfig.CommandName.IsNotEmpty()) { playername = userConfig.CommandName; } GStrings.LoadStrings(); V_InitFonts(); buttonMap.SetGameAliases(); Mus_Init(); InitStatistics(); M_Init(); SetDefaultStrings(); return gi->app_main(); } //========================================================================== // // // //========================================================================== CVAR(String, combatmacro0, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro1, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro2, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro3, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro4, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro5, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro6, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro7, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro8, "", CVAR_ARCHIVE | CVAR_USERINFO) CVAR(String, combatmacro9, "", CVAR_ARCHIVE | CVAR_USERINFO) FStringCVar* const CombatMacros[] = { &combatmacro0, &combatmacro1, &combatmacro2, &combatmacro3, &combatmacro4, &combatmacro5, &combatmacro6, &combatmacro7, &combatmacro8, &combatmacro9}; void CONFIG_ReadCombatMacros() { FScanner sc; sc.Open("demolition/combatmacros.txt"); for (auto s : CombatMacros) { sc.MustGetToken(TK_StringConst); if (strlen(*s) == 0) *s = sc.String; } } //========================================================================== // // // //========================================================================== static FString CONFIG_GetMD4EntryName(uint8_t const* const md4) { return FStringf("MD4_%08x%08x%08x%08x", B_BIG32(B_UNBUF32(&md4[0])), B_BIG32(B_UNBUF32(&md4[4])), B_BIG32(B_UNBUF32(&md4[8])), B_BIG32(B_UNBUF32(&md4[12]))); } int32_t CONFIG_GetMapBestTime(char const* const mapname, uint8_t const* const mapmd4) { auto m = CONFIG_GetMD4EntryName(mapmd4); if (GameConfig->SetSection("MapTimes")) { auto s = GameConfig->GetValueForKey(m); if (s) (int)strtoull(s, nullptr, 0); } return -1; } int CONFIG_SetMapBestTime(uint8_t const* const mapmd4, int32_t tm) { FStringf t("%d", tm); auto m = CONFIG_GetMD4EntryName(mapmd4); if (GameConfig->SetSection("MapTimes")) { GameConfig->SetValueForKey(m, t); } return 0; } //========================================================================== // // // //========================================================================== int32_t MouseDigitalFunctions[MAXMOUSEAXES][2]; int32_t MouseAnalogueAxes[MAXMOUSEAXES]; int32_t JoystickFunctions[MAXJOYBUTTONSANDHATS][2]; int32_t JoystickDigitalFunctions[MAXJOYAXES][2]; int32_t JoystickAnalogueAxes[MAXJOYAXES]; int32_t JoystickAnalogueScale[MAXJOYAXES]; int32_t JoystickAnalogueDead[MAXJOYAXES]; int32_t JoystickAnalogueSaturate[MAXJOYAXES]; int32_t JoystickAnalogueInvert[MAXJOYAXES]; static const char* mouseanalogdefaults[MAXMOUSEAXES] = { "analog_turning", "analog_moving", }; static const char* mousedigitaldefaults[MAXMOUSEDIGITAL] = { }; static const char* joystickdefaults[MAXJOYBUTTONSANDHATS] = { "Fire", "Strafe", "Run", "Open", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Aim_Down", "Look_Right", "Aim_Up", "Look_Left", }; static const char* joystickclickeddefaults[MAXJOYBUTTONSANDHATS] = { "", "Inventory", "Jump", "Crouch", }; static const char* joystickanalogdefaults[MAXJOYAXES] = { "analog_turning", "analog_moving", "analog_strafing", }; static const char* joystickdigitaldefaults[MAXJOYDIGITAL] = { "", "", "", "", "", "", "Run", }; //========================================================================== // // // //========================================================================== int32_t CONFIG_AnalogNameToNum(const char* func) { if (!func) return -1; if (!Bstrcasecmp(func, "analog_turning")) { return analog_turning; } if (!Bstrcasecmp(func, "analog_strafing")) { return analog_strafing; } if (!Bstrcasecmp(func, "analog_moving")) { return analog_moving; } if (!Bstrcasecmp(func, "analog_lookingupanddown")) { return analog_lookingupanddown; } return -1; } //========================================================================== // // // //========================================================================== const char* CONFIG_AnalogNumToName(int32_t func) { switch (func) { case analog_turning: return "analog_turning"; case analog_strafing: return "analog_strafing"; case analog_moving: return "analog_moving"; case analog_lookingupanddown: return "analog_lookingupanddown"; } return NULL; } void CONFIG_SetupMouse(void) { CONTROL_MouseEnabled = (in_mouse && CONTROL_MousePresent); } void CONFIG_SetupJoystick(void) { const char* val; FString section = currentGame + ".ControllerSettings"; if (!GameConfig->SetSection(section)) return; for (int i = 0; i < MAXJOYBUTTONSANDHATS; i++) { section.Format("ControllerButton%d", i); val = GameConfig->GetValueForKey(section); if (val) JoystickFunctions[i][0] = buttonMap.FindButtonIndex(val); section.Format("ControllerButtonClicked%d", i); val = GameConfig->GetValueForKey(section); if (val) JoystickFunctions[i][1] = buttonMap.FindButtonIndex(val); } // map over the axes for (int i = 0; i < MAXJOYAXES; i++) { section.Format("ControllerAnalogAxes%d", i); val = GameConfig->GetValueForKey(section); if (val) JoystickAnalogueAxes[i] = CONFIG_AnalogNameToNum(val); section.Format("ControllerDigitalAxes%d_0", i); val = GameConfig->GetValueForKey(section); if (val) JoystickDigitalFunctions[i][0] = buttonMap.FindButtonIndex(val); section.Format("ControllerDigitalAxes%d_1", i); val = GameConfig->GetValueForKey(section); if (val) JoystickDigitalFunctions[i][1] = buttonMap.FindButtonIndex(val); section.Format("ControllerAnalogScale%d", i); val = GameConfig->GetValueForKey(section); if (val) JoystickAnalogueScale[i] = (int32_t)strtoull(val, nullptr, 0); section.Format("ControllerAnalogInvert%d", i); val = GameConfig->GetValueForKey(section); if (val) JoystickAnalogueInvert[i] = (int32_t)strtoull(val, nullptr, 0); section.Format("ControllerAnalogDead%d", i); val = GameConfig->GetValueForKey(section); if (val) JoystickAnalogueDead[i] = (int32_t)strtoull(val, nullptr, 0); section.Format("ControllerAnalogSaturate%d", i); val = GameConfig->GetValueForKey(section); if (val) JoystickAnalogueSaturate[i] = (int32_t)strtoull(val, nullptr, 0); } for (int i = 0; i < MAXJOYAXES; i++) { CONTROL_MapAnalogAxis(i, JoystickAnalogueAxes[i], controldevice_joystick); CONTROL_MapDigitalAxis(i, JoystickDigitalFunctions[i][0], 0, controldevice_joystick); CONTROL_MapDigitalAxis(i, JoystickDigitalFunctions[i][1], 1, controldevice_joystick); CONTROL_SetAnalogAxisScale(i, JoystickAnalogueScale[i], controldevice_joystick); CONTROL_SetAnalogAxisInvert(i, JoystickAnalogueInvert[i], controldevice_joystick); } CONTROL_JoystickEnabled = (in_joystick && CONTROL_JoyPresent); // JBF 20040215: evil and nasty place to do this, but joysticks are evil and nasty too for (int i=0; i= KEY_FIRSTMOUSEBUTTON && key < KEY_FIRSTJOYBUTTON) || (key >= KEY_MWHEELUP && key <= KEY_MWHEELLEFT)) { auto scan = KB_ScanCodeToString(key); if (scan) return scan; } } return ""; } char const* CONFIG_GetGameFuncOnJoystick(int gameFunc) { auto binding = buttonMap.GetButtonAlias(gameFunc); auto keys = Bindings.GetKeysForCommand(binding); for (auto key : keys) { if (key >= KEY_FIRSTJOYBUTTON && !(key >= KEY_MWHEELUP && key <= KEY_MWHEELLEFT)) { auto scan = KB_ScanCodeToString(key); if (scan) return scan; } } return ""; } // FIXME: Consider the mouse as well! FString CONFIG_GetBoundKeyForLastInput(int gameFunc) { if (CONTROL_LastSeenInput == LastSeenInput::Joystick) { FString name = CONFIG_GetGameFuncOnJoystick(gameFunc); if (name.IsNotEmpty()) { return name; } } FString name = CONFIG_GetGameFuncOnKeyboard(gameFunc); if (name.IsNotEmpty()) { return name; } name = CONFIG_GetGameFuncOnMouse(gameFunc); if (name.IsNotEmpty()) { return name; } name = CONFIG_GetGameFuncOnJoystick(gameFunc); if (name.IsNotEmpty()) { return name; } return "UNBOUND"; } void CONFIG_InitMouseAndController() { memset(MouseDigitalFunctions, -1, sizeof(MouseDigitalFunctions)); memset(JoystickFunctions, -1, sizeof(JoystickFunctions)); memset(JoystickDigitalFunctions, -1, sizeof(JoystickDigitalFunctions)); for (int i = 0; i < MAXMOUSEAXES; i++) { MouseDigitalFunctions[i][0] = buttonMap.FindButtonIndex(mousedigitaldefaults[i * 2]); MouseDigitalFunctions[i][1] = buttonMap.FindButtonIndex(mousedigitaldefaults[i * 2 + 1]); CONTROL_MapDigitalAxis(i, MouseDigitalFunctions[i][0], 0, controldevice_mouse); CONTROL_MapDigitalAxis(i, MouseDigitalFunctions[i][1], 1, controldevice_mouse); MouseAnalogueAxes[i] = CONFIG_AnalogNameToNum(mouseanalogdefaults[i]); CONTROL_MapAnalogAxis(i, MouseAnalogueAxes[i], controldevice_mouse); } CONFIG_SetupMouse(); CONFIG_SetupJoystick(); inputState.ClearKeysDown(); inputState.keyFlushChars(); inputState.keyFlushScans(); } void CONFIG_PutNumber(const char* key, int number) { FStringf str("%d", number); GameConfig->SetValueForKey(key, str); } void CONFIG_WriteControllerSettings() { FString buf; if (in_joystick) { FString section = currentGame + ".ControllerSettings"; GameConfig->SetSection(section); for (int dummy = 0; dummy < MAXJOYBUTTONSANDHATS; dummy++) { if (buttonMap.GetButtonName(JoystickFunctions[dummy][0])) { buf.Format("ControllerButton%d", dummy); GameConfig->SetValueForKey(buf, buttonMap.GetButtonName(JoystickFunctions[dummy][0])); } if (buttonMap.GetButtonName(JoystickFunctions[dummy][1])) { buf.Format("ControllerButtonClicked%d", dummy); GameConfig->SetValueForKey(buf, buttonMap.GetButtonName(JoystickFunctions[dummy][1])); } } for (int dummy = 0; dummy < MAXJOYAXES; dummy++) { if (CONFIG_AnalogNumToName(JoystickAnalogueAxes[dummy])) { buf.Format("ControllerAnalogAxes%d", dummy); GameConfig->SetValueForKey(buf, CONFIG_AnalogNumToName(JoystickAnalogueAxes[dummy])); } if (buttonMap.GetButtonName(JoystickDigitalFunctions[dummy][0])) { buf.Format("ControllerDigitalAxes%d_0", dummy); GameConfig->SetValueForKey(buf, buttonMap.GetButtonName(JoystickDigitalFunctions[dummy][0])); } if (buttonMap.GetButtonName(JoystickDigitalFunctions[dummy][1])) { buf.Format("ControllerDigitalAxes%d_1", dummy); GameConfig->SetValueForKey(buf, buttonMap.GetButtonName(JoystickDigitalFunctions[dummy][1])); } buf.Format("ControllerAnalogScale%d", dummy); CONFIG_PutNumber(buf, JoystickAnalogueScale[dummy]); buf.Format("ControllerAnalogInvert%d", dummy); CONFIG_PutNumber(buf, JoystickAnalogueInvert[dummy]); buf.Format("ControllerAnalogDead%d", dummy); CONFIG_PutNumber(buf, JoystickAnalogueDead[dummy]); buf.Format("ControllerAnalogSaturate%d", dummy); CONFIG_PutNumber(buf, JoystickAnalogueSaturate[dummy]); } } }