diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bafed6718..66e82ce39 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -77,6 +77,7 @@ add_executable(BLANKART MACOSX_BUNDLE WIN32 r_plane.cpp r_segs.cpp r_skins.c + r_voicepreference.cpp r_sky.c r_splats.c r_things.cpp diff --git a/src/d_main.cpp b/src/d_main.cpp index ee57bbe24..c95774944 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -54,6 +54,7 @@ #include "p_saveg.h" #include "r_main.h" #include "r_local.h" +#include "r_voicepreference.hpp" // Preferences directory #include "s_sound.h" #include "st_stuff.h" #include "v_video.h" @@ -1564,6 +1565,7 @@ void D_SRB2Main(void) strcatbf(liveeventbackup, srb2home, PATHSEP); snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", srb2home); + snprintf(preferencesdir, sizeof preferencesdir, "%s" PATHSEP "preferences" PATHSEP, srb2home); #else // DEFAULTDIR snprintf(srb2home, sizeof srb2home, "%s", userhome); if (dedicated) @@ -1576,6 +1578,7 @@ void D_SRB2Main(void) strcatbf(liveeventbackup, userhome, PATHSEP); snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", userhome); + snprintf(preferencesdir, sizeof preferencesdir, "%s" PATHSEP "preferences" PATHSEP, userhome); #endif // DEFAULTDIR } @@ -1796,6 +1799,8 @@ void D_SRB2Main(void) //--------------------------------------------------------- CONFIG.CFG M_FirstLoadConfig(); // WARNING : this do a "COM_BufExecute()" + R_LoadVoicePreferences(); + VID_PrepareModeList(); // Regenerate Modelist according to cv_fullscreen // set user default mode or mode set at cmdline diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 336573254..9e7e3aa2b 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -29,6 +29,7 @@ #include "p_mobj.h" #include "r_local.h" #include "r_skins.h" +#include "r_voicepreference.hpp" #include "p_local.h" #include "p_setup.h" #include "s_sound.h" @@ -147,10 +148,12 @@ static void Followercolor2_OnChange(void); static void Followercolor3_OnChange(void); static void Followercolor4_OnChange(void); +/* static void Voice_OnChange(void); static void Voice2_OnChange(void); static void Voice3_OnChange(void); static void Voice4_OnChange(void); +*/ static void Color_OnChange(void); static void Color2_OnChange(void); @@ -266,7 +269,14 @@ static void Command_Isgamemodified_f(void); static void Command_Cheats_f(void); static void Command_ListSkins(void); + +static void Command_Voice(void); +static void Command_Voice2(void); +static void Command_Voice3(void); +static void Command_Voice4(void); + static void Command_ListVoices(void); +static void Command_ListVoicePrefs(void); #ifdef _DEBUG static void Command_Togglemodified_f(void); @@ -370,13 +380,7 @@ consvar_t cv_followercolor[MAXSPLITSCREENPLAYERS] = { CVAR_INIT ("followercolor4", "Default", CV_SAVE|CV_CALL|CV_NOINIT, Followercolor_cons_t, Followercolor4_OnChange) }; -// player's voices...also, uh, saved. -consvar_t cv_voice[MAXSPLITSCREENPLAYERS] = { - CVAR_INIT ("voice", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice_OnChange), - CVAR_INIT ("voice2", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice2_OnChange), - CVAR_INIT ("voice3", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice3_OnChange), - CVAR_INIT ("voice4", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice4_OnChange) -}; +// Player voice data now saved to its own config static CV_PossibleValue_t restatvalue_cons_t[] = {{0, "MIN"}, {9, "MAX"}, {0, NULL}}; consvar_t cv_dummyrestatspeed[MAXSPLITSCREENPLAYERS] = { @@ -399,6 +403,14 @@ consvar_t cv_dummyrestatrandom[MAXSPLITSCREENPLAYERS] = { CVAR_INIT ("restatrandom4", "No", 0, CV_YesNo, NULL) }; +// Toggle for automatic voice preferences on voice change +consvar_t cv_autovoxpref[MAXSPLITSCREENPLAYERS] = { + CVAR_INIT ("voicepref_auto", "On", CV_SAVE|CV_NOINIT, CV_OnOff, NULL), + CVAR_INIT ("voicepref_auto2", "On", CV_SAVE|CV_NOINIT, CV_OnOff, NULL), + CVAR_INIT ("voicepref_auto3", "On", CV_SAVE|CV_NOINIT, CV_OnOff, NULL), + CVAR_INIT ("voicepref_auto4", "On", CV_SAVE|CV_NOINIT, CV_OnOff, NULL) +}; + consvar_t cv_skipmapcheck = CVAR_INIT ("skipmapcheck", "Off", CV_SAVE, CV_OnOff, NULL); consvar_t cv_usemouse = CVAR_INIT ("use_mouse", "Off", CV_SAVE|CV_CALL,usemouse_cons_t, I_StartupMouse); @@ -1341,7 +1353,7 @@ void D_RegisterClientCommands(void) CV_RegisterVar(&cv_followercolor[i]); CV_RegisterVar(&cv_jitterlegacy[i]); CV_RegisterVar(&cv_driftmode[i]); - CV_RegisterVar(&cv_voice[i]); + CV_RegisterVar(&cv_autovoxpref[i]); CV_RegisterVar(&cv_dummyrestatspeed[i]); CV_RegisterVar(&cv_dummyrestatweight[i]); @@ -1550,8 +1562,14 @@ void D_RegisterClientCommands(void) CV_RegisterVar(&cv_discordasks); #endif + COM_AddCommand("voice", Command_Voice); + COM_AddCommand("voice2", Command_Voice2); + COM_AddCommand("voice3", Command_Voice3); + COM_AddCommand("voice4", Command_Voice4); + COM_AddCommand("listskins", Command_ListSkins); COM_AddCommand("listvoices", Command_ListVoices); + COM_AddCommand("listvoiceprefs", Command_ListVoicePrefs); CV_RegisterVar(&cv_connectawaittime); CV_RegisterVar(&cv_serverinfoscreen); @@ -2055,6 +2073,12 @@ VaguePartyDescription (int playernum, int size, int default_color) static INT32 snacpending[MAXSPLITSCREENPLAYERS] = {0,0,0,0}; static INT32 chmappending = 0; +static void setVoiceDataByName(UINT8 n, const char * vox_name) +{ + strncpy(localvoicedata[n].name, vox_name, VOICENAMESIZE); + localvoicedata[n].name[32] = 0; +} + // name, color, skin, or voice has changed // static void SendNameAndColor(UINT8 n) @@ -2064,6 +2088,7 @@ static void SendNameAndColor(UINT8 n) kartvoice_t *voice; kartvoice_t *valuevoice; INT32 prevskin; + kartvoicenum_t prefvox = MAXSKINVOICES; char buf[MAXPLAYERNAME+12]; char *p; @@ -2117,7 +2142,7 @@ static void SendNameAndColor(UINT8 n) && fastcmp(cv_skin[n].string, skins[player->skin].name) && cv_follower[n].value == player->followerskin && cv_followercolor[n].value == player->followercolor - && fasticmp(cv_voice[n].string, voice->name)) + && fasticmp(localvoicedata[n].name, voice->name)) return; // We'll handle it later if we're not playing. @@ -2143,13 +2168,14 @@ static void SendNameAndColor(UINT8 n) player->followercolor = cv_followercolor[n].value; if (metalrecording && n == 0) - { // Starring Metal Sonic as themselves, obviously. + { // Starring Metal Sonic as themselves, obviously. SetPlayerSkinByNum(playernum, 5); CV_StealthSet(&cv_skin[n], skins[5].name); } - else if ((foundskin = R_SkinAvailable(cv_skin[n].string)) != -1 && R_SkinUsable(playernum, foundskin)) + else if ((foundskin = R_SkinAvailable(cv_skin[n].string)) != -1 && + R_SkinUsable(playernum, foundskin)) { - prevskin = cv_skin[n].value; + prevskin = player->skin; cv_skin[n].value = foundskin; SetPlayerSkin(playernum, cv_skin[n].string); @@ -2158,19 +2184,31 @@ static void SendNameAndColor(UINT8 n) // Reset the voice. if (prevskin != player->skin) { - if (cv_voice[n].string) - { - voxid = R_FindIDForVoice(&skins[player->skin], cv_voice[n].string); + // First off: Check for a preferred voice for this skin + prefvox = + R_GetLocalPreferredVoiceForSkin(n, skins[player->skin].name); - if (voxid == MAXSKINVOICES) + if (localvoicedata[n].name[0] != 0) + { + if (prefvox != MAXSKINVOICES) { - // No dice; use the default voice. - valuevoice = &skins[player->skin].voices[0]; + // Skin's changing, let's try their preference + + // Give the player a heads-up + CONS_Alert( + CONS_NOTICE, + M_GetText("Using preferred voice \"%s\" for " + "this skin.\n"), + skins[player->skin].voices[prefvox].name); + valuevoice = &skins[player->skin].voices[prefvox]; + + // Preferences get to set local data for free! + setVoiceDataByName(n, valuevoice->name); } else { - - valuevoice = &skins[player->skin].voices[voxid]; + // No dice; use the default voice. + valuevoice = &skins[player->skin].voices[0]; } } else @@ -2179,7 +2217,11 @@ static void SendNameAndColor(UINT8 n) valuevoice = &skins[player->skin].voices[0]; } - CV_StealthSet(&cv_voice[n], valuevoice->name); + if (player->jointime > 1) + { + // We're officially in the game; assume any skin/voice change is 100% on purpose + setVoiceDataByName(n, valuevoice->name); + } } } else @@ -2194,19 +2236,30 @@ static void SendNameAndColor(UINT8 n) // Reset the voice. if (prevskin != player->skin) { - if (cv_voice[n].string) - { - voxid = R_FindIDForVoice(&skins[player->skin], cv_voice[n].string); + // First off: Check for a preferred voice for this skin + prefvox = + R_GetLocalPreferredVoiceForSkin(n, skins[player->skin].name); - if (voxid == MAXSKINVOICES) + if (localvoicedata[n].name[0] != 0) + { + if (prefvox != MAXSKINVOICES) { - // No dice; use the default voice. - valuevoice = &skins[player->skin].voices[0]; + // Skin's changing, let's try their preference + + // Give the player a heads-up + CONS_Alert(CONS_NOTICE, + M_GetText("Using preferred voice " + "\"%s\" for this skin.\n"), + skins[player->skin].voices[prefvox].name); + valuevoice = &skins[player->skin].voices[prefvox]; + + // Preferences get to set local data for free! + setVoiceDataByName(n, valuevoice->name); } else { - - valuevoice = &skins[player->skin].voices[voxid]; + // No dice; use the default voice. + valuevoice = &skins[player->skin].voices[0]; } } else @@ -2215,23 +2268,27 @@ static void SendNameAndColor(UINT8 n) valuevoice = &skins[player->skin].voices[0]; } - CV_StealthSet(&cv_voice[n], valuevoice->name); + if (player->jointime > 1) + { + // We're officially in the game; assume any skin/voice change is 100% on purpose + setVoiceDataByName(n, valuevoice->name); + } } } // Need to update voices after the fact. - if (!cv_voice[n].string) + if (localvoicedata[n].name[0] == 0) { - CV_StealthSet(&cv_voice[n], skins[player->skin].voices[0].name); + setVoiceDataByName(n, skins[player->skin].voices[0].name); } - SetPlayerVoice(playernum, cv_voice[n].string); + SetPlayerVoice(playernum, localvoicedata[n].name); valuevoice = P_GetMobjVoice(player->mo); if (valuevoice) { - CV_StealthSet(&cv_voice[n], valuevoice->name); + setVoiceDataByName(n, valuevoice->name); } else { @@ -2241,15 +2298,23 @@ static void SendNameAndColor(UINT8 n) if (valuevoice) { - CV_StealthSet(&cv_voice[n], valuevoice->name); + setVoiceDataByName(n, valuevoice->name); } else { // ...still nothing? - CV_StealthSet(&cv_voice[n], skins[player->skin].voices[0].name); + setVoiceDataByName(n, skins[player->skin].voices[0].name); } } + if (cv_autovoxpref[n].value && player->voice_id != 0) // Please please PLEASE not for defaults... + { + // Auto-assign a preference based on our final voice. + // This overwrites anything currently present + R_SetLocalPreferredVoiceForSkin( + n, skins[player->skin].name, localvoicedata[n].name); + } + return; } @@ -2271,10 +2336,22 @@ static void SendNameAndColor(UINT8 n) { CV_StealthSet(&cv_skin[n], skins[player->skin].name); - if (R_FindIDForVoice(&skins[player->skin], cv_voice[n].string) == MAXSKINVOICES) + if (R_FindIDForVoice(&skins[player->skin], localvoicedata[n].name) == MAXSKINVOICES) { - // Our voice is no longer valid, set it to that of our skin's. - CV_StealthSet(&cv_voice[n], skins[player->skin].voices[0].name); + // Our voice is no longer valid. + + // Check if we have a preference + prefvox = R_GetLocalPreferredVoiceForSkin(n, skins[player->skin].name); + + // If we don't, set it to that of our skin's. + if (prefvox != MAXSKINVOICES) + { + setVoiceDataByName(n, skins[player->skin].voices[prefvox].name); + } + else + { + setVoiceDataByName(n, skins[player->skin].voices[0].name); + } } } @@ -2286,17 +2363,41 @@ static void SendNameAndColor(UINT8 n) CV_StealthSet(&cv_skin[n], DEFAULTSKIN); cv_skin[n].value = 0; - CV_StealthSet(&cv_voice[n], "sonic_voice"); - cv_voice[n].value = 0; + setVoiceDataByName(n, "default"); + localvoicedata[n].value = 0; } // Need to update voices after the fact. - if (!cv_voice[n].string) + if (localvoicedata[n].name[0] == 0) { - CV_StealthSet(&cv_voice[n], skins[cv_skin[n].value].voices[0].name); + setVoiceDataByName(n, skins[cv_skin[n].value].voices[0].name); } - if (R_FindIDForVoice(&skins[cv_skin[n].value], cv_voice[n].string) == MAXSKINVOICES) + const boolean skinmightchange = (player->skin != cv_skin[n].value); + + if (skinmightchange) + { + // We're very likely changing skins. Check for preferences! + prefvox = R_GetLocalPreferredVoiceForSkin(n, skins[cv_skin[n].value].name); + + if (prefvox != MAXSKINVOICES) + { + // Update local voice data + setVoiceDataByName(n, skins[cv_skin[n].value].voices[prefvox].name); + + // Give the player a heads-up + CONS_Alert(CONS_NOTICE, M_GetText("Using preferred voice \"%s\" for this skin.\n"), skins[cv_skin[n].value].voices[prefvox].name); + } + else if (player->jointime >= 1) + { + // Nothing found; fall back to the default voice. + setVoiceDataByName(n, skins[cv_skin[n].value].voices[0].name); + } + + // Then, let it lead into the standard finder for verification. + } + + if (R_FindIDForVoice(&skins[cv_skin[n].value], localvoicedata[n].name) == MAXSKINVOICES) { // In the chance our current voice isn't valid, // rescan our current voice and send that over the network. @@ -2304,7 +2405,7 @@ static void SendNameAndColor(UINT8 n) if (valuevoice) { - CV_StealthSet(&cv_voice[n], valuevoice->name); + setVoiceDataByName(n, valuevoice->name); } else { @@ -2312,29 +2413,44 @@ static void SendNameAndColor(UINT8 n) // No need to compare parents. valuevoice = &skins[cv_skin[n].value].voices[player->voice_id]; + // Once again: check preferences + prefvox = R_GetLocalPreferredVoiceForSkin(n, skins[cv_skin[n].value].name); + if (valuevoice) { - CV_StealthSet(&cv_voice[n], valuevoice->name); + setVoiceDataByName(n, valuevoice->name); + } + else if (prefvox != MAXSKINVOICES) + { + setVoiceDataByName(n, skins[cv_skin[n].value].voices[prefvox].name); } else { // ...still nothing? - CV_StealthSet(&cv_voice[n], skins[cv_skin[n].value].voices[0].name); + setVoiceDataByName(n, skins[cv_skin[n].value].voices[0].name); } } } // After EVERYTHING, do one last check on our voice so we can set the value. - INT32 cvar_voxid = R_FindIDForVoice(&skins[cv_skin[n].value], cv_voice[n].string); + INT32 cvar_voxid = R_FindIDForVoice(&skins[cv_skin[n].value], localvoicedata[n].name); if (cvar_voxid == MAXSKINVOICES) { // ...huh?! Reset to the default. - cv_voice[n].value = 0; + localvoicedata[n].value = 0; } else { - cv_voice[n].value = cvar_voxid; + localvoicedata[n].value = cvar_voxid; + } + + if (cv_autovoxpref[n].value && localvoicedata[n].value != 0) + { + // Auto-assign a preference based on our final voice, if it isn't the default. Defaults should be manually set. + // This overwrites anything currently present (again, given it's not the default voice). + R_SetLocalPreferredVoiceForSkin( + n, skins[cv_skin[n].value].name, localvoicedata[n].name); } // Finally write out the complete packet and send it off. @@ -2343,15 +2459,15 @@ static void SendNameAndColor(UINT8 n) WRITEUINT16(p, (UINT16)cv_skin[n].value); WRITEINT32(p, (INT32)cv_follower[n].value); WRITEUINT16(p, (UINT16)cv_followercolor[n].value); - WRITEUINT16(p, (UINT16)cv_voice[n].value); + WRITEUINT16(p, (UINT16)localvoicedata[n].value); SendNetXCmdForPlayer(n, XD_NAMEANDCOLOR, buf, p - buf); } -static void Got_NameAndColor(UINT8 **cp, INT32 playernum) +static void Got_NameAndColor(UINT8** cp, INT32 playernum) { - player_t *p = &players[playernum]; - char name[MAXPLAYERNAME+1]; + player_t* p = &players[playernum]; + char name[MAXPLAYERNAME + 1]; UINT16 color, followercolor; UINT16 skin, voice; INT32 follower; @@ -2419,26 +2535,45 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum) if (kick) { - CONS_Alert(CONS_WARNING, M_GetText("Illegal color change received from %s (team: %d), color: %d)\n"), player_names[playernum], p->ctfteam, p->skincolor); + CONS_Alert( + CONS_WARNING, + M_GetText( + "Illegal color change received from %s (team: %d), color: %d)\n"), + player_names[playernum], + p->ctfteam, + p->skincolor); SendKick(playernum, KICK_MSG_CON_FAIL); return; } } // set skin - if (cv_forceskin.value >= 0 && K_CanChangeRules(true)) // Server wants everyone to use the same player + if (cv_forceskin.value >= 0 && + K_CanChangeRules(true)) // Server wants everyone to use the same player { const INT32 forcedskin = cv_forceskin.value; SetPlayerSkinByNum(playernum, forcedskin); + kartvoicenum_t forcedvox = 0; + if (localplayer != -1) { + // You know the drill: check for preferences + forcedvox = + R_GetLocalPreferredVoiceForSkin(localplayer, skins[forcedskin].name); + + if (forcedvox == MAXSKINVOICES) + { + // If we find nothing, just fall back to the default + forcedvox = 0; + } + CV_StealthSet(&cv_skin[localplayer], skins[forcedskin].name); - CV_StealthSet(&cv_voice[localplayer], skins[forcedskin].voices[0].name); + setVoiceDataByName(localplayer, skins[forcedskin].voices[forcedvox].name); } // set voice - SetPlayerVoiceByNum(playernum, skins[forcedskin].voices[0].id); + SetPlayerVoiceByNum(playernum, skins[forcedskin].voices[forcedvox].id); } else { @@ -2449,7 +2584,7 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum) if (localplayer != -1) { - CV_StealthSet(&cv_voice[localplayer], skins[p->skin].voices[p->voice_id].name); + setVoiceDataByName(localplayer, skins[p->skin].voices[p->voice_id].name); } } @@ -8280,6 +8415,257 @@ static void Command_ListSkins(void) } } +static void __voice_cmd_func(INT32 pid, UINT8 pnum) +{ + int vo_id = 0; + int skin_id = 0; + boolean ingame = false; + + if (!numskins) + { + // There aren't even any skins yet! + return; + } + + kartvoice_t* myvoice = P_GetMobjVoice(players[pid].mo); + + if (!myvoice) + { + // ...how?! + myvoice = &skins[players[pid].skin].voices[0]; + } + + if (Playing() && (pnum < 1 || splitscreen)) + { + ingame = true; + + if (pnum < 1) + { + if (!(cht_debug || devparm) && + !(multiplayer || netgame) // In single player. + && (gamestate != + GS_WAITINGPLAYERS)) // allows command line -warp x +skin y + { + setVoiceDataByName(pnum, myvoice->name); + return; + } + } + + if (P_PlayerMoving(pid) && COM_Argc() > 0) + { + CONS_Alert(CONS_NOTICE, + M_GetText("You can't change your voice at the moment.\n")); + setVoiceDataByName(pnum, myvoice->name); + return; + } + } + + // ...now that we've dealt with the pleasantries... + + if (COM_Argc() > 2) + { + // Assume we're trying to define a preferred voice + if (sscanf(COM_Argv(1), " %d", &skin_id) == 0) + { + // Assume the player is searching by skin name instead + skin_id = R_SkinAvailable(COM_Argv(1)); + + if (skin_id == -1) + { + CONS_Alert( + CONS_NOTICE, + M_GetText( + "No skin exists with the name %s. \n(TIP: Type \"skin\" in " + "the console to see your current skin!)\n"), + COM_Argv(1)); + return; + } + } + else if (skin_id < 0) + { + CONS_Alert(CONS_NOTICE, M_GetText("Skin ID cannot be negative.\n")); + return; + } + else if (skin_id >= numskins) + { + // Make sure the ID isn't invalid + CONS_Alert(CONS_NOTICE, + M_GetText("Skin ID is out of range (0 to %d).\n"), + numskins - 1); + return; + } + + // Now, check for voices in the same way + if (sscanf(COM_Argv(2), " %d", &vo_id) == 0) + { + // Assume the player is searching by voice name instead + vo_id = R_FindIDForVoice(&skins[skin_id], COM_Argv(2)); + + if (vo_id == MAXSKINVOICES) + { + // Found nothing. Make sure we're not playing as this skin, or at least not playing yet! + if (!ingame || !fasticmp(skins[skin_id].name, skins[players[pid].skin].name)) + { + // Okay, good, we're not, Set a preference. + CONS_Printf("Set local player %d's preferred voice for skin \"%s\" to %s.\n", pnum + 1, skins[skin_id].name, COM_Argv(2)); + } + else + { + // SHIT, we are. Break the bad news. + CONS_Alert(CONS_NOTICE, M_GetText("No voice exists for skin %s with the name %s. A preference has been set instead.\n(TIP: Type \"listvoices %s\" in the console to see this skin's voices!)\n"), skins[skin_id].name, COM_Argv(2), skins[skin_id].name); + } + + R_SetLocalPreferredVoiceForSkin(pnum, skins[skin_id].name, va("%s", COM_Argv(2))); + return; + } + } + else if (vo_id < 0) + { + CONS_Alert(CONS_NOTICE, M_GetText("Voice ID cannot be negative.\n")); + return; + } + else if (vo_id >= skins[skin_id].numvoices) + { + // Make sure the ID isn't invalid + CONS_Alert(CONS_NOTICE, + M_GetText("Voice ID is out of range (0 to %d).\n"), + skins[skin_id].numvoices - 1); + return; + } + + // Check if we're PLAYING AS the skin we're about to set a voice for + if (!fasticmp(skins[skin_id].name, skins[players[pid].skin].name)) + { + // No? Well, at least set a preference for the voice we want... + R_SetLocalPreferredVoiceForSkin( + pnum, skins[skin_id].name, skins[skin_id].voices[vo_id].name); + + CONS_Printf( + "Set local player %d's preferred voice for skin \"%s\" to %s.\n", + pnum + 1, + skins[skin_id].name, + skins[skin_id].voices[vo_id].name); + return; + } + + // Yes? Even still, set a preference! + R_SetLocalPreferredVoiceForSkin( + pnum, skins[skin_id].name, skins[skin_id].voices[vo_id].name); + + // FINALLY... set the damn voice, and send it over the network + setVoiceDataByName(pnum, skins[skin_id].voices[vo_id].name); + SendNameAndColor(pnum); + } + else if (COM_Argc() > 1) // More standard, "cvar-like" behavior + { + skin_t* set_skin = &skins[players[pid].skin]; + + if (sscanf(COM_Argv(1), " %d", &vo_id) != 0) + { + if ((!Playing()) || (pnum > 0 && !splitscreen)) + { + // Not in a game, and certainly not playing. Use what + // the player's cvar has + set_skin = &skins[cv_skin[pnum].value]; + + if (!set_skin) + set_skin = &skins[0]; + } + // Setting our voice by ID + if (vo_id < 0) + { + CONS_Alert(CONS_NOTICE, + M_GetText("Voice ID cannot be negative.\n")); + return; + } + else if (vo_id >= set_skin->numvoices) + { + // Make sure the ID isn't invalid + CONS_Alert(CONS_NOTICE, + M_GetText("Voice ID is out of range (0 to %d).\n"), + set_skin->numvoices - 1); + return; + } + } + else if (fasticmp(COM_Argv(1), "--help")) + { + char command_name[7] = "voice"; + + command_name[5] = (pid > 0) ? (49 + pid) : 0; + command_name[6] = 0; + + // Anyone who names their skin "--help" can suck it. + CONS_Printf("Usage: \"%s \" to set a preference, regardless of if the voice is actually set or not\nAlternatively: \"%s\" alone to see the current voice, or \"%s \" to set a voice for the current skin without setting a preference.\n", command_name, command_name, command_name); + return; + } + else + { + // Setting voice by name + if ((!Playing()) || (pnum > 0 && !splitscreen)) + { + // Not in a game, and certainly not playing. Just let them set + // the voice; the networking system will catch if something + // is wrong + setVoiceDataByName(pnum, COM_Argv(1)); + return; + } + + vo_id = R_FindIDForVoice(set_skin, COM_Argv(1)); + if (vo_id == MAXSKINVOICES) + { + // No dice + CONS_Alert(CONS_NOTICE, + M_GetText("No voice exists for skin %s with the name " + "%s. \n(TIP: Type \"listvoices %s\" in the " + "console to see this skin's voices!)\n"), + set_skin->name, + COM_Argv(1), + set_skin->name); + return; + } + } + + // FINALLY... set the damn voice, and send it over the network + setVoiceDataByName(pnum, set_skin->voices[vo_id].name); + SendNameAndColor(pnum); + } + else + { + // No args; print our current voice like a cvar would + if (localvoicedata[pnum].name[0] != 0) + CONS_Printf( + "Current voice for local player %d is %s.\nType \"voice default\" to " + "return to your default voice.\n", + pnum + 1, + localvoicedata[pnum].name); + else + CONS_Printf( + "No current voice assigned for local player %d yet. Are you in the " + "title screen?\n", + pnum + 1); + } +} + +static void Command_Voice(void) +{ + __voice_cmd_func(consoleplayer, 0); +} + +static void Command_Voice2(void) +{ + __voice_cmd_func(g_localplayers[1], 1); +} + +static void Command_Voice3(void) +{ + __voice_cmd_func(g_localplayers[2], 2); +} + +static void Command_Voice4(void) +{ + __voice_cmd_func(g_localplayers[3], 3); +} + __attribute__((used)) static void appendStrToCharVec(cvector(char) * v, const char * str) { int i, len; @@ -8544,6 +8930,121 @@ static void Command_ListVoices(void) } } +// Used as a constant pointer for the voice preference list. +char prefvoiceliststr[PREFLISTSIZE] = {0}; + +#define LISTVOICEPREFHELP "Usage: \"listvoiceprefs \" OR \"listvoiceprefs \" OR \"listvoiceprefs\"\nNote: \"listvoiceprefs \" and \"listvoiceprefs\" alone will only show Player 1's voice preferences.\n" + +#if MAXSPLITSCREENPLAYERS == 1 + +#define OVERPLAYERLIMIT "There is only %d local player.\n" + +#else + +#define OVERPLAYERLIMIT "There are only %d local players.\n" + +#endif + +static void Command_ListVoicePrefs(void) +{ + INT32 pnum = 0; + INT32 page = 1; + + if (COM_Argc() == 2) + { + // Assume player 1 (pnum 0) + if (sscanf(COM_Argv(1), " %d", &page) == 0) + { + if (fasticmp(COM_Argv(1), "--help")) + { + CONS_Printf(LISTVOICEPREFHELP); + return; + } + + page = 1; + } + else + { + if (page < 0) + { + CONS_Printf("Page number cannot be negative.\n"); + return; + } + else if (page == 0) + { + CONS_Printf("There is no 0th page. (What are you, a programmer?)\n"); + return; + } + } + } + else if (COM_Argc() > 2) + { + if (sscanf(COM_Argv(1), " %d", &pnum) == 0) + { + CONS_Printf("Player number cannot be a string.\n"); + } + else if (pnum > MAXSPLITSCREENPLAYERS) + { + CONS_Printf(OVERPLAYERLIMIT, MAXSPLITSCREENPLAYERS); + return; + } + else if (pnum < 0) + { + CONS_Printf("Player number cannot be negative.\n"); + return; + } + else if (pnum != 0) // The "0th player" is Player 1 + { + pnum -= 1; + } + + if (sscanf(COM_Argv(2), " %d", &page) == 0) + { + page = 1; + } + else + { + if (page < 0) + { + CONS_Printf("Page number cannot be negative.\n"); + return; + } + else if (page == 0) + { + CONS_Printf("There is no 0th page. (What are you, a programmer?)\n"); + return; + } + } + } + else if (COM_Argc() <= 1) + { + // Assume player 1, page 1 + page = 1; + pnum = 0; + } + else + { + // Too many args! List the help blurb. + CONS_Printf(LISTVOICEPREFHELP); + return; + } + + INT32 str_size = R_MakePreferredVoicesList(pnum, prefvoiceliststr, PREFLISTSIZE, page); + + if (str_size <= 0) + { + CONS_Printf("(local player %d) You have no voice preferences.\nUse voice to assign a preferred voice.\n", pnum+1); + return; // Don't do shit, we have no string + } + + prefvoiceliststr[str_size] = 0; // Add a terminator so the printer isn't combing through such a massive list... + + CONS_Printf("%s", prefvoiceliststr); +} + +#undef LISTVOICESHELP + + /** Sends a color change for the console player, unless that player is moving. * \sa cv_playercolor, Color2_OnChange, Skin_OnChange * \author Graue @@ -8651,54 +9152,7 @@ static void Color4_OnChange(void) lastgoodcolor[3] = cv_playercolor[3].value; } -static void __voice_cvar_func(INT32 pid, UINT8 pnum) -{ - if (!numskins) - { - // There aren't even any skins yet! - return; - } - - kartvoice_t *myvoice = P_GetMobjVoice(players[pid].mo); - - if (!myvoice) - { - // ...how?! - myvoice = &skins[players[pid].skin].voices[0]; - } - - if (R_FindIDForVoice(&skins[players[pid].skin], cv_voice[pnum].string) == MAXSKINVOICES) - { - CONS_Alert(CONS_NOTICE, M_GetText("Voice \"%s\" does not exist for this skin.\n"), cv_voice[pnum].string); - CV_StealthSet(&cv_voice[pnum], myvoice->name); - return; - } - - if (!Playing()) - return; // do whatever you want - - if (pnum > 0 && !splitscreen) - return; // do whatever you want - - if (pnum < 1) - { - if (!(cht_debug || devparm) && !(multiplayer || netgame) // In single player. - && (gamestate != GS_WAITINGPLAYERS)) // allows command line -warp x +skin y - { - CV_StealthSet(&cv_voice[pnum], myvoice->name); - return; - } - } - - if (!P_PlayerMoving(pid)) - SendNameAndColor(pnum); - else - { - CONS_Alert(CONS_NOTICE, M_GetText("You can't change your voice at the moment.\n")); - CV_StealthSet(&cv_voice[pnum], myvoice->name); - } -} - +/* static void Voice_OnChange(void) { __voice_cvar_func(consoleplayer, 0); @@ -8718,6 +9172,7 @@ static void Voice4_OnChange(void) { __voice_cvar_func(g_localplayers[3], 3); } +*/ /** Displays the result of the chat being muted or unmuted. * The server or remote admin should already know and be able to talk diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 0fef71239..625372227 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -22,6 +22,11 @@ extern "C" { #endif +// Big overshoot +#define PREFLISTSIZE (4096) + +extern char prefvoiceliststr[PREFLISTSIZE]; + extern consvar_t cv_showgremlins; // console vars @@ -30,7 +35,7 @@ extern consvar_t cv_playercolor[MAXSPLITSCREENPLAYERS]; extern consvar_t cv_skin[MAXSPLITSCREENPLAYERS]; extern consvar_t cv_follower[MAXSPLITSCREENPLAYERS]; extern consvar_t cv_followercolor[MAXSPLITSCREENPLAYERS]; -extern consvar_t cv_voice[MAXSPLITSCREENPLAYERS]; +extern consvar_t cv_autovoxpref[MAXSPLITSCREENPLAYERS]; extern consvar_t cv_dummyrestatspeed[MAXSPLITSCREENPLAYERS]; extern consvar_t cv_dummyrestatweight[MAXSPLITSCREENPLAYERS]; extern consvar_t cv_dummyrestatrandom[MAXSPLITSCREENPLAYERS]; diff --git a/src/d_player.h b/src/d_player.h index b98241613..b09445548 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -33,6 +33,8 @@ extern "C" { #endif +typedef UINT16 kartvoicenum_t; + // Extra abilities/settings for skins (combinable stuff) typedef enum { diff --git a/src/r_skins.c b/src/r_skins.c index 8e1e8728d..eec0e1ffe 100644 --- a/src/r_skins.c +++ b/src/r_skins.c @@ -57,6 +57,14 @@ CV_PossibleValue_t skin_cons_t[MAXSKINS+1]; CV_PossibleValue_t Forceskin_cons_t[MAXSKINS+2]; +// FIXME: Save voice data to a separate file +kartvoiceinfo_t localvoicedata[MAXSPLITSCREENPLAYERS] = { + [0] = {.name = "default", .value = 0}, + [1] = {.name = "default", .value = 0}, + [2] = {.name = "default", .value = 0}, + [3] = {.name = "default", .value = 0} +}; + // // P_GetSkinSprite2 // For non-super players, tries each sprite2's immediate predecessor until it finds one with a number of frames or ends up at standing. @@ -735,8 +743,20 @@ void SetPlayerVoiceByNum(INT32 playernum, UINT16 voicenum) if (player->voice_id == voicenum) { - // Hey... this voice is the same as before! - return; + // Same ID? Seems suspicious... + + if (P_MobjWasRemoved(player->mo)) + return; // Unfortunately, we have no object, so we'll assume it's a dupe... + + if (!player->mo->voice) + return; // We somehow have no voice either! + + // Check the actual voice structs to be absolutely sure. + if (player->mo->voice == &skins[player->skin].voices[voicenum]) + { + // Hey... this voice is the same as before! + return; + } } player->voice_id = voicenum; diff --git a/src/r_skins.h b/src/r_skins.h index 8599eaf0b..2fa757d57 100644 --- a/src/r_skins.h +++ b/src/r_skins.h @@ -40,6 +40,15 @@ extern consvar_t cv_skinselectstyle, cv_skinselectsort; #define MAXSKINVOICES 16 // 16 voices * 4096 skins = 65536 voices in total :^) +// Replacement for cv_voice +struct kartvoiceinfo_t +{ + char name[VOICENAMESIZE+1]; + kartvoicenum_t value; +}; + +extern kartvoiceinfo_t localvoicedata[MAXSPLITSCREENPLAYERS]; + // Player voice struct struct kartvoice_t { @@ -92,7 +101,7 @@ struct skin_t // If an mobj_t doesn't have a voice, it defaults to this value, given a skin is in use. kartvoice_t voices[MAXSKINVOICES]; - UINT16 numvoices; + kartvoicenum_t numvoices; // Proxy value for the (now removed) soundsid array. // This is ONLY used in a Lua context. diff --git a/src/r_voicepreference.cpp b/src/r_voicepreference.cpp new file mode 100644 index 000000000..11b7eff7a --- /dev/null +++ b/src/r_voicepreference.cpp @@ -0,0 +1,531 @@ +// BLANKART +//----------------------------------------------------------------------------- +// Copyright (C) 2018-2025 by Kart Krew. +// Copyright (C) 2026 by "yama". +// Copyright (C) 2026 Blankart Team. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file r_voicepreference.cpp +/// \brief Voice preference system and file I/O + +#include "r_voicepreference.hpp" + +#include +#include +#include + +#include "d_main.h" +#include "d_player.h" // General player data +#include "doomdef.h" +#include "m_misc.h" +#include "r_skins.h" // kartvoice +#include "v_video.h" // Colormaps +#include "z_zone.h" + +extern "C" +{ + std::unordered_map localvoiceprefs[MAXSPLITSCREENPLAYERS]; + + char preferencesdir[512] = "preferences" PATHSEP; + + char voicepref_buffer[VOICEPREFBUFSIZE] = {0}; + + // Assign a preferred voice for a given skin name to a local player. + void R_SetLocalPreferredVoiceForSkin(UINT8 local_pid, + const char* skin_name, + const char* vox_name) + { + if (auto search = localvoiceprefs[local_pid].find(skin_name); + search != localvoiceprefs[local_pid].end()) + { + // Voice exists; overwrite the current preference + localvoiceprefs[local_pid][skin_name].assign( + vox_name, std::max(static_cast(VOICENAMESIZE), strlen(vox_name))); + return; + } + + // Entry doesn't exist yet; make a new one + localvoiceprefs[local_pid][skin_name] = std::string(vox_name); + } + + // Search through a local (non-netsynched) hashmap for a player's preferred voice for the given + // skin name. Returns MAXSKINVOICES if nothing is found. + kartvoicenum_t R_GetLocalPreferredVoiceForSkin(UINT8 local_pid, const char* skin_name) + { + if (auto search = localvoiceprefs[local_pid].find(skin_name); + search != localvoiceprefs[local_pid].end()) + { + // Preferred voice exists in this map; begin searching through the skin's voices to make + // sure we can load it in for real + const std::string found_vox = localvoiceprefs[local_pid][skin_name]; + + const INT32 skin_id = R_SkinAvailable(skin_name); + + if (skin_id == -1) + { + // Skin does not exist + return MAXSKINVOICES; + } + + char check_vox[VOICENAMESIZE + 1] = {0}; + + found_vox.copy(check_vox, VOICENAMESIZE); + check_vox[VOICENAMESIZE] = 0; + + // Return whatever the search result gives us. If it's nothing, it'll return + // MAXSKINVOICES + return R_FindIDForVoice(&skins[skin_id], check_vox); + } + + // Nope, no dice! + return MAXSKINVOICES; + } + + // Assign a preferred voice for a given skin ID number to a local player. + void R_SetLocalPreferredVoiceForSkinByID(UINT8 local_pid, INT32 skin_id, const char* vox_name) + { + if (skin_id >= numskins) + { + // Out of range; do nothing + return; + } + + // Preferred voice exists in this map; make sure the skin's loaded in, then overwrite + const INT32 _skin_id = R_SkinAvailable(skins[skin_id].name); + + if (_skin_id < 0) // Doing a less-than-zero check so any garbage data in the negatives gets + // ensnared by this... + { + // Skin does not exist + return; + } + + // Okay, this skin exists! Write them in! + // This does things unconditionally; if it already exists, the current preference is + // overwritten. If it *doesn't*, the unordered map will add a new entry. It's a win-win! + if (auto search = localvoiceprefs[local_pid].find(skins[_skin_id].name); + search != localvoiceprefs[local_pid].end()) + { + // Voice exists; overwrite the current preference + localvoiceprefs[local_pid][skins[_skin_id].name].assign( + vox_name, std::max(static_cast(VOICENAMESIZE), strlen(vox_name))); + return; + } + + localvoiceprefs[local_pid][skins[_skin_id].name] = vox_name; + } + + // Search through a local (non-netsynched) hashmap for a player's preferred voice for the given + // skin ID. Returns MAXSKINVOICES if nothing is found. + kartvoicenum_t R_GetLocalPreferredVoiceForSkinByID(UINT8 local_pid, INT32 skin_id) + { + if (skin_id >= numskins) + { + // Out of range; do nothing + return MAXSKINVOICES; + } + + // Preferred voice exists in this map; make sure the skin's loaded in, then overwrite + const INT32 _skin_id = R_SkinAvailable(skins[skin_id].name); + + if (_skin_id < 0) // Doing a less-than-zero check so any garbage data in the negatives gets + // ensnared by this... + { + // Skin does not exist + return MAXSKINVOICES; + } + + // We are now 100% sure the skin 100% exists. Make sure it exists in the player's preference + // list + if (auto search = localvoiceprefs[local_pid].find(skins[_skin_id].name); + search != localvoiceprefs[local_pid].end()) + { + // Preferred voice exists in this map; begin searching through the skin's voices to make + // sure we can load it in for real + const std::string_view found_vox = localvoiceprefs[local_pid][skins[_skin_id].name]; + + char check_vox[VOICENAMESIZE + 1] = {0}; + + found_vox.copy(check_vox, VOICENAMESIZE); + check_vox[VOICENAMESIZE] = 0; + + // Return whatever the search result gives us. If it's nothing, it'll return + // MAXSKINVOICES + return R_FindIDForVoice(&skins[_skin_id], check_vox); + } + + // Nope, no dice! + return MAXSKINVOICES; + } + + // Clones a list of a the given local player's preferred voices to a string pointer. + // Returns true or false if any data is written to the string in question. + INT32 R_MakePreferredVoicesList(UINT8 local_pid, + char* str_ptr, + size_t str_capacity, + INT32 page_num) + { + std::string output = ""; + char color_prefix[2] = {0}; + char temp_skin_name[SKINNAMESIZE + 1] = {0}; + char temp_vox_name[VOICENAMESIZE + 1] = {0}; + + UINT16 chatcolor = 0; + INT32 skin_id = -1; + INT32 i = 0; + const INT32 real_page_num = std::max(0, page_num - 1); + const INT32 preflistsize = static_cast(localvoiceprefs[local_pid].size()); + boolean noskin = false; + + INT32 numpages = (preflistsize / MAXPREFSKINSPERPAGE) + 1; + + if (page_num > numpages) + { + output += va("There %s only %d %s.\n", + (numpages != 1) ? "are" : "is", + numpages, + (numpages != 1) ? "pages" : "page"); + + memset(str_ptr, 0, str_capacity); + + // After everything, copy the output to our destination pointer + output.copy(str_ptr, str_capacity); + + return static_cast(output.size()); + } + + for (const auto& n : localvoiceprefs[local_pid]) + { + i++; + noskin = false; + + if (i < (MAXPREFSKINSPERPAGE * real_page_num)) + continue; // Not at the initial entry for this page yet. + + if (i > (MAXPREFSKINSPERPAGE)) + break; // Duck out immediately + + memset(temp_skin_name, 0, sizeof(temp_skin_name)); + + n.first.copy(temp_skin_name, SKINNAMESIZE); + temp_skin_name[SKINNAMESIZE] = 0; + + skin_id = R_SkinAvailable(temp_skin_name); + + if (skin_id < 0) // Doing a less-than-zero check so any garbage data in the negatives + // gets ensnared by this... + { + // Skin does not exist; print a greyed-out version of their codename + noskin = true; + } + + memset(temp_vox_name, 0, sizeof(temp_vox_name)); + + n.second.copy(temp_vox_name, VOICENAMESIZE); + temp_vox_name[VOICENAMESIZE] = 0; + + chatcolor = skincolors[skins[skin_id].prefcolor].chatcolor; + + if (chatcolor > V_TANMAP) + { + sprintf(color_prefix, "%c", '\x80'); + } + else + { + sprintf(color_prefix, "%c", '\x80' + (chatcolor >> V_CHARCOLORSHIFT)); + } + + color_prefix[1] = 0; + + // Write the entire output in a single go + if (noskin) + output += + va("\x82Local player %d's preferred voice for \x86\"%s\" (skin not " + "loaded)\x82:\x80 %s\n\n", + local_pid + 1, + temp_skin_name, + temp_vox_name); + else + output += + va("\x82Local player %d's preferred voice for %s%s\x82 (\x80\"%s\"\x82):\x80 " + "%s\n\n", + local_pid + 1, + color_prefix, + skins[skin_id].realname, + skins[skin_id].name, + temp_vox_name); + } + + if (output.size() <= 0) + { + // Do absolutely nothing and return + return -1; + } + + // Write out our page count + output += va("Total %d %s. Page (%d/%d)\n", + preflistsize, + (preflistsize != 1) ? "preferences" : "preference", + page_num, + numpages); + + // For debug/size estimation; we have 4096 skins, man. + // CONS_Printf("Final output string size: %d\n", static_cast(output.size())); + + // After everything, copy the output to our destination pointer + output.copy(str_ptr, str_capacity); + + // Finally, return the signal that we succeeded + return static_cast(output.size()); + } + + // Saves voice preferences to a file. + // Returns false if any of the four files fail to save; returns true otherwise. + boolean R_SaveVoicePreferences() + { + static char voxpref_file[MAXSPLITSCREENPLAYERS][21] = {"blanpreferences.cfg", + "blanpreferences2.cfg", + "blanpreferences3.cfg", + "blanpreferences4.cfg"}; + + FILE* f = NULL; + + char currconfig[MAX_WADPATH + 4] = {0}; + char backupfile[MAX_WADPATH + 4] = {0}; + + char temp_skin_name[SKINNAMESIZE + 1] = {0}; + char temp_vox_name[VOICENAMESIZE + 1] = {0}; + + INT32 i = 0; + INT32 skin_id = 0; + std::string file_output = ""; + boolean noskin = false; + + M_MkdirEach(preferencesdir, M_PathParts(preferencesdir) - 2, 0755); + + INT32 snum = 0; + for (snum = 0; snum < MAXSPLITSCREENPLAYERS; snum++) + { + file_output.clear(); + + // Clear out config directory + memset(currconfig, 0, sizeof(currconfig)); + memset(backupfile, 0, sizeof(backupfile)); + + // Write a new config directory + snprintf(currconfig, sizeof(currconfig), "%s%s", preferencesdir, voxpref_file[snum]); + currconfig[sizeof(currconfig) - 1] = '\0'; + + // Create backup of the config file + snprintf(backupfile, sizeof(backupfile), "%s.bak", currconfig); + backupfile[sizeof(backupfile) - 1] = '\0'; + + FILE* config = fopen(currconfig, "r"); + + if (config != NULL) + { + fclose(config); + if (FIL_CopyFile(currconfig, backupfile) == false) + { + CONS_Alert(CONS_WARNING, + "Failed to create a backup of the configuration file. Will not " + "attempt to write to file.\n"); + return false; + } + } + + if (!strstr(currconfig, ".cfg")) + { + CONS_Alert(CONS_NOTICE, M_GetText("Config filename must be .cfg\n")); + return false; + } + + f = fopen(currconfig, "w"); + if (!f) + { + CONS_Alert(CONS_ERROR, + M_GetText("Couldn't save game config file %s for player %d\n"), + currconfig, + snum + 1); + return false; + } + + fprintf(f, va("// BlanKart preferences file for local player %d.\n", snum + 1)); + fprintf( + f, + "// Due to the nature of the unordered map system, data here may be shuffled!\n"); + + // Save our voice + fprintf(f, "Voice %s\n", localvoicedata[snum].name); + + i = 0; + for (const auto& n : localvoiceprefs[snum]) + { + noskin = false; + i++; + + memset(voicepref_buffer, 0, sizeof(voicepref_buffer)); + memset(temp_skin_name, 0, sizeof(temp_skin_name)); + + n.first.copy(temp_skin_name, SKINNAMESIZE); + temp_skin_name[SKINNAMESIZE] = 0; + + skin_id = R_SkinAvailable(temp_skin_name); + + if (skin_id < 0) // Doing a less-than-zero check so any garbage data in the + // negatives gets ensnared by this... + { + // Skin does not exist; write the last name we remember it being + noskin = true; + } + + memset(temp_vox_name, 0, sizeof(temp_vox_name)); + + n.second.copy(temp_vox_name, VOICENAMESIZE); + temp_vox_name[VOICENAMESIZE] = 0; + + // Write the entire output in a single go + file_output += va("VoicePref %s %s\n", + (noskin) ? temp_skin_name : skins[skin_id].name, + temp_vox_name); + } + + if (file_output.size() > 0) + { + file_output.copy(voicepref_buffer, VOICEPREFBUFSIZE); + voicepref_buffer[VOICEPREFBUFSIZE - 1] = 0; + + fprintf(f, voicepref_buffer); + } + + fclose(f); + } + + return true; + } + + // Loads voice preferences from a file. + // Returns false if all 4 players fail to load a config; true otherwise. + boolean R_LoadVoicePreferences() + { + static char voxpref_file[MAXSPLITSCREENPLAYERS][21] = {"blanpreferences.cfg", + "blanpreferences2.cfg", + "blanpreferences3.cfg", + "blanpreferences4.cfg"}; + + FILE* f = NULL; + + char currconfig[MAX_WADPATH + 4] = {0}; + char backupfile[MAX_WADPATH + 4] = {0}; + + char buffer[MAXLINELEN] = {0}; + + char* word; + char* word2; + char* word3; + + INT32 i = 0; + INT32 snum = 0; + INT32 strikes = 0; + + std::vector data_to_load[2]; + char name_buffer[2][VOICENAMESIZE] = {0}; + + for (snum = 0; snum < MAXSPLITSCREENPLAYERS; snum++) + { + // Clear out config directory + memset(currconfig, 0, sizeof(currconfig)); + memset(backupfile, 0, sizeof(backupfile)); + + // Write a new config directory + snprintf(currconfig, sizeof(currconfig), "%s%s", preferencesdir, voxpref_file[snum]); + currconfig[sizeof(currconfig) - 1] = '\0'; + + f = fopen(currconfig, "r"); + + if (!f) + { + CONS_Alert(CONS_WARNING, + M_GetText("Could not open preferences for player %d\n"), + snum + 1); + strikes++; + continue; + } + + for (i = 0; fgets(buffer, (int)sizeof(buffer), f); i++) + { + word = strtok(buffer, " "); + + if (word) + strupr(word); + else + continue; + + if (fastcmp(word, "//")) + { + // This is a comment, ignore it + continue; + } + + if (fastcmp(word, "VOICE")) + { + word2 = strtok(NULL, " \t\r\n"); + + if (word2) + { + memset(localvoicedata[snum].name, 0, sizeof(localvoicedata[snum].name)); + strncpy(localvoicedata[snum].name, word2, strlen(word2)); + } + + continue; + } + else if (fastcmp(word, "VOICEPREF")) + { + word2 = strtok(NULL, " "); + + if (word2) + { + word3 = strtok(NULL, " \t\r\n"); + + if (word3) + { + data_to_load[0].push_back(std::string(word2)); + data_to_load[1].push_back(std::string(word3)); + } + } + + continue; + } + } + + fclose(f); + + // Load in the data, back-to-front to avoid annoying shuffling + while (data_to_load[0].size() > 0) + { + i = static_cast(data_to_load[0].size() - 1); + memset(name_buffer[0], 0, sizeof(name_buffer[0])); + memset(name_buffer[1], 0, sizeof(name_buffer[0])); + + // Set the buffer data + data_to_load[0].at(i).copy(name_buffer[0], sizeof(name_buffer[0])); + data_to_load[1].at(i).copy(name_buffer[1], sizeof(name_buffer[1])); + + name_buffer[0][sizeof(name_buffer[0]) - 1] = 0; + name_buffer[1][sizeof(name_buffer[1]) - 1] = 0; + + // Pop the entry from the array + data_to_load[0].pop_back(); + data_to_load[1].pop_back(); + + // Set preference in-game + R_SetLocalPreferredVoiceForSkin(snum, name_buffer[0], name_buffer[1]); + } + } + + return (strikes >= 4) ? false : true; + } +} diff --git a/src/r_voicepreference.hpp b/src/r_voicepreference.hpp new file mode 100644 index 000000000..49bd497d5 --- /dev/null +++ b/src/r_voicepreference.hpp @@ -0,0 +1,48 @@ +// BLANKART +//----------------------------------------------------------------------------- +// Copyright (C) 2018-2025 by Kart Krew. +// Copyright (C) 2026 by "yama". +// Copyright (C) 2026 Blankart Team. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file r_voicepreference.hpp +/// \brief Voice preference system and file I/O + +#include "r_skins.h" // kartvoicenum_t + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define MAXPREFSKINSPERPAGE (10) + +#define VOICEPREFSTRSIZE (sizeof("VoicePrefX \n") + SKINNAMESIZE + VOICENAMESIZE) +#define VOICEPREFBUFSIZE ((VOICEPREFSTRSIZE) * MAXSKINS) + 1 + + // Where to save preferences + extern char preferencesdir[512]; + extern char voicepref_buffer[VOICEPREFBUFSIZE]; + + void R_SetLocalPreferredVoiceForSkin(UINT8 local_pid, + const char* skin_name, + const char* vox_name); + kartvoicenum_t R_GetLocalPreferredVoiceForSkin(UINT8 local_pid, const char* skin_name); + + void R_SetLocalPreferredVoiceForSkinByID(UINT8 local_pid, INT32 skin_id, const char* vox_name); + kartvoicenum_t R_GetLocalPreferredVoiceForSkinByID(UINT8 local_pid, INT32 skin_id); + + INT32 R_MakePreferredVoicesList(UINT8 local_pid, + char* str_ptr, + size_t str_capacity, + INT32 page_num); + + boolean R_SaveVoicePreferences(); + boolean R_LoadVoicePreferences(); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/sdl/i_system.cpp b/src/sdl/i_system.cpp index c7d6eabf7..8d92bf722 100644 --- a/src/sdl/i_system.cpp +++ b/src/sdl/i_system.cpp @@ -185,6 +185,7 @@ static char returnWadPath[256]; #include "../g_game.h" #include "../filesrch.h" #include "../s_sound.h" +#include "../r_voicepreference.hpp" #include "../core/thread_pool.h" #include "endtxt.h" #include "sdlmain.h" @@ -1406,6 +1407,8 @@ void I_Quit(void) is_quitting = true; SDLforceUngrabMouse(); + + R_SaveVoicePreferences(); M_SaveConfig(NULL); //save game config, cvars.. D_SaveBan(); // save the ban list @@ -1502,6 +1505,7 @@ FUNCIERROR void ATTRNORETURN I_Error(const char *error, ...) SDL_Quit(); if (errorcount == 8) { + R_SaveVoicePreferences(); M_SaveConfig(NULL); G_SaveGameData(); } @@ -1538,6 +1542,7 @@ FUNCIERROR void ATTRNORETURN I_Error(const char *error, ...) I_OutputMsg("\nI_Error(): %s\n", buffer); // --- + R_SaveVoicePreferences(); M_SaveConfig(NULL); // save game config, cvars.. D_SaveBan(); // save the ban list G_SaveGameData(); // Tails 12-08-2002 diff --git a/src/typedef.h b/src/typedef.h index 1b2614c8a..c1938d58c 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -389,6 +389,7 @@ TYPEDEF (visffloor_t); TYPEDEF (portal_t); // r_skins.h +TYPEDEF (kartvoiceinfo_t); TYPEDEF (kartvoice_t); TYPEDEF (skin_t);