Chat plugin rewrite. (#436)

This commit is contained in:
shavitush 2017-08-01 19:13:07 +03:00
parent 066f60c057
commit 8ff72fed0a
10 changed files with 658 additions and 985 deletions

View File

@ -17,6 +17,12 @@ Zones are trigger based and are very lightweight.
The zones plugin includes some less common features such as: Multiple tracks (main/bonus), zone editing (after setup), snapping zones to walls/corners/grid, zone setup using the cursor's position, configurable sprite/colors for zone types, zone tracks (main/bonus - can be extended), manual adjustments of coordinates before confirmations, teleport zones, glitch zones, no-limit zones (for styles like 400-velocity), flat/3D boxes for zone rendering, an API and more.
#### shavit-chat
The chat plugin manipulates chat messages sent by players.
It includes custom chat names, tags, colors and all can be defined by the players/admins.
Admins need the chat flag, or the "shavit_chat" override (good for a donator perk).
There's a user-friendly command named !cchelp so the users can easily understand what's going on.
#### shavit-hud
The HUD plugin is `bhoptimer`'s OSD frontend.
It shows most (if not all) of the information that the player needs to see.

View File

@ -22,6 +22,7 @@ Including a records system, map zones (start/end marks etc), bonuses, HUD with u
* [DHooks](http://users.alliedmods.net/~drifter/builds/dhooks/2.0/) - required for 250/260 runspeed for all weapons.
* [Bunnyhop Statistics](https://forums.alliedmods.net/showthread.php?t=286135) - to show amount of scrolls for non-auto styles in the key display. CS:S only!
* [SteamWorks](https://forums.alliedmods.net/showthread.php?t=229556) - for the `{serverip}` advertisement variable.
* [Chat-Processor](https://github.com/Drixevel/Chat-Processor) - if you're enabling the `shavit-chat` module.
# Installation:
1. If you want to use MySQL (**VERY RECOMMENDED**) add a database entry in addons/sourcemod/configs/databases.cfg, call it "shavit". The plugin also supports the "sqlite" driver. You can also skip this step and not modify databases.cfg.

View File

@ -1,110 +0,0 @@
// Use SteamID3 for a value if you want a per-client setting.
// "Custom" is for per-client settings.
// "Ranks" is for rank range settings. It is limited to 64 entries and the way you sort this file will be also sorted in-game when a player uses /ranks.
//
// Available settings:
// "rank_from" - rank range to start with
// "rank_to" - rank range to end with; you can use "infinity" if it's for every player below "rank_from".
//
// "prefix" - prefix before the name (don't add a space after it)
// "name" - custom name appearance (color from prefix will be applied here too)
// "message" - the message itself
// "clantag" - custom clan tag
//
// Global variables:
// {default} - default color
// {team} - team color
// {green} - green color
// {name} - player name
// {clan} - clan tag
// {message} - message text
//
// If you use variables that aren't compatible with the game, it might break some stuff.
// The default config will work with both CS:S and CS:GO, you can modify it for your needs.
// Use `sm_reloadchat` in your server to reload this config.
//
// For CS:S, you have the following variables available: (NOTE: The format for colors is like HTML. RRGGBBAA)
// {RGB} - like \x07, usage: "{RGB}FF0000" for red
// {RGBA} - like \x08, usage: "{RGBA}FF0000FE" for 254 alpha red
// {RGBX} - random rgb color
// {RGBAX} - random rgba color
//
// CS:GO colors:
// {team} will become purple for spectators
// {blue}
// {bluegrey}
// {darkblue}
// {darkred}
// {gold}
// {grey}
// {grey2}
// {lightgreen}
// {lightred}
// {lime}
// {orchid}
// {yellow}
//
"Chat"
{
"[U:1:204506329]" // my steamid for example, use steamid3
{
"prefix" "{green}/dev/"
"name" "{default}{team}{clan}{name}"
"clantag" "shave"
}
"-1" // lookup is due, shouldn't happen unless there's some error!
{
"rank_from" "-1"
"rank_to" "-1"
"prefix" ""
"name" "{team}{name}"
"message" "{message}"
}
"0" // unranked
{
"rank_from" "0"
"rank_to" "0"
"prefix" "[Unranked]"
"name" "{team}{name}"
"message" "{message}"
}
"1"
{
"rank_from" "1"
"rank_to" "1"
"prefix" "{green}ONE TRUE GOD"
"name" "{clan}{team}{name}"
}
"2"
{
"rank_from" "2"
"rank_to" "2"
"prefix" "LEGENDARY"
"name" "{name}"
}
"3"
{
"rank_from" "3"
"rank_to" "3"
"prefix" "HERO"
"name" "{team}{name}"
}
"4"
{
"rank_from" "4"
"rank_to" "infinity"
"prefix" "scrub!"
}
}

View File

@ -1,857 +0,0 @@
/*
* shavit's Timer - Chat
* by: shavit
*
* This file is part of shavit's Timer.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3.0, as published by the
* Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <sourcemod>
#include <cstrike>
#undef REQUIRE_PLUGIN
#include <basecomm>
#include <rtler>
#include <chat-processor>
#define USES_CHAT_COLORS
#include <shavit>
#pragma newdecls required
#pragma semicolon 1
#pragma dynamic 131072
// cache
float gF_LastMessage[MAXPLAYERS+1];
char gS_Cached_Prefix[MAXPLAYERS+1][32];
char gS_Cached_Name[MAXPLAYERS+1][MAX_NAME_LENGTH*2];
char gS_Cached_Message[MAXPLAYERS+1][255];
char gS_Cached_ClanTag[MAXPLAYERS+1][32];
StringMap gSM_Custom_Prefix = null;
StringMap gSM_Custom_Name = null;
StringMap gSM_Custom_Message = null;
StringMap gSM_Custom_ClanTag = null;
int gI_TotalChatRanks = 0;
int gI_UnassignedTitle = -1;
int gI_UnrankedTitle = -1;
Dynamic gD_ChatRanks[64]; // limited to 64 chat ranks right now, i really don't think there's a need for more.
// modules
bool gB_BaseComm = false;
bool gB_RTLer = false;
bool gB_ChatProcessor = false;
// game-related
EngineVersion gEV_Type = Engine_Unknown;
public Plugin myinfo =
{
name = "[shavit] Chat",
author = "shavit",
description = "Chat handler for shavit's bhop timer.",
version = SHAVIT_VERSION,
url = "https://github.com/shavitush/bhoptimer"
}
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
// natives
CreateNative("Shavit_FormatChat", Native_FormatChat);
// registers library, check "bool LibraryExists(const char[] name)" in order to use with other plugins
RegPluginLibrary("shavit-chat");
return APLRes_Success;
}
public void OnAllPluginsLoaded()
{
if(!LibraryExists("shavit-rankings"))
{
SetFailState("shavit-rankings is required for the plugin to work.");
}
// modules
gB_BaseComm = LibraryExists("basecomm");
gB_RTLer = LibraryExists("rtler");
gB_ChatProcessor = LibraryExists("chat-processor");
}
public void OnPluginStart()
{
LoadTranslations("shavit-chat.phrases");
// game specific
gEV_Type = GetEngineVersion();
// commands
RegAdminCmd("sm_reloadchat", Command_ReloadChat, ADMFLAG_ROOT, "Reload chat config.");
RegConsoleCmd("sm_chatranks", Command_ChatRanks, "Shows a list of all the possible chat ranks.");
RegConsoleCmd("sm_ranks", Command_ChatRanks, "Shows a list of all the possible chat ranks. Alias for sm_chatranks.");
// hooks
HookEvent("player_spawn", Player_Spawn);
}
public void OnPluginEnd()
{
ResetCache();
}
public void OnMapStart()
{
LoadConfig();
}
public void OnClientPutInServer(int client)
{
gF_LastMessage[client] = GetEngineTime();
if(gI_TotalChatRanks >= 1 && gD_ChatRanks[0].IsValid)
{
gD_ChatRanks[0].GetString("prefix", gS_Cached_Prefix[client], 32);
gD_ChatRanks[0].GetString("name", gS_Cached_Name[client], MAX_NAME_LENGTH*2);
gD_ChatRanks[0].GetString("message", gS_Cached_Message[client], 255);
gD_ChatRanks[0].GetString("clantag", gS_Cached_ClanTag[client], 32);
UpdateClanTag(client);
}
}
public void OnClientSettingsChanged(int client)
{
UpdateClanTag(client);
}
void UpdateClanTag(int client)
{
if(IsValidClient(client) && strlen(gS_Cached_ClanTag[client]) > 0)
{
CS_SetClientClanTag(client, gS_Cached_ClanTag[client]);
}
}
public void OnClientAuthorized(int client, const char[] auth)
{
LoadChatCache(client);
}
public void OnLibraryAdded(const char[] name)
{
if(StrEqual(name, "basecomm"))
{
gB_BaseComm = true;
}
else if(StrEqual(name, "rtler"))
{
gB_RTLer = true;
}
else if(StrEqual(name, "chat-processor"))
{
gB_ChatProcessor = true;
}
}
public void OnLibraryRemoved(const char[] name)
{
if(StrEqual(name, "basecomm"))
{
gB_BaseComm = false;
}
else if(StrEqual(name, "rtler"))
{
gB_RTLer = false;
}
else if(StrEqual(name, "chat-processor"))
{
gB_ChatProcessor = false;
}
}
public void Shavit_OnRankUpdated(int client)
{
LoadChatCache(client);
}
void LoadChatCache(int client)
{
// assign rank properties
int iRank = Shavit_GetRank(client);
int iTitle = (iRank == -1)? gI_UnassignedTitle:gI_UnrankedTitle;
if(iRank <= 0)
{
gD_ChatRanks[iTitle].GetString("prefix", gS_Cached_Prefix[client], 32);
gD_ChatRanks[iTitle].GetString("name", gS_Cached_Name[client], MAX_NAME_LENGTH*2);
gD_ChatRanks[iTitle].GetString("message", gS_Cached_Message[client], 255);
gD_ChatRanks[iTitle].GetString("clantag", gS_Cached_ClanTag[client], 255);
}
else
{
for(int i = 0; i < gI_TotalChatRanks; i++)
{
if(gD_ChatRanks[i].IsValid)
{
int iFrom = gD_ChatRanks[i].GetInt("rank_from");
int iTo = gD_ChatRanks[i].GetInt("rank_to");
if(iRank < iFrom || (iRank > iTo && iTo != -3))
{
continue;
}
gD_ChatRanks[i].GetString("prefix", gS_Cached_Prefix[client], 32);
gD_ChatRanks[i].GetString("name", gS_Cached_Name[client], MAX_NAME_LENGTH*2);
gD_ChatRanks[i].GetString("message", gS_Cached_Message[client], 255);
gD_ChatRanks[i].GetString("clantag", gS_Cached_ClanTag[client], 255);
}
}
}
char[] sAuthID = new char[32];
if(GetClientAuthId(client, AuthId_Steam3, sAuthID, 32))
{
char[] sBuffer = new char[255];
if(gSM_Custom_Prefix.GetString(sAuthID, sBuffer, 255))
{
strcopy(gS_Cached_Prefix[client], 32, sBuffer);
}
if(gSM_Custom_Name.GetString(sAuthID, sBuffer, 255))
{
strcopy(gS_Cached_Name[client], MAX_NAME_LENGTH*2, sBuffer);
}
if(gSM_Custom_Message.GetString(sAuthID, sBuffer, 255))
{
strcopy(gS_Cached_Message[client], 255, sBuffer);
}
if(gSM_Custom_ClanTag.GetString(sAuthID, sBuffer, 255))
{
strcopy(gS_Cached_ClanTag[client], 32, sBuffer);
}
}
UpdateClanTag(client);
}
void ResetCache()
{
for(int i = 0; i < 64; i++)
{
if(gD_ChatRanks[i].IsValid)
{
gD_ChatRanks[i].Dispose();
}
}
gI_TotalChatRanks = 0;
gI_UnassignedTitle = -1;
gI_UnrankedTitle = -1;
}
void LoadConfig()
{
delete gSM_Custom_Prefix;
delete gSM_Custom_Name;
delete gSM_Custom_Message;
delete gSM_Custom_ClanTag;
gSM_Custom_Prefix = new StringMap();
gSM_Custom_Name = new StringMap();
gSM_Custom_Message = new StringMap();
gSM_Custom_ClanTag = new StringMap();
ResetCache();
KeyValues kvConfig = new KeyValues("Chat");
char[] sFile = new char[PLATFORM_MAX_PATH];
BuildPath(Path_SM, sFile, PLATFORM_MAX_PATH, "configs/shavit-chat.cfg");
if(!kvConfig.ImportFromFile(sFile))
{
SetFailState("File %s could not be found or accessed.", sFile);
}
if(kvConfig.GotoFirstSubKey())
{
char[] sBuffer = new char[255];
do
{
kvConfig.GetSectionName(sBuffer, 255);
char[] sPrefix = new char[32];
kvConfig.GetString("prefix", sPrefix, 32);
char[] sName = new char[MAX_NAME_LENGTH*2];
kvConfig.GetString("name", sName, MAX_NAME_LENGTH*2);
char[] sMessage = new char[255];
kvConfig.GetString("message", sMessage, 255);
char[] sCustomClanTag = new char[32];
kvConfig.GetString("clantag", sCustomClanTag, 32);
// custom
if(StrContains(sBuffer[0], "[U:") != -1)
{
if(strlen(sPrefix) > 0)
{
gSM_Custom_Prefix.SetString(sBuffer, sPrefix);
}
if(strlen(sName) > 0)
{
gSM_Custom_Name.SetString(sBuffer, sName);
}
if(strlen(sMessage) > 0)
{
gSM_Custom_Message.SetString(sBuffer, sMessage);
}
if(strlen(sCustomClanTag) > 0)
{
gSM_Custom_ClanTag.SetString(sBuffer, sCustomClanTag);
}
}
// ranks
else
{
int iFrom = kvConfig.GetNum("rank_from", -2);
if(iFrom == -2)
{
LogError("Invalid \"rank_from\" value for \"%s\": %d or non-existant.", sBuffer, iFrom);
continue;
}
char[] sTo = new char[16];
kvConfig.GetString("rank_to", sTo, 16, "-2");
int iTo = StrEqual(sTo, "infinity", false)? -3:StringToInt(sTo);
if(iTo == -2)
{
LogError("Invalid \"rank_to\" value for \"%s\": %d or non-existant.", sBuffer, iTo);
continue;
}
gD_ChatRanks[gI_TotalChatRanks] = Dynamic();
gD_ChatRanks[gI_TotalChatRanks].SetInt("rank_from", iFrom);
gD_ChatRanks[gI_TotalChatRanks].SetInt("rank_to", iTo);
gD_ChatRanks[gI_TotalChatRanks].SetString("prefix", sPrefix, 32);
gD_ChatRanks[gI_TotalChatRanks].SetString("name", (strlen(sName) > 0)? sName:"{name}", MAX_NAME_LENGTH*2);
gD_ChatRanks[gI_TotalChatRanks].SetString("message", (strlen(sMessage) > 0)? sMessage:"{message}", 255);
gD_ChatRanks[gI_TotalChatRanks].SetString("clantag", (strlen(sCustomClanTag) > 0)? sCustomClanTag:"", 32);
if(iFrom == -1 && gI_UnassignedTitle == -1)
{
gI_UnassignedTitle = gI_TotalChatRanks;
}
else if(iFrom == 0 && gI_UnrankedTitle == -1)
{
gI_UnrankedTitle = gI_TotalChatRanks;
}
gI_TotalChatRanks++;
}
}
while(kvConfig.GotoNextKey());
}
else
{
LogError("File %s might be empty?", sFile);
}
delete kvConfig;
for(int i = 1; i <= MaxClients; i++)
{
if(IsValidClient(i)) // late loading
{
OnClientPutInServer(i);
char[] sAuth = new char[32];
if(GetClientAuthId(i, AuthId_Steam3, sAuth, 32))
{
OnClientAuthorized(i, sAuth);
}
}
}
}
public Action Command_ChatRanks(int client, int args)
{
if(!IsValidClient(client))
{
return Plugin_Handled;
}
// dummies
// char[] sExample = "Example."; // I tried using this variable, but it seemed to pick up "List of Chat ranks:" instead, I wonder why..
int[] clients = new int[1];
clients[0] = client;
char[] sChatMessage = new char[64];
FormatEx(sChatMessage, 64, "\x01%T", "ChatRankList", client);
ChatMessage(client, clients, 1, sChatMessage);
for(int i = gI_TotalChatRanks - 1; i >= 0; i--)
{
if(gD_ChatRanks[i].IsValid)
{
int iFrom = gD_ChatRanks[i].GetInt("rank_from");
if(iFrom <= 0)
{
continue; // don't show unranked/due-lookup 'chat ranks'
}
int iTo = gD_ChatRanks[i].GetInt("rank_to");
char[] sRankText = new char[16];
if(iFrom == iTo)
{
FormatEx(sRankText, 16, "#%d", iFrom);
}
else
{
PrintToConsole(client, "%d", iTo);
if(iTo == -3)
{
FormatEx(sRankText, 16, "#%d - ∞", iFrom, iTo);
}
else
{
FormatEx(sRankText, 16, "#%d - #%d", iFrom, iTo);
}
}
char[] sExampleMessage = new char[32];
FormatEx(sExampleMessage, 64, "\x01%T", "ExampleMessage", client);
char[] sPrefix = new char[32];
gD_ChatRanks[i].GetString("prefix", sPrefix, 32);
FormatVariables(client, sPrefix, 32, sPrefix, sExampleMessage);
char[] sName = new char[MAX_NAME_LENGTH*2];
gD_ChatRanks[i].GetString("name", sName, MAX_NAME_LENGTH*2);
FormatVariables(client, sName, MAX_NAME_LENGTH*2, sName, sExampleMessage);
char[] sMessage = new char[255];
gD_ChatRanks[i].GetString("message", sMessage, 255);
FormatVariables(client, sMessage, 255, sMessage, sExampleMessage);
char[] sBuffer = new char[300];
FormatEx(sBuffer, 300, "%s\x04[%s]\x01 %s%s %s %s %s", gEV_Type == Engine_CSGO? " ":"", sRankText, strlen(sPrefix) == 0? "\x03":"", sPrefix, sName, gEV_Type == Engine_CSGO? ":\x01":"\x01:", sMessage);
ChatMessage(client, clients, 1, sBuffer);
}
}
return Plugin_Handled;
}
public Action Command_ReloadChat(int client, int args)
{
LoadConfig();
ReplyToCommand(client, "%T", "ReloadChat", client);
return Plugin_Handled;
}
public Action OnChatMessage(int &author, ArrayList recipients, eChatFlags &flag, char[] name, char[] message, bool &bProcessColors, bool &bRemoveColors)
{
if(!gB_ChatProcessor)
{
return Plugin_Continue;
}
char[] sBuffer = new char[255];
char[] sPrefix = new char[32];
if(strlen(gS_Cached_Prefix[author]) > 0)
{
FormatVariables(author, sBuffer, 255, gS_Cached_Prefix[author], message);
int iLen = strlen(sBuffer);
sBuffer[iLen] = (iLen > 0)? ' ':'\0';
strcopy(sPrefix, 32, sBuffer);
}
char[] sName = new char[MAX_NAME_LENGTH*2];
if(strlen(gS_Cached_Name[author]) > 0)
{
FormatVariables(author, sBuffer, 255, gS_Cached_Name[author], message);
strcopy(sName, MAX_NAME_LENGTH*2, sBuffer);
}
else
{
FormatEx(sName, MAX_NAME_LENGTH*2, "\x03%N", author);
}
char[] sFormattedText = new char[MAXLENGTH_MESSAGE];
strcopy(sFormattedText, MAXLENGTH_MESSAGE, message);
// solve shitty exploits
ReplaceString(sFormattedText, MAXLENGTH_MESSAGE, "\n", "");
ReplaceString(sFormattedText, MAXLENGTH_MESSAGE, "\t", "");
ReplaceString(sFormattedText, MAXLENGTH_MESSAGE, " ", " ");
TrimString(sFormattedText);
if(strlen(gS_Cached_Message[author]) > 0)
{
FormatVariables(author, sBuffer, 255, gS_Cached_Message[author], sFormattedText);
strcopy(sFormattedText, 255, sBuffer);
}
else
{
strcopy(sFormattedText, 255, message);
}
FormatEx(name, MAXLENGTH_NAME, "%s%s%s", (gEV_Type == Engine_CSGO)? " ":"", sPrefix, sName);
strcopy(message, MAXLENGTH_MESSAGE, sFormattedText);
return Plugin_Changed;
}
public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
{
if(gB_ChatProcessor || !IsValidClient(client) || !IsClientAuthorized(client) || (gB_BaseComm && BaseComm_IsClientGagged(client)))
{
return Plugin_Continue;
}
if(GetEngineTime() - gF_LastMessage[client] < 0.70)
{
return Plugin_Handled;
}
bool bTeam = StrEqual(command, "say_team");
if(bTeam || (CheckCommandAccess(client, "sm_say", ADMFLAG_CHAT) && sArgs[0] == '@'))
{
return Plugin_Handled;
}
char[] sMessage = new char[300];
strcopy(sMessage, 300, sArgs);
if(ReplaceString(sMessage[0], 4, "!", "sm_") > 0 || ReplaceString(sMessage[0], 4, "/", "sm_") > 0)
{
bool bCmd = false;
Handle hCon = FindFirstConCommand(sMessage, 300, bCmd);
if(hCon != null)
{
FindNextConCommand(hCon, sMessage, 300, bCmd);
delete hCon;
if(bCmd)
{
return Plugin_Handled;
}
}
}
gF_LastMessage[client] = GetEngineTime();
int iTeam = GetClientTeam(client);
FormatChatLine(client, sArgs, IsPlayerAlive(client), iTeam, bTeam, sMessage, 300);
int[] clients = new int[MaxClients];
int count = 0;
PrintToServer("%N: %s", client, sArgs);
for(int i = 1; i <= MaxClients; i++)
{
if(IsValidClient(i))
{
if(GetClientTeam(i) == iTeam || !bTeam)
{
clients[count++] = i;
PrintToConsole(i, "%N: %s", client, sArgs);
}
}
}
ChatMessage(client, clients, count, sMessage);
return Plugin_Handled;
}
void FormatChatLine(int client, const char[] sMessage, bool bAlive, int iTeam, bool bTeam, char[] buffer, int maxlen)
{
char[] sTeam = new char[32];
char[] sTeamName = new char[32];
if(!bTeam)
{
if(iTeam == CS_TEAM_SPECTATOR)
{
FormatEx(sTeamName, 32, "%T", "TeamSpec", client);
strcopy(sTeam, 32, sTeamName);
}
}
else
{
switch(iTeam)
{
case CS_TEAM_SPECTATOR:
{
FormatEx(sTeamName, 32, "%T", "TeamSpectator", client);
strcopy(sTeam, 32, sTeamName);
}
case CS_TEAM_T:
{
FormatEx(sTeamName, 32, "%T", "TeamT", client);
strcopy(sTeam, 32, sTeamName);
}
case CS_TEAM_CT:
{
FormatEx(sTeamName, 32, "%T", "TeamCT", client);
strcopy(sTeam, 32, sTeamName);
}
}
}
char[] sAuthID = new char[32];
GetClientAuthId(client, AuthId_Steam3, sAuthID, 32);
char[] sBuffer = new char[255];
char[] sNewPrefix = new char[32];
if(strlen(gS_Cached_Prefix[client]) > 0)
{
FormatVariables(client, sBuffer, 255, gS_Cached_Prefix[client], sMessage);
int iLen = strlen(sBuffer);
sBuffer[iLen] = (iLen > 0)? ' ':'\0';
strcopy(sNewPrefix, 32, sBuffer);
}
char[] sNewName = new char[MAX_NAME_LENGTH*2];
if(strlen(gS_Cached_Name[client]) > 0)
{
FormatVariables(client, sBuffer, 255, gS_Cached_Name[client], sMessage);
strcopy(sNewName, MAX_NAME_LENGTH*2, sBuffer);
}
else
{
FormatEx(sNewName, MAX_NAME_LENGTH*2, "\x03%N", client);
}
char[] sFormattedText = new char[maxlen];
strcopy(sFormattedText, maxlen, sMessage);
// solve shitty exploits
ReplaceString(sFormattedText, maxlen, "\n", "");
ReplaceString(sFormattedText, maxlen, "\t", "");
ReplaceString(sFormattedText, maxlen, " ", " ");
TrimString(sFormattedText);
if(gB_RTLer)
{
char[][] sExploded = new char[96][96]; // fixed size from RTLer
ExplodeString(sFormattedText, " ", sExploded, 96, 96);
bool bRTLify = true;
for(int i = 0; i < 96; i++)
{
if(strlen(sExploded[i]) > 32)
{
bRTLify = false;
break;
}
}
if(bRTLify)
{
RTLify(sFormattedText, maxlen, sFormattedText);
}
}
if(strlen(gS_Cached_Message[client]) > 0)
{
FormatVariables(client, sBuffer, 255, gS_Cached_Message[client], sFormattedText);
strcopy(sFormattedText, 255, sBuffer);
}
FormatEx(buffer, maxlen, "\x01%s%s%s\x03%s%s %s %s", gEV_Type == Engine_CSGO? " ":"", (bAlive || iTeam == CS_TEAM_SPECTATOR)? "":"*DEAD* ", sTeam, sNewPrefix, sNewName, gEV_Type == Engine_CSGO? ":\x01":"\x01:", sFormattedText);
}
void FormatVariables(int client, char[] buffer, int maxlen, const char[] formattingrules, const char[] message)
{
char[] sTempFormattingRules = new char[maxlen];
strcopy(sTempFormattingRules, maxlen, formattingrules);
for(int i = 0; i < sizeof(gS_GlobalColorNames); i++)
{
ReplaceString(sTempFormattingRules, maxlen, gS_GlobalColorNames[i], gS_GlobalColors[i]);
}
if(gEV_Type == Engine_CSS)
{
ReplaceString(sTempFormattingRules, maxlen, "{RGB}", "\x07");
ReplaceString(sTempFormattingRules, maxlen, "{RGBA}", "\x08");
char[] sColorBuffer = new char[16];
do
{
Format(sColorBuffer, 16, "\x07%x%x%x", RealRandomInt(1, 255), RealRandomInt(1, 255), RealRandomInt(1, 255));
}
while(ReplaceStringEx(sTempFormattingRules, maxlen, "{RGBX}", sColorBuffer) > 0);
do
{
Format(sColorBuffer, 16, "\x08%x%x%x%x", RealRandomInt(1, 255), RealRandomInt(1, 255), RealRandomInt(1, 255), RealRandomInt(1, 255));
}
while(ReplaceStringEx(sTempFormattingRules, maxlen, "{RGBAX}", sColorBuffer) > 0);
}
else
{
for(int i = 0; i < sizeof(gS_CSGOColorNames); i++)
{
ReplaceString(sTempFormattingRules, maxlen, gS_CSGOColorNames[i], gS_CSGOColors[i]);
}
}
char[] sName = new char[MAX_NAME_LENGTH];
GetClientName(client, sName, MAX_NAME_LENGTH);
ReplaceString(sTempFormattingRules, maxlen, "{name}", sName);
char[] sCustomClanTag = new char[32];
CS_GetClientClanTag(client, sCustomClanTag, 32);
int iLen = strlen(sCustomClanTag);
sCustomClanTag[iLen] = (iLen > 0)? ' ':'\0';
ReplaceString(sTempFormattingRules, maxlen, "{clan}", sCustomClanTag);
ReplaceString(sTempFormattingRules, maxlen, "{message}", message);
strcopy(buffer, maxlen, sTempFormattingRules);
}
void ChatMessage(int from, int[] clients, int count, const char[] sMessage)
{
Handle hSayText2 = StartMessage("SayText2", clients, count);
if(hSayText2 != null)
{
if(gEV_Type == Engine_CSGO)
{
PbSetInt(hSayText2, "ent_idx", from);
PbSetBool(hSayText2, "chat", true);
PbSetString(hSayText2, "msg_name", sMessage);
for(int i = 1; i <= 4; i++)
{
PbAddString(hSayText2, "params", "");
}
}
else
{
BfWriteByte(hSayText2, from);
BfWriteByte(hSayText2, true);
BfWriteString(hSayText2, sMessage);
}
EndMessage();
}
}
public void Player_Spawn(Event event, const char[] name, bool dontBroadcast)
{
int client = GetClientOfUserId(event.GetInt("userid"));
if(!IsFakeClient(client))
{
LoadChatCache(client);
}
}
public int Native_FormatChat(Handle handler, int numParams)
{
int client = GetNativeCell(1);
if(!IsValidClient(client))
{
ThrowNativeError(200, "Invalid client index %d", client);
return -1;
}
char[] sMessage = new char[255];
GetNativeString(2, sMessage, 255);
char[] sBuffer = new char[300];
FormatChatLine(client, sMessage, IsPlayerAlive(client), GetClientTeam(client), view_as<bool>(GetNativeCell(3)), sBuffer, 300);
int maxlength = GetNativeCell(5);
return SetNativeString(6, sBuffer, maxlength);
}
// from SMLib
int RealRandomInt(int min, int max)
{
int random = GetURandomInt();
if(random == 0)
{
random++;
}
return RoundToCeil(float(random) / (float(2147483647) / float(max - min + 1))) + min - 1;
}

View File

@ -0,0 +1,79 @@
#if defined _chat_processor_included
#endinput
#endif
#define _chat_processor_included
//Globals
#define MAXLENGTH_FLAG 32
#define MAXLENGTH_NAME 128
#define MAXLENGTH_MESSAGE 128
#define MAXLENGTH_BUFFER 255
//Natives
/**
* Retrieves the current format string assigned from a flag string.
* Example: "Cstrike_Chat_All" = "{1} : {2}"
* You can find the config formats in either the translations or the configs.
*
* param sFlag Flag string to retrieve the format string from.
* param sBuffer Format string from the flag string.
* param iSize Size of the format string buffer.
*
* noreturn
**/
native void ChatProcessor_GetFlagFormatString(const char[] sFlag, char[] sBuffer, int iSize);
//Forwards
/**
* Called while sending a chat message before It's sent.
* Limits on the name and message strings can be found above.
*
* param author Author that created the message.
* param recipients Array of clients who will receive the message.
* param flagstring Flag string to determine the type of message.
* param name Name string of the author to be pushed.
* param message Message string from the author to be pushed.
* param processcolors Toggle to process colors in the buffer strings.
* param removecolors Toggle to remove colors in the buffer strings. (Requires bProcessColors = true)
*
* return types
* - Plugin_Continue Stops the message.
* - Plugin_Stop Stops the message.
* - Plugin_Changed Fires the post-forward below and prints out a message.
* - Plugin_Handled Fires the post-forward below but doesn't print a message.
**/
forward Action CP_OnChatMessage(int& author, ArrayList recipients, char[] flagstring, char[] name, char[] message, bool& processcolors, bool& removecolors);
/**
* Called after the chat message is sent to the designated clients by the author.
*
* param author Author that sent the message.
* param recipients Array of clients who received the message.
* param flagstring Flag string to determine the type of message.
* param formatstring Format string used in the message based on the flag string.
* param name Name string of the author.
* param message Message string from the author.
* param processcolors Check if colors were processed in the buffer strings.
* param removecolors Check if colors were removed from the buffer strings.
*
* noreturn
**/
forward void CP_OnChatMessagePost(int author, ArrayList recipients, const char[] flagstring, const char[] formatstring, const char[] name, const char[] message, bool processcolors, bool removecolors);
#if !defined REQUIRE_PLUGIN
public __pl_chat_processor_SetNTVOptional()
{
MarkNativeAsOptional("ChatProcessor_GetFlagFormatString");
}
#endif
public SharedPlugin __pl_chat_processor =
{
name = "chat-processor",
file = "chat-processor.smx",
#if defined REQUIRE_PLUGIN
required = 1
#else
required = 0
#endif
};

View File

@ -803,21 +803,6 @@ native int Shavit_GetRankedPlayers();
*/
native int Shavit_ForceHUDUpdate(int client, bool spectators);
/**
* Formats chats exactly like the chat handler does.
* Takes team, alive status, rank and all those stuff into account.
* Called from shavit-chat
*
* @param client Client index.
* @param message Message to use.
* @param team Simulate a team chat message?
* @param buffer Buffer to store the formatted line on.
* @param maxlen Length of 'buffer'
* @error Error code 200 if client isn't valid.
* @return -1 on error, SP_ERROR_NONE on success.
*/
native int Shavit_FormatChat(int client, const char[] message, const bool team, char[] buffer, int maxlen);
/**
* Opens the stats menu for a client.
*

507
scripting/shavit-chat.sp Normal file
View File

@ -0,0 +1,507 @@
/*
* shavit's Timer - Chat
* by: shavit
*
* This file is part of shavit's Timer.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3.0, as published by the
* Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Note: For donator perks, give donators a custom flag and then override it to have "shavit_chat".
#include <sourcemod>
#include <chat-processor>
#undef REQUIRE_PLUGIN
#define USES_CHAT_COLORS
#include <shavit>
// database
Database gH_SQL = null;
char gS_MySQLPrefix[32];
// cache
EngineVersion gEV_Type = Engine_Unknown;
bool gB_AllowCustom[MAXPLAYERS+1];
bool gB_NameEnabled[MAXPLAYERS+1];
char gS_CustomName[MAXPLAYERS+1][128];
bool gB_MessageEnabled[MAXPLAYERS+1];
char gS_CustomMessage[MAXPLAYERS+1][16];
public void OnPluginStart()
{
gEV_Type = GetEngineVersion();
LoadTranslations("shavit-common.phrases");
LoadTranslations("shavit-chat.phrases");
RegConsoleCmd("sm_cchelp", Command_CCHelp, "Provides help with setting a custom chat name/message color.");
RegConsoleCmd("sm_ccname", Command_CCName, "Toggles/sets a custom chat name. Usage: sm_ccname <text> or sm_ccname \"off\" to disable.");
RegConsoleCmd("sm_ccmsg", Command_CCMessage, "Toggles/sets a custom chat message color. Usage: sm_ccmsg <color> or sm_ccmsg \"off\" to disable.");
RegConsoleCmd("sm_ccmessage", Command_CCMessage, "Toggles/sets a custom chat message color. Usage: sm_ccmessage <color> or sm_ccmessage \"off\" to disable.");
RegAdminCmd("sm_cclist", Command_CCList, ADMFLAG_CHAT, "Print the custom chat setting of all online players.");
for(int i = 1; i <= MaxClients; i++)
{
if(IsClientInGame(i) && !IsFakeClient(i))
{
OnClientPostAdminCheck(i);
}
}
if(LibraryExists("shavit"))
{
Shavit_GetDB(gH_SQL);
SQL_SetPrefix();
SetSQLInfo();
}
}
public void OnLibraryAdded(const char[] name)
{
if(StrEqual(name, "shavit"))
{
Shavit_GetDB(gH_SQL);
SQL_SetPrefix();
SetSQLInfo();
}
}
public void OnLibraryRemoved(const char[] name)
{
if(StrEqual(name, "shavit"))
{
gH_SQL = null;
}
}
public void Shavit_OnDatabaseLoaded(Database db)
{
gH_SQL = db;
}
public Action CheckForSQLInfo(Handle Timer)
{
return SetSQLInfo();
}
Action SetSQLInfo()
{
if(gH_SQL == null)
{
Shavit_GetDB(gH_SQL);
CreateTimer(0.5, CheckForSQLInfo);
}
else
{
SQL_DBConnect();
return Plugin_Stop;
}
return Plugin_Continue;
}
void SQL_SetPrefix()
{
char[] sFile = new char[PLATFORM_MAX_PATH];
BuildPath(Path_SM, sFile, PLATFORM_MAX_PATH, "configs/shavit-prefix.txt");
File fFile = OpenFile(sFile, "r");
if(fFile == null)
{
SetFailState("Cannot open \"configs/shavit-prefix.txt\". Make sure this file exists and that the server has read permissions to it.");
}
char[] sLine = new char[PLATFORM_MAX_PATH*2];
while(fFile.ReadLine(sLine, PLATFORM_MAX_PATH*2))
{
TrimString(sLine);
strcopy(gS_MySQLPrefix, 32, sLine);
break;
}
delete fFile;
}
public void OnClientDisconnect(int client)
{
gB_AllowCustom[client] = false;
}
public void OnClientPutInServer(int client)
{
gB_AllowCustom[client] = false;
gB_NameEnabled[client] = false;
strcopy(gS_CustomName[client], 128, "");
gB_MessageEnabled[client] = false;
strcopy(gS_CustomMessage[client], 128, "");
}
public void OnClientPostAdminCheck(int client)
{
gB_AllowCustom[client] = CheckCommandAccess(client, "shavit_chat", ADMFLAG_CHAT);
if(gH_SQL != null)
{
LoadFromDatabase(client);
}
}
public Action Command_CCHelp(int client, int args)
{
if(client == 0)
{
ReplyToCommand(client, "%t", "NoConsole");
return Plugin_Handled;
}
Shavit_PrintToChat(client, "%T", "CheckConsole", client);
PrintToConsole(client, "%T\n", "CCHelp_Intro", client);
PrintToConsole(client, "%T", "CCHelp_Generic", client);
PrintToConsole(client, "%T", "CCHelp_GenericVariables", client);
if(gEV_Type == Engine_CSS)
{
PrintToConsole(client, "%T", "CCHelp_CSS_1", client);
PrintToConsole(client, "%T", "CCHelp_CSS_2", client);
}
else
{
PrintToConsole(client, "%T", "CCHelp_CSGO_1", client);
}
return Plugin_Handled;
}
public Action Command_CCName(int client, int args)
{
if(client == 0)
{
ReplyToCommand(client, "%t", "NoConsole");
return Plugin_Handled;
}
if(!gB_AllowCustom[client])
{
Shavit_PrintToChat(client, "%T", "NoCommandAccess", client);
return Plugin_Handled;
}
char[] sArgs = new char[128];
GetCmdArgString(sArgs, 128);
TrimString(sArgs);
FormatColors(sArgs, 128, true, true);
if(args == 0 || strlen(sArgs) == 0)
{
Shavit_PrintToChat(client, "%T", "ArgumentsMissing", client, "sm_ccname <text>");
Shavit_PrintToChat(client, "%T", "ChatCurrent", client, sArgs);
return Plugin_Handled;
}
else if(StrEqual(sArgs, "off"))
{
Shavit_PrintToChat(client, "%T", "NameOff", client, sArgs);
gB_NameEnabled[client] = false;
SaveToDatabase(client);
return Plugin_Handled;
}
Shavit_PrintToChat(client, "%T", "ChatUpdated", client);
gB_NameEnabled[client] = true;
strcopy(gS_CustomName[client], 128, sArgs);
SaveToDatabase(client);
return Plugin_Handled;
}
public Action Command_CCMessage(int client, int args)
{
if(client == 0)
{
ReplyToCommand(client, "%t", "NoConsole");
return Plugin_Handled;
}
if(!gB_AllowCustom[client])
{
Shavit_PrintToChat(client, "%T", "NoCommandAccess", client);
return Plugin_Handled;
}
char[] sArgs = new char[32];
GetCmdArgString(sArgs, 32);
TrimString(sArgs);
FormatColors(sArgs, 32, true, true);
if(args == 0 || strlen(sArgs) == 0)
{
Shavit_PrintToChat(client, "%T", "ArgumentsMissing", client, "sm_ccmsg <text>");
Shavit_PrintToChat(client, "%T", "ChatCurrent", client, sArgs);
return Plugin_Handled;
}
else if(StrEqual(sArgs, "off"))
{
Shavit_PrintToChat(client, "%T", "MessageOff", client, sArgs);
gB_MessageEnabled[client] = false;
SaveToDatabase(client);
return Plugin_Handled;
}
Shavit_PrintToChat(client, "%T", "ChatUpdated", client);
gB_MessageEnabled[client] = true;
strcopy(gS_CustomMessage[client], 16, sArgs);
SaveToDatabase(client);
return Plugin_Handled;
}
public Action Command_CCList(int client, int args)
{
ReplyToCommand(client, "%T", "CheckConsole", client);
for(int i = 1; i <= MaxClients; i++)
{
if(gB_AllowCustom[i])
{
PrintToConsole(client, "%N (%d/%d) (name: \"%s\"; message: \"%s\")", i, i, GetClientUserId(i), gS_CustomName[i], gS_CustomMessage[i])
}
}
return Plugin_Handled;
}
public Action CP_OnChatMessage(int &author, ArrayList recipients, char[] flagstring, char[] name, char[] message, bool &processcolors, bool &removecolors)
{
if(!gB_AllowCustom[author])
{
return Plugin_Continue;
}
Action retvalue = Plugin_Continue;
if(gB_NameEnabled[author] && strlen(gS_CustomName[author]) > 0)
{
char[] sName = new char[MAX_NAME_LENGTH];
GetClientName(author, sName, MAX_NAME_LENGTH);
ReplaceString(gS_CustomName[author], MAXLENGTH_NAME, "{name}", sName);
strcopy(name, MAXLENGTH_NAME, gS_CustomName[author]);
FormatRandom(name, MAXLENGTH_NAME);
retvalue = Plugin_Changed;
}
if(gB_MessageEnabled[author] && strlen(gS_CustomMessage[author]) > 0)
{
Format(message, MAXLENGTH_MESSAGE, "%s%s", gS_CustomMessage[author], message);
FormatRandom(message, MAXLENGTH_MESSAGE);
retvalue = Plugin_Changed;
}
removecolors = true;
processcolors = false;
return retvalue;
}
void FormatColors(char[] buffer, int size, bool colors, bool escape)
{
if(colors)
{
for(int i = 0; i < sizeof(gS_GlobalColorNames); i++)
{
ReplaceString(buffer, size, gS_GlobalColorNames[i], gS_GlobalColors[i]);
}
for(int i = 0; i < sizeof(gS_CSGOColorNames); i++)
{
ReplaceString(buffer, size, gS_CSGOColorNames[i], gS_CSGOColors[i]);
}
ReplaceString(buffer, size, "^", "\x07");
ReplaceString(buffer, size, "{RGB}", "\x07");
ReplaceString(buffer, size, "&", "\x08");
ReplaceString(buffer, size, "{RGBA}", "\x08");
}
if(escape)
{
ReplaceString(buffer, size, "%%", "");
}
}
void FormatRandom(char[] buffer, int size)
{
char[] temp = new char[8];
do
{
int color = ((RealRandomInt(0, 255) & 0xFF) << 16);
color |= ((RealRandomInt(0, 255) & 0xFF) << 8);
color |= (RealRandomInt(0, 255) & 0xFF);
FormatEx(temp, 16, "\x07%06X", color);
}
while(ReplaceStringEx(buffer, size, "{rand}", temp) > 0);
}
int RealRandomInt(int min, int max)
{
int random = GetURandomInt();
if(random == 0)
{
random++;
}
return (RoundToCeil(float(random) / (float(2147483647) / float(max - min + 1))) + min - 1);
}
void SQL_DBConnect()
{
if(gH_SQL != null)
{
char[] sQuery = new char[512];
FormatEx(sQuery, 512, "CREATE TABLE IF NOT EXISTS `%schat` (`auth` VARCHAR(32) NOT NULL, `name` INT NOT NULL DEFAULT 0, `ccname` VARCHAR(128), `message` INT NOT NULL DEFAULT 0, `ccmessage` VARCHAR(16), PRIMARY KEY (`auth`));", gS_MySQLPrefix);
gH_SQL.Query(SQL_CreateTable_Callback, sQuery, 0, DBPrio_High);
}
}
public void SQL_CreateTable_Callback(Database db, DBResultSet results, const char[] error, any data)
{
if(results == null)
{
LogError("Timer error! Chat table creation failed. Reason: %s", error);
return;
}
for(int i = 1; i <= MaxClients; i++)
{
if(gB_AllowCustom[i])
{
LoadFromDatabase(i);
}
}
}
void SaveToDatabase(int client)
{
char[] sAuthID3 = new char[32];
if(!GetClientAuthId(client, AuthId_Steam3, sAuthID3, 32))
{
return;
}
int iLength = ((strlen(gS_CustomName[client]) * 2) + 1);
char[] sEscapedName = new char[iLength];
gH_SQL.Escape(gS_CustomName[client], sEscapedName, iLength);
iLength = ((strlen(gS_CustomMessage[client]) * 2) + 1);
char[] sEscapedMessage = new char[iLength];
gH_SQL.Escape(gS_CustomMessage[client], sEscapedMessage, iLength);
char[] sQuery = new char[512];
FormatEx(sQuery, 512, "REPLACE INTO %schat (auth, name, ccname, message, ccmessage) VALUES ('%s', %d, '%s', %d, '%s');", gS_MySQLPrefix, sAuthID3, gB_NameEnabled[client], sEscapedName, gB_MessageEnabled[client], sEscapedMessage);
gH_SQL.Query(SQL_UpdateUser_Callback, sQuery, 0, DBPrio_High);
}
public void SQL_UpdateUser_Callback(Database db, DBResultSet results, const char[] error, any data)
{
if(results == null)
{
LogError("Timer error! Failed to insert chat data. Reason: %s", error);
return;
}
}
void LoadFromDatabase(int client)
{
char[] sAuthID3 = new char[32];
if(!GetClientAuthId(client, AuthId_Steam3, sAuthID3, 32))
{
return;
}
char sQuery[256];
FormatEx(sQuery, 256, "SELECT name, ccname, message, ccmessage FROM %schat WHERE auth = '%s';", gS_MySQLPrefix, sAuthID3);
gH_SQL.Query(SQL_GetChat_Callback, sQuery, GetClientSerial(client), DBPrio_Low);
}
public void SQL_GetChat_Callback(Database db, DBResultSet results, const char[] error, any data)
{
if(results == null)
{
LogError("Timer (Chat cache update) SQL query failed. Reason: %s", error);
return;
}
int client = GetClientFromSerial(data);
if(client == 0)
{
return;
}
while(results.FetchRow())
{
gB_NameEnabled[client] = view_as<bool>(results.FetchInt(0));
results.FetchString(1, gS_CustomName[client], 128);
gB_MessageEnabled[client] = view_as<bool>(results.FetchInt(2));
results.FetchString(3, gS_CustomMessage[client], 16);
}
}

View File

@ -190,9 +190,6 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max
public void OnPluginStart()
{
LoadTranslations("shavit-common.phrases");
LoadTranslations("shavit-misc.phrases");
// cache
gEV_Type = GetEngineVersion();
@ -262,6 +259,8 @@ public void OnPluginStart()
// phrases
LoadTranslations("common.phrases");
LoadTranslations("shavit-common.phrases");
LoadTranslations("shavit-misc.phrases");
// advertisements
gA_Advertisements = new ArrayList(300);

View File

@ -0,0 +1,49 @@
"Phrases"
{
// ---------- Commands ---------- //
"ChatUpdated"
{
"en" "Setting updated."
}
"ChatCurrent"
{
"#format" "{1:s}"
"en" "Current setting: {1}"
}
"NameOff"
{
"en" "Custom chat name is now off."
}
"MessageOff"
{
"en" "Custom chat text color is now off."
}
"CheckConsole"
{
"en" "Check your console."
}
"CCHelp_Intro"
{
"en" "- You have access to custom chat.\nUsage information:"
}
"CCHelp_Generic"
{
"en" "- !ccname can be used to change your custom chat name. Use \"!ccname off\" to toggle it off.\n- !ccmsg can be used to change your custom message prefix/color. Use \"!ccmsgg off\" to toggle it off.\nThere are custom variables, such as:"
}
"CCHelp_GenericVariables"
{
"en" "- {name} - your in-game name.\n- {rand} - a random color.\n- {team} - your team color.\n- {green} - green color."
}
"CCHelp_CSS_1"
{
"en" "- To use a custom color, write ^RRGGBB (HEX/HTML colors). For example, ^FFFFFF is white and ^000000 is black."
}
"CCHelp_CSS_2"
{
"en" "- You can also use a set alpha (opacity in HEX, where FF is 255). &RRGGBBAA. For example, &FFFFFF7F is 127 alpha, so all text after the color will have 50% opacity."
}
"CCHelp_CSGO_1"
{
"en" "- The following colors are also available: {blue}, {bluegrey}, {darkblue}, {darkred}, {gold}, {grey}, {grey2}, {lightgreen}, {lightred}, {lime}, {orchid}, {yellow} and {palered}."
}
}

View File

@ -17,4 +17,18 @@
{
"en" "Bonus"
}
// ---------- Commands ---------- //
"NoCommandAccess"
{
"en" "You do not have access to this command."
}
"NoConsole"
{
"en" "Run this command from inside the game."
}
"ArgumentsMissing"
{
"#format" "{1:s}"
"en" "Usage: {1}"
}
}