ddnet/src/game/client/components/chat.cpp
heinrich5991 49bc150afd Refactor: Distinguish between two concepts previously called "Dummy"
On the one hand variables called "Dummy" would tell us whether the
current action refers to the currently inactive tee ("dummy"). On the
other hand, these variables could tell us whether the current action
refers to the main connection to the server, or the secondary one. The
latter use case is now renamed to "Client", with the choices
`CLIENT_MAIN`, `CLIENT_DUMMY` (and `CLIENT_CONTACT`).

Perhaps better names could be found, especially since `Client` also
refers to the engine client class in the game code.

I tried to not fix bugs unless it would complicate the code.
2022-01-21 01:54:14 +01:00

1388 lines
41 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <base/tl/string.h>
#include <engine/editor.h>
#include <engine/engine.h>
#include <engine/graphics.h>
#include <engine/keys.h>
#include <engine/shared/config.h>
#include <engine/shared/csv.h>
#include <engine/textrender.h>
#include <game/generated/client_data.h>
#include <game/generated/protocol.h>
#include <game/client/animstate.h>
#include <game/client/gameclient.h>
#include <game/client/components/console.h>
#include <game/client/components/scoreboard.h>
#include <game/client/components/skins.h>
#include <game/client/components/sounds.h>
#include <game/localization.h>
#include "chat.h"
CChat::CChat()
{
for(auto &Line : m_aLines)
{
// reset the container indices, so the text containers can be deleted on reset
Line.m_TextContainerIndex = -1;
Line.m_QuadContainerIndex = -1;
}
#define CHAT_COMMAND(name, params, flags, callback, userdata, help) RegisterCommand(name, params, flags, help);
#include <game/ddracechat.h>
m_Commands.sort_range();
m_Mode = MODE_NONE;
Reset();
}
void CChat::RegisterCommand(const char *pName, const char *pParams, int flags, const char *pHelp)
{
m_Commands.add_unsorted(CCommand{pName, pParams});
}
void CChat::RebuildChat()
{
for(auto &Line : m_aLines)
{
if(Line.m_TextContainerIndex != -1)
TextRender()->DeleteTextContainer(Line.m_TextContainerIndex);
Line.m_TextContainerIndex = -1;
if(Line.m_QuadContainerIndex != -1)
Graphics()->DeleteQuadContainer(Line.m_QuadContainerIndex);
Line.m_QuadContainerIndex = -1;
// recalculate sizes
Line.m_YOffset[0] = -1.f;
Line.m_YOffset[1] = -1.f;
}
}
void CChat::OnWindowResize()
{
RebuildChat();
}
void CChat::Reset()
{
for(auto &Line : m_aLines)
{
if(Line.m_TextContainerIndex != -1)
TextRender()->DeleteTextContainer(Line.m_TextContainerIndex);
if(Line.m_QuadContainerIndex != -1)
Graphics()->DeleteQuadContainer(Line.m_QuadContainerIndex);
Line.m_Time = 0;
Line.m_aText[0] = 0;
Line.m_aName[0] = 0;
Line.m_Friend = false;
Line.m_TextContainerIndex = -1;
Line.m_QuadContainerIndex = -1;
Line.m_TimesRepeated = 0;
Line.m_HasRenderTee = false;
}
m_PrevScoreBoardShowed = false;
m_PrevShowChat = false;
m_ReverseTAB = false;
m_Show = false;
m_InputUpdate = false;
m_ChatStringOffset = 0;
m_CompletionUsed = false;
m_CompletionChosen = -1;
m_aCompletionBuffer[0] = 0;
m_PlaceholderOffset = 0;
m_PlaceholderLength = 0;
m_pHistoryEntry = 0x0;
m_PendingChatCounter = 0;
m_LastChatSend = 0;
m_CurrentLine = 0;
DisableMode();
for(int64_t &LastSoundPlayed : m_aLastSoundPlayed)
LastSoundPlayed = 0;
}
void CChat::OnRelease()
{
m_Show = false;
}
void CChat::OnStateChange(int NewState, int OldState)
{
if(OldState <= IClient::STATE_CONNECTING)
Reset();
}
void CChat::ConSay(IConsole::IResult *pResult, void *pUserData)
{
((CChat *)pUserData)->Say(0, pResult->GetString(0));
}
void CChat::ConSayTeam(IConsole::IResult *pResult, void *pUserData)
{
((CChat *)pUserData)->Say(1, pResult->GetString(0));
}
void CChat::ConChat(IConsole::IResult *pResult, void *pUserData)
{
const char *pMode = pResult->GetString(0);
if(str_comp(pMode, "all") == 0)
((CChat *)pUserData)->EnableMode(0);
else if(str_comp(pMode, "team") == 0)
((CChat *)pUserData)->EnableMode(1);
else
((CChat *)pUserData)->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "console", "expected all or team as mode");
if(pResult->GetString(1)[0] || g_Config.m_ClChatReset)
((CChat *)pUserData)->m_Input.Set(pResult->GetString(1));
}
void CChat::ConShowChat(IConsole::IResult *pResult, void *pUserData)
{
((CChat *)pUserData)->m_Show = pResult->GetInteger(0) != 0;
}
void CChat::ConEcho(IConsole::IResult *pResult, void *pUserData)
{
((CChat *)pUserData)->Echo(pResult->GetString(0));
}
void CChat::ConchainChatOld(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
((CChat *)pUserData)->RebuildChat();
}
void CChat::Echo(const char *pString)
{
AddLine(-2, 0, pString);
}
void CChat::OnConsoleInit()
{
Console()->Register("say", "r[message]", CFGFLAG_CLIENT, ConSay, this, "Say in chat");
Console()->Register("say_team", "r[message]", CFGFLAG_CLIENT, ConSayTeam, this, "Say in team chat");
Console()->Register("chat", "s['team'|'all'] ?r[message]", CFGFLAG_CLIENT, ConChat, this, "Enable chat with all/team mode");
Console()->Register("+show_chat", "", CFGFLAG_CLIENT, ConShowChat, this, "Show chat");
Console()->Register("echo", "r[message]", CFGFLAG_CLIENT, ConEcho, this, "Echo the text in chat window");
Console()->Chain("cl_chat_old", ConchainChatOld, this);
}
bool CChat::OnInput(IInput::CEvent Event)
{
if(m_Mode == MODE_NONE)
return false;
if(Input()->ModifierIsPressed() && Input()->KeyPress(KEY_V))
{
const char *Text = Input()->GetClipboardText();
if(Text)
{
// if the text has more than one line, we send all lines except the last one
// the last one is set as in the text field
char aLine[256];
int i, Begin = 0;
for(i = 0; i < str_length(Text); i++)
{
if(Text[i] == '\n')
{
int max = minimum(i - Begin + 1, (int)sizeof(aLine));
str_copy(aLine, Text + Begin, max);
Begin = i + 1;
SayChat(aLine);
while(Text[i] == '\n')
i++;
}
}
int max = minimum(i - Begin + 1, (int)sizeof(aLine));
str_copy(aLine, Text + Begin, max);
m_Input.Append(aLine);
}
}
if(Input()->ModifierIsPressed() && Input()->KeyPress(KEY_C))
{
Input()->SetClipboardText(m_Input.GetString());
}
if(Input()->ModifierIsPressed()) // jump to spaces and special ASCII characters
{
int SearchDirection = 0;
if(Input()->KeyPress(KEY_LEFT) || Input()->KeyPress(KEY_BACKSPACE))
SearchDirection = -1;
else if(Input()->KeyPress(KEY_RIGHT) || Input()->KeyPress(KEY_DELETE))
SearchDirection = 1;
if(SearchDirection != 0)
{
int OldOffset = m_Input.GetCursorOffset();
int FoundAt = SearchDirection > 0 ? m_Input.GetLength() - 1 : 0;
for(int i = m_Input.GetCursorOffset() + SearchDirection; SearchDirection > 0 ? i < m_Input.GetLength() - 1 : i > 0; i += SearchDirection)
{
int Next = i + SearchDirection;
if((m_Input.GetString()[Next] == ' ') ||
(m_Input.GetString()[Next] >= 32 && m_Input.GetString()[Next] <= 47) ||
(m_Input.GetString()[Next] >= 58 && m_Input.GetString()[Next] <= 64) ||
(m_Input.GetString()[Next] >= 91 && m_Input.GetString()[Next] <= 96))
{
FoundAt = i;
if(SearchDirection < 0)
FoundAt++;
break;
}
}
if(Input()->KeyPress(KEY_BACKSPACE))
{
if(m_Input.GetCursorOffset() != 0)
{
char aText[512];
str_copy(aText, m_Input.GetString(), FoundAt + 1);
if(m_Input.GetCursorOffset() != str_length(m_Input.GetString()))
str_append(aText, m_Input.GetString() + m_Input.GetCursorOffset(), str_length(m_Input.GetString()));
m_Input.Set(aText);
}
}
else if(Input()->KeyPress(KEY_DELETE))
{
if(m_Input.GetCursorOffset() != m_Input.GetLength())
{
char aText[512];
aText[0] = '\0';
str_copy(aText, m_Input.GetString(), m_Input.GetCursorOffset() + 1);
if(FoundAt != m_Input.GetLength())
str_append(aText, m_Input.GetString() + FoundAt, sizeof(aText));
m_Input.Set(aText);
FoundAt = OldOffset;
}
}
m_Input.SetCursorOffset(FoundAt);
}
}
if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE)
{
DisableMode();
m_pClient->OnRelease();
if(g_Config.m_ClChatReset)
m_Input.Clear();
}
else if(Event.m_Flags & IInput::FLAG_PRESS && (Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER))
{
if(m_Input.GetString()[0])
{
bool AddEntry = false;
if(m_LastChatSend + time_freq() < time())
{
Say(m_Mode == MODE_ALL ? 0 : 1, m_Input.GetString());
AddEntry = true;
}
else if(m_PendingChatCounter < 3)
{
++m_PendingChatCounter;
AddEntry = true;
}
if(AddEntry)
{
CHistoryEntry *pEntry = m_History.Allocate(sizeof(CHistoryEntry) + m_Input.GetLength());
pEntry->m_Team = m_Mode == MODE_ALL ? 0 : 1;
mem_copy(pEntry->m_aText, m_Input.GetString(), m_Input.GetLength() + 1);
}
}
m_pHistoryEntry = 0x0;
DisableMode();
m_pClient->OnRelease();
m_Input.Clear();
}
if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_TAB)
{
// fill the completion buffer
if(!m_CompletionUsed)
{
const char *pCursor = m_Input.GetString() + m_Input.GetCursorOffset();
for(int Count = 0; Count < m_Input.GetCursorOffset() && *(pCursor - 1) != ' '; --pCursor, ++Count)
;
m_PlaceholderOffset = pCursor - m_Input.GetString();
for(m_PlaceholderLength = 0; *pCursor && *pCursor != ' '; ++pCursor)
++m_PlaceholderLength;
str_truncate(m_aCompletionBuffer, sizeof(m_aCompletionBuffer), m_Input.GetString() + m_PlaceholderOffset, m_PlaceholderLength);
}
if(m_aCompletionBuffer[0] == '/')
{
CCommand *pCompletionCommand = 0;
const size_t NumCommands = m_Commands.size();
if(m_ReverseTAB && m_CompletionUsed)
m_CompletionChosen--;
else if(!m_ReverseTAB)
m_CompletionChosen++;
m_CompletionChosen = (m_CompletionChosen + 2 * NumCommands) % (2 * NumCommands);
m_CompletionUsed = true;
const char *pCommandStart = m_aCompletionBuffer + 1;
for(size_t i = 0; i < 2 * NumCommands; ++i)
{
int SearchType;
int Index;
if(m_ReverseTAB)
{
SearchType = ((m_CompletionChosen - i + 2 * NumCommands) % (2 * NumCommands)) / NumCommands;
Index = (m_CompletionChosen - i + NumCommands) % NumCommands;
}
else
{
SearchType = ((m_CompletionChosen + i) % (2 * NumCommands)) / NumCommands;
Index = (m_CompletionChosen + i) % NumCommands;
}
auto &Command = m_Commands[Index];
if(str_comp_nocase_num(Command.pName, pCommandStart, str_length(pCommandStart)) == 0)
{
pCompletionCommand = &Command;
m_CompletionChosen = Index + SearchType * NumCommands;
break;
}
}
// insert the command
if(pCompletionCommand)
{
char aBuf[256];
// add part before the name
str_truncate(aBuf, sizeof(aBuf), m_Input.GetString(), m_PlaceholderOffset);
// add the command
str_append(aBuf, "/", sizeof(aBuf));
str_append(aBuf, pCompletionCommand->pName, sizeof(aBuf));
// add separator
const char *pSeparator = pCompletionCommand->pParams[0] == '\0' ? "" : " ";
str_append(aBuf, pSeparator, sizeof(aBuf));
if(*pSeparator)
str_append(aBuf, pSeparator, sizeof(aBuf));
// add part after the name
str_append(aBuf, m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength, sizeof(aBuf));
m_PlaceholderLength = str_length(pSeparator) + str_length(pCompletionCommand->pName) + 1;
m_OldChatStringLength = m_Input.GetLength();
m_Input.Set(aBuf); // TODO: Use Add instead
m_Input.SetCursorOffset(m_PlaceholderOffset + m_PlaceholderLength);
m_InputUpdate = true;
}
}
else
{
// find next possible name
const char *pCompletionString = 0;
if(m_ReverseTAB && m_CompletionUsed)
m_CompletionChosen--;
else if(!m_ReverseTAB)
m_CompletionChosen++;
m_CompletionChosen = (m_CompletionChosen + 2 * MAX_CLIENTS) % (2 * MAX_CLIENTS);
m_CompletionUsed = true;
for(int i = 0; i < 2 * MAX_CLIENTS; ++i)
{
int SearchType;
int Index;
if(m_ReverseTAB)
{
SearchType = ((m_CompletionChosen - i + 2 * MAX_CLIENTS) % (2 * MAX_CLIENTS)) / MAX_CLIENTS;
Index = (m_CompletionChosen - i + MAX_CLIENTS) % MAX_CLIENTS;
}
else
{
SearchType = ((m_CompletionChosen + i) % (2 * MAX_CLIENTS)) / MAX_CLIENTS;
Index = (m_CompletionChosen + i) % MAX_CLIENTS;
}
if(!m_pClient->m_Snap.m_paInfoByName[Index])
continue;
int Index2 = m_pClient->m_Snap.m_paInfoByName[Index]->m_ClientID;
bool Found = false;
if(SearchType == 1)
{
if(str_utf8_comp_nocase_num(m_pClient->m_aClients[Index2].m_aName, m_aCompletionBuffer, str_length(m_aCompletionBuffer)) &&
str_utf8_find_nocase(m_pClient->m_aClients[Index2].m_aName, m_aCompletionBuffer))
Found = true;
}
else if(!str_utf8_comp_nocase_num(m_pClient->m_aClients[Index2].m_aName, m_aCompletionBuffer, str_length(m_aCompletionBuffer)))
Found = true;
if(Found)
{
pCompletionString = m_pClient->m_aClients[Index2].m_aName;
m_CompletionChosen = Index + SearchType * MAX_CLIENTS;
break;
}
}
// insert the name
if(pCompletionString)
{
char aBuf[256];
// add part before the name
str_truncate(aBuf, sizeof(aBuf), m_Input.GetString(), m_PlaceholderOffset);
// add the name
str_append(aBuf, pCompletionString, sizeof(aBuf));
// add separator
const char *pSeparator = "";
if(*(m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength) != ' ')
pSeparator = m_PlaceholderOffset == 0 ? ": " : " ";
else if(m_PlaceholderOffset == 0)
pSeparator = ":";
if(*pSeparator)
str_append(aBuf, pSeparator, sizeof(aBuf));
// add part after the name
str_append(aBuf, m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength, sizeof(aBuf));
m_PlaceholderLength = str_length(pSeparator) + str_length(pCompletionString);
m_OldChatStringLength = m_Input.GetLength();
m_Input.Set(aBuf); // TODO: Use Add instead
m_Input.SetCursorOffset(m_PlaceholderOffset + m_PlaceholderLength);
m_InputUpdate = true;
}
}
}
else
{
// reset name completion process
if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key != KEY_TAB)
{
if(Event.m_Key != KEY_LSHIFT)
{
m_CompletionChosen = -1;
m_CompletionUsed = false;
}
}
m_OldChatStringLength = m_Input.GetLength();
m_Input.ProcessInput(Event);
m_InputUpdate = true;
}
if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_LSHIFT)
{
m_ReverseTAB = true;
}
else if(Event.m_Flags & IInput::FLAG_RELEASE && Event.m_Key == KEY_LSHIFT)
{
m_ReverseTAB = false;
}
if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_UP)
{
if(m_pHistoryEntry)
{
CHistoryEntry *pTest = m_History.Prev(m_pHistoryEntry);
if(pTest)
m_pHistoryEntry = pTest;
}
else
m_pHistoryEntry = m_History.Last();
if(m_pHistoryEntry)
m_Input.Set(m_pHistoryEntry->m_aText);
}
else if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_DOWN)
{
if(m_pHistoryEntry)
m_pHistoryEntry = m_History.Next(m_pHistoryEntry);
if(m_pHistoryEntry)
m_Input.Set(m_pHistoryEntry->m_aText);
else
m_Input.Clear();
}
return true;
}
void CChat::EnableMode(int Team)
{
if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
return;
if(m_Mode == MODE_NONE)
{
if(Team)
m_Mode = MODE_TEAM;
else
m_Mode = MODE_ALL;
Input()->SetIMEState(true);
Input()->Clear();
m_CompletionChosen = -1;
m_CompletionUsed = false;
}
}
void CChat::DisableMode()
{
if(m_Mode != MODE_NONE)
{
Input()->SetIMEState(false);
m_Mode = MODE_NONE;
}
}
void CChat::OnMessage(int MsgType, void *pRawMsg)
{
if(MsgType == NETMSGTYPE_SV_CHAT)
{
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
AddLine(pMsg->m_ClientID, pMsg->m_Team, pMsg->m_pMessage);
}
}
bool CChat::LineShouldHighlight(const char *pLine, const char *pName)
{
const char *pHL = str_utf8_find_nocase(pLine, pName);
if(pHL)
{
int Length = str_length(pName);
if(Length > 0 && (pLine == pHL || pHL[-1] == ' ') && (pHL[Length] == 0 || pHL[Length] == ' ' || pHL[Length] == '.' || pHL[Length] == '!' || pHL[Length] == ',' || pHL[Length] == '?' || pHL[Length] == ':'))
return true;
}
return false;
}
#define SAVES_FILE "ddnet-saves.txt"
const char *SAVES_HEADER[] = {
"Time",
"Player",
"Map",
"Code",
};
void CChat::StoreSave(const char *pText)
{
const char *pStart = str_find(pText, "Team successfully saved by ");
const char *pMid = str_find(pText, ". Use '/load ");
const char *pOn = str_find(pText, "' on ");
const char *pEnd = str_find(pText, pOn ? " to continue" : "' to continue");
if(!pStart || !pMid || !pEnd || pMid < pStart || pEnd < pMid || (pOn && (pOn < pMid || pEnd < pOn)))
return;
char aName[16];
str_truncate(aName, sizeof(aName), pStart + 27, pMid - pStart - 27);
char aSaveCode[64];
str_truncate(aSaveCode, sizeof(aSaveCode), pMid + 13, (pOn ? pOn : pEnd) - pMid - 13);
char aTimestamp[20];
str_timestamp_format(aTimestamp, sizeof(aTimestamp), FORMAT_SPACE);
// TODO: Find a simple way to get the names of team members. This doesn't
// work since team is killed first, then save message gets sent:
/*
for(int i = 0; i < MAX_CLIENTS; i++)
{
const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_paInfoByDDTeam[i];
if(!pInfo)
continue;
pInfo->m_Team // All 0
}
*/
IOHANDLE File = Storage()->OpenFile(SAVES_FILE, IOFLAG_APPEND, IStorage::TYPE_SAVE);
if(!File)
return;
const char *apColumns[4] = {
aTimestamp,
aName,
Client()->GetCurrentMap(),
aSaveCode,
};
if(io_tell(File) == 0)
{
CsvWrite(File, 4, SAVES_HEADER);
}
CsvWrite(File, 4, apColumns);
io_close(File);
}
void CChat::AddLine(int ClientID, int Team, const char *pLine)
{
if(*pLine == 0 ||
(ClientID == -1 && !g_Config.m_ClShowChatSystem) ||
(ClientID >= 0 && (m_pClient->m_aClients[ClientID].m_aName[0] == '\0' || // unknown client
m_pClient->m_aClients[ClientID].m_ChatIgnore ||
(m_pClient->m_Snap.m_LocalClientID != ClientID && g_Config.m_ClShowChatFriends && !m_pClient->m_aClients[ClientID].m_Friend) ||
(m_pClient->m_Snap.m_LocalClientID != ClientID && m_pClient->m_aClients[ClientID].m_Foe))))
return;
// trim right and set maximum length to 256 utf8-characters
int Length = 0;
const char *pStr = pLine;
const char *pEnd = 0;
while(*pStr)
{
const char *pStrOld = pStr;
int Code = str_utf8_decode(&pStr);
// check if unicode is not empty
if(!str_utf8_isspace(Code))
{
pEnd = 0;
}
else if(pEnd == 0)
pEnd = pStrOld;
if(++Length >= 256)
{
*(const_cast<char *>(pStr)) = 0;
break;
}
}
if(pEnd != 0)
*(const_cast<char *>(pEnd)) = 0;
bool Highlighted = false;
char *p = const_cast<char *>(pLine);
bool IsTeamLine = Team == 1;
bool IsWhisperLine = Team >= 2;
// Only empty string left
if(*p == 0)
return;
auto &&FChatMsgCheckAndPrint = [=](CLine *pLine) {
if(pLine->m_ClientID < 0) // server or client message
{
if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
StoreSave(pLine->m_aText);
}
char aBuf[1024];
str_format(aBuf, sizeof(aBuf), "%s%s%s", pLine->m_aName, pLine->m_ClientID >= 0 ? ": " : "", pLine->m_aText);
ColorRGBA ChatLogColor{1, 1, 1, 1};
if(pLine->m_Highlighted)
{
ChatLogColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageHighlightColor));
}
else
{
if(pLine->m_Friend && g_Config.m_ClMessageFriend)
ChatLogColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageFriendColor));
else if(pLine->m_Team)
ChatLogColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageTeamColor));
else if(pLine->m_ClientID == -1) // system
ChatLogColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageSystemColor));
else if(pLine->m_ClientID == -2) // client
ChatLogColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageClientColor));
else // regular message
ChatLogColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageColor));
}
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, pLine->m_Whisper ? "whisper" : (pLine->m_Team ? "teamchat" : "chat"), aBuf, ChatLogColor);
};
while(*p)
{
Highlighted = false;
pLine = p;
// find line separator and strip multiline
while(*p)
{
if(*p++ == '\n')
{
*(p - 1) = 0;
break;
}
}
CLine *pCurrentLine = &m_aLines[m_CurrentLine];
// If it's a client message, m_aText will have ": " prepended so we have to work around it.
if(pCurrentLine->m_Team == IsTeamLine && pCurrentLine->m_Whisper == IsWhisperLine && pCurrentLine->m_ClientID == ClientID && str_comp(pCurrentLine->m_aText, pLine) == 0)
{
pCurrentLine->m_TimesRepeated++;
if(pCurrentLine->m_TextContainerIndex != -1)
TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex);
pCurrentLine->m_TextContainerIndex = -1;
if(pCurrentLine->m_QuadContainerIndex != -1)
Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex);
pCurrentLine->m_QuadContainerIndex = -1;
pCurrentLine->m_Time = time();
pCurrentLine->m_YOffset[0] = -1.f;
pCurrentLine->m_YOffset[1] = -1.f;
FChatMsgCheckAndPrint(pCurrentLine);
return;
}
m_CurrentLine = (m_CurrentLine + 1) % MAX_LINES;
pCurrentLine = &m_aLines[m_CurrentLine];
pCurrentLine->m_TimesRepeated = 0;
pCurrentLine->m_Time = time();
pCurrentLine->m_YOffset[0] = -1.0f;
pCurrentLine->m_YOffset[1] = -1.0f;
pCurrentLine->m_ClientID = ClientID;
pCurrentLine->m_Team = IsTeamLine;
pCurrentLine->m_Whisper = IsWhisperLine;
pCurrentLine->m_NameColor = -2;
if(pCurrentLine->m_TextContainerIndex != -1)
TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex);
pCurrentLine->m_TextContainerIndex = -1;
if(pCurrentLine->m_QuadContainerIndex != -1)
Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex);
pCurrentLine->m_QuadContainerIndex = -1;
// check for highlighted name
if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
if(ClientID >= 0 && ClientID != m_pClient->m_LocalIDs[0])
{
// main character
if(LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_LocalIDs[0]].m_aName))
Highlighted = true;
// dummy
if(m_pClient->Client()->DummyConnected() && LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_LocalIDs[1]].m_aName))
Highlighted = true;
}
}
else
{
// on demo playback use local id from snap directly,
// since m_LocalIDs isn't valid there
if(LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName))
Highlighted = true;
}
pCurrentLine->m_Highlighted = Highlighted;
if(pCurrentLine->m_ClientID < 0) // server or client message
{
str_copy(pCurrentLine->m_aName, "*** ", sizeof(pCurrentLine->m_aName));
str_format(pCurrentLine->m_aText, sizeof(pCurrentLine->m_aText), "%s", pLine);
}
else
{
if(m_pClient->m_aClients[ClientID].m_Team == TEAM_SPECTATORS)
pCurrentLine->m_NameColor = TEAM_SPECTATORS;
if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)
{
if(m_pClient->m_aClients[ClientID].m_Team == TEAM_RED)
pCurrentLine->m_NameColor = TEAM_RED;
else if(m_pClient->m_aClients[ClientID].m_Team == TEAM_BLUE)
pCurrentLine->m_NameColor = TEAM_BLUE;
}
if(Team == 2) // whisper send
{
str_format(pCurrentLine->m_aName, sizeof(pCurrentLine->m_aName), "→ %s", m_pClient->m_aClients[ClientID].m_aName);
pCurrentLine->m_NameColor = TEAM_BLUE;
pCurrentLine->m_Highlighted = false;
Highlighted = false;
}
else if(Team == 3) // whisper recv
{
str_format(pCurrentLine->m_aName, sizeof(pCurrentLine->m_aName), "← %s", m_pClient->m_aClients[ClientID].m_aName);
pCurrentLine->m_NameColor = TEAM_RED;
pCurrentLine->m_Highlighted = true;
Highlighted = true;
}
else
str_format(pCurrentLine->m_aName, sizeof(pCurrentLine->m_aName), "%s", m_pClient->m_aClients[ClientID].m_aName);
str_format(pCurrentLine->m_aText, sizeof(pCurrentLine->m_aText), "%s", pLine);
pCurrentLine->m_Friend = m_pClient->m_aClients[ClientID].m_Friend;
}
pCurrentLine->m_HasRenderTee = false;
pCurrentLine->m_Friend = ClientID >= 0 ? m_pClient->m_aClients[ClientID].m_Friend : false;
if(pCurrentLine->m_ClientID >= 0 && pCurrentLine->m_aName[0] != '\0')
{
if(!g_Config.m_ClChatOld)
{
pCurrentLine->m_CustomColoredSkin = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_CustomColoredSkin;
if(pCurrentLine->m_CustomColoredSkin)
pCurrentLine->m_RenderSkin = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_ColorableRenderSkin;
else
pCurrentLine->m_RenderSkin = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_OriginalRenderSkin;
str_copy(pCurrentLine->m_aSkinName, m_pClient->m_aClients[pCurrentLine->m_ClientID].m_aSkinName, sizeof(pCurrentLine->m_aSkinName));
pCurrentLine->m_ColorBody = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_ColorBody;
pCurrentLine->m_ColorFeet = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_ColorFeet;
pCurrentLine->m_RenderSkinMetrics = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_SkinMetrics;
pCurrentLine->m_HasRenderTee = true;
}
}
FChatMsgCheckAndPrint(pCurrentLine);
}
// play sound
int64_t Now = time();
if(ClientID == -1)
{
if(Now - m_aLastSoundPlayed[CHAT_SERVER] >= time_freq() * 3 / 10)
{
if(g_Config.m_SndServerMessage)
{
m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_SERVER, 0);
m_aLastSoundPlayed[CHAT_SERVER] = Now;
}
}
}
else if(ClientID == -2) // Client message
{
// No sound yet
}
else if(Highlighted && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
if(Now - m_aLastSoundPlayed[CHAT_HIGHLIGHT] >= time_freq() * 3 / 10)
{
char aBuf[1024];
str_format(aBuf, sizeof(aBuf), "%s: %s", m_aLines[m_CurrentLine].m_aName, m_aLines[m_CurrentLine].m_aText);
Client()->Notify("DDNet Chat", aBuf);
if(g_Config.m_SndHighlight)
{
m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_HIGHLIGHT, 0);
m_aLastSoundPlayed[CHAT_HIGHLIGHT] = Now;
}
if(g_Config.m_ClEditor)
{
GameClient()->Editor()->UpdateMentions();
}
}
}
else if(Team != 2)
{
if(Now - m_aLastSoundPlayed[CHAT_CLIENT] >= time_freq() * 3 / 10)
{
bool PlaySound = m_aLines[m_CurrentLine].m_Team ? g_Config.m_SndTeamChat : g_Config.m_SndChat;
#if defined(CONF_VIDEORECORDER)
if(IVideo::Current())
{
PlaySound &= g_Config.m_ClVideoShowChat;
}
#endif
if(PlaySound)
{
m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_CLIENT, 0);
m_aLastSoundPlayed[CHAT_CLIENT] = Now;
}
}
}
}
void CChat::RefindSkins()
{
for(auto &Line : m_aLines)
{
if(Line.m_HasRenderTee)
{
const CSkin *pSkin = m_pClient->m_Skins.Get(m_pClient->m_Skins.Find(Line.m_aSkinName));
if(Line.m_CustomColoredSkin)
Line.m_RenderSkin = pSkin->m_ColorableSkin;
else
Line.m_RenderSkin = pSkin->m_OriginalSkin;
Line.m_RenderSkinMetrics = pSkin->m_Metrics;
}
}
}
void CChat::OnPrepareLines()
{
float x = 5.0f;
float y = 300.0f - 28.0f;
float FontSize = FONT_SIZE;
float ScreenRatio = Graphics()->ScreenAspect();
bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (ScreenRatio > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9)
bool ForceRecreate = IsScoreBoardOpen != m_PrevScoreBoardShowed;
bool ShowLargeArea = m_Show || g_Config.m_ClShowChat == 2;
ForceRecreate |= ShowLargeArea != m_PrevShowChat;
m_PrevScoreBoardShowed = IsScoreBoardOpen;
m_PrevShowChat = ShowLargeArea;
float RealMsgPaddingX = MESSAGE_PADDING_X;
float RealMsgPaddingY = MESSAGE_PADDING_Y;
float RealMsgPaddingTee = MESSAGE_TEE_SIZE + MESSAGE_TEE_PADDING_RIGHT;
if(g_Config.m_ClChatOld)
{
RealMsgPaddingX = 0;
RealMsgPaddingY = 0;
RealMsgPaddingTee = 0;
}
int64_t Now = time();
float LineWidth = (IsScoreBoardOpen ? 85.0f : 200.0f) - (RealMsgPaddingX * 1.5f) - RealMsgPaddingTee;
float HeightLimit = IsScoreBoardOpen ? 180.0f : m_PrevShowChat ? 50.0f : 200.0f;
float Begin = x;
float TextBegin = Begin + RealMsgPaddingX / 2.0f;
CTextCursor Cursor;
int OffsetType = IsScoreBoardOpen ? 1 : 0;
for(int i = 0; i < MAX_LINES; i++)
{
int r = ((m_CurrentLine - i) + MAX_LINES) % MAX_LINES;
if(Now > m_aLines[r].m_Time + 16 * time_freq() && !m_PrevShowChat)
break;
if(m_aLines[r].m_TextContainerIndex != -1 && !ForceRecreate)
continue;
if(m_aLines[r].m_TextContainerIndex != -1)
TextRender()->DeleteTextContainer(m_aLines[r].m_TextContainerIndex);
m_aLines[r].m_TextContainerIndex = -1;
if(m_aLines[r].m_QuadContainerIndex != -1)
Graphics()->DeleteQuadContainer(m_aLines[r].m_QuadContainerIndex);
m_aLines[r].m_QuadContainerIndex = -1;
char aName[64 + 12] = "";
if(g_Config.m_ClShowIDs && m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0')
{
if(m_aLines[r].m_ClientID < 10)
str_format(aName, sizeof(aName), "%d: ", m_aLines[r].m_ClientID);
else
str_format(aName, sizeof(aName), "%d: ", m_aLines[r].m_ClientID);
}
str_append(aName, m_aLines[r].m_aName, sizeof(aName));
char aCount[12];
if(m_aLines[r].m_ClientID < 0)
str_format(aCount, sizeof(aCount), "[%d] ", m_aLines[r].m_TimesRepeated + 1);
else
str_format(aCount, sizeof(aCount), " [%d]", m_aLines[r].m_TimesRepeated + 1);
if(g_Config.m_ClChatOld)
{
m_aLines[r].m_HasRenderTee = false;
}
// get the y offset (calculate it if we haven't done that yet)
if(m_aLines[r].m_YOffset[OffsetType] < 0.0f)
{
TextRender()->SetCursor(&Cursor, TextBegin, 0.0f, FontSize, 0);
Cursor.m_LineWidth = LineWidth;
if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0')
{
Cursor.m_X += RealMsgPaddingTee;
if(m_aLines[r].m_Friend && g_Config.m_ClMessageFriend)
{
TextRender()->TextEx(&Cursor, "", -1);
}
}
TextRender()->TextEx(&Cursor, aName, -1);
if(m_aLines[r].m_TimesRepeated > 0)
TextRender()->TextEx(&Cursor, aCount, -1);
if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0')
{
TextRender()->TextEx(&Cursor, ": ", -1);
}
CTextCursor AppendCursor = Cursor;
if(!IsScoreBoardOpen && !g_Config.m_ClChatOld)
{
AppendCursor.m_StartX = Cursor.m_X;
AppendCursor.m_LineWidth -= (Cursor.m_LongestLineWidth - Cursor.m_StartX);
}
TextRender()->TextEx(&AppendCursor, m_aLines[r].m_aText, -1);
m_aLines[r].m_YOffset[OffsetType] = AppendCursor.m_Y + AppendCursor.m_FontSize + RealMsgPaddingY;
}
y -= m_aLines[r].m_YOffset[OffsetType];
// cut off if msgs waste too much space
if(y < HeightLimit)
break;
// the position the text was created
m_aLines[r].m_TextYOffset = y + RealMsgPaddingY / 2.f;
int CurRenderFlags = TextRender()->GetRenderFlags();
TextRender()->SetRenderFlags(CurRenderFlags | ETextRenderFlags::TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD);
// reset the cursor
TextRender()->SetCursor(&Cursor, TextBegin, m_aLines[r].m_TextYOffset, FontSize, TEXTFLAG_RENDER);
Cursor.m_LineWidth = LineWidth;
// Message is from valid player
if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0')
{
Cursor.m_X += RealMsgPaddingTee;
if(m_aLines[r].m_Friend && g_Config.m_ClMessageFriend)
{
const char *pHeartStr = "";
ColorRGBA rgb = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageFriendColor));
TextRender()->TextColor(rgb.WithAlpha(1.f));
if(m_aLines[r].m_TextContainerIndex == -1)
m_aLines[r].m_TextContainerIndex = TextRender()->CreateTextContainer(&Cursor, pHeartStr);
else
TextRender()->AppendTextContainer(&Cursor, m_aLines[r].m_TextContainerIndex, pHeartStr);
}
}
// render name
ColorRGBA NameColor;
if(m_aLines[r].m_ClientID == -1) // system
{
NameColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageSystemColor));
}
else if(m_aLines[r].m_ClientID == -2) // client
{
NameColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageClientColor));
}
else if(m_aLines[r].m_Team)
{
NameColor = CalculateNameColor(ColorHSLA(g_Config.m_ClMessageTeamColor));
}
else if(m_aLines[r].m_NameColor == TEAM_RED)
NameColor = ColorRGBA(1.0f, 0.5f, 0.5f, 1.f); // red
else if(m_aLines[r].m_NameColor == TEAM_BLUE)
NameColor = ColorRGBA(0.7f, 0.7f, 1.0f, 1.f); // blue
else if(m_aLines[r].m_NameColor == TEAM_SPECTATORS)
NameColor = ColorRGBA(0.75f, 0.5f, 0.75f, 1.f); // spectator
else if(m_aLines[r].m_ClientID >= 0 && g_Config.m_ClChatTeamColors && m_pClient->m_Teams.Team(m_aLines[r].m_ClientID))
{
NameColor = color_cast<ColorRGBA>(ColorHSLA(m_pClient->m_Teams.Team(m_aLines[r].m_ClientID) / 64.0f, 1.0f, 0.75f));
}
else
NameColor = ColorRGBA(0.8f, 0.8f, 0.8f, 1.f);
TextRender()->TextColor(NameColor);
if(m_aLines[r].m_TextContainerIndex == -1)
m_aLines[r].m_TextContainerIndex = TextRender()->CreateTextContainer(&Cursor, aName);
else
TextRender()->AppendTextContainer(&Cursor, m_aLines[r].m_TextContainerIndex, aName);
if(m_aLines[r].m_TimesRepeated > 0)
{
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.3f);
if(m_aLines[r].m_TextContainerIndex == -1)
m_aLines[r].m_TextContainerIndex = TextRender()->CreateTextContainer(&Cursor, aCount);
else
TextRender()->AppendTextContainer(&Cursor, m_aLines[r].m_TextContainerIndex, aCount);
}
if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0')
{
TextRender()->TextColor(NameColor);
if(m_aLines[r].m_TextContainerIndex == -1)
m_aLines[r].m_TextContainerIndex = TextRender()->CreateTextContainer(&Cursor, ": ");
else
TextRender()->AppendTextContainer(&Cursor, m_aLines[r].m_TextContainerIndex, ": ");
}
// render line
ColorRGBA Color;
if(m_aLines[r].m_ClientID == -1) // system
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageSystemColor));
else if(m_aLines[r].m_ClientID == -2) // client
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageClientColor));
else if(m_aLines[r].m_Highlighted) // highlighted
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageHighlightColor));
else if(m_aLines[r].m_Team) // team message
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageTeamColor));
else // regular message
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageColor));
TextRender()->TextColor(Color);
CTextCursor AppendCursor = Cursor;
if(!IsScoreBoardOpen && !g_Config.m_ClChatOld)
{
AppendCursor.m_LineWidth -= (Cursor.m_LongestLineWidth - Cursor.m_StartX);
AppendCursor.m_StartX = Cursor.m_X;
}
if(m_aLines[r].m_TextContainerIndex == -1)
m_aLines[r].m_TextContainerIndex = TextRender()->CreateTextContainer(&AppendCursor, m_aLines[r].m_aText);
else
TextRender()->AppendTextContainer(&AppendCursor, m_aLines[r].m_TextContainerIndex, m_aLines[r].m_aText);
if(!g_Config.m_ClChatOld && (m_aLines[r].m_aText[0] != '\0' || m_aLines[r].m_aName[0] != '\0'))
{
float Height = m_aLines[r].m_YOffset[OffsetType];
Graphics()->SetColor(1, 1, 1, 1);
m_aLines[r].m_QuadContainerIndex = RenderTools()->CreateRoundRectQuadContainer(Begin, y, (AppendCursor.m_LongestLineWidth - TextBegin) + RealMsgPaddingX * 1.5f, Height, MESSAGE_ROUNDING, CUI::CORNER_ALL);
}
TextRender()->SetRenderFlags(CurRenderFlags);
if(m_aLines[r].m_TextContainerIndex != -1)
TextRender()->UploadTextContainer(m_aLines[r].m_TextContainerIndex);
}
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
}
void CChat::OnRender()
{
// send pending chat messages
if(m_PendingChatCounter > 0 && m_LastChatSend + time_freq() < time())
{
CHistoryEntry *pEntry = m_History.Last();
for(int i = m_PendingChatCounter - 1; pEntry; --i, pEntry = m_History.Prev(pEntry))
{
if(i == 0)
{
Say(pEntry->m_Team, pEntry->m_aText);
break;
}
}
--m_PendingChatCounter;
}
float Width = 300.0f * Graphics()->ScreenAspect();
Graphics()->MapScreen(0.0f, 0.0f, Width, 300.0f);
float x = 5.0f;
float y = 300.0f - 20.0f;
if(m_Mode != MODE_NONE)
{
// render chat input
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, x, y, 8.0f, TEXTFLAG_RENDER);
Cursor.m_LineWidth = Width - 190.0f;
Cursor.m_MaxLines = 2;
if(m_Mode == MODE_ALL)
TextRender()->TextEx(&Cursor, Localize("All"), -1);
else if(m_Mode == MODE_TEAM)
TextRender()->TextEx(&Cursor, Localize("Team"), -1);
else
TextRender()->TextEx(&Cursor, Localize("Chat"), -1);
TextRender()->TextEx(&Cursor, ": ", -1);
// IME candidate editing
bool Editing = false;
int EditingCursor = Input()->GetEditingCursor();
if(Input()->GetIMEState())
{
if(str_length(Input()->GetIMEEditingText()))
{
m_Input.Editing(Input()->GetIMEEditingText(), EditingCursor);
Editing = true;
}
}
// check if the visible text has to be moved
if(m_InputUpdate)
{
if(m_ChatStringOffset > 0 && m_Input.GetLength(Editing) < m_OldChatStringLength)
m_ChatStringOffset = maximum(0, m_ChatStringOffset - (m_OldChatStringLength - m_Input.GetLength(Editing)));
if(m_ChatStringOffset > m_Input.GetCursorOffset(Editing))
m_ChatStringOffset -= m_ChatStringOffset - m_Input.GetCursorOffset(Editing);
else
{
CTextCursor Temp = Cursor;
Temp.m_Flags = 0;
TextRender()->TextEx(&Temp, m_Input.GetString(Editing) + m_ChatStringOffset, m_Input.GetCursorOffset(Editing) - m_ChatStringOffset);
TextRender()->TextEx(&Temp, "|", -1);
while(Temp.m_LineCount > 2)
{
++m_ChatStringOffset;
Temp = Cursor;
Temp.m_Flags = 0;
TextRender()->TextEx(&Temp, m_Input.GetString(Editing) + m_ChatStringOffset, m_Input.GetCursorOffset(Editing) - m_ChatStringOffset);
TextRender()->TextEx(&Temp, "|", -1);
}
}
m_InputUpdate = false;
}
TextRender()->TextEx(&Cursor, m_Input.GetString(Editing) + m_ChatStringOffset, m_Input.GetCursorOffset(Editing) - m_ChatStringOffset);
static float MarkerOffset = TextRender()->TextWidth(0, 8.0f, "|", -1, -1.0f) / 3;
CTextCursor Marker = Cursor;
Marker.m_X -= MarkerOffset;
TextRender()->TextEx(&Marker, "|", -1);
TextRender()->TextEx(&Cursor, m_Input.GetString(Editing) + m_Input.GetCursorOffset(Editing), -1);
if(m_pClient->m_GameConsole.IsClosed())
Input()->SetEditingPosition(Marker.m_X, Marker.m_Y + Marker.m_FontSize);
}
#if defined(CONF_VIDEORECORDER)
if(!((g_Config.m_ClShowChat && !IVideo::Current()) || (g_Config.m_ClVideoShowChat && IVideo::Current())))
#else
if(!g_Config.m_ClShowChat)
#endif
return;
y -= 8.0f;
OnPrepareLines();
float ScreenRatio = Graphics()->ScreenAspect();
bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (ScreenRatio > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9)
int64_t Now = time();
float HeightLimit = IsScoreBoardOpen ? 180.0f : m_PrevShowChat ? 50.0f : 200.0f;
int OffsetType = IsScoreBoardOpen ? 1 : 0;
float RealMsgPaddingX = MESSAGE_PADDING_X;
float RealMsgPaddingY = MESSAGE_PADDING_Y;
if(g_Config.m_ClChatOld)
{
RealMsgPaddingX = 0;
RealMsgPaddingY = 0;
}
for(int i = 0; i < MAX_LINES; i++)
{
int r = ((m_CurrentLine - i) + MAX_LINES) % MAX_LINES;
if(Now > m_aLines[r].m_Time + 16 * time_freq() && !m_PrevShowChat)
break;
y -= m_aLines[r].m_YOffset[OffsetType];
// cut off if msgs waste too much space
if(y < HeightLimit)
break;
float Blend = Now > m_aLines[r].m_Time + 14 * time_freq() && !m_PrevShowChat ? 1.0f - (Now - m_aLines[r].m_Time - 14 * time_freq()) / (2.0f * time_freq()) : 1.0f;
// Draw backgrounds for messages in one batch
if(!g_Config.m_ClChatOld)
{
Graphics()->TextureClear();
if(m_aLines[r].m_QuadContainerIndex != -1)
{
Graphics()->SetColor(0, 0, 0, 0.12f * Blend);
Graphics()->RenderQuadContainerEx(m_aLines[r].m_QuadContainerIndex, 0, -1, 0, ((y + RealMsgPaddingY / 2.0f) - m_aLines[r].m_TextYOffset));
}
}
if(m_aLines[r].m_TextContainerIndex != -1)
{
if(!g_Config.m_ClChatOld && m_aLines[r].m_HasRenderTee)
{
CTeeRenderInfo RenderInfo;
RenderInfo.m_CustomColoredSkin = m_aLines[r].m_CustomColoredSkin;
if(m_aLines[r].m_CustomColoredSkin)
RenderInfo.m_ColorableRenderSkin = m_aLines[r].m_RenderSkin;
else
RenderInfo.m_OriginalRenderSkin = m_aLines[r].m_RenderSkin;
RenderInfo.m_SkinMetrics = m_aLines[r].m_RenderSkinMetrics;
RenderInfo.m_ColorBody = m_aLines[r].m_ColorBody;
RenderInfo.m_ColorFeet = m_aLines[r].m_ColorFeet;
RenderInfo.m_Size = MESSAGE_TEE_SIZE;
float RowHeight = FONT_SIZE + RealMsgPaddingY;
float OffsetTeeY = MESSAGE_TEE_SIZE / 2.0f;
float FullHeightMinusTee = RowHeight - MESSAGE_TEE_SIZE;
CAnimState *pIdleState = CAnimState::GetIdle();
vec2 OffsetToMid;
RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &RenderInfo, OffsetToMid);
vec2 TeeRenderPos(x + (RealMsgPaddingX + MESSAGE_TEE_SIZE) / 2.0f, y + OffsetTeeY + FullHeightMinusTee / 2.0f + OffsetToMid.y);
RenderTools()->RenderTee(pIdleState, &RenderInfo, EMOTE_NORMAL, vec2(1, 0.1f), TeeRenderPos, Blend);
}
STextRenderColor TextOutline(0.f, 0.f, 0.f, 0.3f * Blend);
STextRenderColor Text(1.f, 1.f, 1.f, Blend);
TextRender()->RenderTextContainer(m_aLines[r].m_TextContainerIndex, &Text, &TextOutline, 0, (y + RealMsgPaddingY / 2.0f) - m_aLines[r].m_TextYOffset);
}
}
}
void CChat::Say(int Team, const char *pLine)
{
m_LastChatSend = time();
// send chat message
CNetMsg_Cl_Say Msg;
Msg.m_Team = Team;
Msg.m_pMessage = pLine;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
}
void CChat::SayChat(const char *pLine)
{
if(!pLine || str_length(pLine) < 1)
return;
bool AddEntry = false;
if(m_LastChatSend + time_freq() < time())
{
Say(m_Mode == MODE_ALL ? 0 : 1, pLine);
AddEntry = true;
}
else if(m_PendingChatCounter < 3)
{
++m_PendingChatCounter;
AddEntry = true;
}
if(AddEntry)
{
CHistoryEntry *pEntry = m_History.Allocate(sizeof(CHistoryEntry) + str_length(pLine) - 1);
pEntry->m_Team = m_Mode == MODE_ALL ? 0 : 1;
mem_copy(pEntry->m_aText, pLine, str_length(pLine));
}
}