#include namespace Components { Dvar::Var Chat::cg_chatWidth; Dvar::Var Chat::sv_disableChat; Game::dvar_t** Chat::cg_chatHeight = reinterpret_cast(0x7ED398); Game::dvar_t** Chat::cg_chatTime = reinterpret_cast(0x9F5DE8); bool Chat::SendChat; std::mutex Chat::AccessMutex; std::unordered_set Chat::MuteList; bool Chat::CanAddCallback = true; std::vector Chat::SayCallbacks; const char* Chat::EvaluateSay(char* text, Game::gentity_t* player, int mode) { SendChat = true; const auto _0 = gsl::finally([] { CanAddCallback = true; }); // Prevent callbacks from adding a new callback (would make the vector iterator invalid) CanAddCallback = false; if (text[1] == '/') { SendChat = false; text[1] = text[0]; ++text; } std::unique_lock lock(AccessMutex); if (MuteList.contains(Game::svs_clients[player->s.number].steamID)) { lock.unlock(); SendChat = false; Game::SV_GameSendServerCommand(player->s.number, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"You are muted\"", 0x65)); } // Test whether the lock is still locked if (lock.owns_lock()) { lock.unlock(); } for (const auto& callback : SayCallbacks) { if (!ChatCallback(player, callback.getPos(), (text + 1), mode)) { SendChat = false; } } if (sv_disableChat.get()) { SendChat = false; Game::SV_GameSendServerCommand(player->s.number, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"Chat is disabled\"", 0x65)); } TextRenderer::StripMaterialTextIcons(text, text, strlen(text) + 1); Game::Scr_AddEntity(player); Game::Scr_AddString(text + 1); Game::Scr_NotifyLevel(Game::SL_GetString("say", 0), 2); return text; } __declspec(naked) void Chat::PreSayStub() { __asm { mov eax, [esp + 0x100 + 0x10] push eax pushad push [esp + 0x100 + 0x30] // mode push [esp + 0x100 + 0x2C] // player push eax // text call EvaluateSay add esp, 0xC mov [esp + 0x20], eax popad pop eax mov [esp + 0x100 + 0x10], eax jmp PlayerName::CleanStrStub } } __declspec(naked) void Chat::PostSayStub() { __asm { // eax is used by the callee push eax xor eax, eax mov al, SendChat test al, al jnz return // Don't send the chat pop eax retn return: pop eax // Jump to the target push 5DF620h retn } } void Chat::CheckChatLineEnd(const char*& inputBuffer, char*& lineBuffer, float& len, const int chatHeight, const float chatWidth, char*& lastSpacePos, char*& lastFontIconPos, const int lastColor) { if (len > chatWidth) { if (lastSpacePos && lastSpacePos > lastFontIconPos) { inputBuffer += lastSpacePos - lineBuffer + 1; lineBuffer = lastSpacePos; } else if (lastFontIconPos) { inputBuffer += lastFontIconPos - lineBuffer; lineBuffer = lastFontIconPos; } *lineBuffer = 0; len = 0.0f; Game::cgsArray[0].teamChatMsgTimes[Game::cgsArray[0].teamChatPos % chatHeight] = Game::cgArray[0].time; Game::cgsArray[0].teamChatPos++; lineBuffer = Game::cgsArray[0].teamChatMsgs[Game::cgsArray[0].teamChatPos % chatHeight]; lineBuffer[0] = '^'; lineBuffer[1] = CharForColorIndex(lastColor); lineBuffer += 2; lastSpacePos = nullptr; lastFontIconPos = nullptr; } } void Chat::CG_AddToTeamChat(const char* text) { // Text can only be 150 characters maximum. This is bigger than the teamChatMsgs buffers with 160 characters // Therefore it is not needed to check for buffer lengths const auto chatHeight = (*cg_chatHeight)->current.integer; const auto chatWidth = static_cast(cg_chatWidth.get()); const auto chatTime = (*cg_chatTime)->current.integer; if (chatHeight <= 0 || static_cast(chatHeight) > std::extent_v || chatWidth <= 0 || chatTime <= 0) { Game::cgsArray[0].teamLastChatPos = 0; Game::cgsArray[0].teamChatPos = 0; return; } TextRenderer::FontIconInfo fontIconInfo{}; auto len = 0.0f; auto lastColor = static_cast(TEXT_COLOR_DEFAULT); char* lastSpace = nullptr; char* lastFontIcon = nullptr; char* p = Game::cgsArray[0].teamChatMsgs[Game::cgsArray[0].teamChatPos % chatHeight]; p[0] = '\0'; while (*text) { CheckChatLineEnd(text, p, len, chatHeight, chatWidth, lastSpace, lastFontIcon, lastColor); const char* fontIconEndPos = &text[1]; if (text[0] == TextRenderer::FONT_ICON_SEPARATOR_CHARACTER && TextRenderer::IsFontIcon(fontIconEndPos, fontIconInfo)) { // The game calculates width on a per character base. Since the width of a font icon is calculated based on the height of the font // which is roughly double as much as the average width of a character without an additional multiplier the calculated len of the font icon // would be less than it most likely would be rendered. Therefore apply a guessed 2.0f multiplier at this location which makes // the calculated width of a font icon roughly comparable to the width of an average character of the font. const auto normalizedFontIconWidth = TextRenderer::GetNormalizedFontIconWidth(fontIconInfo); const auto fontIconWidth = normalizedFontIconWidth * FONT_ICON_CHAT_WIDTH_CALCULATION_MULTIPLIER; len += fontIconWidth; lastFontIcon = p; for(; text < fontIconEndPos; text++) { p[0] = text[0]; p++; } CheckChatLineEnd(text, p, len, chatHeight, chatWidth, lastSpace, lastFontIcon, lastColor); } else if (text[0] == '^' && text[1] != 0 && text[1] >= TextRenderer::COLOR_FIRST_CHAR && text[1] <= TextRenderer::COLOR_LAST_CHAR) { p[0] = '^'; p[1] = text[1]; lastColor = ColorIndexForChar(text[1]); p += 2; text += 2; } else { if (text[0] == ' ') lastSpace = p; *p++ = *text++; len += 1.0f; } } *p = 0; Game::cgsArray[0].teamChatMsgTimes[Game::cgsArray[0].teamChatPos % chatHeight] = Game::cgArray[0].time; Game::cgsArray[0].teamChatPos++; if (Game::cgsArray[0].teamChatPos - Game::cgsArray[0].teamLastChatPos > chatHeight) Game::cgsArray[0].teamLastChatPos = Game::cgsArray[0].teamChatPos + 1 - chatHeight; } __declspec(naked) void Chat::CG_AddToTeamChat_Stub() { __asm { pushad push ecx call CG_AddToTeamChat add esp, 4h popad ret } } void Chat::MuteClient(const Game::client_t* client) { std::unique_lock lock(AccessMutex); if (!MuteList.contains(client->steamID)) { MuteList.insert(client->steamID); lock.unlock(); Logger::Print("{} was muted\n", client->name); Game::SV_GameSendServerCommand(client->gentity->s.number, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"You were muted\"", 0x65)); return; } lock.unlock(); Logger::Print("{} is already muted\n", client->name); Game::SV_GameSendServerCommand(-1, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"%s is already muted\"", 0x65, client->name)); } void Chat::UnmuteClient(const Game::client_t* client) { UnmuteInternal(client->steamID); Logger::Print("{} was unmuted\n", client->name); Game::SV_GameSendServerCommand(client->gentity->s.number, Game::SV_CMD_CAN_IGNORE, Utils::String::VA("%c \"You were unmuted\"", 0x65)); } void Chat::UnmuteInternal(const std::uint64_t id, bool everyone) { std::unique_lock lock(AccessMutex); if (everyone) MuteList.clear(); else MuteList.erase(id); } void Chat::AddChatCommands() { Command::AddSV("muteClient", [](Command::Params* params) { if (!Dvar::Var("sv_running").get()) { Logger::Print("Server is not running.\n"); return; } const auto* cmd = params->get(0); if (params->size() < 2) { Logger::Print("Usage: {} : prevent the player from using the chat\n", cmd); return; } const auto* client = Game::SV_GetPlayerByNum(); if (client != nullptr) { MuteClient(client); } }); Command::AddSV("unmute", [](Command::Params* params) { if (!Dvar::Var("sv_running").get()) { Logger::Print("Server is not running.\n"); return; } const auto* cmd = params->get(0); if (params->size() < 2) { Logger::Print("Usage: {} \n{} all = unmute everyone\n", cmd, cmd); return; } const auto* client = Game::SV_GetPlayerByNum(); if (client != nullptr) { UnmuteClient(client); return; } if (std::strcmp(params->get(1), "all") == 0) { Logger::Print("All players were unmuted\n"); UnmuteInternal(0, true); } else { const auto steamId = std::strtoull(params->get(1), nullptr, 16); UnmuteInternal(steamId); } }); } int Chat::GetCallbackReturn() { if (Game::scrVmPub->inparamcount == 0) { // Nothing. Let's not mute the player return 1; } Game::Scr_ClearOutParams(); Game::scrVmPub->outparamcount = Game::scrVmPub->inparamcount; Game::scrVmPub->inparamcount = 0; const auto* result = &Game::scrVmPub->top[1 - Game::scrVmPub->outparamcount]; if (result->type != Game::scrParamType_t::VAR_INTEGER) { // Garbage was returned return 1; } return result->u.intValue; } int Chat::ChatCallback(Game::gentity_s* self, const char* codePos, const char* message, int mode) { const auto entityId = Game::Scr_GetEntityId(self->s.number, 0); Scripting::StackIsolation _; Game::Scr_AddInt(mode); Game::Scr_AddString(message); Game::VariableValue value; value.type = Game::scrParamType_t::VAR_OBJECT; value.u.uintValue = entityId; Game::AddRefToValue(value.type, value.u); const auto localId = Game::AllocThread(entityId); const auto result = Game::VM_Execute_0(localId, codePos, 2); Game::RemoveRefToObject(result); return GetCallbackReturn(); } void Chat::AddScriptFunctions() { Script::AddFunction("OnPlayerSay", [] // gsc: OnPlayerSay() { if (Game::Scr_GetNumParam() != 1) { Game::Scr_Error("^1OnPlayerSay: Needs one function pointer!\n"); return; } if (!CanAddCallback) { Game::Scr_Error("^1OnPlayerSay: Cannot add a callback in this context"); return; } const auto* func = Script::GetCodePosForParam(0); SayCallbacks.emplace_back(func); }); } Chat::Chat() { cg_chatWidth = Dvar::Register("cg_chatWidth", 52, 1, std::numeric_limits::max(), Game::DVAR_ARCHIVE, "The normalized maximum width of a chat message"); sv_disableChat = Dvar::Register("sv_disableChat", false, Game::dvar_flag::DVAR_NONE, "Disable chat messages from clients"); Events::OnSVInit(AddChatCommands); // Intercept chat sending Utils::Hook(0x4D000B, PreSayStub, HOOK_CALL).install()->quick(); Utils::Hook(0x4D00D4, PostSayStub, HOOK_CALL).install()->quick(); Utils::Hook(0x4D0110, PostSayStub, HOOK_CALL).install()->quick(); // Change logic that does word splitting with new lines for chat messages to support fonticons Utils::Hook(0x592E10, CG_AddToTeamChat_Stub, HOOK_JUMP).install()->quick(); AddScriptFunctions(); // Avoid duplicates Events::OnVMShutdown([] { SayCallbacks.clear(); }); } }