lots of changes
This commit is contained in:
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user