#include namespace Components { char CardTitles::CustomTitles[Game::MAX_CLIENTS][18]; Dvar::Var CardTitles::CustomTitle; CClient* CardTitles::GetClientByIndex(std::uint32_t index) { return &reinterpret_cast(0x8E77B0)[index]; } int CardTitles::GetPlayerCardClientInfo(int lookupResult, Game::PlayerCardData* data) { auto result = lookupResult; const auto* username = Dvar::Var("name").get(); if (std::strcmp(data->name, username) == 0) { result += 0xFE000000; } else { for (std::size_t i = 0; i < Game::MAX_CLIENTS; ++i) { CClient* c = GetClientByIndex(i); if (c != nullptr) { if (!std::strcmp(data->name, c->Name)) { // Since a 4 byte integer is overkill for a row num: We can use it to store the customprefix + clientNum and use a 2 byte integer for the row number result += 0xFF000000; result += i * 0x10000; break; } } } } return result; } void __declspec(naked) CardTitles::GetPlayerCardClientInfoStub() { __asm { push eax pushad push esi push eax call GetPlayerCardClientInfo add esp, 8 mov [esp + 20h], eax popad pop eax pop esi pop ebp mov [ebx + 4], eax pop ebx retn } } const char* CardTitles::TableLookupByRowHook(Game::Operand* operand, tablelookuprequest_s* request) { std::uint8_t prefix = (request->tableRow >> (8 * 3)) & 0xFF; std::uint8_t data = (request->tableRow >> (8 * 2)) & 0xFF; if (data >= Game::MAX_CLIENTS) return nullptr; if (request->tablename == "mp/cardTitleTable.csv"s) { if (prefix != 0x00) { // Column 1 = CardTitle if (request->tableColumn == 1) { if (prefix == 0xFE) { if (!CustomTitle.get().empty()) { // 0xFF in front of the title to skip localization. Or else it will wait for a couple of seconds for the asset of type localize const auto* title = Utils::String::VA("\x15%s", CustomTitle.get()); // prepare return value operand->internals.stringVal.string = title; operand->dataType = Game::VAL_STRING; return title; } } else if (prefix == 0xFF) { if (CustomTitles[data][0] != '\0') { const auto* title = Utils::String::VA("\x15%s", CustomTitles[data]); // prepare return value operand->internals.stringVal.string = title; operand->dataType = Game::VAL_STRING; return title; } } } // If the title was changed it already returned at this point so... // Remove prefix and data to make being readable to the normal lookuprequest request->tableRow = static_cast(*(reinterpret_cast(&request->tableRow))); } } return nullptr; } __declspec(naked) void CardTitles::TableLookupByRowHookStub() { __asm { push eax pushad push esi push ebx call TableLookupByRowHook add esp, 8 mov [esp + 20h], eax popad pop eax cmp eax, 0 jz OriginalTitle pop ecx mov ecx, DWORD ptr[esi + 4] retn OriginalTitle: mov eax, [esi + 50h] cmp eax, 3 push 62DCC7h retn } } void CardTitles::SendCustomTitlesToClients() { std::string list; for (std::size_t i = 0; i < Game::MAX_CLIENTS; ++i) { char playerTitle[18]{}; if (Game::svs_clients[i].userinfo[0] != '\0') { strncpy_s(playerTitle, Game::Info_ValueForKey(Game::svs_clients[i].userinfo, "customTitle"), _TRUNCATE); } else { playerTitle[0] = '\0'; } list.append(std::format("\\{}\\{}", std::to_string(i), playerTitle)); } const auto* command = Utils::String::Format("{:c} customTitles \"{}\"", 21, list); Game::SV_GameSendServerCommand(-1, Game::SV_CMD_CAN_IGNORE, command); } void CardTitles::ParseCustomTitles(const char* msg) { for (std::size_t i = 0; i < Game::MAX_CLIENTS; ++i) { const auto index = std::to_string(i); const auto* playerTitle = Game::Info_ValueForKey(msg, index.data()); if (playerTitle[0] == '\0') { CustomTitles[i][0] = '\0'; } else { Game::I_strncpyz(CustomTitles[i], playerTitle, sizeof(CustomTitles[0]) / sizeof(char)); } } } CardTitles::CardTitles() { Scheduler::Once([] { CustomTitle = Dvar::Register("customTitle", "", Game::DVAR_USERINFO | Game::DVAR_ARCHIVE, "Custom card title"); }, Scheduler::Pipeline::MAIN); std::memset(&CustomTitles, 0, sizeof(char[Game::MAX_CLIENTS][18])); ServerCommands::OnCommand(21, [](Command::Params* params) { if (std::strcmp(params->get(1), "customTitles") == 0) { if (params->size() == 3) { ParseCustomTitles(params->get(2)); return true; } } return false; }); Utils::Hook(0x62EB26, GetPlayerCardClientInfoStub).install()->quick(); // Table lookup stuff Utils::Hook(0x62DCC1, TableLookupByRowHookStub).install()->quick(); Utils::Hook::Nop(0x62DCC6, 1); } }