iw4x-client/src/Components/Modules/Chat.cpp

346 lines
9.0 KiB
C++
Raw Normal View History

2022-02-27 07:53:44 -05:00
#include <STDInclude.hpp>
namespace Components
{
Dvar::Var Chat::cg_chatWidth;
2022-05-31 11:57:44 -04:00
Dvar::Var Chat::sv_disableChat;
Game::dvar_t** Chat::cg_chatHeight = reinterpret_cast<Game::dvar_t**>(0x7ED398);
Game::dvar_t** Chat::cg_chatTime = reinterpret_cast<Game::dvar_t**>(0x9F5DE8);
bool Chat::SendChat;
2022-01-10 13:42:58 -05:00
std::mutex Chat::AccessMutex;
2022-01-01 08:08:02 -05:00
std::unordered_set<std::uint64_t> Chat::MuteList;
const char* Chat::EvaluateSay(char* text, Game::gentity_t* player)
{
2022-01-01 08:08:02 -05:00
Chat::SendChat = true;
if (text[1] == '/')
{
2022-01-01 08:08:02 -05:00
Chat::SendChat = false;
text[1] = text[0];
++text;
}
2022-01-19 09:05:40 -05:00
std::unique_lock<std::mutex> lock(Chat::AccessMutex);
2022-05-31 12:38:09 -04:00
if (Chat::MuteList.contains(Game::svs_clients[player->s.number].steamID))
2022-01-01 08:08:02 -05:00
{
2022-01-19 09:05:40 -05:00
lock.unlock();
2022-01-01 08:08:02 -05:00
Chat::SendChat = false;
2022-05-31 11:57:44 -04:00
Game::SV_GameSendServerCommand(player->s.number, Game::SV_CMD_CAN_IGNORE,
2022-01-01 08:08:02 -05:00
Utils::String::VA("%c \"You are muted\"", 0x65));
}
2022-01-19 09:05:40 -05:00
// Test whether the lock is still locked
if (lock.owns_lock())
{
lock.unlock();
}
2022-05-31 11:57:44 -04:00
if (Chat::sv_disableChat.get<bool>())
{
Chat::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 + 100h + 10h]
push eax
pushad
2022-05-03 07:44:18 -04:00
push [esp + 100h + 28h]
push eax
call Chat::EvaluateSay
add esp, 8h
2022-05-03 07:44:18 -04:00
mov [esp + 20h], eax
popad
pop eax
2022-05-03 07:44:18 -04:00
mov [esp + 100h + 10h], eax
jmp PlayerName::CleanStrStub
}
}
__declspec(naked) void Chat::PostSayStub()
{
__asm
{
// eax is used by the callee
push eax
xor eax, eax
mov al, Chat::SendChat
test al, al
jnz return
// Don't send the chat
pop eax
retn
2021-09-20 12:21:38 -04:00
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)
2022-05-03 07:44:18 -04:00
{
if (len > chatWidth)
{
if (lastSpacePos && lastSpacePos > lastFontIconPos)
{
inputBuffer += lastSpacePos - lineBuffer + 1;
lineBuffer = lastSpacePos;
}
else if (lastFontIconPos)
{
inputBuffer += lastFontIconPos - lineBuffer;
lineBuffer = lastFontIconPos;
}
2022-05-03 07:44:18 -04:00
*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;
}
2022-05-03 07:44:18 -04:00
}
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<float>(cg_chatWidth.get<int>());
const auto chatTime = (*cg_chatTime)->current.integer;
2022-05-03 07:41:46 -04:00
if (chatHeight <= 0 || static_cast<unsigned>(chatHeight) > std::extent_v<decltype(Game::cgs_t::teamChatMsgs)> || chatWidth <= 0 || chatTime <= 0)
{
Game::cgsArray[0].teamLastChatPos = 0;
Game::cgsArray[0].teamChatPos = 0;
return;
}
2021-09-20 12:21:38 -04:00
TextRenderer::FontIconInfo fontIconInfo{};
auto len = 0.0f;
auto lastColor = static_cast<int>(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];
2022-05-03 07:44:18 -04:00
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)
2021-09-20 12:21:38 -04:00
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
}
}
2022-01-01 08:08:02 -05:00
void Chat::MuteClient(const Game::client_t* client)
{
2022-01-19 09:05:40 -05:00
std::unique_lock<std::mutex> lock(Chat::AccessMutex);
2022-01-10 13:42:58 -05:00
2022-05-31 12:38:09 -04:00
if (!Chat::MuteList.contains(client->steamID))
2022-01-01 08:08:02 -05:00
{
Chat::MuteList.insert(client->steamID);
2022-01-19 09:05:40 -05:00
lock.unlock();
2022-01-01 08:08:02 -05:00
Logger::Print("%s was muted\n", client->name);
2022-05-31 11:57:44 -04:00
Game::SV_GameSendServerCommand(client->gentity->s.number, Game::SV_CMD_CAN_IGNORE,
2022-01-01 08:08:02 -05:00
Utils::String::VA("%c \"You were muted\"", 0x65));
return;
}
2022-01-19 09:05:40 -05:00
lock.unlock();
2022-01-01 08:08:02 -05:00
Logger::Print("%s is already muted\n", client->name);
2022-05-31 11:57:44 -04:00
Game::SV_GameSendServerCommand(-1, Game::SV_CMD_CAN_IGNORE,
2022-01-01 08:08:02 -05:00
Utils::String::VA("%c \"%s is already muted\"", 0x65, client->name));
}
void Chat::UnmuteClient(const Game::client_t* client)
{
2022-01-10 13:42:58 -05:00
Chat::UnmuteInternal(client->steamID);
2022-01-01 08:08:02 -05:00
Logger::Print("%s was unmuted\n", client->name);
2022-05-31 11:57:44 -04:00
Game::SV_GameSendServerCommand(client->gentity->s.number, Game::SV_CMD_CAN_IGNORE,
2022-01-01 08:08:02 -05:00
Utils::String::VA("%c \"You were unmuted\"", 0x65));
}
2022-01-10 13:42:58 -05:00
void Chat::UnmuteInternal(const std::uint64_t id, bool everyone)
{
2022-01-19 09:05:40 -05:00
std::unique_lock<std::mutex> lock(Chat::AccessMutex);
2022-01-10 13:42:58 -05:00
if (everyone)
Chat::MuteList.clear();
else
Chat::MuteList.erase(id);
}
2022-01-01 08:08:02 -05:00
void Chat::AddChatCommands()
{
Command::AddSV("muteClient", [](Command::Params* params)
{
if (!Dvar::Var("sv_running").get<bool>())
{
Logger::Print("Server is not running.\n");
return;
}
const auto* cmd = params->get(0);
2022-03-17 14:50:20 -04:00
if (params->size() < 2)
2022-01-01 08:08:02 -05:00
{
Logger::Print("Usage: %s <client number> : prevent the player from using the chat\n", cmd);
return;
}
const auto* client = Game::SV_GetPlayerByNum();
if (client != nullptr)
{
Chat::MuteClient(client);
}
});
Command::AddSV("unmute", [](Command::Params* params)
{
if (!Dvar::Var("sv_running").get<bool>())
{
Logger::Print("Server is not running.\n");
return;
}
const auto* cmd = params->get(0);
2022-03-17 14:50:20 -04:00
if (params->size() < 2)
2022-01-01 08:08:02 -05:00
{
2022-01-01 16:21:25 -05:00
Logger::Print("Usage: %s <client number or guid>\n%s all = unmute everyone\n", cmd, cmd);
2022-01-01 08:08:02 -05:00
return;
}
const auto* client = Game::SV_GetPlayerByNum();
2022-01-01 16:21:25 -05:00
if (client != nullptr)
2022-01-01 08:08:02 -05:00
{
2022-01-01 16:21:25 -05:00
Chat::UnmuteClient(client);
return;
}
2022-03-17 14:50:20 -04:00
if (std::strcmp(params->get(1), "all") == 0)
2022-01-01 16:21:25 -05:00
{
Logger::Print("All players were unmuted\n");
2022-01-10 13:42:58 -05:00
Chat::UnmuteInternal(0, true);
2022-01-01 08:08:02 -05:00
}
else
{
2022-01-10 13:42:58 -05:00
const auto steamId = std::strtoull(params->get(1), nullptr, 16);
Chat::UnmuteInternal(steamId);
2022-01-01 08:08:02 -05:00
}
});
}
Chat::Chat()
{
2022-01-01 08:08:02 -05:00
Dvar::OnInit([]
{
2022-05-31 11:57:44 -04:00
Chat::cg_chatWidth = Dvar::Register<int>("cg_chatWidth", 52, 1,
std::numeric_limits<int>::max(), Game::dvar_flag::DVAR_ARCHIVE, "The normalized maximum width of a chat message");
Chat::sv_disableChat = Dvar::Register<bool>("sv_disableChat", false,
Game::dvar_flag::DVAR_NONE, "Disable chat messages from clients");
2022-01-01 08:08:02 -05:00
Chat::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();
}
}