Voice system overhaul

Lots of things to bring it up to speed with the Lua dubs system.

To note:
- The `voice` cvars have been replaced with commands that are more robust
  in scope. These commands accept multiple arguments:
  - 1 arg (`voice` alone): Displays your current voice
  - 2 args (`voice <vox_name>`): Sets your voice to the provided voice,
    given it exists.
  - 3 args (`voice <skin_name> <vox_name>`): Sets your voice
    (given you're playing as the skin) and logs a preference.
  - `voice --help`: Explains everything above
- A preferenecs system now exists, which is loaded in on-boot and saved
  when quitting:
  - When you swap skins, the game checks an unordered map of preferences,
    indexed by skin name. If the skin you're changing to both exists in
    your preference list, and has your preferred voice loaded in the
    server, a notice will appear and you'll automatically use your
    preferred voice.
  - `listvoiceprefs` now exists, displaying voice preferences for the
    given splitscreen player. If no additional args are provided,
    it defaults to Player 1's first page of preferences.
  - `voicepref_auto` now exists for all splitscreen players to
    automatically assign preferences when you change your voice away
    from the default. The default voice is not logged by `voicepref_auto`;
    you have to do that manually. This cvar is toggled On by default.
- Voice settings and preferences are now saved to a `blanpreferences.cfg`
  file, which is unique for each local player; each local player has their
  own preference file.
This commit is contained in:
yamamama 2026-04-29 04:42:07 -04:00
parent adf647b66e
commit 416885a7c7
11 changed files with 1193 additions and 111 deletions

View file

@ -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

View file

@ -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

View file

@ -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 <skin_name> <voice_name>\" to set a preference, regardless of if the voice is actually set or not\nAlternatively: \"%s\" alone to see the current voice, or \"%s <voice_name>\" 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 <playernum/skinname> <pagenum>\" OR \"listvoiceprefs <pagenum>\" OR \"listvoiceprefs\"\nNote: \"listvoiceprefs <pagenum>\" 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 <skin_name> <voice_name> 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 <graue@oceanbase.org>
@ -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

View file

@ -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];

View file

@ -33,6 +33,8 @@
extern "C" {
#endif
typedef UINT16 kartvoicenum_t;
// Extra abilities/settings for skins (combinable stuff)
typedef enum
{

View file

@ -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;

View file

@ -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.

531
src/r_voicepreference.cpp Normal file
View file

@ -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 <string>
#include <unordered_map>
#include <vector>
#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<std::string, std::string> 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<size_t>(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<size_t>(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<INT32>(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<INT32>(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<INT32>(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<INT32>(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<std::string> 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<INT32>(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;
}
}

48
src/r_voicepreference.hpp Normal file
View file

@ -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

View file

@ -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

View file

@ -389,6 +389,7 @@ TYPEDEF (visffloor_t);
TYPEDEF (portal_t);
// r_skins.h
TYPEDEF (kartvoiceinfo_t);
TYPEDEF (kartvoice_t);
TYPEDEF (skin_t);