1 | /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ |
2 | /* If you are missing that file, acquire a complete release at teeworlds.com. */ |
3 | |
4 | #include <engine/editor.h> |
5 | #include <engine/graphics.h> |
6 | #include <engine/keys.h> |
7 | #include <engine/shared/config.h> |
8 | #include <engine/shared/csv.h> |
9 | #include <engine/textrender.h> |
10 | |
11 | #include <game/generated/protocol.h> |
12 | |
13 | #include <game/client/animstate.h> |
14 | #include <game/client/components/scoreboard.h> |
15 | #include <game/client/components/skins.h> |
16 | #include <game/client/components/sounds.h> |
17 | #include <game/client/gameclient.h> |
18 | #include <game/localization.h> |
19 | |
20 | #include "chat.h" |
21 | |
22 | char CChat::ms_aDisplayText[MAX_LINE_LENGTH] = {'\0'}; |
23 | |
24 | CChat::CChat() |
25 | { |
26 | for(auto &Line : m_aLines) |
27 | { |
28 | // reset the container indices, so the text containers can be deleted on reset |
29 | Line.m_TextContainerIndex.Reset(); |
30 | Line.m_QuadContainerIndex = -1; |
31 | } |
32 | |
33 | m_Mode = MODE_NONE; |
34 | |
35 | m_Input.SetClipboardLineCallback([this](const char *pStr) { SendChatQueued(pLine: pStr); }); |
36 | m_Input.SetCalculateOffsetCallback([this]() { return m_IsInputCensored; }); |
37 | m_Input.SetDisplayTextCallback([this](char *pStr, size_t NumChars) { |
38 | m_IsInputCensored = false; |
39 | if( |
40 | g_Config.m_ClStreamerMode && |
41 | (str_startswith(str: pStr, prefix: "/login " ) || |
42 | str_startswith(str: pStr, prefix: "/register " ) || |
43 | str_startswith(str: pStr, prefix: "/code " ) || |
44 | str_startswith(str: pStr, prefix: "/timeout " ) || |
45 | str_startswith(str: pStr, prefix: "/save " ) || |
46 | str_startswith(str: pStr, prefix: "/load " ))) |
47 | { |
48 | bool Censor = false; |
49 | const size_t NumLetters = minimum(a: NumChars, b: sizeof(ms_aDisplayText) - 1); |
50 | for(size_t i = 0; i < NumLetters; ++i) |
51 | { |
52 | if(Censor) |
53 | ms_aDisplayText[i] = '*'; |
54 | else |
55 | ms_aDisplayText[i] = pStr[i]; |
56 | if(pStr[i] == ' ') |
57 | { |
58 | Censor = true; |
59 | m_IsInputCensored = true; |
60 | } |
61 | } |
62 | ms_aDisplayText[NumLetters] = '\0'; |
63 | return ms_aDisplayText; |
64 | } |
65 | return pStr; |
66 | }); |
67 | } |
68 | |
69 | void CChat::RegisterCommand(const char *pName, const char *pParams, const char *pHelpText) |
70 | { |
71 | // Don't allow duplicate commands. |
72 | for(const auto &Command : m_vCommands) |
73 | if(str_comp(a: Command.m_aName, b: pName) == 0) |
74 | return; |
75 | |
76 | m_vCommands.emplace_back(args&: pName, args&: pParams, args&: pHelpText); |
77 | m_CommandsNeedSorting = true; |
78 | } |
79 | |
80 | void CChat::UnregisterCommand(const char *pName) |
81 | { |
82 | m_vCommands.erase(first: std::remove_if(first: m_vCommands.begin(), last: m_vCommands.end(), pred: [pName](const CCommand &Command) { return str_comp(a: Command.m_aName, b: pName) == 0; }), last: m_vCommands.end()); |
83 | } |
84 | |
85 | void CChat::RebuildChat() |
86 | { |
87 | for(auto &Line : m_aLines) |
88 | { |
89 | TextRender()->DeleteTextContainer(TextContainerIndex&: Line.m_TextContainerIndex); |
90 | Graphics()->DeleteQuadContainer(ContainerIndex&: Line.m_QuadContainerIndex); |
91 | // recalculate sizes |
92 | Line.m_aYOffset[0] = -1.f; |
93 | Line.m_aYOffset[1] = -1.f; |
94 | } |
95 | } |
96 | |
97 | void CChat::OnWindowResize() |
98 | { |
99 | RebuildChat(); |
100 | } |
101 | |
102 | void CChat::Reset() |
103 | { |
104 | for(auto &Line : m_aLines) |
105 | { |
106 | TextRender()->DeleteTextContainer(TextContainerIndex&: Line.m_TextContainerIndex); |
107 | Graphics()->DeleteQuadContainer(ContainerIndex&: Line.m_QuadContainerIndex); |
108 | Line.m_Time = 0; |
109 | Line.m_aText[0] = 0; |
110 | Line.m_aName[0] = 0; |
111 | Line.m_Friend = false; |
112 | Line.m_TimesRepeated = 0; |
113 | Line.m_HasRenderTee = false; |
114 | } |
115 | m_PrevScoreBoardShowed = false; |
116 | m_PrevShowChat = false; |
117 | |
118 | m_Show = false; |
119 | m_CompletionUsed = false; |
120 | m_CompletionChosen = -1; |
121 | m_aCompletionBuffer[0] = 0; |
122 | m_PlaceholderOffset = 0; |
123 | m_PlaceholderLength = 0; |
124 | m_pHistoryEntry = 0x0; |
125 | m_PendingChatCounter = 0; |
126 | m_LastChatSend = 0; |
127 | m_CurrentLine = 0; |
128 | m_IsInputCensored = false; |
129 | m_EditingNewLine = true; |
130 | m_ServerSupportsCommandInfo = false; |
131 | m_CommandsNeedSorting = false; |
132 | mem_zero(block: m_aCurrentInputText, size: sizeof(m_aCurrentInputText)); |
133 | DisableMode(); |
134 | m_vCommands.clear(); |
135 | |
136 | for(int64_t &LastSoundPlayed : m_aLastSoundPlayed) |
137 | LastSoundPlayed = 0; |
138 | } |
139 | |
140 | void CChat::OnRelease() |
141 | { |
142 | m_Show = false; |
143 | } |
144 | |
145 | void CChat::OnStateChange(int NewState, int OldState) |
146 | { |
147 | if(OldState <= IClient::STATE_CONNECTING) |
148 | Reset(); |
149 | } |
150 | |
151 | void CChat::ConSay(IConsole::IResult *pResult, void *pUserData) |
152 | { |
153 | ((CChat *)pUserData)->SendChat(Team: 0, pLine: pResult->GetString(Index: 0)); |
154 | } |
155 | |
156 | void CChat::ConSayTeam(IConsole::IResult *pResult, void *pUserData) |
157 | { |
158 | ((CChat *)pUserData)->SendChat(Team: 1, pLine: pResult->GetString(Index: 0)); |
159 | } |
160 | |
161 | void CChat::ConChat(IConsole::IResult *pResult, void *pUserData) |
162 | { |
163 | const char *pMode = pResult->GetString(Index: 0); |
164 | if(str_comp(a: pMode, b: "all" ) == 0) |
165 | ((CChat *)pUserData)->EnableMode(Team: 0); |
166 | else if(str_comp(a: pMode, b: "team" ) == 0) |
167 | ((CChat *)pUserData)->EnableMode(Team: 1); |
168 | else |
169 | ((CChat *)pUserData)->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "console" , pStr: "expected all or team as mode" ); |
170 | |
171 | if(pResult->GetString(Index: 1)[0] || g_Config.m_ClChatReset) |
172 | ((CChat *)pUserData)->m_Input.Set(pResult->GetString(Index: 1)); |
173 | } |
174 | |
175 | void CChat::ConShowChat(IConsole::IResult *pResult, void *pUserData) |
176 | { |
177 | ((CChat *)pUserData)->m_Show = pResult->GetInteger(Index: 0) != 0; |
178 | } |
179 | |
180 | void CChat::ConEcho(IConsole::IResult *pResult, void *pUserData) |
181 | { |
182 | ((CChat *)pUserData)->Echo(pString: pResult->GetString(Index: 0)); |
183 | } |
184 | |
185 | void CChat::ConchainChatOld(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) |
186 | { |
187 | pfnCallback(pResult, pCallbackUserData); |
188 | ((CChat *)pUserData)->RebuildChat(); |
189 | } |
190 | |
191 | void CChat::ConchainChatFontSize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) |
192 | { |
193 | pfnCallback(pResult, pCallbackUserData); |
194 | CChat *pChat = (CChat *)pUserData; |
195 | pChat->EnsureCoherentWidth(); |
196 | pChat->RebuildChat(); |
197 | } |
198 | |
199 | void CChat::ConchainChatWidth(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) |
200 | { |
201 | pfnCallback(pResult, pCallbackUserData); |
202 | CChat *pChat = (CChat *)pUserData; |
203 | pChat->EnsureCoherentFontSize(); |
204 | pChat->RebuildChat(); |
205 | } |
206 | |
207 | void CChat::Echo(const char *pString) |
208 | { |
209 | AddLine(ClientId: CLIENT_MSG, Team: 0, pLine: pString); |
210 | } |
211 | |
212 | void CChat::OnConsoleInit() |
213 | { |
214 | Console()->Register(pName: "say" , pParams: "r[message]" , Flags: CFGFLAG_CLIENT, pfnFunc: ConSay, pUser: this, pHelp: "Say in chat" ); |
215 | Console()->Register(pName: "say_team" , pParams: "r[message]" , Flags: CFGFLAG_CLIENT, pfnFunc: ConSayTeam, pUser: this, pHelp: "Say in team chat" ); |
216 | Console()->Register(pName: "chat" , pParams: "s['team'|'all'] ?r[message]" , Flags: CFGFLAG_CLIENT, pfnFunc: ConChat, pUser: this, pHelp: "Enable chat with all/team mode" ); |
217 | Console()->Register(pName: "+show_chat" , pParams: "" , Flags: CFGFLAG_CLIENT, pfnFunc: ConShowChat, pUser: this, pHelp: "Show chat" ); |
218 | Console()->Register(pName: "echo" , pParams: "r[message]" , Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: ConEcho, pUser: this, pHelp: "Echo the text in chat window" ); |
219 | } |
220 | |
221 | void CChat::OnInit() |
222 | { |
223 | Reset(); |
224 | Console()->Chain(pName: "cl_chat_old" , pfnChainFunc: ConchainChatOld, pUser: this); |
225 | Console()->Chain(pName: "cl_chat_size" , pfnChainFunc: ConchainChatFontSize, pUser: this); |
226 | Console()->Chain(pName: "cl_chat_width" , pfnChainFunc: ConchainChatWidth, pUser: this); |
227 | } |
228 | |
229 | bool CChat::OnInput(const IInput::CEvent &Event) |
230 | { |
231 | if(m_Mode == MODE_NONE) |
232 | return false; |
233 | |
234 | if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) |
235 | { |
236 | DisableMode(); |
237 | m_pClient->OnRelease(); |
238 | if(g_Config.m_ClChatReset) |
239 | { |
240 | m_Input.Clear(); |
241 | m_pHistoryEntry = nullptr; |
242 | } |
243 | } |
244 | else if(Event.m_Flags & IInput::FLAG_PRESS && (Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER)) |
245 | { |
246 | if(m_CommandsNeedSorting) |
247 | { |
248 | std::sort(first: m_vCommands.begin(), last: m_vCommands.end()); |
249 | m_CommandsNeedSorting = false; |
250 | } |
251 | |
252 | SendChatQueued(pLine: m_Input.GetString()); |
253 | m_pHistoryEntry = nullptr; |
254 | DisableMode(); |
255 | m_pClient->OnRelease(); |
256 | m_Input.Clear(); |
257 | } |
258 | if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_TAB) |
259 | { |
260 | const bool ShiftPressed = Input()->ShiftIsPressed(); |
261 | |
262 | // fill the completion buffer |
263 | if(!m_CompletionUsed) |
264 | { |
265 | const char *pCursor = m_Input.GetString() + m_Input.GetCursorOffset(); |
266 | for(size_t Count = 0; Count < m_Input.GetCursorOffset() && *(pCursor - 1) != ' '; --pCursor, ++Count) |
267 | ; |
268 | m_PlaceholderOffset = pCursor - m_Input.GetString(); |
269 | |
270 | for(m_PlaceholderLength = 0; *pCursor && *pCursor != ' '; ++pCursor) |
271 | ++m_PlaceholderLength; |
272 | |
273 | str_truncate(dst: m_aCompletionBuffer, dst_size: sizeof(m_aCompletionBuffer), src: m_Input.GetString() + m_PlaceholderOffset, truncation_len: m_PlaceholderLength); |
274 | } |
275 | |
276 | if(!m_CompletionUsed && m_aCompletionBuffer[0] != '/') |
277 | { |
278 | // Create the completion list of player names through which the player can iterate |
279 | const char *PlayerName, *FoundInput; |
280 | m_PlayerCompletionListLength = 0; |
281 | for(auto &PlayerInfo : m_pClient->m_Snap.m_apInfoByName) |
282 | { |
283 | if(PlayerInfo) |
284 | { |
285 | PlayerName = m_pClient->m_aClients[PlayerInfo->m_ClientId].m_aName; |
286 | FoundInput = str_utf8_find_nocase(haystack: PlayerName, needle: m_aCompletionBuffer); |
287 | if(FoundInput != 0) |
288 | { |
289 | m_aPlayerCompletionList[m_PlayerCompletionListLength].ClientId = PlayerInfo->m_ClientId; |
290 | // The score for suggesting a player name is determined by the distance of the search input to the beginning of the player name |
291 | m_aPlayerCompletionList[m_PlayerCompletionListLength].Score = (int)(FoundInput - PlayerName); |
292 | m_PlayerCompletionListLength++; |
293 | } |
294 | } |
295 | } |
296 | std::stable_sort(first: m_aPlayerCompletionList, last: m_aPlayerCompletionList + m_PlayerCompletionListLength, |
297 | comp: [](const CRateablePlayer &p1, const CRateablePlayer &p2) -> bool { |
298 | return p1.Score < p2.Score; |
299 | }); |
300 | } |
301 | |
302 | if(m_aCompletionBuffer[0] == '/' && !m_vCommands.empty()) |
303 | { |
304 | CCommand *pCompletionCommand = 0; |
305 | |
306 | const size_t NumCommands = m_vCommands.size(); |
307 | |
308 | if(ShiftPressed && m_CompletionUsed) |
309 | m_CompletionChosen--; |
310 | else if(!ShiftPressed) |
311 | m_CompletionChosen++; |
312 | m_CompletionChosen = (m_CompletionChosen + 2 * NumCommands) % (2 * NumCommands); |
313 | |
314 | m_CompletionUsed = true; |
315 | |
316 | const char *pCommandStart = m_aCompletionBuffer + 1; |
317 | for(size_t i = 0; i < 2 * NumCommands; ++i) |
318 | { |
319 | int SearchType; |
320 | int Index; |
321 | |
322 | if(ShiftPressed) |
323 | { |
324 | SearchType = ((m_CompletionChosen - i + 2 * NumCommands) % (2 * NumCommands)) / NumCommands; |
325 | Index = (m_CompletionChosen - i + NumCommands) % NumCommands; |
326 | } |
327 | else |
328 | { |
329 | SearchType = ((m_CompletionChosen + i) % (2 * NumCommands)) / NumCommands; |
330 | Index = (m_CompletionChosen + i) % NumCommands; |
331 | } |
332 | |
333 | auto &Command = m_vCommands[Index]; |
334 | |
335 | if(str_startswith_nocase(str: Command.m_aName, prefix: pCommandStart)) |
336 | { |
337 | pCompletionCommand = &Command; |
338 | m_CompletionChosen = Index + SearchType * NumCommands; |
339 | break; |
340 | } |
341 | } |
342 | |
343 | // insert the command |
344 | if(pCompletionCommand) |
345 | { |
346 | char aBuf[MAX_LINE_LENGTH]; |
347 | // add part before the name |
348 | str_truncate(dst: aBuf, dst_size: sizeof(aBuf), src: m_Input.GetString(), truncation_len: m_PlaceholderOffset); |
349 | |
350 | // add the command |
351 | str_append(dst&: aBuf, src: "/" ); |
352 | str_append(dst&: aBuf, src: pCompletionCommand->m_aName); |
353 | |
354 | // add separator |
355 | const char *pSeparator = pCompletionCommand->m_aParams[0] == '\0' ? "" : " " ; |
356 | str_append(dst&: aBuf, src: pSeparator); |
357 | if(*pSeparator) |
358 | str_append(dst&: aBuf, src: pSeparator); |
359 | |
360 | // add part after the name |
361 | str_append(dst&: aBuf, src: m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength); |
362 | |
363 | m_PlaceholderLength = str_length(str: pSeparator) + str_length(str: pCompletionCommand->m_aName) + 1; |
364 | m_Input.Set(aBuf); |
365 | m_Input.SetCursorOffset(m_PlaceholderOffset + m_PlaceholderLength); |
366 | } |
367 | } |
368 | else |
369 | { |
370 | // find next possible name |
371 | const char *pCompletionString = 0; |
372 | if(m_PlayerCompletionListLength > 0) |
373 | { |
374 | // We do this in a loop, if a player left the game during the repeated pressing of Tab, they are skipped |
375 | CGameClient::CClientData *pCompletionClientData; |
376 | for(int i = 0; i < m_PlayerCompletionListLength; ++i) |
377 | { |
378 | if(ShiftPressed && m_CompletionUsed) |
379 | { |
380 | m_CompletionChosen--; |
381 | } |
382 | else if(!ShiftPressed) |
383 | { |
384 | m_CompletionChosen++; |
385 | } |
386 | if(m_CompletionChosen < 0) |
387 | { |
388 | m_CompletionChosen += m_PlayerCompletionListLength; |
389 | } |
390 | m_CompletionChosen %= m_PlayerCompletionListLength; |
391 | m_CompletionUsed = true; |
392 | |
393 | pCompletionClientData = &m_pClient->m_aClients[m_aPlayerCompletionList[m_CompletionChosen].ClientId]; |
394 | if(!pCompletionClientData->m_Active) |
395 | { |
396 | continue; |
397 | } |
398 | |
399 | pCompletionString = pCompletionClientData->m_aName; |
400 | break; |
401 | } |
402 | } |
403 | |
404 | // insert the name |
405 | if(pCompletionString) |
406 | { |
407 | char aBuf[MAX_LINE_LENGTH]; |
408 | // add part before the name |
409 | str_truncate(dst: aBuf, dst_size: sizeof(aBuf), src: m_Input.GetString(), truncation_len: m_PlaceholderOffset); |
410 | |
411 | // quote the name |
412 | char aQuoted[128]; |
413 | if(m_Input.GetString()[0] == '/' && (str_find(haystack: pCompletionString, needle: " " ) || str_find(haystack: pCompletionString, needle: "\"" ))) |
414 | { |
415 | // escape the name |
416 | str_copy(dst&: aQuoted, src: "\"" ); |
417 | char *pDst = aQuoted + str_length(str: aQuoted); |
418 | str_escape(dst: &pDst, src: pCompletionString, end: aQuoted + sizeof(aQuoted)); |
419 | str_append(dst&: aQuoted, src: "\"" ); |
420 | |
421 | pCompletionString = aQuoted; |
422 | } |
423 | |
424 | // add the name |
425 | str_append(dst&: aBuf, src: pCompletionString); |
426 | |
427 | // add separator |
428 | const char *pSeparator = "" ; |
429 | if(*(m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength) != ' ') |
430 | pSeparator = m_PlaceholderOffset == 0 ? ": " : " " ; |
431 | else if(m_PlaceholderOffset == 0) |
432 | pSeparator = ":" ; |
433 | if(*pSeparator) |
434 | str_append(dst&: aBuf, src: pSeparator); |
435 | |
436 | // add part after the name |
437 | str_append(dst&: aBuf, src: m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength); |
438 | |
439 | m_PlaceholderLength = str_length(str: pSeparator) + str_length(str: pCompletionString); |
440 | m_Input.Set(aBuf); |
441 | m_Input.SetCursorOffset(m_PlaceholderOffset + m_PlaceholderLength); |
442 | } |
443 | } |
444 | } |
445 | else |
446 | { |
447 | // reset name completion process |
448 | if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key != KEY_TAB && Event.m_Key != KEY_LSHIFT && Event.m_Key != KEY_RSHIFT) |
449 | { |
450 | m_CompletionChosen = -1; |
451 | m_CompletionUsed = false; |
452 | } |
453 | |
454 | m_Input.ProcessInput(Event); |
455 | } |
456 | |
457 | if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_UP) |
458 | { |
459 | if(m_EditingNewLine) |
460 | { |
461 | str_copy(dst&: m_aCurrentInputText, src: m_Input.GetString()); |
462 | m_EditingNewLine = false; |
463 | } |
464 | |
465 | if(m_pHistoryEntry) |
466 | { |
467 | CHistoryEntry *pTest = m_History.Prev(pCurrent: m_pHistoryEntry); |
468 | |
469 | if(pTest) |
470 | m_pHistoryEntry = pTest; |
471 | } |
472 | else |
473 | m_pHistoryEntry = m_History.Last(); |
474 | |
475 | if(m_pHistoryEntry) |
476 | m_Input.Set(m_pHistoryEntry->m_aText); |
477 | } |
478 | else if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_DOWN) |
479 | { |
480 | if(m_pHistoryEntry) |
481 | m_pHistoryEntry = m_History.Next(pCurrent: m_pHistoryEntry); |
482 | |
483 | if(m_pHistoryEntry) |
484 | { |
485 | m_Input.Set(m_pHistoryEntry->m_aText); |
486 | } |
487 | else if(!m_EditingNewLine) |
488 | { |
489 | m_Input.Set(m_aCurrentInputText); |
490 | m_EditingNewLine = true; |
491 | } |
492 | } |
493 | |
494 | return true; |
495 | } |
496 | |
497 | void CChat::EnableMode(int Team) |
498 | { |
499 | if(Client()->State() == IClient::STATE_DEMOPLAYBACK) |
500 | return; |
501 | |
502 | if(m_Mode == MODE_NONE) |
503 | { |
504 | if(Team) |
505 | m_Mode = MODE_TEAM; |
506 | else |
507 | m_Mode = MODE_ALL; |
508 | |
509 | Input()->Clear(); |
510 | m_CompletionChosen = -1; |
511 | m_CompletionUsed = false; |
512 | m_Input.Activate(Priority: EInputPriority::CHAT); |
513 | } |
514 | } |
515 | |
516 | void CChat::DisableMode() |
517 | { |
518 | if(m_Mode != MODE_NONE) |
519 | { |
520 | m_Mode = MODE_NONE; |
521 | m_Input.Deactivate(); |
522 | } |
523 | } |
524 | |
525 | void CChat::OnMessage(int MsgType, void *pRawMsg) |
526 | { |
527 | if(m_pClient->m_SuppressEvents) |
528 | return; |
529 | |
530 | if(MsgType == NETMSGTYPE_SV_CHAT) |
531 | { |
532 | CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; |
533 | AddLine(ClientId: pMsg->m_ClientId, Team: pMsg->m_Team, pLine: pMsg->m_pMessage); |
534 | } |
535 | else if(MsgType == NETMSGTYPE_SV_COMMANDINFO) |
536 | { |
537 | CNetMsg_Sv_CommandInfo *pMsg = (CNetMsg_Sv_CommandInfo *)pRawMsg; |
538 | if(!m_ServerSupportsCommandInfo) |
539 | { |
540 | m_vCommands.clear(); |
541 | m_ServerSupportsCommandInfo = true; |
542 | } |
543 | RegisterCommand(pName: pMsg->m_pName, pParams: pMsg->m_pArgsFormat, pHelpText: pMsg->m_pHelpText); |
544 | } |
545 | else if(MsgType == NETMSGTYPE_SV_COMMANDINFOREMOVE) |
546 | { |
547 | CNetMsg_Sv_CommandInfoRemove *pMsg = (CNetMsg_Sv_CommandInfoRemove *)pRawMsg; |
548 | UnregisterCommand(pName: pMsg->m_pName); |
549 | } |
550 | } |
551 | |
552 | bool CChat::LineShouldHighlight(const char *pLine, const char *pName) |
553 | { |
554 | const char *pHL = str_utf8_find_nocase(haystack: pLine, needle: pName); |
555 | |
556 | if(pHL) |
557 | { |
558 | int Length = str_length(str: pName); |
559 | |
560 | if(Length > 0 && (pLine == pHL || pHL[-1] == ' ') && (pHL[Length] == 0 || pHL[Length] == ' ' || pHL[Length] == '.' || pHL[Length] == '!' || pHL[Length] == ',' || pHL[Length] == '?' || pHL[Length] == ':')) |
561 | return true; |
562 | } |
563 | |
564 | return false; |
565 | } |
566 | |
567 | #define SAVES_FILE "ddnet-saves.txt" |
568 | const char *[] = { |
569 | "Time" , |
570 | "Player" , |
571 | "Map" , |
572 | "Code" , |
573 | }; |
574 | |
575 | void CChat::StoreSave(const char *pText) |
576 | { |
577 | const char *pStart = str_find(haystack: pText, needle: "Team successfully saved by " ); |
578 | const char *pMid = str_find(haystack: pText, needle: ". Use '/load " ); |
579 | const char *pOn = str_find(haystack: pText, needle: "' on " ); |
580 | const char *pEnd = str_find(haystack: pText, needle: pOn ? " to continue" : "' to continue" ); |
581 | |
582 | if(!pStart || !pMid || !pEnd || pMid < pStart || pEnd < pMid || (pOn && (pOn < pMid || pEnd < pOn))) |
583 | return; |
584 | |
585 | char aName[16]; |
586 | str_truncate(dst: aName, dst_size: sizeof(aName), src: pStart + 27, truncation_len: pMid - pStart - 27); |
587 | |
588 | char aSaveCode[64]; |
589 | |
590 | str_truncate(dst: aSaveCode, dst_size: sizeof(aSaveCode), src: pMid + 13, truncation_len: (pOn ? pOn : pEnd) - pMid - 13); |
591 | |
592 | char aTimestamp[20]; |
593 | str_timestamp_format(buffer: aTimestamp, buffer_size: sizeof(aTimestamp), FORMAT_SPACE); |
594 | |
595 | // TODO: Find a simple way to get the names of team members. This doesn't |
596 | // work since team is killed first, then save message gets sent: |
597 | /* |
598 | for(int i = 0; i < MAX_CLIENTS; i++) |
599 | { |
600 | const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_paInfoByDDTeam[i]; |
601 | if(!pInfo) |
602 | continue; |
603 | pInfo->m_Team // All 0 |
604 | } |
605 | */ |
606 | |
607 | IOHANDLE File = Storage()->OpenFile(SAVES_FILE, Flags: IOFLAG_APPEND, Type: IStorage::TYPE_SAVE); |
608 | if(!File) |
609 | return; |
610 | |
611 | const char *apColumns[4] = { |
612 | aTimestamp, |
613 | aName, |
614 | Client()->GetCurrentMap(), |
615 | aSaveCode, |
616 | }; |
617 | |
618 | if(io_tell(io: File) == 0) |
619 | { |
620 | CsvWrite(File, NumColumns: 4, ppColumns: SAVES_HEADER); |
621 | } |
622 | CsvWrite(File, NumColumns: 4, ppColumns: apColumns); |
623 | io_close(io: File); |
624 | } |
625 | |
626 | void CChat::AddLine(int ClientId, int Team, const char *pLine) |
627 | { |
628 | if(*pLine == 0 || |
629 | (ClientId == SERVER_MSG && !g_Config.m_ClShowChatSystem) || |
630 | (ClientId >= 0 && (m_pClient->m_aClients[ClientId].m_aName[0] == '\0' || // unknown client |
631 | m_pClient->m_aClients[ClientId].m_ChatIgnore || |
632 | (m_pClient->m_Snap.m_LocalClientId != ClientId && g_Config.m_ClShowChatFriends && !m_pClient->m_aClients[ClientId].m_Friend) || |
633 | (m_pClient->m_Snap.m_LocalClientId != ClientId && g_Config.m_ClShowChatTeamMembersOnly && m_pClient->IsOtherTeam(ClientId) && m_pClient->m_Teams.Team(ClientId: m_pClient->m_Snap.m_LocalClientId) != TEAM_FLOCK) || |
634 | (m_pClient->m_Snap.m_LocalClientId != ClientId && m_pClient->m_aClients[ClientId].m_Foe)))) |
635 | return; |
636 | |
637 | // trim right and set maximum length to 256 utf8-characters |
638 | int Length = 0; |
639 | const char *pStr = pLine; |
640 | const char *pEnd = 0; |
641 | while(*pStr) |
642 | { |
643 | const char *pStrOld = pStr; |
644 | int Code = str_utf8_decode(ptr: &pStr); |
645 | |
646 | // check if unicode is not empty |
647 | if(!str_utf8_isspace(code: Code)) |
648 | { |
649 | pEnd = 0; |
650 | } |
651 | else if(pEnd == 0) |
652 | pEnd = pStrOld; |
653 | |
654 | if(++Length >= MAX_LINE_LENGTH) |
655 | { |
656 | *(const_cast<char *>(pStr)) = 0; |
657 | break; |
658 | } |
659 | } |
660 | if(pEnd != 0) |
661 | *(const_cast<char *>(pEnd)) = 0; |
662 | |
663 | bool Highlighted = false; |
664 | char *p = const_cast<char *>(pLine); |
665 | |
666 | // Only empty string left |
667 | if(*p == 0) |
668 | return; |
669 | |
670 | auto &&FChatMsgCheckAndPrint = [this](CLine *pLine_) { |
671 | if(pLine_->m_ClientId < 0) // server or client message |
672 | { |
673 | if(Client()->State() != IClient::STATE_DEMOPLAYBACK) |
674 | StoreSave(pText: pLine_->m_aText); |
675 | } |
676 | |
677 | char aBuf[1024]; |
678 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%s%s" , pLine_->m_aName, pLine_->m_ClientId >= 0 ? ": " : "" , pLine_->m_aText); |
679 | |
680 | ColorRGBA ChatLogColor{1, 1, 1, 1}; |
681 | if(pLine_->m_Highlighted) |
682 | { |
683 | ChatLogColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageHighlightColor)); |
684 | } |
685 | else |
686 | { |
687 | if(pLine_->m_Friend && g_Config.m_ClMessageFriend) |
688 | ChatLogColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageFriendColor)); |
689 | else if(pLine_->m_Team) |
690 | ChatLogColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageTeamColor)); |
691 | else if(pLine_->m_ClientId == SERVER_MSG) |
692 | ChatLogColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageSystemColor)); |
693 | else if(pLine_->m_ClientId == CLIENT_MSG) |
694 | ChatLogColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageClientColor)); |
695 | else // regular message |
696 | ChatLogColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageColor)); |
697 | } |
698 | |
699 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: pLine_->m_Whisper ? "whisper" : (pLine_->m_Team ? "teamchat" : "chat" ), pStr: aBuf, PrintColor: ChatLogColor); |
700 | }; |
701 | |
702 | while(*p) |
703 | { |
704 | Highlighted = false; |
705 | pLine = p; |
706 | // find line separator and strip multiline |
707 | while(*p) |
708 | { |
709 | if(*p++ == '\n') |
710 | { |
711 | *(p - 1) = 0; |
712 | break; |
713 | } |
714 | } |
715 | |
716 | CLine *pCurrentLine = &m_aLines[m_CurrentLine]; |
717 | |
718 | // Team Number: |
719 | // 0 = global; 1 = team; 2 = sending whisper; 3 = receiving whisper |
720 | |
721 | // If it's a client message, m_aText will have ": " prepended so we have to work around it. |
722 | if(pCurrentLine->m_TeamNumber == Team && pCurrentLine->m_ClientId == ClientId && str_comp(a: pCurrentLine->m_aText, b: pLine) == 0) |
723 | { |
724 | pCurrentLine->m_TimesRepeated++; |
725 | TextRender()->DeleteTextContainer(TextContainerIndex&: pCurrentLine->m_TextContainerIndex); |
726 | Graphics()->DeleteQuadContainer(ContainerIndex&: pCurrentLine->m_QuadContainerIndex); |
727 | pCurrentLine->m_Time = time(); |
728 | pCurrentLine->m_aYOffset[0] = -1.f; |
729 | pCurrentLine->m_aYOffset[1] = -1.f; |
730 | |
731 | FChatMsgCheckAndPrint(pCurrentLine); |
732 | return; |
733 | } |
734 | |
735 | m_CurrentLine = (m_CurrentLine + 1) % MAX_LINES; |
736 | |
737 | pCurrentLine = &m_aLines[m_CurrentLine]; |
738 | pCurrentLine->m_TimesRepeated = 0; |
739 | pCurrentLine->m_Time = time(); |
740 | pCurrentLine->m_aYOffset[0] = -1.0f; |
741 | pCurrentLine->m_aYOffset[1] = -1.0f; |
742 | pCurrentLine->m_ClientId = ClientId; |
743 | pCurrentLine->m_TeamNumber = Team; |
744 | pCurrentLine->m_Team = Team == 1; |
745 | pCurrentLine->m_Whisper = Team >= 2; |
746 | pCurrentLine->m_NameColor = -2; |
747 | pCurrentLine->m_Friend = false; |
748 | pCurrentLine->m_HasRenderTee = false; |
749 | |
750 | TextRender()->DeleteTextContainer(TextContainerIndex&: pCurrentLine->m_TextContainerIndex); |
751 | Graphics()->DeleteQuadContainer(ContainerIndex&: pCurrentLine->m_QuadContainerIndex); |
752 | |
753 | // check for highlighted name |
754 | if(Client()->State() != IClient::STATE_DEMOPLAYBACK) |
755 | { |
756 | if(ClientId >= 0 && ClientId != m_pClient->m_aLocalIds[0] && (!m_pClient->Client()->DummyConnected() || ClientId != m_pClient->m_aLocalIds[1])) |
757 | { |
758 | // main character |
759 | Highlighted |= LineShouldHighlight(pLine, pName: m_pClient->m_aClients[m_pClient->m_aLocalIds[0]].m_aName); |
760 | // dummy |
761 | Highlighted |= m_pClient->Client()->DummyConnected() && LineShouldHighlight(pLine, pName: m_pClient->m_aClients[m_pClient->m_aLocalIds[1]].m_aName); |
762 | } |
763 | } |
764 | else |
765 | { |
766 | // on demo playback use local id from snap directly, |
767 | // since m_aLocalIds isn't valid there |
768 | Highlighted |= m_pClient->m_Snap.m_LocalClientId >= 0 && LineShouldHighlight(pLine, pName: m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_aName); |
769 | } |
770 | |
771 | pCurrentLine->m_Highlighted = Highlighted; |
772 | |
773 | if(pCurrentLine->m_ClientId == SERVER_MSG) |
774 | { |
775 | str_copy(dst&: pCurrentLine->m_aName, src: "*** " ); |
776 | str_copy(dst&: pCurrentLine->m_aText, src: pLine); |
777 | } |
778 | else if(pCurrentLine->m_ClientId == CLIENT_MSG) |
779 | { |
780 | str_copy(dst&: pCurrentLine->m_aName, src: "— " ); |
781 | str_copy(dst&: pCurrentLine->m_aText, src: pLine); |
782 | } |
783 | else |
784 | { |
785 | auto &LineAuthor = m_pClient->m_aClients[pCurrentLine->m_ClientId]; |
786 | |
787 | if(LineAuthor.m_Team == TEAM_SPECTATORS) |
788 | pCurrentLine->m_NameColor = TEAM_SPECTATORS; |
789 | |
790 | if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) |
791 | { |
792 | if(LineAuthor.m_Team == TEAM_RED) |
793 | pCurrentLine->m_NameColor = TEAM_RED; |
794 | else if(LineAuthor.m_Team == TEAM_BLUE) |
795 | pCurrentLine->m_NameColor = TEAM_BLUE; |
796 | } |
797 | |
798 | if(Team == TEAM_WHISPER_SEND) |
799 | { |
800 | str_format(buffer: pCurrentLine->m_aName, buffer_size: sizeof(pCurrentLine->m_aName), format: "→ %s" , LineAuthor.m_aName); |
801 | pCurrentLine->m_NameColor = TEAM_BLUE; |
802 | pCurrentLine->m_Highlighted = false; |
803 | Highlighted = false; |
804 | } |
805 | else if(Team == TEAM_WHISPER_RECV) |
806 | { |
807 | str_format(buffer: pCurrentLine->m_aName, buffer_size: sizeof(pCurrentLine->m_aName), format: "← %s" , LineAuthor.m_aName); |
808 | pCurrentLine->m_NameColor = TEAM_RED; |
809 | pCurrentLine->m_Highlighted = true; |
810 | Highlighted = true; |
811 | } |
812 | else |
813 | str_copy(dst&: pCurrentLine->m_aName, src: LineAuthor.m_aName); |
814 | |
815 | str_copy(dst&: pCurrentLine->m_aText, src: pLine); |
816 | pCurrentLine->m_Friend = LineAuthor.m_Friend; |
817 | |
818 | if(pCurrentLine->m_aName[0] != '\0') |
819 | { |
820 | if(!g_Config.m_ClChatOld) |
821 | { |
822 | pCurrentLine->m_CustomColoredSkin = LineAuthor.m_RenderInfo.m_CustomColoredSkin; |
823 | if(pCurrentLine->m_CustomColoredSkin) |
824 | pCurrentLine->m_RenderSkin = LineAuthor.m_RenderInfo.m_ColorableRenderSkin; |
825 | else |
826 | pCurrentLine->m_RenderSkin = LineAuthor.m_RenderInfo.m_OriginalRenderSkin; |
827 | |
828 | str_copy(dst&: pCurrentLine->m_aSkinName, src: LineAuthor.m_aSkinName); |
829 | pCurrentLine->m_ColorBody = LineAuthor.m_RenderInfo.m_ColorBody; |
830 | pCurrentLine->m_ColorFeet = LineAuthor.m_RenderInfo.m_ColorFeet; |
831 | |
832 | pCurrentLine->m_RenderSkinMetrics = LineAuthor.m_RenderInfo.m_SkinMetrics; |
833 | pCurrentLine->m_HasRenderTee = true; |
834 | } |
835 | } |
836 | } |
837 | |
838 | FChatMsgCheckAndPrint(pCurrentLine); |
839 | } |
840 | |
841 | // play sound |
842 | int64_t Now = time(); |
843 | if(ClientId == SERVER_MSG) |
844 | { |
845 | if(Now - m_aLastSoundPlayed[CHAT_SERVER] >= time_freq() * 3 / 10) |
846 | { |
847 | if(g_Config.m_SndServerMessage) |
848 | { |
849 | m_pClient->m_Sounds.Play(Channel: CSounds::CHN_GUI, SetId: SOUND_CHAT_SERVER, Vol: 0); |
850 | m_aLastSoundPlayed[CHAT_SERVER] = Now; |
851 | } |
852 | } |
853 | } |
854 | else if(ClientId == CLIENT_MSG) |
855 | { |
856 | // No sound yet |
857 | } |
858 | else if(Highlighted && Client()->State() != IClient::STATE_DEMOPLAYBACK) |
859 | { |
860 | if(Now - m_aLastSoundPlayed[CHAT_HIGHLIGHT] >= time_freq() * 3 / 10) |
861 | { |
862 | char aBuf[1024]; |
863 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s" , m_aLines[m_CurrentLine].m_aName, m_aLines[m_CurrentLine].m_aText); |
864 | Client()->Notify(pTitle: "DDNet Chat" , pMessage: aBuf); |
865 | if(g_Config.m_SndHighlight) |
866 | { |
867 | m_pClient->m_Sounds.Play(Channel: CSounds::CHN_GUI, SetId: SOUND_CHAT_HIGHLIGHT, Vol: 0); |
868 | m_aLastSoundPlayed[CHAT_HIGHLIGHT] = Now; |
869 | } |
870 | |
871 | if(g_Config.m_ClEditor) |
872 | { |
873 | GameClient()->Editor()->UpdateMentions(); |
874 | } |
875 | } |
876 | } |
877 | else if(Team != TEAM_WHISPER_SEND) |
878 | { |
879 | if(Now - m_aLastSoundPlayed[CHAT_CLIENT] >= time_freq() * 3 / 10) |
880 | { |
881 | bool PlaySound = m_aLines[m_CurrentLine].m_Team ? g_Config.m_SndTeamChat : g_Config.m_SndChat; |
882 | #if defined(CONF_VIDEORECORDER) |
883 | if(IVideo::Current()) |
884 | { |
885 | PlaySound &= (bool)g_Config.m_ClVideoShowChat; |
886 | } |
887 | #endif |
888 | if(PlaySound) |
889 | { |
890 | m_pClient->m_Sounds.Play(Channel: CSounds::CHN_GUI, SetId: SOUND_CHAT_CLIENT, Vol: 0); |
891 | m_aLastSoundPlayed[CHAT_CLIENT] = Now; |
892 | } |
893 | } |
894 | } |
895 | } |
896 | |
897 | void CChat::OnRefreshSkins() |
898 | { |
899 | for(auto &Line : m_aLines) |
900 | { |
901 | if(Line.m_HasRenderTee) |
902 | { |
903 | const CSkin *pSkin = m_pClient->m_Skins.Find(pName: Line.m_aSkinName); |
904 | if(Line.m_CustomColoredSkin) |
905 | Line.m_RenderSkin = pSkin->m_ColorableSkin; |
906 | else |
907 | Line.m_RenderSkin = pSkin->m_OriginalSkin; |
908 | |
909 | Line.m_RenderSkinMetrics = pSkin->m_Metrics; |
910 | } |
911 | else |
912 | { |
913 | Line.m_RenderSkin.Reset(); |
914 | } |
915 | } |
916 | } |
917 | |
918 | void CChat::OnPrepareLines(float y) |
919 | { |
920 | float x = 5.0f; |
921 | float FontSize = this->FontSize(); |
922 | |
923 | const bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (Graphics()->ScreenAspect() > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9) |
924 | const bool ShowLargeArea = m_Show || (m_Mode != MODE_NONE && g_Config.m_ClShowChat == 1) || g_Config.m_ClShowChat == 2; |
925 | const bool ForceRecreate = IsScoreBoardOpen != m_PrevScoreBoardShowed || ShowLargeArea != m_PrevShowChat; |
926 | m_PrevScoreBoardShowed = IsScoreBoardOpen; |
927 | m_PrevShowChat = ShowLargeArea; |
928 | |
929 | const int TeeSize = MessageTeeSize(); |
930 | float RealMsgPaddingX = MessagePaddingX(); |
931 | float RealMsgPaddingY = MessagePaddingY(); |
932 | float RealMsgPaddingTee = TeeSize + MESSAGE_TEE_PADDING_RIGHT; |
933 | |
934 | if(g_Config.m_ClChatOld) |
935 | { |
936 | RealMsgPaddingX = 0; |
937 | RealMsgPaddingY = 0; |
938 | RealMsgPaddingTee = 0; |
939 | } |
940 | |
941 | int64_t Now = time(); |
942 | float LineWidth = (IsScoreBoardOpen ? maximum(a: 85.f, b: (FontSize * 85.0f / 6.f)) : g_Config.m_ClChatWidth) - (RealMsgPaddingX * 1.5f) - RealMsgPaddingTee; |
943 | |
944 | float HeightLimit = IsScoreBoardOpen ? 180.0f : (m_PrevShowChat ? 50.0f : 200.0f); |
945 | float Begin = x; |
946 | float TextBegin = Begin + RealMsgPaddingX / 2.0f; |
947 | CTextCursor Cursor; |
948 | int OffsetType = IsScoreBoardOpen ? 1 : 0; |
949 | |
950 | for(int i = 0; i < MAX_LINES; i++) |
951 | { |
952 | CLine &Line = m_aLines[((m_CurrentLine - i) + MAX_LINES) % MAX_LINES]; |
953 | |
954 | if(Now > Line.m_Time + 16 * time_freq() && !m_PrevShowChat) |
955 | break; |
956 | |
957 | if(Line.m_TextContainerIndex.Valid() && !ForceRecreate) |
958 | continue; |
959 | |
960 | TextRender()->DeleteTextContainer(TextContainerIndex&: Line.m_TextContainerIndex); |
961 | Graphics()->DeleteQuadContainer(ContainerIndex&: Line.m_QuadContainerIndex); |
962 | |
963 | char aName[64 + 12] = "" ; |
964 | |
965 | if(g_Config.m_ClShowIds && Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') |
966 | { |
967 | if(Line.m_ClientId < 10) |
968 | str_format(buffer: aName, buffer_size: sizeof(aName), format: " %d: " , Line.m_ClientId); |
969 | else |
970 | str_format(buffer: aName, buffer_size: sizeof(aName), format: "%d: " , Line.m_ClientId); |
971 | } |
972 | |
973 | str_append(dst&: aName, src: Line.m_aName); |
974 | |
975 | char aCount[12]; |
976 | if(Line.m_ClientId < 0) |
977 | str_format(buffer: aCount, buffer_size: sizeof(aCount), format: "[%d] " , Line.m_TimesRepeated + 1); |
978 | else |
979 | str_format(buffer: aCount, buffer_size: sizeof(aCount), format: " [%d]" , Line.m_TimesRepeated + 1); |
980 | |
981 | const char *pText = Line.m_aText; |
982 | if(Config()->m_ClStreamerMode && Line.m_ClientId == SERVER_MSG) |
983 | { |
984 | if(str_startswith(str: Line.m_aText, prefix: "Team save in progress. You'll be able to load with '/load" ) && str_endswith(str: Line.m_aText, suffix: "if it fails" )) |
985 | { |
986 | pText = "Team save in progress. You'll be able to load with '/load ***' if save is successful or with '/load *** *** ***' if it fails" ; |
987 | } |
988 | else if(str_startswith(str: Line.m_aText, prefix: "Team successfully saved by " ) && str_endswith(str: Line.m_aText, suffix: " to continue" )) |
989 | { |
990 | pText = "Team successfully saved by ***. Use '/load ***' to continue" ; |
991 | } |
992 | } |
993 | |
994 | if(g_Config.m_ClChatOld) |
995 | { |
996 | Line.m_HasRenderTee = false; |
997 | } |
998 | |
999 | // get the y offset (calculate it if we haven't done that yet) |
1000 | if(Line.m_aYOffset[OffsetType] < 0.0f) |
1001 | { |
1002 | TextRender()->SetCursor(pCursor: &Cursor, x: TextBegin, y: 0.0f, FontSize, Flags: 0); |
1003 | Cursor.m_LineWidth = LineWidth; |
1004 | |
1005 | if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') |
1006 | { |
1007 | Cursor.m_X += RealMsgPaddingTee; |
1008 | |
1009 | if(Line.m_Friend && g_Config.m_ClMessageFriend) |
1010 | { |
1011 | TextRender()->TextEx(pCursor: &Cursor, pText: "♥ " ); |
1012 | } |
1013 | } |
1014 | |
1015 | TextRender()->TextEx(pCursor: &Cursor, pText: aName); |
1016 | if(Line.m_TimesRepeated > 0) |
1017 | TextRender()->TextEx(pCursor: &Cursor, pText: aCount); |
1018 | |
1019 | if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') |
1020 | { |
1021 | TextRender()->TextEx(pCursor: &Cursor, pText: ": " ); |
1022 | } |
1023 | |
1024 | CTextCursor AppendCursor = Cursor; |
1025 | AppendCursor.m_LongestLineWidth = 0.0f; |
1026 | if(!IsScoreBoardOpen && !g_Config.m_ClChatOld) |
1027 | { |
1028 | AppendCursor.m_StartX = Cursor.m_X; |
1029 | AppendCursor.m_LineWidth -= Cursor.m_LongestLineWidth; |
1030 | } |
1031 | |
1032 | TextRender()->TextEx(pCursor: &AppendCursor, pText); |
1033 | |
1034 | Line.m_aYOffset[OffsetType] = AppendCursor.Height() + RealMsgPaddingY; |
1035 | } |
1036 | |
1037 | y -= Line.m_aYOffset[OffsetType]; |
1038 | |
1039 | // cut off if msgs waste too much space |
1040 | if(y < HeightLimit) |
1041 | break; |
1042 | |
1043 | // the position the text was created |
1044 | Line.m_TextYOffset = y + RealMsgPaddingY / 2.f; |
1045 | |
1046 | int CurRenderFlags = TextRender()->GetRenderFlags(); |
1047 | TextRender()->SetRenderFlags(CurRenderFlags | ETextRenderFlags::TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD); |
1048 | |
1049 | // reset the cursor |
1050 | TextRender()->SetCursor(pCursor: &Cursor, x: TextBegin, y: Line.m_TextYOffset, FontSize, Flags: TEXTFLAG_RENDER); |
1051 | Cursor.m_LineWidth = LineWidth; |
1052 | |
1053 | // Message is from valid player |
1054 | if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') |
1055 | { |
1056 | Cursor.m_X += RealMsgPaddingTee; |
1057 | |
1058 | if(Line.m_Friend && g_Config.m_ClMessageFriend) |
1059 | { |
1060 | TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageFriendColor)).WithAlpha(alpha: 1.f)); |
1061 | TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: Line.m_TextContainerIndex, pCursor: &Cursor, pText: "♥ " ); |
1062 | } |
1063 | } |
1064 | |
1065 | // render name |
1066 | ColorRGBA NameColor; |
1067 | if(Line.m_ClientId == SERVER_MSG) |
1068 | NameColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageSystemColor)); |
1069 | else if(Line.m_ClientId == CLIENT_MSG) |
1070 | NameColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageClientColor)); |
1071 | else if(Line.m_Team) |
1072 | NameColor = CalculateNameColor(TextColorHSL: ColorHSLA(g_Config.m_ClMessageTeamColor)); |
1073 | else if(Line.m_NameColor == TEAM_RED) |
1074 | NameColor = ColorRGBA(1.0f, 0.5f, 0.5f, 1.f); |
1075 | else if(Line.m_NameColor == TEAM_BLUE) |
1076 | NameColor = ColorRGBA(0.7f, 0.7f, 1.0f, 1.f); |
1077 | else if(Line.m_NameColor == TEAM_SPECTATORS) |
1078 | NameColor = ColorRGBA(0.75f, 0.5f, 0.75f, 1.f); |
1079 | else if(Line.m_ClientId >= 0 && g_Config.m_ClChatTeamColors && m_pClient->m_Teams.Team(ClientId: Line.m_ClientId)) |
1080 | NameColor = m_pClient->GetDDTeamColor(DDTeam: m_pClient->m_Teams.Team(ClientId: Line.m_ClientId), Lightness: 0.75f); |
1081 | else |
1082 | NameColor = ColorRGBA(0.8f, 0.8f, 0.8f, 1.f); |
1083 | |
1084 | TextRender()->TextColor(rgb: NameColor); |
1085 | TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: Line.m_TextContainerIndex, pCursor: &Cursor, pText: aName); |
1086 | |
1087 | if(Line.m_TimesRepeated > 0) |
1088 | { |
1089 | TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.3f); |
1090 | TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: Line.m_TextContainerIndex, pCursor: &Cursor, pText: aCount); |
1091 | } |
1092 | |
1093 | if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') |
1094 | { |
1095 | TextRender()->TextColor(rgb: NameColor); |
1096 | TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: Line.m_TextContainerIndex, pCursor: &Cursor, pText: ": " ); |
1097 | } |
1098 | |
1099 | // render line |
1100 | ColorRGBA Color; |
1101 | if(Line.m_ClientId == SERVER_MSG) |
1102 | Color = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageSystemColor)); |
1103 | else if(Line.m_ClientId == CLIENT_MSG) |
1104 | Color = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageClientColor)); |
1105 | else if(Line.m_Highlighted) |
1106 | Color = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageHighlightColor)); |
1107 | else if(Line.m_Team) |
1108 | Color = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageTeamColor)); |
1109 | else // regular message |
1110 | Color = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClMessageColor)); |
1111 | |
1112 | TextRender()->TextColor(rgb: Color); |
1113 | |
1114 | CTextCursor AppendCursor = Cursor; |
1115 | AppendCursor.m_LongestLineWidth = 0.0f; |
1116 | if(!IsScoreBoardOpen && !g_Config.m_ClChatOld) |
1117 | { |
1118 | AppendCursor.m_StartX = Cursor.m_X; |
1119 | AppendCursor.m_LineWidth -= Cursor.m_LongestLineWidth; |
1120 | } |
1121 | |
1122 | TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: Line.m_TextContainerIndex, pCursor: &AppendCursor, pText); |
1123 | |
1124 | if(!g_Config.m_ClChatOld && (Line.m_aText[0] != '\0' || Line.m_aName[0] != '\0')) |
1125 | { |
1126 | float FullWidth = RealMsgPaddingX * 1.5f; |
1127 | if(!IsScoreBoardOpen && !g_Config.m_ClChatOld) |
1128 | { |
1129 | FullWidth += Cursor.m_LongestLineWidth + AppendCursor.m_LongestLineWidth; |
1130 | } |
1131 | else |
1132 | { |
1133 | FullWidth += maximum(a: Cursor.m_LongestLineWidth, b: AppendCursor.m_LongestLineWidth); |
1134 | } |
1135 | Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1); |
1136 | Line.m_QuadContainerIndex = Graphics()->CreateRectQuadContainer(x: Begin, y, w: FullWidth, h: Line.m_aYOffset[OffsetType], r: MessageRounding(), Corners: IGraphics::CORNER_ALL); |
1137 | } |
1138 | |
1139 | TextRender()->SetRenderFlags(CurRenderFlags); |
1140 | if(Line.m_TextContainerIndex.Valid()) |
1141 | TextRender()->UploadTextContainer(TextContainerIndex: Line.m_TextContainerIndex); |
1142 | } |
1143 | |
1144 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
1145 | } |
1146 | |
1147 | void CChat::OnRender() |
1148 | { |
1149 | if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) |
1150 | return; |
1151 | |
1152 | // send pending chat messages |
1153 | if(m_PendingChatCounter > 0 && m_LastChatSend + time_freq() < time()) |
1154 | { |
1155 | CHistoryEntry *pEntry = m_History.Last(); |
1156 | for(int i = m_PendingChatCounter - 1; pEntry; --i, pEntry = m_History.Prev(pCurrent: pEntry)) |
1157 | { |
1158 | if(i == 0) |
1159 | { |
1160 | SendChat(Team: pEntry->m_Team, pLine: pEntry->m_aText); |
1161 | break; |
1162 | } |
1163 | } |
1164 | --m_PendingChatCounter; |
1165 | } |
1166 | |
1167 | const float Height = 300.0f; |
1168 | const float Width = Height * Graphics()->ScreenAspect(); |
1169 | Graphics()->MapScreen(TopLeftX: 0.0f, TopLeftY: 0.0f, BottomRightX: Width, BottomRightY: Height); |
1170 | |
1171 | float x = 5.0f; |
1172 | float y = 300.0f - 20.0f * FontSize() / 6.f; |
1173 | float ScaledFontSize = FontSize() * (8 / 6.f); |
1174 | if(m_Mode != MODE_NONE) |
1175 | { |
1176 | // render chat input |
1177 | CTextCursor Cursor; |
1178 | TextRender()->SetCursor(pCursor: &Cursor, x, y, FontSize: ScaledFontSize, Flags: TEXTFLAG_RENDER); |
1179 | Cursor.m_LineWidth = Width - 190.0f; |
1180 | |
1181 | if(m_Mode == MODE_ALL) |
1182 | TextRender()->TextEx(pCursor: &Cursor, pText: Localize(pStr: "All" )); |
1183 | else if(m_Mode == MODE_TEAM) |
1184 | TextRender()->TextEx(pCursor: &Cursor, pText: Localize(pStr: "Team" )); |
1185 | else |
1186 | TextRender()->TextEx(pCursor: &Cursor, pText: Localize(pStr: "Chat" )); |
1187 | |
1188 | TextRender()->TextEx(pCursor: &Cursor, pText: ": " ); |
1189 | |
1190 | const float MessageMaxWidth = Cursor.m_LineWidth - (Cursor.m_X - Cursor.m_StartX); |
1191 | const CUIRect ClippingRect = {.x: Cursor.m_X, .y: Cursor.m_Y, .w: MessageMaxWidth, .h: 2.25f * Cursor.m_FontSize}; |
1192 | const float XScale = Graphics()->ScreenWidth() / Width; |
1193 | const float YScale = Graphics()->ScreenHeight() / Height; |
1194 | Graphics()->ClipEnable(x: (int)(ClippingRect.x * XScale), y: (int)(ClippingRect.y * YScale), w: (int)(ClippingRect.w * XScale), h: (int)(ClippingRect.h * YScale)); |
1195 | |
1196 | float ScrollOffset = m_Input.GetScrollOffset(); |
1197 | float ScrollOffsetChange = m_Input.GetScrollOffsetChange(); |
1198 | |
1199 | m_Input.Activate(Priority: EInputPriority::CHAT); // Ensure that the input is active |
1200 | const CUIRect InputCursorRect = {.x: Cursor.m_X, .y: Cursor.m_Y - ScrollOffset, .w: 0.0f, .h: 0.0f}; |
1201 | const bool WasChanged = m_Input.WasChanged(); |
1202 | const bool WasCursorChanged = m_Input.WasCursorChanged(); |
1203 | const bool Changed = WasChanged || WasCursorChanged; |
1204 | const STextBoundingBox BoundingBox = m_Input.Render(pRect: &InputCursorRect, FontSize: Cursor.m_FontSize, Align: TEXTALIGN_TL, Changed, LineWidth: MessageMaxWidth, LineSpacing: 0.0f); |
1205 | |
1206 | Graphics()->ClipDisable(); |
1207 | |
1208 | // Scroll up or down to keep the caret inside the clipping rect |
1209 | const float CaretPositionY = m_Input.GetCaretPosition().y - ScrollOffsetChange; |
1210 | if(CaretPositionY < ClippingRect.y) |
1211 | ScrollOffsetChange -= ClippingRect.y - CaretPositionY; |
1212 | else if(CaretPositionY + Cursor.m_FontSize > ClippingRect.y + ClippingRect.h) |
1213 | ScrollOffsetChange += CaretPositionY + Cursor.m_FontSize - (ClippingRect.y + ClippingRect.h); |
1214 | |
1215 | Ui()->DoSmoothScrollLogic(pScrollOffset: &ScrollOffset, pScrollOffsetChange: &ScrollOffsetChange, ViewPortSize: ClippingRect.h, TotalSize: BoundingBox.m_H); |
1216 | |
1217 | m_Input.SetScrollOffset(ScrollOffset); |
1218 | m_Input.SetScrollOffsetChange(ScrollOffsetChange); |
1219 | } |
1220 | |
1221 | #if defined(CONF_VIDEORECORDER) |
1222 | if(!((g_Config.m_ClShowChat && !IVideo::Current()) || (g_Config.m_ClVideoShowChat && IVideo::Current()))) |
1223 | #else |
1224 | if(!g_Config.m_ClShowChat) |
1225 | #endif |
1226 | return; |
1227 | |
1228 | y -= ScaledFontSize; |
1229 | |
1230 | OnPrepareLines(y); |
1231 | |
1232 | bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (Graphics()->ScreenAspect() > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9) |
1233 | |
1234 | int64_t Now = time(); |
1235 | float HeightLimit = IsScoreBoardOpen ? 180.0f : (m_PrevShowChat ? 50.0f : 200.0f); |
1236 | int OffsetType = IsScoreBoardOpen ? 1 : 0; |
1237 | |
1238 | float RealMsgPaddingX = MessagePaddingX(); |
1239 | float RealMsgPaddingY = MessagePaddingY(); |
1240 | |
1241 | if(g_Config.m_ClChatOld) |
1242 | { |
1243 | RealMsgPaddingX = 0; |
1244 | RealMsgPaddingY = 0; |
1245 | } |
1246 | |
1247 | for(int i = 0; i < MAX_LINES; i++) |
1248 | { |
1249 | CLine &Line = m_aLines[((m_CurrentLine - i) + MAX_LINES) % MAX_LINES]; |
1250 | if(Now > Line.m_Time + 16 * time_freq() && !m_PrevShowChat) |
1251 | break; |
1252 | |
1253 | y -= Line.m_aYOffset[OffsetType]; |
1254 | |
1255 | // cut off if msgs waste too much space |
1256 | if(y < HeightLimit) |
1257 | break; |
1258 | |
1259 | float Blend = Now > Line.m_Time + 14 * time_freq() && !m_PrevShowChat ? 1.0f - (Now - Line.m_Time - 14 * time_freq()) / (2.0f * time_freq()) : 1.0f; |
1260 | |
1261 | // Draw backgrounds for messages in one batch |
1262 | if(!g_Config.m_ClChatOld) |
1263 | { |
1264 | Graphics()->TextureClear(); |
1265 | if(Line.m_QuadContainerIndex != -1) |
1266 | { |
1267 | Graphics()->SetColor(r: 0, g: 0, b: 0, a: 0.12f * Blend); |
1268 | Graphics()->RenderQuadContainerEx(ContainerIndex: Line.m_QuadContainerIndex, QuadOffset: 0, QuadDrawNum: -1, X: 0, Y: ((y + RealMsgPaddingY / 2.0f) - Line.m_TextYOffset)); |
1269 | } |
1270 | } |
1271 | |
1272 | if(Line.m_TextContainerIndex.Valid()) |
1273 | { |
1274 | if(!g_Config.m_ClChatOld && Line.m_HasRenderTee) |
1275 | { |
1276 | const int TeeSize = MessageTeeSize(); |
1277 | CTeeRenderInfo RenderInfo; |
1278 | RenderInfo.m_CustomColoredSkin = Line.m_CustomColoredSkin; |
1279 | if(Line.m_CustomColoredSkin) |
1280 | RenderInfo.m_ColorableRenderSkin = Line.m_RenderSkin; |
1281 | else |
1282 | RenderInfo.m_OriginalRenderSkin = Line.m_RenderSkin; |
1283 | RenderInfo.m_SkinMetrics = Line.m_RenderSkinMetrics; |
1284 | |
1285 | RenderInfo.m_ColorBody = Line.m_ColorBody; |
1286 | RenderInfo.m_ColorFeet = Line.m_ColorFeet; |
1287 | RenderInfo.m_Size = TeeSize; |
1288 | |
1289 | float RowHeight = FontSize() + RealMsgPaddingY; |
1290 | float OffsetTeeY = TeeSize / 2.0f; |
1291 | float FullHeightMinusTee = RowHeight - TeeSize; |
1292 | |
1293 | const CAnimState *pIdleState = CAnimState::GetIdle(); |
1294 | vec2 OffsetToMid; |
1295 | CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &RenderInfo, TeeOffsetToMid&: OffsetToMid); |
1296 | vec2 TeeRenderPos(x + (RealMsgPaddingX + TeeSize) / 2.0f, y + OffsetTeeY + FullHeightMinusTee / 2.0f + OffsetToMid.y); |
1297 | RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &RenderInfo, Emote: EMOTE_NORMAL, Dir: vec2(1, 0.1f), Pos: TeeRenderPos, Alpha: Blend); |
1298 | } |
1299 | |
1300 | const ColorRGBA TextColor = TextRender()->DefaultTextColor().WithMultipliedAlpha(alpha: Blend); |
1301 | const ColorRGBA TextOutlineColor = TextRender()->DefaultTextOutlineColor().WithMultipliedAlpha(alpha: Blend); |
1302 | TextRender()->RenderTextContainer(TextContainerIndex: Line.m_TextContainerIndex, TextColor, TextOutlineColor, X: 0, Y: (y + RealMsgPaddingY / 2.0f) - Line.m_TextYOffset); |
1303 | } |
1304 | } |
1305 | } |
1306 | |
1307 | void CChat::EnsureCoherentFontSize() const |
1308 | { |
1309 | // Adjust font size based on width |
1310 | if(g_Config.m_ClChatWidth / (float)g_Config.m_ClChatFontSize >= CHAT_FONTSIZE_WIDTH_RATIO) |
1311 | return; |
1312 | |
1313 | // We want to keep a ration between font size and font width so that we don't have a weird rendering |
1314 | g_Config.m_ClChatFontSize = g_Config.m_ClChatWidth / CHAT_FONTSIZE_WIDTH_RATIO; |
1315 | } |
1316 | |
1317 | void CChat::EnsureCoherentWidth() const |
1318 | { |
1319 | // Adjust width based on font size |
1320 | if(g_Config.m_ClChatWidth / (float)g_Config.m_ClChatFontSize >= CHAT_FONTSIZE_WIDTH_RATIO) |
1321 | return; |
1322 | |
1323 | // We want to keep a ration between font size and font width so that we don't have a weird rendering |
1324 | g_Config.m_ClChatWidth = CHAT_FONTSIZE_WIDTH_RATIO * g_Config.m_ClChatFontSize; |
1325 | } |
1326 | |
1327 | // ----- send functions ----- |
1328 | |
1329 | void CChat::SendChat(int Team, const char *pLine) |
1330 | { |
1331 | // don't send empty messages |
1332 | if(*str_utf8_skip_whitespaces(str: pLine) == '\0') |
1333 | return; |
1334 | |
1335 | m_LastChatSend = time(); |
1336 | |
1337 | // send chat message |
1338 | CNetMsg_Cl_Say Msg; |
1339 | Msg.m_Team = Team; |
1340 | Msg.m_pMessage = pLine; |
1341 | Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL); |
1342 | } |
1343 | |
1344 | void CChat::SendChatQueued(const char *pLine) |
1345 | { |
1346 | if(!pLine || str_length(str: pLine) < 1) |
1347 | return; |
1348 | |
1349 | bool AddEntry = false; |
1350 | |
1351 | if(m_LastChatSend + time_freq() < time()) |
1352 | { |
1353 | SendChat(Team: m_Mode == MODE_ALL ? 0 : 1, pLine); |
1354 | AddEntry = true; |
1355 | } |
1356 | else if(m_PendingChatCounter < 3) |
1357 | { |
1358 | ++m_PendingChatCounter; |
1359 | AddEntry = true; |
1360 | } |
1361 | |
1362 | if(AddEntry) |
1363 | { |
1364 | const int Length = str_length(str: pLine); |
1365 | CHistoryEntry *pEntry = m_History.Allocate(Size: sizeof(CHistoryEntry) + Length); |
1366 | pEntry->m_Team = m_Mode == MODE_ALL ? 0 : 1; |
1367 | str_copy(dst: pEntry->m_aText, src: pLine, dst_size: Length + 1); |
1368 | } |
1369 | } |
1370 | |