/** * [CSRD] Simple Chat Processor * * Simple Chat Processor almost-compatible library for TF2. * Attempts to fix the one-recipient SayText2 messages, and does not depend on server-side * localization nonsense (everyone sees "(TEAM)" and "(DEAD)" in their language). */ #pragma semicolon 1 #include #include #pragma newdecls required #define PLUGIN_VERSION "0.3.0" public Plugin myinfo = { name = "[CSRD] Simple Chat Processor", author = "nosoop (based off of Simple Plugins' implementation)", description = "Simple Chat Processor almost-compatible library for TF2-specific fixes.", version = PLUGIN_VERSION, url = "https://git.csrd.science/" } #define PACKED_TOKEN_DELIMITER ":" #define CHATFLAGS_INVALID (0) #define CHATFLAGS_ALL (1 << 0) #define CHATFLAGS_TEAM (1 << 1) #define CHATFLAGS_SPEC (1 << 2) #define CHATFLAGS_DEAD (1 << 3) public APLRes AskPluginLoad2(Handle hPluginSelf, bool late, char[] error, int maxlen) { MarkNativeAsOptional("GetUserMessageType"); CreateNative("GetMessageFlags", Native_GetMessageFlags); RegPluginLibrary("scp"); return APLRes_Success; } // Holds player-unique messages sent in the current frame. StringMap g_QueuedMessages[MAXPLAYERS+1]; Handle g_fwdOnChatMessage, g_fwdOnChatMessagePost; int g_ChatFlags; public void OnPluginStart() { UserMsg umSayText2 = GetUserMessageId("SayText2"); if (umSayText2 != INVALID_MESSAGE_ID) { HookUserMessage(umSayText2, OnSayText2, true); } else { SetFailState("Game does not use SayText2."); } g_fwdOnChatMessage = CreateGlobalForward("OnChatMessage", ET_Hook, Param_CellByRef, Param_Cell, Param_String, Param_String); g_fwdOnChatMessagePost = CreateGlobalForward("OnChatMessage_Post", ET_Ignore, Param_Cell, Param_Cell, Param_String, Param_String); for (int i = 1; i < MaxClients; i++) { if (IsClientInGame(i)) { OnClientPutInServer(i); } } HookEvent("player_say", OnPlayerSayPost, EventHookMode_Post); } public void OnClientPutInServer(int client) { g_QueuedMessages[client] = new StringMap(); } public void OnClientDisconnect(int client) { if (g_QueuedMessages[client] && g_QueuedMessages[client].Size > 0) { // delete remaining queued messages, don't bother sending StringMapSnapshot messages = g_QueuedMessages[client].Snapshot(); for (int m = 0; m < messages.Length; m++) { char packedMessage[192]; messages.GetKey(m, packedMessage, sizeof(packedMessage)); ArrayList clientList; g_QueuedMessages[client].GetValue(packedMessage, clientList); delete clientList; } delete messages; g_QueuedMessages[client].Clear(); } delete g_QueuedMessages[client]; } /** * Collects previously fired UTIL_SayText2Filter events and holds them in our own internal * buffer to be manipulated at a later time. Each usermessage is fired separately. * * This is handled by game/server/client.cpp::Host_Say */ Action OnSayText2(UserMsg id, Handle buffer, const int[] clients, int nClients, bool reliable, bool init) { BfRead bitbuf = view_as(buffer); int author = bitbuf.ReadByte(); if (!author) { return Plugin_Continue; } bitbuf.ReadByte(); // bChat, unused? char localizationToken[32]; bitbuf.ReadString(localizationToken, sizeof(localizationToken)); if (StrContains(localizationToken, "TF_Chat_") == -1) { return Plugin_Continue; } if (!ParseChatMessageFlags(localizationToken)) { return Plugin_Continue; } char name[MAX_NAME_LENGTH]; bitbuf.ReadString(name, sizeof(name)); char message[128]; bitbuf.ReadString(message, sizeof(message)); /** * Pack messages based on localization token and message. * Any new similar usermessages in the same frame (matching message and flags) get their * recipients added to the same entry. */ char packedMessage[192]; Format(packedMessage, sizeof(packedMessage), "%s" ... PACKED_TOKEN_DELIMITER ... "%s", localizationToken, message); ArrayList recipients; if (!g_QueuedMessages[author].GetValue(packedMessage, recipients)) { recipients = new ArrayList(); g_QueuedMessages[author].SetValue(packedMessage, recipients); } for (int i = 0; i < nClients; i++) { recipients.Push(clients[i]); } return Plugin_Handled; } void OnPlayerSayPost(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); FlushQueuedMessages(client); } void FlushQueuedMessages(int author) { /** * Iterate through all queued messages from OnSayText2 */ if (!g_QueuedMessages[author] || g_QueuedMessages[author].Size == 0) { return; } StringMapSnapshot messages = g_QueuedMessages[author].Snapshot(); for (int m = 0; m < messages.Length; m++) { char packedMessage[192], localizationToken[32], message[128]; messages.GetKey(m, packedMessage, sizeof(packedMessage)); ArrayList clientList; g_QueuedMessages[author].GetValue(packedMessage, clientList); // unpack localization and message from key int d = SplitString(packedMessage, PACKED_TOKEN_DELIMITER, localizationToken, sizeof(localizationToken)); strcopy(message, sizeof(message), packedMessage[d]); char name[MAX_NAME_LENGTH + 1]; GetClientName(author, name, sizeof(name)); // Prepare chat message flags. g_ChatFlags = ParseChatMessageFlags(localizationToken); // Forward call. Action forwardResult = ForwardOnChatMessage(author, clientList, name, sizeof(name), message, sizeof(message)); // Proceed to display message on continue or changed, else drop message. if (forwardResult < Plugin_Handled) { // convert ArrayList to client array int clients[MAXPLAYERS + 1], nClients; for (int i = 0; i < clientList.Length; i++) { int recipient = clientList.Get(i); // display to author and players that did not mute the author if (ShouldTransmitMessage(author, recipient)) { clients[nClients++] = recipient; } } // since it's not commented on in the SDK, we can only speculate on why the // developers decided to send SayText2 messages individually // probably cheat clients? SayText(author, clients, nClients, localizationToken, name, message); ForwardOnChatMessagePost(author, clientList, name, message); } delete clientList; g_ChatFlags = CHATFLAGS_INVALID; } delete messages; g_QueuedMessages[author].Clear(); } Action ForwardOnChatMessage(int &author, ArrayList clientList, char[] name, int nameLength, char[] message, int messageLength) { Action forwardResult; Call_StartForward(g_fwdOnChatMessage); Call_PushCellRef(author); Call_PushCell(clientList); Call_PushStringEx(name, nameLength, SM_PARAM_STRING_UTF8 | SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK); Call_PushStringEx(message, messageLength, SM_PARAM_STRING_UTF8 | SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK); int error = Call_Finish(forwardResult); if (error) { ThrowNativeError(error, "Forward failed"); return Plugin_Stop; } return forwardResult; } bool ShouldTransmitMessage(int author, int recipient) { return recipient == author || !IsClientMuted(recipient, author); } void ForwardOnChatMessagePost(int author, ArrayList clientList, const char[] name, const char[] message) { Call_StartForward(g_fwdOnChatMessagePost); Call_PushCell(author); Call_PushCell(clientList); Call_PushString(name); Call_PushString(message); int error = Call_Finish(); if (error) { ThrowNativeError(error, "Forward failed"); } } int Native_GetMessageFlags(Handle hPlugin, int argc) { return g_ChatFlags; } int ParseChatMessageFlags(const char[] localizationToken) { int chatFlags; if (StrContains(localizationToken, "all", false) != -1) { // send to all players, living and dead chatFlags |= CHATFLAGS_ALL; } if (StrContains(localizationToken, "team", false) != -1) { // send only to players on the same team chatFlags |= CHATFLAGS_TEAM; } if (StrContains(localizationToken, "spec", false) != -1) { // send only to players in spec chatFlags |= CHATFLAGS_SPEC; } if (StrContains(localizationToken, "dead", false) != -1) { // send to dead players and team members only chatFlags |= CHATFLAGS_DEAD; } return chatFlags; } void SayText(int author, int[] clients, int nClients, const char[] localizationToken, const char[] name, const char[] message) { for (int i; i < nClients; i++) { int temp[1]; temp[0] = clients[i]; Handle buffer = StartMessage("SayText2", temp, 1, USERMSG_RELIABLE | USERMSG_BLOCKHOOKS); BfWrite bitbuf = view_as(buffer); bitbuf.WriteByte(author); bitbuf.WriteByte(true); bitbuf.WriteString(localizationToken); bitbuf.WriteString(name); bitbuf.WriteString(message); EndMessage(); } }