diff --git a/src/base/system.c b/src/base/system.c index 2ea2e0d7d..738d62b2d 100644 --- a/src/base/system.c +++ b/src/base/system.c @@ -1913,6 +1913,38 @@ void str_clean_whitespaces(char *str_in) } } +/* removes leading and trailing spaces */ +void str_clean_whitespaces_simple(char *str_in) +{ + char *read = str_in; + char *write = str_in; + + /* skip initial whitespace */ + while(*read == ' ') + read++; + + /* end of read string is detected in the loop */ + while(1) + { + /* skip whitespace */ + int found_whitespace = 0; + for(; *read == ' ' && !found_whitespace; read++) + found_whitespace = 1; + /* if not at the end of the string, put a found whitespace here */ + if(*read) + { + if(found_whitespace) + *write++ = ' '; + *write++ = *read++; + } + else + { + *write = 0; + break; + } + } +} + char *str_skip_to_whitespace(char *str) { while(*str && (*str != ' ' && *str != '\t' && *str != '\n')) @@ -1920,6 +1952,13 @@ char *str_skip_to_whitespace(char *str) return str; } +const char *str_skip_to_whitespace_const(const char *str) +{ + while(*str && (*str != ' ' && *str != '\t' && *str != '\n')) + str++; + return str; +} + char *str_skip_whitespaces(char *str) { while(*str && (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r')) diff --git a/src/base/system.h b/src/base/system.h index a21e4b811..ed5345d28 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -912,6 +912,18 @@ int str_check_pathname(const char* str); */ void str_clean_whitespaces(char *str); +/* + Function: str_clean_whitespaces_simple + Removes leading and trailing spaces + + Parameters: + str - String to clean up + + Remarks: + - The strings are treated as zero-terminated strings. +*/ +void str_clean_whitespaces_simple(char *str); + /* Function: str_skip_to_whitespace Skips leading non-whitespace characters(all but ' ', '\t', '\n', '\r'). @@ -928,6 +940,12 @@ void str_clean_whitespaces(char *str); */ char *str_skip_to_whitespace(char *str); +/* + Function: str_skip_to_whitespace_const + See str_skip_to_whitespace. +*/ +const char *str_skip_to_whitespace_const(const char *str); + /* Function: str_skip_whitespaces Skips leading whitespace characters(' ', '\t', '\n', '\r'). diff --git a/src/game/client/components/chat.cpp b/src/game/client/components/chat.cpp index 1f9c2ecaa..8fb64b6a3 100644 --- a/src/game/client/components/chat.cpp +++ b/src/game/client/components/chat.cpp @@ -22,9 +22,28 @@ CChat::CChat() { + // init chat commands (must be in alphabetical order) + static CChatCommand s_aapCommands[] = { + {"/all", " - Switch to all chat", &Com_All}, + {"/friend", " ", &Com_Befriend}, + {"/m", " ", &Com_Mute}, + {"/mute", " ", &Com_Mute}, + {"/r", " - Reply to a whisper", &Com_Reply}, + {"/team", " - Switch to team chat", &Com_Team}, + {"/w", " ", &Com_Whisper}, + {"/whisper", " ", &Com_Whisper}, + }; + const int CommandsCount = sizeof(s_aapCommands) / sizeof(CChatCommand); + m_pCommands = new CChatCommands(s_aapCommands, CommandsCount); + OnReset(); } +CChat::~CChat() +{ + delete m_pCommands; +} + void CChat::OnReset() { for(int i = 0; i < MAX_LINES; i++) @@ -35,6 +54,8 @@ void CChat::OnReset() } m_Mode = CHAT_NONE; + // m_WhisperTarget = -1; + m_LastWhisperFrom = -1; m_ReverseCompletion = false; m_Show = false; m_InputUpdate = false; @@ -47,6 +68,8 @@ void CChat::OnReset() m_pHistoryEntry = 0x0; m_PendingChatCounter = 0; m_LastChatSend = 0; + m_IgnoreCommand = false; + m_pCommands->Reset(); for(int i = 0; i < CHAT_NUM; ++i) m_aLastSoundPlayed[i] = 0; @@ -160,36 +183,50 @@ bool CChat::OnInput(IInput::CEvent Event) if(Event.m_Flags&IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) { - m_Mode = CHAT_NONE; - m_pClient->OnRelease(); + if(IsTypingCommand()) + { + m_IgnoreCommand = true; + } + else + { + m_Mode = CHAT_NONE; + m_pClient->OnRelease(); + } } else if(Event.m_Flags&IInput::FLAG_PRESS && (Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER)) { - if(m_Input.GetString()[0]) + if(IsTypingCommand() && ExecuteCommand()) { - bool AddEntry = false; - - if(m_PendingChatCounter == 0 && m_LastChatSend+time_freq() < time_get()) - { - Say(m_Mode, 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_Mode = m_Mode; - mem_copy(pEntry->m_aText, m_Input.GetString(), m_Input.GetLength()+1); - } + // everything is handled within + } + else + { + if(m_Input.GetString()[0]) + { + bool AddEntry = false; + + if(m_PendingChatCounter == 0 && m_LastChatSend+time_freq() < time_get()) + { + Say(m_Mode, 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_Mode = m_Mode; + mem_copy(pEntry->m_aText, m_Input.GetString(), m_Input.GetLength()+1); + } + } + m_pHistoryEntry = 0x0; + m_Mode = CHAT_NONE; + m_pClient->OnRelease(); } - m_pHistoryEntry = 0x0; - m_Mode = CHAT_NONE; - m_pClient->OnRelease(); } if(Event.m_Flags&IInput::FLAG_PRESS && Event.m_Key == KEY_TAB) { @@ -324,48 +361,74 @@ bool CChat::OnInput(IInput::CEvent Event) if(Event.m_Flags&IInput::FLAG_PRESS && Event.m_Key == KEY_UP) { - if(m_pHistoryEntry) + if(IsTypingCommand()) { - CHistoryEntry *pTest = m_History.Prev(m_pHistoryEntry); - - if(pTest) - m_pHistoryEntry = pTest; + m_pCommands->SelectPreviousCommand(); } else - m_pHistoryEntry = m_History.Last(); + { + if(m_pHistoryEntry) + { + CHistoryEntry *pTest = m_History.Prev(m_pHistoryEntry); - if(m_pHistoryEntry) - m_Input.Set(m_pHistoryEntry->m_aText); + 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); + if(IsTypingCommand()) + { + m_pCommands->SelectNextCommand(); + } else - m_Input.Clear(); + { + 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 Mode) +void CChat::EnableMode(int Mode, const char* pText) { if(Client()->State() == IClient::STATE_DEMOPLAYBACK) return; - if(m_Mode == CHAT_NONE) + m_Mode = Mode; + ClearInput(); + + if(pText) // optional text to initalize with { - m_Mode = Mode; - m_Input.Clear(); - Input()->Clear(); - m_CompletionChosen = -1; + m_Input.Set(pText); + m_Input.SetCursorOffset(str_length(pText)); + m_InputUpdate = true; } } +void CChat::ClearInput() +{ + m_Input.Clear(); + Input()->Clear(); + m_CompletionChosen = -1; + + m_IgnoreCommand = false; + m_pCommands->Reset(); +} + void CChat::OnMessage(int MsgType, void *pRawMsg) { if(MsgType == NETMSGTYPE_SV_CHAT) @@ -377,7 +440,7 @@ void CChat::OnMessage(int MsgType, void *pRawMsg) void CChat::AddLine(int ClientID, int Mode, const char *pLine, int TargetID) { - if(*pLine == 0 || (ClientID != -1 && (!g_Config.m_ClShowsocial || !m_pClient->m_aClients[ClientID].m_Active || // unknown client + if(*pLine == 0 || (ClientID >= 0 && (!g_Config.m_ClShowsocial || !m_pClient->m_aClients[ClientID].m_Active || // unknown client m_pClient->m_aClients[ClientID].m_ChatIgnore || g_Config.m_ClFilterchat == 2 || (m_pClient->m_LocalClientID != ClientID && g_Config.m_ClFilterchat == 1 && !m_pClient->m_aClients[ClientID].m_Friend)))) @@ -385,7 +448,7 @@ void CChat::AddLine(int ClientID, int Mode, const char *pLine, int TargetID) if(Mode == CHAT_WHISPER) { // unknown client - if(ClientID == -1 || !m_pClient->m_aClients[ClientID].m_Active || TargetID == -1 || !m_pClient->m_aClients[TargetID].m_Active) + if(ClientID < 0 || !m_pClient->m_aClients[ClientID].m_Active || TargetID < 0 || !m_pClient->m_aClients[TargetID].m_Active) return; // should be sender or receiver if(ClientID != m_pClient->m_LocalClientID && TargetID != m_pClient->m_LocalClientID) @@ -451,7 +514,7 @@ void CChat::AddLine(int ClientID, int Mode, const char *pLine, int TargetID) // check for highlighted name Highlighted = false; // do not highlight our own messages, whispers and system messages - if(Mode != CHAT_WHISPER && ClientID != -1 && ClientID != m_pClient->m_LocalClientID) + if(Mode != CHAT_WHISPER && ClientID >= 0 && ClientID != m_pClient->m_LocalClientID) { const char *pHL = str_find_nocase(pLine, m_pClient->m_aClients[m_pClient->m_LocalClientID].m_aName); if(pHL) @@ -474,11 +537,16 @@ void CChat::AddLine(int ClientID, int Mode, const char *pLine, int TargetID) if(Mode == CHAT_WHISPER && ClientID == m_pClient->m_LocalClientID && TargetID >= 0) NameCID = TargetID; - if(ClientID == -1) // server message + if(ClientID == SERVER_MSG) { m_aLines[m_CurrentLine].m_aName[0] = 0; str_format(m_aLines[m_CurrentLine].m_aText, sizeof(m_aLines[m_CurrentLine].m_aText), "*** %s", pLine); } + else if(ClientID == CLIENT_MSG) + { + m_aLines[m_CurrentLine].m_aName[0] = 0; + str_format(m_aLines[m_CurrentLine].m_aText, sizeof(m_aLines[m_CurrentLine].m_aText), "— %s", pLine); + } else { if(m_pClient->m_aClients[ClientID].m_Team == TEAM_SPECTATORS) @@ -509,9 +577,12 @@ void CChat::AddLine(int ClientID, int Mode, const char *pLine, int TargetID) Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, aBufMode, aBuf, Highlighted || Mode == CHAT_WHISPER); } + if(Mode == CHAT_WHISPER && m_pClient->m_LocalClientID != ClientID) + m_LastWhisperFrom = ClientID; // we received a a whisper + // play sound int64 Now = time_get(); - if(ClientID == -1) + if(ClientID < 0) { if(Now-m_aLastSoundPlayed[CHAT_SERVER] >= time_freq()*3/10) { @@ -566,6 +637,8 @@ void CChat::OnRender() const int LocalCID = m_pClient->m_LocalClientID; const CGameClient::CClientData& LocalClient = m_pClient->m_aClients[LocalCID]; const int LocalTteam = LocalClient.m_Team; + // bool showCommands; + float CategoryWidth = 0; if(m_Mode == CHAT_WHISPER && !m_pClient->m_aClients[m_WhisperTarget].m_Active) m_Mode = CHAT_NONE; @@ -573,8 +646,8 @@ void CChat::OnRender() { // calculate category text size // TODO: rework TextRender. Writing the same code twice to calculate a simple thing as width is ridiculus - float CategoryWidth = 0; float CategoryHeight; + const float IconOffsetX = m_Mode == CHAT_WHISPER ? 6.0f : 0.0f; const float CategoryFontSize = 8.0f; const float InputFontSize = 8.0f; char aCatText[48]; @@ -617,8 +690,6 @@ void CChat::OnRender() else if(m_Mode == CHAT_WHISPER) CatRectColor = CRCWhisper; - const float IconOffsetX = m_Mode == CHAT_WHISPER ? 6.0f : 0.0f; - CUIRect CatRect; CatRect.x = 0; CatRect.y = y; @@ -758,7 +829,7 @@ void CChat::OnRender() else if(Line.m_Mode == CHAT_WHISPER) str_format(aBuf, sizeof(aBuf), "[%s] ", Localize("Whisper")); - if(Line.m_ClientID != -1) + if(Line.m_ClientID >= 0) { Cursor.m_X += RenderTools()->GetClientIdRectSize(Cursor.m_FontSize); str_append(aBuf, Line.m_aName, sizeof(aBuf)); @@ -908,7 +979,7 @@ void CChat::OnRender() } // render name - if(Line.m_ClientID == -1) + if(Line.m_ClientID < 0) TextColor = ColorSystem; else if(Line.m_Mode == CHAT_WHISPER) TextColor = ColorWhisper; @@ -923,7 +994,7 @@ void CChat::OnRender() else TextColor = ColorAllPre; - if(Line.m_ClientID != -1) + if(Line.m_ClientID >= 0) { int NameCID = Line.m_ClientID; if(Line.m_Mode == CHAT_WHISPER && Line.m_ClientID == m_pClient->m_LocalClientID && Line.m_TargetID >= 0) @@ -938,7 +1009,7 @@ void CChat::OnRender() } // render line - if(Line.m_ClientID == -1) + if(Line.m_ClientID < 0) TextColor = ColorSystem; else if(Line.m_Mode == CHAT_WHISPER) TextColor = ColorWhisper; @@ -964,6 +1035,8 @@ void CChat::OnRender() TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); TextRender()->TextOutlineColor(0.0f, 0.0f, 0.0f, 0.3f); + + HandleCommands(x+CategoryWidth, Height - 24.f, 200.0f-CategoryWidth); } void CChat::Say(int Mode, const char *pLine) @@ -977,3 +1050,319 @@ void CChat::Say(int Mode, const char *pLine) Msg.m_pMessage = pLine; Client()->SendPackMsg(&Msg, MSGFLAG_VITAL); } + +bool CChat::IsTypingCommand() const +{ + return m_Input.GetString()[0] == '/' && !m_IgnoreCommand; +} + +// chat commands handlers +void CChat::HandleCommands(float x, float y, float w) +{ + // render commands menu + if(m_Mode != CHAT_NONE && IsTypingCommand()) + { + const float Alpha = 0.90f; + const float LineWidth = w; + const float LineHeight = 8.0f; + + m_pCommands->Filter(m_Input.GetString()); // flag active commands, update selected command + const int ActiveCount = m_pCommands->CountActiveCommands(); + + if(ActiveCount > 0) // at least one command to display + { + CUIRect Rect = {x, y-(ActiveCount+1)*LineHeight, LineWidth, (ActiveCount+1)*LineHeight}; + RenderTools()->DrawUIRect(&Rect, vec4(0.125f, 0.125f, 0.125f, Alpha), CUI::CORNER_ALL, 3.0f); + + // render notification + { + y -= LineHeight; + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, Rect.x + 5.0f, y, 5.0f, TEXTFLAG_RENDER); + TextRender()->TextColor(0.5f, 0.5f, 0.5f, 1.0f); + TextRender()->TextEx(&Cursor, Localize("Press Enter to confirm or Esc to cancel"), -1); + } + + // render commands + for(int i = ActiveCount - 1; i >= 0; i--) + { + y -= LineHeight; + CUIRect HighlightRect = {Rect.x, y, LineWidth, LineHeight-1}; + + // retrieve command + const CChatCommand* pCommand = m_pCommands->GetCommand(i); + + // draw selection box + if(pCommand == m_pCommands->GetSelectedCommand()) + RenderTools()->DrawUIRect(&HighlightRect, vec4(0.25f, 0.25f, 0.6f, Alpha), CUI::CORNER_ALL, 2.0f); + + // print command + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, Rect.x + 5.0f, y, 5.0f, TEXTFLAG_RENDER); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + TextRender()->TextEx(&Cursor, pCommand->m_pCommandText, -1); + TextRender()->TextColor(0.5f, 0.5f, 0.5f, 1.0f); + TextRender()->TextEx(&Cursor, pCommand->m_pHelpText, -1); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + } + } + } +} + +bool CChat::ExecuteCommand() +{ + if(m_pCommands->CountActiveCommands() == 0) + return false; + + const char* pCommandStr = m_Input.GetString(); + const CChatCommand* pCommand = m_pCommands->GetSelectedCommand(); + dbg_assert(pCommand != 0, "selected command does not exist"); + bool IsFullMatch = str_find(pCommandStr, pCommand->m_pCommandText); // if the command text is fully inside pCommandStr (aka, not a shortcut) + + if(IsFullMatch) + { + // execute command + if(pCommand->m_pfnFunc != 0) + pCommand->m_pfnFunc(this, pCommandStr); + } + else + { + // autocomplete command + char aBuf[128]; + str_copy(aBuf, pCommand->m_pCommandText, sizeof(aBuf)); + str_append(aBuf, " ", sizeof(aBuf)); + + m_Input.Set(aBuf); + m_Input.SetCursorOffset(str_length(aBuf)); + m_InputUpdate = true; + } + return true; +} + +// returns -1 if not found or duplicate +int CChat::IdentifyNameParameter(const char* pCommand) const +{ + // retrieve name parameter + const char* pParameter = str_skip_to_whitespace_const(pCommand); + if(!pParameter) + return -1; + + // do not count leading and trailing whitespaces + char aName[MAX_NAME_LENGTH]; + str_copy(aName, pParameter+1, sizeof(aName)); + str_clean_whitespaces_simple(aName); + + int TargetID = -1; + for(int i = 0; i < MAX_CLIENTS; ++i) + { + if(!m_pClient->m_aClients[i].m_Active || i == m_pClient->m_LocalClientID) // skip local user + continue; + if(str_length(m_pClient->m_aClients[i].m_aName) == str_length(aName) && str_comp(m_pClient->m_aClients[i].m_aName, aName) == 0) + { + // name strictly matches + if(TargetID != -1) + { + // duplicate; be conservative + dbg_msg("chat", "name duplicate found, aborting whisper command"); + return -1; + } + TargetID = i; + } + } + return TargetID; +} + + +// callback functions for commands +void CChat::Com_All(CChat *pChatData, const char* pCommand) +{ + const char* pParameter = str_skip_to_whitespace_const(pCommand); + char *pBuf = 0x0; + if(pParameter++ && *pParameter) // skip the first space + { + // save the parameter in a buffer before EnableMode clears it + pBuf = (char*)mem_alloc(str_length(pParameter) + 1, 1); + str_copy(pBuf, pParameter, str_length(pParameter) + 1); + } + pChatData->EnableMode(CHAT_ALL, pBuf); + mem_free(pBuf); +} + +void CChat::Com_Team(CChat *pChatData, const char* pCommand) +{ + const char* pParameter = str_skip_to_whitespace_const(pCommand); + char *pBuf = 0x0; + if(pParameter++ && *pParameter) // skip the first space + { + // save the parameter in a buffer before EnableMode clears it + pBuf = (char*)mem_alloc(str_length(pParameter) + 1, 1); + str_copy(pBuf, pParameter, str_length(pParameter) + 1); + } + pChatData->EnableMode(CHAT_TEAM, pBuf); + mem_free(pBuf); +} + +void CChat::Com_Reply(CChat *pChatData, const char* pCommand) +{ + if(pChatData->m_LastWhisperFrom == -1) + pChatData->ClearInput(); // just reset the chat + else + { + pChatData->m_WhisperTarget = pChatData->m_LastWhisperFrom; + + const char* pParameter = str_skip_to_whitespace_const(pCommand); + char *pBuf = 0x0; + if(pParameter++ && *pParameter) // skip the first space + { + // save the parameter in a buffer before EnableMode clears it + pBuf = (char*)mem_alloc(str_length(pParameter) + 1, 1); + str_copy(pBuf, pParameter, sizeof(pBuf)); + } + pChatData->EnableMode(CHAT_WHISPER, pBuf); + mem_free(pBuf); + } +} + +void CChat::Com_Whisper(CChat *pChatData, const char* pCommand) +{ + int TargetID = pChatData->IdentifyNameParameter(pCommand); + if(TargetID != -1) + { + pChatData->m_WhisperTarget = TargetID; + pChatData->EnableMode(CHAT_WHISPER); + } +} + +void CChat::Com_Mute(CChat *pChatData, const char* pCommand) +{ + int TargetID = pChatData->IdentifyNameParameter(pCommand); + if(TargetID != -1) + { + pChatData->m_pClient->m_aClients[TargetID].m_ChatIgnore ^= 1; + + pChatData->ClearInput(); + + char aMsg[128]; + str_format(aMsg, sizeof(aMsg), pChatData->m_pClient->m_aClients[TargetID].m_ChatIgnore ? Localize("'%s' was muted") : Localize("'%s' was unmuted"), pChatData->m_pClient->m_aClients[TargetID].m_aName); + pChatData->AddLine(CLIENT_MSG, CHAT_ALL, aMsg, -1); + } +} + +void CChat::Com_Befriend(CChat *pChatData, const char* pCommand) +{ + int TargetID = pChatData->IdentifyNameParameter(pCommand); + if(TargetID != -1) + { + bool isFriend = pChatData->m_pClient->m_aClients[TargetID].m_Friend; + if(isFriend) + pChatData->m_pClient->Friends()->RemoveFriend(pChatData->m_pClient->m_aClients[TargetID].m_aName, pChatData->m_pClient->m_aClients[TargetID].m_aClan); + else + pChatData->m_pClient->Friends()->AddFriend(pChatData->m_pClient->m_aClients[TargetID].m_aName, pChatData->m_pClient->m_aClients[TargetID].m_aClan); + pChatData->m_pClient->m_aClients[TargetID].m_Friend ^= 1; + + pChatData->ClearInput(); + + char aMsg[128]; + str_format(aMsg, sizeof(aMsg), !isFriend ? Localize("'%s' was added as a friend") : Localize("'%s' was removed as a friend"), pChatData->m_pClient->m_aClients[TargetID].m_aName); + pChatData->AddLine(CLIENT_MSG, CHAT_ALL, aMsg, -1); + } +} + + +// CChatCommands methods +CChat::CChatCommands::CChatCommands(CChatCommand apCommands[], int Count) : m_apCommands(apCommands), m_Count(Count), m_pSelectedCommand(0x0) { } +CChat::CChatCommands::~CChatCommands() { } + +// selection +void CChat::CChatCommands::Reset() +{ + m_pSelectedCommand = 0x0; +} + +// Example: /whisper command will match "/whi", "/whisper" and "/whisper tee" +void CChat::CChatCommands::Filter(const char* pLine) +{ + char aCommandStr[64]; + str_copy(aCommandStr, pLine, sizeof(aCommandStr)); + // truncate the string at the first whitespace to get the command + char* pFirstWhitespace = str_skip_to_whitespace(aCommandStr); + if(pFirstWhitespace) + *pFirstWhitespace = 0; + + for(int i = 0; i < m_Count; i++) + m_apCommands[i].m_aFiltered = (str_find_nocase(m_apCommands[i].m_pCommandText, aCommandStr) != m_apCommands[i].m_pCommandText); + + // also update selected command + if(!GetSelectedCommand() || GetSelectedCommand()->m_aFiltered) + { + if(CountActiveCommands() > 0) + m_pSelectedCommand = &m_apCommands[GetActiveIndex(0)]; // default to first command + else + m_pSelectedCommand = 0x0; + } +} + +// this will not return a correct value if we are not writing a command (m_Input.GetString()[0] == '/') +int CChat::CChatCommands::CountActiveCommands() const +{ + int n = m_Count; + for(int i = 0; i < m_Count; i++) + n -= m_apCommands[i].m_aFiltered; + return n; +} + +const CChat::CChatCommand* CChat::CChatCommands::GetCommand(int index) const +{ + return &m_apCommands[GetActiveIndex(index)]; +} + +const CChat::CChatCommand* CChat::CChatCommands::GetSelectedCommand() const +{ + return m_pSelectedCommand; +} + +void CChat::CChatCommands::SelectPreviousCommand() +{ + CChatCommand* LastCommand = 0x0; + for(int i = 0; i < m_Count; i++) + { + if(m_apCommands[i].m_aFiltered) + continue; + if(&m_apCommands[i] == m_pSelectedCommand) + { + m_pSelectedCommand = LastCommand; + return; + } + LastCommand = &m_apCommands[i]; + } +} + +void CChat::CChatCommands::SelectNextCommand() +{ + bool FoundSelected = false; + for(int i = 0; i < m_Count; i++) + { + if(m_apCommands[i].m_aFiltered) + continue; + if(FoundSelected) + { + m_pSelectedCommand = &m_apCommands[i]; + return; + } + if(&m_apCommands[i] == m_pSelectedCommand) + FoundSelected = true; + } +} + +int CChat::CChatCommands::GetActiveIndex(int index) const +{ + for(int i = 0; i < m_Count; i++) + { + if(m_apCommands[i].m_aFiltered) + index++; + if(i == index) + return i; + } + dbg_break(); + return -1; +} diff --git a/src/game/client/components/chat.h b/src/game/client/components/chat.h index 732a7b79a..d40ee8979 100644 --- a/src/game/client/components/chat.h +++ b/src/game/client/components/chat.h @@ -2,6 +2,7 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #ifndef GAME_CLIENT_COMPONENTS_CHAT_H #define GAME_CLIENT_COMPONENTS_CHAT_H +#include #include #include #include @@ -28,10 +29,17 @@ class CChat : public CComponent bool m_Highlighted; }; + // client IDs for special messages + enum + { + CLIENT_MSG = -2, + SERVER_MSG = -1, + }; + CLine m_aLines[MAX_LINES]; int m_CurrentLine; - // chat + // chat sounds enum { CHAT_SERVER=0, @@ -42,6 +50,7 @@ class CChat : public CComponent int m_Mode; int m_WhisperTarget; + int m_LastWhisperFrom; bool m_Show; bool m_InputUpdate; int m_ChatStringOffset; @@ -64,6 +73,51 @@ class CChat : public CComponent int64 m_LastChatSend; int64 m_aLastSoundPlayed[CHAT_NUM]; + // chat commands + struct CChatCommand + { + const char* m_pCommandText; + const char* m_pHelpText; + void (*m_pfnFunc)(CChat *pChatData, const char* pCommand); + bool m_aFiltered; // 0 = shown, 1 = hidden + }; + + class CChatCommands + { + CChatCommand *m_apCommands; + int m_Count; + CChatCommand *m_pSelectedCommand; + + private: + int GetActiveIndex(int index) const; + public: + CChatCommands(CChatCommand apCommands[], int Count); + ~CChatCommands(); + void Reset(); + void Filter(const char* pLine); + int CountActiveCommands() const; + const CChatCommand* GetCommand(int index) const; + const CChatCommand* GetSelectedCommand() const; + void SelectPreviousCommand(); + void SelectNextCommand(); + }; + + CChatCommands *m_pCommands; + bool m_IgnoreCommand; + bool IsTypingCommand() const; + void HandleCommands(float x, float y, float w); + bool ExecuteCommand(); + int IdentifyNameParameter(const char* pCommand) const; + + static void Com_All(CChat *pChatData, const char* pCommand); + static void Com_Team(CChat *pChatData, const char* pCommand); + static void Com_Reply(CChat *pChatData, const char* pCommand); + static void Com_Whisper(CChat *pChatData, const char* pCommand); + static void Com_Mute(CChat *pChatData, const char* pCommand); + static void Com_Befriend(CChat *pChatData, const char* pCommand); + + void ClearInput(); + static void ConSay(IConsole::IResult *pResult, void *pUserData); static void ConSayTeam(IConsole::IResult *pResult, void *pUserData); static void ConWhisper(IConsole::IResult *pResult, void *pUserData); @@ -72,12 +126,13 @@ class CChat : public CComponent public: CChat(); + ~CChat(); bool IsActive() const { return m_Mode != CHAT_NONE; } void AddLine(int ClientID, int Team, const char *pLine, int TargetID = -1); - void EnableMode(int Team); + void EnableMode(int Team, const char* pText = NULL); void Say(int Team, const char *pLine);