mirror of
https://github.com/ddnet/ddnet.git
synced 2024-10-21 16:28:19 +00:00
880ced2de0
Main and dummy tee cannot ping each other anymore. Other main and dummy tees can still ping your own main and dummy tees. We must ensure to only use `m_aLocalIDs[1]` when the dummy is connected, as this ID is not updated when no dummy is connected, so it can refer to another client if you disconnect your dummy and another client subsequently takes its client ID. Closes #3699.
1347 lines
40 KiB
C++
1347 lines
40 KiB
C++
/* (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 <engine/editor.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/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>
|
||
#undef CHAT_COMMAND
|
||
std::sort(m_vCommands.begin(), m_vCommands.end());
|
||
|
||
m_Mode = MODE_NONE;
|
||
}
|
||
|
||
void CChat::RegisterCommand(const char *pName, const char *pParams, int flags, const char *pHelp)
|
||
{
|
||
m_vCommands.emplace_back(pName, pParams);
|
||
}
|
||
|
||
void CChat::RebuildChat()
|
||
{
|
||
for(auto &Line : m_aLines)
|
||
{
|
||
TextRender()->DeleteTextContainer(Line.m_TextContainerIndex);
|
||
Graphics()->DeleteQuadContainer(Line.m_QuadContainerIndex);
|
||
// recalculate sizes
|
||
Line.m_aYOffset[0] = -1.f;
|
||
Line.m_aYOffset[1] = -1.f;
|
||
}
|
||
}
|
||
|
||
void CChat::OnWindowResize()
|
||
{
|
||
RebuildChat();
|
||
}
|
||
|
||
void CChat::Reset()
|
||
{
|
||
for(auto &Line : m_aLines)
|
||
{
|
||
TextRender()->DeleteTextContainer(Line.m_TextContainerIndex);
|
||
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_TimesRepeated = 0;
|
||
Line.m_HasRenderTee = false;
|
||
}
|
||
m_PrevScoreBoardShowed = false;
|
||
m_PrevShowChat = 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(CLIENT_MSG, 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 | CFGFLAG_STORE, ConEcho, this, "Echo the text in chat window");
|
||
}
|
||
|
||
void CChat::OnInit()
|
||
{
|
||
Reset();
|
||
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 *pText = Input()->GetClipboardText();
|
||
if(pText)
|
||
{
|
||
// 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(pText); i++)
|
||
{
|
||
if(pText[i] == '\n')
|
||
{
|
||
int max = minimum(i - Begin + 1, (int)sizeof(aLine));
|
||
str_copy(aLine, pText + Begin, max);
|
||
Begin = i + 1;
|
||
SayChat(aLine);
|
||
while(pText[i] == '\n')
|
||
i++;
|
||
}
|
||
}
|
||
int max = minimum(i - Begin + 1, (int)sizeof(aLine));
|
||
str_copy(aLine, pText + 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)
|
||
{
|
||
const bool ShiftPressed = Input()->ShiftIsPressed();
|
||
|
||
// 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_CompletionUsed && m_aCompletionBuffer[0] != '/')
|
||
{
|
||
// Create the completion list of player names through which the player can iterate
|
||
const char *PlayerName, *FoundInput;
|
||
m_PlayerCompletionListLength = 0;
|
||
for(auto &PlayerInfo : m_pClient->m_Snap.m_apInfoByName)
|
||
{
|
||
if(PlayerInfo)
|
||
{
|
||
PlayerName = m_pClient->m_aClients[PlayerInfo->m_ClientID].m_aName;
|
||
FoundInput = str_utf8_find_nocase(PlayerName, m_aCompletionBuffer);
|
||
if(FoundInput != 0)
|
||
{
|
||
m_aPlayerCompletionList[m_PlayerCompletionListLength].ClientID = PlayerInfo->m_ClientID;
|
||
// The score for suggesting a player name is determined by the distance of the search input to the beginning of the player name
|
||
m_aPlayerCompletionList[m_PlayerCompletionListLength].Score = (int)(FoundInput - PlayerName);
|
||
m_PlayerCompletionListLength++;
|
||
}
|
||
}
|
||
}
|
||
std::stable_sort(m_aPlayerCompletionList, m_aPlayerCompletionList + m_PlayerCompletionListLength,
|
||
[](const CRateablePlayer &p1, const CRateablePlayer &p2) -> bool {
|
||
return p1.Score < p2.Score;
|
||
});
|
||
}
|
||
|
||
if(m_aCompletionBuffer[0] == '/')
|
||
{
|
||
CCommand *pCompletionCommand = 0;
|
||
|
||
const size_t NumCommands = m_vCommands.size();
|
||
|
||
if(ShiftPressed && m_CompletionUsed)
|
||
m_CompletionChosen--;
|
||
else if(!ShiftPressed)
|
||
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(ShiftPressed)
|
||
{
|
||
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_vCommands[Index];
|
||
|
||
if(str_startswith(Command.m_pName, pCommandStart))
|
||
{
|
||
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->m_pName, sizeof(aBuf));
|
||
|
||
// add separator
|
||
const char *pSeparator = pCompletionCommand->m_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->m_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_PlayerCompletionListLength > 0)
|
||
{
|
||
// We do this in a loop, if a player left the game during the repeated pressing of Tab, they are skipped
|
||
CGameClient::CClientData *pCompletionClientData;
|
||
for(int i = 0; i < m_PlayerCompletionListLength; ++i)
|
||
{
|
||
if(ShiftPressed && m_CompletionUsed)
|
||
{
|
||
m_CompletionChosen--;
|
||
}
|
||
else if(!ShiftPressed)
|
||
{
|
||
m_CompletionChosen++;
|
||
}
|
||
if(m_CompletionChosen < 0)
|
||
{
|
||
m_CompletionChosen += m_PlayerCompletionListLength;
|
||
}
|
||
m_CompletionChosen %= m_PlayerCompletionListLength;
|
||
m_CompletionUsed = true;
|
||
|
||
pCompletionClientData = &m_pClient->m_aClients[m_aPlayerCompletionList[m_CompletionChosen].ClientID];
|
||
if(!pCompletionClientData->m_Active)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
pCompletionString = pCompletionClientData->m_aName;
|
||
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 && Event.m_Key != KEY_LSHIFT && Event.m_Key != KEY_RSHIFT)
|
||
{
|
||
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_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(m_pClient->m_SuppressEvents)
|
||
return;
|
||
|
||
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 == SERVER_MSG && !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);
|
||
|
||
// Only empty string left
|
||
if(*p == 0)
|
||
return;
|
||
|
||
auto &&FChatMsgCheckAndPrint = [this](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 == SERVER_MSG)
|
||
ChatLogColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageSystemColor));
|
||
else if(pLine_->m_ClientID == CLIENT_MSG)
|
||
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];
|
||
|
||
// Team Number:
|
||
// 0 = global; 1 = team; 2 = sending whisper; 3 = receiving whisper
|
||
|
||
// If it's a client message, m_aText will have ": " prepended so we have to work around it.
|
||
if(pCurrentLine->m_TeamNumber == Team && pCurrentLine->m_ClientID == ClientID && str_comp(pCurrentLine->m_aText, pLine) == 0)
|
||
{
|
||
pCurrentLine->m_TimesRepeated++;
|
||
TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex);
|
||
Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex);
|
||
pCurrentLine->m_Time = time();
|
||
pCurrentLine->m_aYOffset[0] = -1.f;
|
||
pCurrentLine->m_aYOffset[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_aYOffset[0] = -1.0f;
|
||
pCurrentLine->m_aYOffset[1] = -1.0f;
|
||
pCurrentLine->m_ClientID = ClientID;
|
||
pCurrentLine->m_TeamNumber = Team;
|
||
pCurrentLine->m_Team = Team == 1;
|
||
pCurrentLine->m_Whisper = Team >= 2;
|
||
pCurrentLine->m_NameColor = -2;
|
||
|
||
TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex);
|
||
Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex);
|
||
|
||
// check for highlighted name
|
||
if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
|
||
{
|
||
if(ClientID >= 0 && ClientID != m_pClient->m_aLocalIDs[0] && (!m_pClient->Client()->DummyConnected() || ClientID != m_pClient->m_aLocalIDs[1]))
|
||
{
|
||
// main character
|
||
Highlighted |= LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_aLocalIDs[0]].m_aName);
|
||
// dummy
|
||
Highlighted |= m_pClient->Client()->DummyConnected() && LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_aLocalIDs[1]].m_aName);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// on demo playback use local id from snap directly,
|
||
// since m_aLocalIDs isn't valid there
|
||
Highlighted |= m_pClient->m_Snap.m_LocalClientID >= 0 && LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName);
|
||
}
|
||
|
||
pCurrentLine->m_Highlighted = Highlighted;
|
||
|
||
if(pCurrentLine->m_ClientID == SERVER_MSG)
|
||
{
|
||
str_copy(pCurrentLine->m_aName, "*** ");
|
||
str_copy(pCurrentLine->m_aText, pLine);
|
||
}
|
||
else if(pCurrentLine->m_ClientID == CLIENT_MSG)
|
||
{
|
||
str_copy(pCurrentLine->m_aName, "— ");
|
||
str_copy(pCurrentLine->m_aText, 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_copy(pCurrentLine->m_aName, m_pClient->m_aClients[ClientID].m_aName);
|
||
|
||
str_copy(pCurrentLine->m_aText, 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);
|
||
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 == SERVER_MSG)
|
||
{
|
||
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 == CLIENT_MSG)
|
||
{
|
||
// 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 &= (bool)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.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;
|
||
|
||
TextRender()->DeleteTextContainer(m_aLines[r].m_TextContainerIndex);
|
||
Graphics()->DeleteQuadContainer(m_aLines[r].m_QuadContainerIndex);
|
||
|
||
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_aYOffset[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_aYOffset[OffsetType] = AppendCursor.m_Y + AppendCursor.m_FontSize + RealMsgPaddingY;
|
||
}
|
||
|
||
y -= m_aLines[r].m_aYOffset[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));
|
||
TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, pHeartStr);
|
||
}
|
||
}
|
||
|
||
// render name
|
||
ColorRGBA NameColor;
|
||
if(m_aLines[r].m_ClientID == SERVER_MSG)
|
||
NameColor = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageSystemColor));
|
||
else if(m_aLines[r].m_ClientID == CLIENT_MSG)
|
||
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);
|
||
else if(m_aLines[r].m_NameColor == TEAM_BLUE)
|
||
NameColor = ColorRGBA(0.7f, 0.7f, 1.0f, 1.f);
|
||
else if(m_aLines[r].m_NameColor == TEAM_SPECTATORS)
|
||
NameColor = ColorRGBA(0.75f, 0.5f, 0.75f, 1.f);
|
||
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);
|
||
TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, aName);
|
||
|
||
if(m_aLines[r].m_TimesRepeated > 0)
|
||
{
|
||
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.3f);
|
||
TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, aCount);
|
||
}
|
||
|
||
if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0')
|
||
{
|
||
TextRender()->TextColor(NameColor);
|
||
TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, ": ");
|
||
}
|
||
|
||
// render line
|
||
ColorRGBA Color;
|
||
if(m_aLines[r].m_ClientID == SERVER_MSG)
|
||
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageSystemColor));
|
||
else if(m_aLines[r].m_ClientID == CLIENT_MSG)
|
||
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageClientColor));
|
||
else if(m_aLines[r].m_Highlighted)
|
||
Color = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClMessageHighlightColor));
|
||
else if(m_aLines[r].m_Team)
|
||
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;
|
||
}
|
||
|
||
TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &AppendCursor, 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_aYOffset[OffsetType];
|
||
Graphics()->SetColor(1, 1, 1, 1);
|
||
m_aLines[r].m_QuadContainerIndex = Graphics()->CreateRectQuadContainer(Begin, y, (AppendCursor.m_LongestLineWidth - TextBegin) + RealMsgPaddingX * 1.5f, Height, MESSAGE_ROUNDING, IGraphics::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(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_aYOffset[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);
|
||
}
|
||
|
||
ColorRGBA TextOutline(0.f, 0.f, 0.f, 0.3f * Blend);
|
||
ColorRGBA 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));
|
||
}
|
||
}
|