lots of changes

This commit is contained in:
2026-10-22 16:17:56 +01:00
parent e4a75367aa
commit ac3ffd5be2
53 changed files with 7716 additions and 193 deletions
+131
View File
@@ -0,0 +1,131 @@
#pragma semicolon 1
#include <sourcemod>
#include <sdktools_sound>
#include <sdktools_stringtables>
#include <cstrike>
#include <clientprefs>
#undef REQUIRE_PLUGIN
#include <ccc>
#include <leader>
#include <zombiereloaded>
#define REQUIRE_PLUGIN
#pragma newdecls required
#define PLUGIN_VERSION "1.0"
ConVar g_cvEnabled;
ConVar g_cvCustom;
ConVar g_cvCooldown;
ConVar g_cvRoundLimit;
ConVar g_cvDead;
ConVar g_cvSound;
bool g_bCCC;
bool g_bLeader;
bool g_bZR;
float g_flNextRadio[MAXPLAYERS + 1];
int g_iRadioCount[MAXPLAYERS + 1];
public Plugin myinfo =
{
name = "Radio",
author = "marisakirisame.net",
description = "Stock CS:S radio reimplemented with custom-chatcolors styling",
version = PLUGIN_VERSION,
url = ""
};
public void OnPluginStart()
{
if (GetUserMessageType() == UM_Protobuf)
SetFailState("This plugin only supports bitbuffer usermessages (CS:S).");
if (GetUserMessageId("SendAudio") == INVALID_MESSAGE_ID || GetUserMessageId("SayText2") == INVALID_MESSAGE_ID)
SetFailState("SendAudio/SayText2 usermessages unavailable.");
g_cvEnabled = CreateConVar("sm_radio_enabled", "1", "Enable the custom radio handling (0 = fully stock behaviour)", _, true, 0.0, true, 1.0);
g_cvCustom = CreateConVar("sm_radio_custom", "1", "Enable the custom radio commands (thanks, cheer); 0 = stock radio set only", _, true, 0.0, true, 1.0);
g_cvCooldown = CreateConVar("sm_radio_cooldown", "1.5", "Seconds a player has to wait between radio messages", _, true, 0.0);
g_cvRoundLimit = CreateConVar("sm_radio_round_limit", "0", "Max radio messages per player per round (engine default = 60, 0 = unlimited)", _, true, 0.0);
g_cvDead = CreateConVar("sm_radio_dead", "0", "Allow dead players to use the radio", _, true, 0.0, true, 1.0);
g_cvSound = CreateConVar("sm_radio_sound", "1", "Play the radio voice line", _, true, 0.0, true, 1.0);
AutoExecConfig(true);
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
Commands_Init();
Grenade_Init();
Bans_Init();
Settings_Init();
}
bool Radio_IsLeader(int client)
{
// A zombified leader gets no [Leader] tag and no cooldown bypass.
if (g_bZR
&& GetFeatureStatus(FeatureType_Native, "ZR_IsClientZombie") == FeatureStatus_Available
&& ZR_IsClientZombie(client))
{
return false;
}
return g_bLeader
&& GetFeatureStatus(FeatureType_Native, "Leader_Is") == FeatureStatus_Available
&& Leader_Is(client);
}
public void OnAllPluginsLoaded()
{
g_bCCC = LibraryExists("ccc");
g_bLeader = LibraryExists("leader");
g_bZR = LibraryExists("zombiereloaded");
}
public void OnLibraryAdded(const char[] name)
{
if (StrEqual(name, "ccc"))
g_bCCC = true;
else if (StrEqual(name, "leader"))
g_bLeader = true;
else if (StrEqual(name, "zombiereloaded"))
g_bZR = true;
}
public void OnLibraryRemoved(const char[] name)
{
if (StrEqual(name, "ccc"))
g_bCCC = false;
else if (StrEqual(name, "leader"))
g_bLeader = false;
else if (StrEqual(name, "zombiereloaded"))
g_bZR = false;
}
public void OnMapStart()
{
Bans_Clear();
Commands_PrecacheSounds();
}
public void OnClientConnected(int client)
{
g_flNextRadio[client] = 0.0;
g_iRadioCount[client] = 0;
Settings_OnClientConnected(client);
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
for (int i = 1; i <= MaxClients; i++)
g_iRadioCount[i] = 0;
}
#include "radio/commands.sp"
#include "radio/grenade.sp"
#include "radio/format.sp"
#include "radio/bans.sp"
#include "radio/settings.sp"
+90
View File
@@ -0,0 +1,90 @@
//----------------------------------------------------------------------------------------------------
// Radio bans: per-map bans keyed by SteamID, so they survive reconnects but die with the map.
//----------------------------------------------------------------------------------------------------
static StringMap g_BannedAuths;
void Bans_Init()
{
g_BannedAuths = new StringMap();
RegAdminCmd("sm_radiomute", Command_RadioBan, ADMFLAG_CHAT, "sm_radiomute <target> - mute target(s)' radio for the rest of the map");
RegAdminCmd("sm_radiounmute", Command_RadioUnban, ADMFLAG_CHAT, "sm_radiounmute <target> - allow target(s) to use the radio again");
}
void Bans_Clear()
{
g_BannedAuths.Clear();
}
bool Bans_IsBanned(int client)
{
char auth[32];
if (!GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)))
return false;
int dummy;
return g_BannedAuths.GetValue(auth, dummy);
}
static void Bans_Set(int client, bool banned)
{
char auth[32];
if (!GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)))
return;
if (banned)
g_BannedAuths.SetValue(auth, 1);
else
g_BannedAuths.Remove(auth);
}
public Action Command_RadioBan(int client, int argc)
{
return Bans_Command(client, argc, true, "sm_radiomute");
}
public Action Command_RadioUnban(int client, int argc)
{
return Bans_Command(client, argc, false, "sm_radiounmute");
}
static Action Bans_Command(int client, int argc, bool ban, const char[] command)
{
if (argc < 1)
{
ReplyToCommand(client, "[SM] Usage: %s <target>", command);
return Plugin_Handled;
}
char pattern[MAX_TARGET_LENGTH];
GetCmdArg(1, pattern, sizeof(pattern));
char targetName[MAX_TARGET_LENGTH];
int targets[MAXPLAYERS];
bool tnIsML;
int count = ProcessTargetString(pattern, client, targets, sizeof(targets),
COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsML);
if (count <= 0)
{
ReplyToTargetError(client, count);
return Plugin_Handled;
}
for (int i = 0; i < count; i++)
Bans_Set(targets[i], ban);
if (ban)
{
ShowActivity2(client, "[SM] ", "Radio-muted %s for the rest of the map.", targetName);
LogAction(client, count == 1 ? targets[0] : -1, "\"%L\" radio-muted \"%s\"", client, targetName);
}
else
{
ShowActivity2(client, "[SM] ", "Radio-unmuted %s.", targetName);
LogAction(client, count == 1 ? targets[0] : -1, "\"%L\" radio-unmuted \"%s\"", client, targetName);
}
return Plugin_Handled;
}
+234
View File
@@ -0,0 +1,234 @@
//----------------------------------------------------------------------------------------------------
// Radio commands: takes over the stock CS:S radio commands (the listener blocks the game's
// handling) and re-emits the voice line plus a custom-chatcolors styled chat line to the team.
//----------------------------------------------------------------------------------------------------
static const char g_sRadioCommands[][] = {
"coverme", "takepoint", "holdpos", "regroup", "followme", "takingfire", "go",
"fallback", "sticktog", "getinpos", "stormfront", "report", "roger", "enemyspot",
"needbackup", "sectorclear", "inposition", "reportingin", "getout", "negative", "enemydown",
"thanks", "cheer"
};
// Everything from this index on is a custom addition, not a stock radio command
// (gated behind sm_radio_custom).
#define RADIO_FIRST_CUSTOM (sizeof(g_sRadioCommands) - 2)
static const char g_sRadioSounds[][] = {
"radio/ct_coverme.wav", "radio/takepoint.wav", "radio/position.wav", "radio/regroup.wav",
"radio/followme.wav", "radio/fireassis.wav", "radio/com_go.wav", "radio/fallback.wav",
"radio/sticktog.wav", "radio/com_getinpos.wav", "radio/stormfront.wav", "radio/com_reportin.wav",
"radio/roger.wav", "radio/ct_enemys.wav", "radio/ct_backup.wav", "radio/clear.wav",
"radio/ct_inpos.wav", "radio/ct_reportingin.wav", "radio/blow.wav", "radio/negative.wav",
"radio/enemydown.wav",
"", "" // thanks/cheer have no stock radio sound; a random bot-chatter wav is picked below
};
#define SOUND_AFFIRMATIVE "radio/ct_affirm.wav"
#define SOUND_FIREINTHEHOLE "radio/ct_fireinhole.wav"
static const char g_sThanksSounds[][] = {
"bot/nice.wav", "bot/nice2.wav", "bot/good_one.wav", "bot/good_one2.wav",
"bot/very_nice.wav", "bot/well_done.wav"
};
static const char g_sCheerSounds[][] = {
"bot/whoo.wav", "bot/whoo2.wav", "bot/yesss.wav", "bot/yesss2.wav",
"bot/yea_baby.wav", "bot/oh_yea.wav", "bot/oh_yea2.wav"
};
// The bot chatter wavs are mastered 6-10 dB hotter than the radio wavs (radio averages
// -16.6 LUFS), so each one carries a gain that brings it down to the radio loudness.
static const float g_flThanksScales[] = {
0.61, 0.44, 0.45, 0.63,
0.43, 0.42
};
static const float g_flCheerScales[] = {
0.33, 0.27, 0.25, 0.32,
0.31, 0.32, 0.39
};
static const char g_sRadioPhrases[][] = {
"Cover Me!", "You take the point.", "Hold This position.", "Regroup Team.",
"Follow Me.", "Taking fire...need assistance!", "Go go go!", "Team, fall back!",
"Stick together, team.", "Get in position and wait for my go.", "Storm the Front!",
"Report in, team.", "Roger that.", "Enemy spotted.", "Need backup.", "Sector clear.",
"I'm in position.", "Reporting in.", "Get out of there, it's gonna blow!",
"Negative.", "Enemy down.",
"Thanks!", "Cheered!!!"
};
static const char g_sCheerColors[][] = {
"FF0000", "FF4500", "FF8C00", "FFD700", "ADFF2F",
"00FF00", "00BFFF", "1E90FF", "0000CD", "9400D3"
};
static const char g_sCheerText[] = "Cheered!!!";
static int g_iCheerOffset;
static void Commands_BuildCheerPhrase(char[] phrase, int maxlen)
{
phrase[0] = '\0';
for (int i = 0; i < sizeof(g_sCheerText) - 1; i++)
{
Format(phrase, maxlen, "%s\x07%s%c", phrase,
g_sCheerColors[(i + g_iCheerOffset) % sizeof(g_sCheerColors)], g_sCheerText[i]);
}
g_iCheerOffset = (g_iCheerOffset + 1) % sizeof(g_sCheerColors);
}
static StringMap g_RadioIndex;
void Commands_PrecacheSounds()
{
for (int i = 0; i < sizeof(g_sRadioSounds); i++)
{
if (g_sRadioSounds[i][0])
PrecacheSound(g_sRadioSounds[i]);
}
PrecacheSound(SOUND_AFFIRMATIVE);
PrecacheSound(SOUND_FIREINTHEHOLE); // grenade.sp
for (int i = 0; i < sizeof(g_sThanksSounds); i++)
PrecacheSound(g_sThanksSounds[i]);
for (int i = 0; i < sizeof(g_sCheerSounds); i++)
PrecacheSound(g_sCheerSounds[i]);
}
void Radio_EmitToPlayers(const int[] players, int playersNum, const char[] sound, float scale = 1.0)
{
for (int i = 0; i < playersNum; i++)
{
float volume = Settings_GetVolume(players[i]);
if (volume <= 0.0)
continue;
EmitSoundToClient(players[i], sound, SOUND_FROM_PLAYER, SNDCHAN_VOICE,
SNDLEVEL_NORMAL, SND_NOFLAGS, volume * scale);
}
}
void Commands_Init()
{
g_RadioIndex = new StringMap();
for (int i = 0; i < sizeof(g_sRadioCommands); i++)
{
g_RadioIndex.SetValue(g_sRadioCommands[i], i);
AddCommandListener(Command_Radio, g_sRadioCommands[i]);
}
}
public Action Command_Radio(int client, const char[] command, int argc)
{
if (!g_cvEnabled.BoolValue)
return Plugin_Continue;
if (client < 1 || client > MaxClients || !IsClientInGame(client))
return Plugin_Continue;
// Console commands are case-insensitive, StringMap keys are not.
char cmd[16];
strcopy(cmd, sizeof(cmd), command);
for (int i = 0; cmd[i]; i++)
cmd[i] = CharToLower(cmd[i]);
int index;
if (!g_RadioIndex.GetValue(cmd, index))
return Plugin_Continue;
// With the custom commands disabled they fall through as unknown commands.
if (index >= RADIO_FIRST_CUSTOM && !g_cvCustom.BoolValue)
return Plugin_Continue;
if (Bans_IsBanned(client))
return Plugin_Handled;
int team = GetClientTeam(client);
if (team != CS_TEAM_T && team != CS_TEAM_CT)
return Plugin_Handled;
if (!IsPlayerAlive(client) && !g_cvDead.BoolValue)
return Plugin_Handled;
bool bLeader = Radio_IsLeader(client);
float flNow = GetGameTime();
bool bPlaySound = g_cvSound.BoolValue;
if (!bLeader)
{
if (flNow < g_flNextRadio[client])
return Plugin_Handled;
int limit = g_cvRoundLimit.IntValue;
if (limit > 0 && g_iRadioCount[client] >= limit)
return Plugin_Handled;
g_flNextRadio[client] = flNow + g_cvCooldown.FloatValue;
}
else if (flNow < g_flNextRadio[client])
{
// Leaders may spam the chat line, but the voice line still honours the cooldown.
bPlaySound = false;
}
else
{
g_flNextRadio[client] = flNow + g_cvCooldown.FloatValue;
}
g_iRadioCount[client]++;
bool bCustom = (index >= RADIO_FIRST_CUSTOM);
int[] players = new int[MaxClients];
int playersNum = 0;
for (int i = 1; i <= MaxClients; i++)
{
if (!IsClientInGame(i) || IsFakeClient(i) || GetClientTeam(i) != team)
continue;
// Players may opt out of the custom commands in !settings.
if (bCustom && !Settings_GetCustom(i))
continue;
players[playersNum++] = i;
}
if (!playersNum)
return Plugin_Handled;
// phrase must hold the coloured cheer line (10 glyphs each prefixed by a 7-byte \x07RRGGBB code)
char sound[32], phrase[128];
float soundScale = 1.0;
strcopy(sound, sizeof(sound), g_sRadioSounds[index]);
strcopy(phrase, sizeof(phrase), g_sRadioPhrases[index]);
// Stock "roger" picks one of two acknowledgements at random.
if (StrEqual(cmd, "roger") && GetRandomInt(0, 1))
{
strcopy(sound, sizeof(sound), SOUND_AFFIRMATIVE);
strcopy(phrase, sizeof(phrase), "Affirmative.");
}
else if (StrEqual(cmd, "thanks"))
{
int pick = GetRandomInt(0, sizeof(g_sThanksSounds) - 1);
strcopy(sound, sizeof(sound), g_sThanksSounds[pick]);
soundScale = g_flThanksScales[pick];
}
else if (StrEqual(cmd, "cheer"))
{
int pick = GetRandomInt(0, sizeof(g_sCheerSounds) - 1);
strcopy(sound, sizeof(sound), g_sCheerSounds[pick]);
soundScale = g_flCheerScales[pick];
Commands_BuildCheerPhrase(phrase, sizeof(phrase));
}
if (bPlaySound && sound[0])
Radio_EmitToPlayers(players, playersNum, sound, soundScale);
char text[256];
Format_RadioLine(client, bLeader, phrase, text, sizeof(text));
Radio_SendChat(client, players, playersNum, text);
return Plugin_Handled;
}
+97
View File
@@ -0,0 +1,97 @@
//----------------------------------------------------------------------------------------------------
// Radio format: assembles the chat line, colouring the sender's tag/name/text through
// custom-chatcolors when it is loaded. Colour mapping mirrors custom-chatcolors.sp.
//----------------------------------------------------------------------------------------------------
// Same prefix Leader3 puts on its own chat lines (Leader3.sp PREFIX).
#define LEADER_PREFIX "\x07FF00E1[Leader] \x01"
// ent_idx = author makes \x03 render as the author's team colour; the chat flag must be
// true (the client's non-chat path mishandles raw coloured text); BLOCKHOOKS keeps chat
// processors (CCC included) from re-processing our synthetic line.
void Radio_SendChat(int author, const int[] players, int playersNum, const char[] text)
{
Handle say = StartMessage("SayText2", players, playersNum, USERMSG_RELIABLE | USERMSG_BLOCKHOOKS);
BfWriteByte(say, author);
BfWriteByte(say, 1);
BfWriteString(say, text);
EndMessage();
}
static void ColorCode(int color, bool alpha, char[] out, int maxlen)
{
switch (color)
{
case COLOR_TEAM: strcopy(out, maxlen, "\x03");
case COLOR_CGREEN: strcopy(out, maxlen, "\x04");
case COLOR_OLIVE: strcopy(out, maxlen, "\x05");
case COLOR_NULL: out[0] = '\0';
case COLOR_NONE: strcopy(out, maxlen, "\x01");
default:
{
if (alpha)
FormatEx(out, maxlen, "\x08%08X", color);
else
FormatEx(out, maxlen, "\x07%06X", color);
}
}
}
void Format_RadioLine(int client, bool leader, const char[] phrase, char[] out, int maxlen)
{
char sender[MAX_NAME_LENGTH + 96];
bool useCCC = g_bCCC && GetFeatureStatus(FeatureType_Native, "CCC_GetColor") == FeatureStatus_Available;
if (useCCC)
{
// CCC stores tags in 64-byte buffers (custom-chatcolors.sp g_sTag) and a processed
// tag can carry an inline \x07RRGGBB code per character, so match CCC's size.
char tag[64], tagCode[16], nameCode[16];
bool alpha;
CCC_GetTag(client, tag, sizeof(tag));
alpha = false;
ColorCode(CCC_GetColor(client, CCC_TagColor, alpha), alpha, tagCode, sizeof(tagCode));
alpha = false;
int nameColor = CCC_GetColor(client, CCC_NameColor, alpha);
if (nameColor == COLOR_NULL && tag[0])
{
// CCC name colour "X" (COLOR_NULL) erases the name so only the tag shows
FormatEx(sender, sizeof(sender), "%s%s", tagCode, tag);
}
else
{
ColorCode(nameColor, alpha, nameCode, sizeof(nameCode));
if (tag[0])
FormatEx(sender, sizeof(sender), "%s%s%s%N", tagCode, tag, nameCode, client);
else
FormatEx(sender, sizeof(sender), "%s%N", nameCode, client);
}
}
else
{
FormatEx(sender, sizeof(sender), "\x03%N", client);
}
char location[64];
GetEntPropString(client, Prop_Send, "m_szLastPlaceName", location, sizeof(location));
// Leading \x01 guards the CS:S quirk where a colour code as the very first byte may not
// render; the \x01 before (RADIO): resets the line to the default orange, so the phrase
// renders like a stock radio message regardless of the player's CCC chat colour.
if (location[0])
{
FormatEx(out, maxlen, "\x01%s%s \x01@ %s (RADIO): %s",
leader ? LEADER_PREFIX : "", sender, location, phrase);
}
else
{
FormatEx(out, maxlen, "\x01%s%s \x01(RADIO): %s",
leader ? LEADER_PREFIX : "", sender, phrase);
}
}
+132
View File
@@ -0,0 +1,132 @@
//----------------------------------------------------------------------------------------------------
// Grenade announce: throwing a grenade makes the game itself emit "Fire in the hole!" via the
// RadioText usermessage (it is not a client command), so intercept it and re-send the line
// styled a frame later. The game's SendAudio (Radio.FireInTheHole) is likewise intercepted and
// re-emitted through EmitSoundToClient so each listener's radio volume preference applies.
//----------------------------------------------------------------------------------------------------
#define FIRE_IN_THE_HOLE_TOKEN "#Cstrike_TitlesTXT_Fire_in_the_hole"
void Grenade_Init()
{
UserMsg RadioText = GetUserMessageId("RadioText");
if (RadioText == INVALID_MESSAGE_ID)
SetFailState("RadioText usermessage unavailable.");
HookUserMessage(RadioText, Hook_RadioText, true);
HookUserMessage(GetUserMessageId("SendAudio"), Hook_SendAudio, true);
}
public Action Hook_SendAudio(UserMsg msg_id, Handle bf, const int[] players, int playersNum, bool reliable, bool init)
{
if (!g_cvEnabled.BoolValue)
return Plugin_Continue;
char sound[32];
BfReadString(bf, sound, sizeof(sound), false);
// Grenade announces are the only radio SendAudio the engine still emits (the command
// listeners block the rest); round events (bomb/win jingles) pass through untouched.
if (!StrEqual(sound, "Radio.FireInTheHole"))
return Plugin_Continue;
// Sounds cannot be emitted from inside a usermessage hook either, so re-emit next frame.
DataPack pack = new DataPack();
pack.WriteCell(playersNum);
for (int i = 0; i < playersNum; i++)
{
int target = players[i];
pack.WriteCell((target >= 1 && target <= MaxClients && IsClientInGame(target)) ? GetClientUserId(target) : 0);
}
RequestFrame(OnFireInTheHoleResend, pack);
return Plugin_Handled;
}
public void OnFireInTheHoleResend(DataPack pack)
{
pack.Reset();
int num = pack.ReadCell();
int[] players = new int[MaxClients];
int playersNum = 0;
for (int i = 0; i < num; i++)
{
int target = GetClientOfUserId(pack.ReadCell());
if (target && !IsFakeClient(target))
players[playersNum++] = target;
}
delete pack;
Radio_EmitToPlayers(players, playersNum, SOUND_FIREINTHEHOLE);
}
public Action Hook_RadioText(UserMsg msg_id, Handle bf, const int[] players, int playersNum, bool reliable, bool init)
{
if (!g_cvEnabled.BoolValue)
return Plugin_Continue;
BfReadByte(bf); // msg_dest
int client = BfReadByte(bf);
if (client < 1 || client > MaxClients || !IsClientInGame(client))
return Plugin_Continue;
// Layout: msg_name, then up to four params. "#Game_radio" carries the phrase token in
// param2, "#Game_radio_location" in param3 (param2 is the nav place name).
char msgName[64], param[64], token[64];
BfReadString(bf, msgName, sizeof(msgName), false); // msg_name
BfReadString(bf, param, sizeof(param), false); // param1 = player name
BfReadString(bf, token, sizeof(token), false); // param2
BfReadString(bf, param, sizeof(param), false); // param3
if (StrEqual(msgName, "#Game_radio_location"))
strcopy(token, sizeof(token), param);
else if (!StrEqual(msgName, "#Game_radio"))
return Plugin_Continue;
if (!StrEqual(token, FIRE_IN_THE_HOLE_TOKEN))
return Plugin_Continue;
if (Bans_IsBanned(client))
return Plugin_Handled;
// Usermessages cannot be sent from inside a usermessage hook, so stash the data and
// recipients (as userids) and re-send next frame.
DataPack pack = new DataPack();
pack.WriteCell(GetClientUserId(client));
pack.WriteCell(playersNum);
for (int i = 0; i < playersNum; i++)
{
int target = players[i];
pack.WriteCell((target >= 1 && target <= MaxClients && IsClientInGame(target)) ? GetClientUserId(target) : 0);
}
RequestFrame(OnGrenadeResend, pack);
return Plugin_Handled;
}
public void OnGrenadeResend(DataPack pack)
{
pack.Reset();
int client = GetClientOfUserId(pack.ReadCell());
int num = pack.ReadCell();
int[] players = new int[MaxClients];
int playersNum = 0;
for (int i = 0; i < num; i++)
{
int target = GetClientOfUserId(pack.ReadCell());
if (target && !IsFakeClient(target))
players[playersNum++] = target;
}
delete pack;
if (!client || !IsClientInGame(client) || !playersNum)
return;
char text[256];
Format_RadioLine(client, Radio_IsLeader(client), "Fire in the hole!", text, sizeof(text));
Radio_SendChat(client, players, playersNum, text);
}
+191
View File
@@ -0,0 +1,191 @@
//----------------------------------------------------------------------------------------------------
// Radio settings: per-player preferences stored as clientprefs cookies, exposed as a "Radio" entry
// in the stock Client Preferences menu (sm_settings / !settings) that opens a sub-menu with the
// voice-line volume and a toggle for receiving the custom radio commands (thanks, cheer).
//----------------------------------------------------------------------------------------------------
static Cookie g_hVolumeCookie;
static Cookie g_hCustomCookie;
static int g_iVolume[MAXPLAYERS + 1];
static bool g_bCustom[MAXPLAYERS + 1];
static const int g_iVolumeSteps[] = {100, 75, 50, 25, 0};
#define RADIO_VOLUME_DEFAULT 100
void Settings_Init()
{
g_hVolumeCookie = new Cookie("radio_volume", "Radio voice line volume in percent (0 = off)", CookieAccess_Protected);
g_hCustomCookie = new Cookie("radio_custom", "Receive the custom radio commands (thanks, cheer)", CookieAccess_Protected);
SetCookieMenuItem(CookieMenu_Radio, 0, "Radio");
// Late load: clients may already be in game with cached cookies.
for (int i = 1; i <= MaxClients; i++)
{
g_iVolume[i] = RADIO_VOLUME_DEFAULT;
g_bCustom[i] = true;
if (IsClientConnected(i) && AreClientCookiesCached(i))
OnClientCookiesCached(i);
}
}
void Settings_OnClientConnected(int client)
{
// until the cookies arrive
g_iVolume[client] = RADIO_VOLUME_DEFAULT;
g_bCustom[client] = true;
}
public void OnClientCookiesCached(int client)
{
char val[8];
g_hVolumeCookie.Get(client, val, sizeof(val));
if (val[0]) // never set = keep the default
{
int volume = StringToInt(val);
if (volume < 0)
volume = 0;
else if (volume > 100)
volume = 100;
g_iVolume[client] = volume;
}
g_hCustomCookie.Get(client, val, sizeof(val));
if (val[0])
g_bCustom[client] = StringToInt(val) != 0;
}
float Settings_GetVolume(int client)
{
return g_iVolume[client] / 100.0;
}
bool Settings_GetCustom(int client)
{
return g_bCustom[client];
}
public void CookieMenu_Radio(int client, CookieMenuAction action, any info, char[] buffer, int maxlen)
{
switch (action)
{
case CookieMenuAction_DisplayOption:
{
FormatEx(buffer, maxlen, "Radio");
}
case CookieMenuAction_SelectOption:
{
Settings_ShowMainMenu(client);
}
}
}
static void Settings_ShowMainMenu(int client)
{
Menu menu = new Menu(Menu_RadioMain);
menu.SetTitle("Radio Settings");
char display[64];
FormatEx(display, sizeof(display), "Sound volume: %d%%", g_iVolume[client]);
menu.AddItem("volume", display);
FormatEx(display, sizeof(display), "Custom Radio (thanks, cheer): %s",
g_bCustom[client] ? "Enabled" : "Disabled");
menu.AddItem("custom", display);
menu.ExitBackButton = true; // back to sm_settings
menu.Display(client, MENU_TIME_FOREVER);
}
public int Menu_RadioMain(Menu menu, MenuAction action, int client, int param)
{
switch (action)
{
case MenuAction_Select:
{
char item[8];
menu.GetItem(param, item, sizeof(item));
if (StrEqual(item, "volume"))
{
Settings_ShowVolumeMenu(client);
}
else if (StrEqual(item, "custom"))
{
g_bCustom[client] = !g_bCustom[client];
g_hCustomCookie.Set(client, g_bCustom[client] ? "1" : "0");
Settings_ShowMainMenu(client); // re-show with the new state
}
}
case MenuAction_Cancel:
{
if (param == MenuCancel_ExitBack)
ShowCookieMenu(client);
}
case MenuAction_End:
{
delete menu;
}
}
return 0;
}
static void Settings_ShowVolumeMenu(int client)
{
Menu menu = new Menu(Menu_RadioVolume);
menu.SetTitle("Radio Sound Volume");
char item[8], display[16];
for (int i = 0; i < sizeof(g_iVolumeSteps); i++)
{
int volume = g_iVolumeSteps[i];
IntToString(volume, item, sizeof(item));
if (volume > 0)
FormatEx(display, sizeof(display), "%d%%", volume);
else
strcopy(display, sizeof(display), "Off");
if (volume == g_iVolume[client])
StrCat(display, sizeof(display), " <<");
menu.AddItem(item, display);
}
menu.ExitBackButton = true; // back to the Radio sub-menu
menu.Display(client, MENU_TIME_FOREVER);
}
public int Menu_RadioVolume(Menu menu, MenuAction action, int client, int param)
{
switch (action)
{
case MenuAction_Select:
{
char item[8];
menu.GetItem(param, item, sizeof(item));
int volume = StringToInt(item);
g_iVolume[client] = volume;
char val[8];
IntToString(volume, val, sizeof(val));
g_hVolumeCookie.Set(client, val);
Settings_ShowVolumeMenu(client); // re-show with the new pick marked
}
case MenuAction_Cancel:
{
if (param == MenuCancel_ExitBack)
Settings_ShowMainMenu(client);
}
case MenuAction_End:
{
delete menu;
}
}
return 0;
}