1 /*
2  * This file is part of OpenTTD.
3  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6  */
7 
8 /** @file network_chat_gui.cpp GUI for handling chat messages. */
9 
10 #include "../stdafx.h"
11 #include "../strings_func.h"
12 #include "../blitter/factory.hpp"
13 #include "../console_func.h"
14 #include "../video/video_driver.hpp"
15 #include "../querystring_gui.h"
16 #include "../town.h"
17 #include "../window_func.h"
18 #include "../toolbar_gui.h"
19 #include "../core/geometry_func.hpp"
20 #include "network.h"
21 #include "network_client.h"
22 #include "network_base.h"
23 
24 #include "../widgets/network_chat_widget.h"
25 
26 #include "table/strings.h"
27 
28 #include <stdarg.h> /* va_list */
29 #include <deque>
30 
31 #include "../safeguards.h"
32 
33 /** The draw buffer must be able to contain the chat message, client name and the "[All]" message,
34  * some spaces and possible translations of [All] to other languages. */
35 static_assert((int)DRAW_STRING_BUFFER >= (int)NETWORK_CHAT_LENGTH + NETWORK_NAME_LENGTH + 40);
36 
37 /** Spacing between chat lines. */
38 static const uint NETWORK_CHAT_LINE_SPACING = 3;
39 
40 /** Container for a message. */
41 struct ChatMessage {
42 	std::string message; ///< The action message.
43 	TextColour colour;  ///< The colour of the message.
44 	std::chrono::steady_clock::time_point remove_time; ///< The time to remove the message.
45 };
46 
47 /* used for chat window */
48 static std::deque<ChatMessage> _chatmsg_list; ///< The actual chat message list.
49 static bool _chatmessage_dirty = false;   ///< Does the chat message need repainting?
50 static bool _chatmessage_visible = false; ///< Is a chat message visible.
51 static bool _chat_tab_completion_active;  ///< Whether tab completion is active.
52 static uint MAX_CHAT_MESSAGES = 0;        ///< The limit of chat messages to show.
53 
54 /**
55  * Time the chat history was marked dirty. This is used to determine if expired
56  * messages have recently expired and should cause a redraw to hide them.
57  */
58 static std::chrono::steady_clock::time_point _chatmessage_dirty_time;
59 
60 /**
61  * The chatbox grows from the bottom so the coordinates are pixels from
62  * the left and pixels from the bottom. The height is the maximum height.
63  */
64 static PointDimension _chatmsg_box;
65 static uint8 *_chatmessage_backup = nullptr; ///< Backup in case text is moved.
66 
67 /**
68  * Test if there are any chat messages to display.
69  * @param show_all Set if all messages should be included, instead of unexpired only.
70  * @return True iff there are chat messages to display.
71  */
HaveChatMessages(bool show_all)72 static inline bool HaveChatMessages(bool show_all)
73 {
74 	if (show_all) return _chatmsg_list.size() != 0;
75 
76 	auto now = std::chrono::steady_clock::now();
77 	for (auto &cmsg : _chatmsg_list) {
78 		if (cmsg.remove_time >= now) return true;
79 	}
80 
81 	return false;
82 }
83 
84 /**
85  * Add a text message to the 'chat window' to be shown
86  * @param colour The colour this message is to be shown in
87  * @param duration The duration of the chat message in seconds
88  * @param message message itself in printf() style
89  */
NetworkAddChatMessage(TextColour colour,uint duration,const std::string & message)90 void CDECL NetworkAddChatMessage(TextColour colour, uint duration, const std::string &message)
91 {
92 	if (_chatmsg_list.size() == MAX_CHAT_MESSAGES) {
93 		_chatmsg_list.pop_back();
94 	}
95 
96 	ChatMessage *cmsg = &_chatmsg_list.emplace_front();
97 	cmsg->message = message;
98 	cmsg->colour = colour;
99 	cmsg->remove_time = std::chrono::steady_clock::now() + std::chrono::seconds(duration);
100 
101 	_chatmessage_dirty_time = std::chrono::steady_clock::now();
102 	_chatmessage_dirty = true;
103 }
104 
105 /** Initialize all font-dependent chat box sizes. */
NetworkReInitChatBoxSize()106 void NetworkReInitChatBoxSize()
107 {
108 	_chatmsg_box.y       = 3 * FONT_HEIGHT_NORMAL;
109 	_chatmsg_box.height  = MAX_CHAT_MESSAGES * (FONT_HEIGHT_NORMAL + NETWORK_CHAT_LINE_SPACING) + 4;
110 	_chatmessage_backup  = ReallocT(_chatmessage_backup, _chatmsg_box.width * _chatmsg_box.height * BlitterFactory::GetCurrentBlitter()->GetBytesPerPixel());
111 }
112 
113 /** Initialize all buffers of the chat visualisation. */
NetworkInitChatMessage()114 void NetworkInitChatMessage()
115 {
116 	MAX_CHAT_MESSAGES    = _settings_client.gui.network_chat_box_height;
117 
118 	_chatmsg_list.clear();
119 	_chatmsg_box.x       = 10;
120 	_chatmsg_box.width   = _settings_client.gui.network_chat_box_width_pct * _screen.width / 100;
121 	NetworkReInitChatBoxSize();
122 	_chatmessage_visible = false;
123 }
124 
125 /** Hide the chatbox */
NetworkUndrawChatMessage()126 void NetworkUndrawChatMessage()
127 {
128 	/* Sometimes we also need to hide the cursor
129 	 *   This is because both textmessage and the cursor take a shot of the
130 	 *   screen before drawing.
131 	 *   Now the textmessage takes its shot and paints its data before the cursor
132 	 *   does, so in the shot of the cursor is the screen-data of the textmessage
133 	 *   included when the cursor hangs somewhere over the textmessage. To
134 	 *   avoid wrong repaints, we undraw the cursor in that case, and everything
135 	 *   looks nicely ;)
136 	 * (and now hope this story above makes sense to you ;))
137 	 */
138 	if (_cursor.visible &&
139 			_cursor.draw_pos.x + _cursor.draw_size.x >= _chatmsg_box.x &&
140 			_cursor.draw_pos.x <= _chatmsg_box.x + _chatmsg_box.width &&
141 			_cursor.draw_pos.y + _cursor.draw_size.y >= _screen.height - _chatmsg_box.y - _chatmsg_box.height &&
142 			_cursor.draw_pos.y <= _screen.height - _chatmsg_box.y) {
143 		UndrawMouseCursor();
144 	}
145 
146 	if (_chatmessage_visible) {
147 		Blitter *blitter = BlitterFactory::GetCurrentBlitter();
148 		int x      = _chatmsg_box.x;
149 		int y      = _screen.height - _chatmsg_box.y - _chatmsg_box.height;
150 		int width  = _chatmsg_box.width;
151 		int height = _chatmsg_box.height;
152 		if (y < 0) {
153 			height = std::max(height + y, std::min(_chatmsg_box.height, _screen.height));
154 			y = 0;
155 		}
156 		if (x + width >= _screen.width) {
157 			width = _screen.width - x;
158 		}
159 		if (width <= 0 || height <= 0) return;
160 
161 		_chatmessage_visible = false;
162 		/* Put our 'shot' back to the screen */
163 		blitter->CopyFromBuffer(blitter->MoveTo(_screen.dst_ptr, x, y), _chatmessage_backup, width, height);
164 		/* And make sure it is updated next time */
165 		VideoDriver::GetInstance()->MakeDirty(x, y, width, height);
166 
167 		_chatmessage_dirty_time = std::chrono::steady_clock::now();
168 		_chatmessage_dirty = true;
169 	}
170 }
171 
172 /** Check if a message is expired. */
NetworkChatMessageLoop()173 void NetworkChatMessageLoop()
174 {
175 	auto now = std::chrono::steady_clock::now();
176 	for (auto &cmsg : _chatmsg_list) {
177 		/* Message has expired, remove from the list */
178 		if (now > cmsg.remove_time && _chatmessage_dirty_time < cmsg.remove_time) {
179 			_chatmessage_dirty_time = now;
180 			_chatmessage_dirty = true;
181 			break;
182 		}
183 	}
184 }
185 
186 /** Draw the chat message-box */
NetworkDrawChatMessage()187 void NetworkDrawChatMessage()
188 {
189 	Blitter *blitter = BlitterFactory::GetCurrentBlitter();
190 	if (!_chatmessage_dirty) return;
191 
192 	const Window *w = FindWindowByClass(WC_SEND_NETWORK_MSG);
193 	bool show_all = (w != nullptr);
194 
195 	/* First undraw if needed */
196 	NetworkUndrawChatMessage();
197 
198 	if (_iconsole_mode == ICONSOLE_FULL) return;
199 
200 	/* Check if we have anything to draw at all */
201 	if (!HaveChatMessages(show_all)) return;
202 
203 	int x      = _chatmsg_box.x;
204 	int y      = _screen.height - _chatmsg_box.y - _chatmsg_box.height;
205 	int width  = _chatmsg_box.width;
206 	int height = _chatmsg_box.height;
207 	if (y < 0) {
208 		height = std::max(height + y, std::min(_chatmsg_box.height, _screen.height));
209 		y = 0;
210 	}
211 	if (x + width >= _screen.width) {
212 		width = _screen.width - x;
213 	}
214 	if (width <= 0 || height <= 0) return;
215 
216 	assert(blitter->BufferSize(width, height) <= (int)(_chatmsg_box.width * _chatmsg_box.height * blitter->GetBytesPerPixel()));
217 
218 	/* Make a copy of the screen as it is before painting (for undraw) */
219 	blitter->CopyToBuffer(blitter->MoveTo(_screen.dst_ptr, x, y), _chatmessage_backup, width, height);
220 
221 	_cur_dpi = &_screen; // switch to _screen painting
222 
223 	auto now = std::chrono::steady_clock::now();
224 	int string_height = 0;
225 	for (auto &cmsg : _chatmsg_list) {
226 		if (!show_all && cmsg.remove_time < now) continue;
227 		SetDParamStr(0, cmsg.message);
228 		string_height += GetStringLineCount(STR_JUST_RAW_STRING, width - 1) * FONT_HEIGHT_NORMAL + NETWORK_CHAT_LINE_SPACING;
229 	}
230 
231 	string_height = std::min<uint>(string_height, MAX_CHAT_MESSAGES * (FONT_HEIGHT_NORMAL + NETWORK_CHAT_LINE_SPACING));
232 
233 	int top = _screen.height - _chatmsg_box.y - string_height - 2;
234 	int bottom = _screen.height - _chatmsg_box.y - 2;
235 	/* Paint a half-transparent box behind the chat messages */
236 	GfxFillRect(_chatmsg_box.x, top - 2, _chatmsg_box.x + _chatmsg_box.width - 1, bottom,
237 			PALETTE_TO_TRANSPARENT, FILLRECT_RECOLOUR // black, but with some alpha for background
238 		);
239 
240 	/* Paint the chat messages starting with the lowest at the bottom */
241 	int ypos = bottom - 2;
242 
243 	for (auto &cmsg : _chatmsg_list) {
244 		if (!show_all && cmsg.remove_time < now) continue;
245 		ypos = DrawStringMultiLine(_chatmsg_box.x + 3, _chatmsg_box.x + _chatmsg_box.width - 1, top, ypos, cmsg.message, cmsg.colour, SA_LEFT | SA_BOTTOM | SA_FORCE) - NETWORK_CHAT_LINE_SPACING;
246 		if (ypos < top) break;
247 	}
248 
249 	/* Make sure the data is updated next flush */
250 	VideoDriver::GetInstance()->MakeDirty(x, y, width, height);
251 
252 	_chatmessage_visible = true;
253 	_chatmessage_dirty = false;
254 }
255 
256 /**
257  * Send an actual chat message.
258  * @param buf The message to send.
259  * @param type The type of destination.
260  * @param dest The actual destination index.
261  */
SendChat(const std::string & buf,DestType type,int dest)262 static void SendChat(const std::string &buf, DestType type, int dest)
263 {
264 	if (buf.empty()) return;
265 	if (!_network_server) {
266 		MyClient::SendChat((NetworkAction)(NETWORK_ACTION_CHAT + type), type, dest, buf, 0);
267 	} else {
268 		NetworkServerSendChat((NetworkAction)(NETWORK_ACTION_CHAT + type), type, dest, buf, CLIENT_ID_SERVER);
269 	}
270 }
271 
272 /** Window to enter the chat message in. */
273 struct NetworkChatWindow : public Window {
274 	DestType dtype;       ///< The type of destination.
275 	int dest;             ///< The identifier of the destination.
276 	QueryString message_editbox; ///< Message editbox.
277 
278 	/**
279 	 * Create a chat input window.
280 	 * @param desc Description of the looks of the window.
281 	 * @param type The type of destination.
282 	 * @param dest The actual destination index.
283 	 */
NetworkChatWindowNetworkChatWindow284 	NetworkChatWindow(WindowDesc *desc, DestType type, int dest) : Window(desc), message_editbox(NETWORK_CHAT_LENGTH)
285 	{
286 		this->dtype   = type;
287 		this->dest    = dest;
288 		this->querystrings[WID_NC_TEXTBOX] = &this->message_editbox;
289 		this->message_editbox.cancel_button = WID_NC_CLOSE;
290 		this->message_editbox.ok_button = WID_NC_SENDBUTTON;
291 
292 		static const StringID chat_captions[] = {
293 			STR_NETWORK_CHAT_ALL_CAPTION,
294 			STR_NETWORK_CHAT_COMPANY_CAPTION,
295 			STR_NETWORK_CHAT_CLIENT_CAPTION
296 		};
297 		assert((uint)this->dtype < lengthof(chat_captions));
298 
299 		this->CreateNestedTree();
300 		this->GetWidget<NWidgetCore>(WID_NC_DESTINATION)->widget_data = chat_captions[this->dtype];
301 		this->FinishInitNested(type);
302 
303 		this->SetFocusedWidget(WID_NC_TEXTBOX);
304 		InvalidateWindowData(WC_NEWS_WINDOW, 0, this->height);
305 		_chat_tab_completion_active = false;
306 
307 		PositionNetworkChatWindow(this);
308 	}
309 
CloseNetworkChatWindow310 	void Close() override
311 	{
312 		InvalidateWindowData(WC_NEWS_WINDOW, 0, 0);
313 		this->Window::Close();
314 	}
315 
FindWindowPlacementAndResizeNetworkChatWindow316 	void FindWindowPlacementAndResize(int def_width, int def_height) override
317 	{
318 		Window::FindWindowPlacementAndResize(_toolbar_width, def_height);
319 	}
320 
321 	/**
322 	 * Find the next item of the list of things that can be auto-completed.
323 	 * @param item The current indexed item to return. This function can, and most
324 	 *     likely will, alter item, to skip empty items in the arrays.
325 	 * @return Returns the char that matched to the index.
326 	 */
ChatTabCompletionNextItemNetworkChatWindow327 	const char *ChatTabCompletionNextItem(uint *item)
328 	{
329 		static char chat_tab_temp_buffer[64];
330 
331 		/* First, try clients */
332 		if (*item < MAX_CLIENT_SLOTS) {
333 			/* Skip inactive clients */
334 			for (NetworkClientInfo *ci : NetworkClientInfo::Iterate(*item)) {
335 				*item = ci->index;
336 				return ci->client_name.c_str();
337 			}
338 			*item = MAX_CLIENT_SLOTS;
339 		}
340 
341 		/* Then, try townnames
342 		 * Not that the following assumes all town indices are adjacent, ie no
343 		 * towns have been deleted. */
344 		if (*item < (uint)MAX_CLIENT_SLOTS + Town::GetPoolSize()) {
345 			for (const Town *t : Town::Iterate(*item - MAX_CLIENT_SLOTS)) {
346 				/* Get the town-name via the string-system */
347 				SetDParam(0, t->index);
348 				GetString(chat_tab_temp_buffer, STR_TOWN_NAME, lastof(chat_tab_temp_buffer));
349 				return &chat_tab_temp_buffer[0];
350 			}
351 		}
352 
353 		return nullptr;
354 	}
355 
356 	/**
357 	 * Find what text to complete. It scans for a space from the left and marks
358 	 *  the word right from that as to complete. It also writes a \0 at the
359 	 *  position of the space (if any). If nothing found, buf is returned.
360 	 */
ChatTabCompletionFindTextNetworkChatWindow361 	static char *ChatTabCompletionFindText(char *buf)
362 	{
363 		char *p = strrchr(buf, ' ');
364 		if (p == nullptr) return buf;
365 
366 		*p = '\0';
367 		return p + 1;
368 	}
369 
370 	/**
371 	 * See if we can auto-complete the current text of the user.
372 	 */
ChatTabCompletionNetworkChatWindow373 	void ChatTabCompletion()
374 	{
375 		static char _chat_tab_completion_buf[NETWORK_CHAT_LENGTH];
376 		assert(this->message_editbox.text.max_bytes == lengthof(_chat_tab_completion_buf));
377 
378 		Textbuf *tb = &this->message_editbox.text;
379 		size_t len, tb_len;
380 		uint item;
381 		char *tb_buf, *pre_buf;
382 		const char *cur_name;
383 		bool second_scan = false;
384 
385 		item = 0;
386 
387 		/* Copy the buffer so we can modify it without damaging the real data */
388 		pre_buf = (_chat_tab_completion_active) ? stredup(_chat_tab_completion_buf) : stredup(tb->buf);
389 
390 		tb_buf  = ChatTabCompletionFindText(pre_buf);
391 		tb_len  = strlen(tb_buf);
392 
393 		while ((cur_name = ChatTabCompletionNextItem(&item)) != nullptr) {
394 			item++;
395 
396 			if (_chat_tab_completion_active) {
397 				/* We are pressing TAB again on the same name, is there another name
398 				 *  that starts with this? */
399 				if (!second_scan) {
400 					size_t offset;
401 					size_t length;
402 
403 					/* If we are completing at the begin of the line, skip the ': ' we added */
404 					if (tb_buf == pre_buf) {
405 						offset = 0;
406 						length = (tb->bytes - 1) - 2;
407 					} else {
408 						/* Else, find the place we are completing at */
409 						offset = strlen(pre_buf) + 1;
410 						length = (tb->bytes - 1) - offset;
411 					}
412 
413 					/* Compare if we have a match */
414 					if (strlen(cur_name) == length && strncmp(cur_name, tb->buf + offset, length) == 0) second_scan = true;
415 
416 					continue;
417 				}
418 
419 				/* Now any match we make on _chat_tab_completion_buf after this, is perfect */
420 			}
421 
422 			len = strlen(cur_name);
423 			if (tb_len < len && strncasecmp(cur_name, tb_buf, tb_len) == 0) {
424 				/* Save the data it was before completion */
425 				if (!second_scan) seprintf(_chat_tab_completion_buf, lastof(_chat_tab_completion_buf), "%s", tb->buf);
426 				_chat_tab_completion_active = true;
427 
428 				/* Change to the found name. Add ': ' if we are at the start of the line (pretty) */
429 				if (pre_buf == tb_buf) {
430 					this->message_editbox.text.Print("%s: ", cur_name);
431 				} else {
432 					this->message_editbox.text.Print("%s %s", pre_buf, cur_name);
433 				}
434 
435 				this->SetDirty();
436 				free(pre_buf);
437 				return;
438 			}
439 		}
440 
441 		if (second_scan) {
442 			/* We walked all possibilities, and the user presses tab again.. revert to original text */
443 			this->message_editbox.text.Assign(_chat_tab_completion_buf);
444 			_chat_tab_completion_active = false;
445 
446 			this->SetDirty();
447 		}
448 		free(pre_buf);
449 	}
450 
OnInitialPositionNetworkChatWindow451 	Point OnInitialPosition(int16 sm_width, int16 sm_height, int window_number) override
452 	{
453 		Point pt = { 0, _screen.height - sm_height - FindWindowById(WC_STATUS_BAR, 0)->height };
454 		return pt;
455 	}
456 
SetStringParametersNetworkChatWindow457 	void SetStringParameters(int widget) const override
458 	{
459 		if (widget != WID_NC_DESTINATION) return;
460 
461 		if (this->dtype == DESTTYPE_CLIENT) {
462 			SetDParamStr(0, NetworkClientInfo::GetByClientID((ClientID)this->dest)->client_name);
463 		}
464 	}
465 
OnClickNetworkChatWindow466 	void OnClick(Point pt, int widget, int click_count) override
467 	{
468 		switch (widget) {
469 			case WID_NC_SENDBUTTON: /* Send */
470 				SendChat(this->message_editbox.text.buf, this->dtype, this->dest);
471 				FALLTHROUGH;
472 
473 			case WID_NC_CLOSE: /* Cancel */
474 				this->Close();
475 				break;
476 		}
477 	}
478 
OnKeyPressNetworkChatWindow479 	EventState OnKeyPress(WChar key, uint16 keycode) override
480 	{
481 		EventState state = ES_NOT_HANDLED;
482 		if (keycode == WKC_TAB) {
483 			ChatTabCompletion();
484 			state = ES_HANDLED;
485 		}
486 		return state;
487 	}
488 
OnEditboxChangedNetworkChatWindow489 	void OnEditboxChanged(int wid) override
490 	{
491 		_chat_tab_completion_active = false;
492 	}
493 
494 	/**
495 	 * Some data on this window has become invalid.
496 	 * @param data Information about the changed data.
497 	 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
498 	 */
OnInvalidateDataNetworkChatWindow499 	void OnInvalidateData(int data = 0, bool gui_scope = true) override
500 	{
501 		if (data == this->dest) this->Close();
502 	}
503 };
504 
505 /** The widgets of the chat window. */
506 static const NWidgetPart _nested_chat_window_widgets[] = {
507 	NWidget(NWID_HORIZONTAL),
508 		NWidget(WWT_CLOSEBOX, COLOUR_GREY, WID_NC_CLOSE),
509 		NWidget(WWT_PANEL, COLOUR_GREY, WID_NC_BACKGROUND),
510 			NWidget(NWID_HORIZONTAL),
511 				NWidget(WWT_TEXT, COLOUR_GREY, WID_NC_DESTINATION), SetMinimalSize(62, 12), SetPadding(1, 0, 1, 0), SetTextColour(TC_BLACK), SetAlignment(SA_TOP | SA_RIGHT), SetDataTip(STR_NULL, STR_NULL),
512 				NWidget(WWT_EDITBOX, COLOUR_GREY, WID_NC_TEXTBOX), SetMinimalSize(100, 12), SetPadding(1, 0, 1, 0), SetResize(1, 0),
513 																	SetDataTip(STR_NETWORK_CHAT_OSKTITLE, STR_NULL),
514 				NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_NC_SENDBUTTON), SetMinimalSize(62, 12), SetPadding(1, 0, 1, 0), SetDataTip(STR_NETWORK_CHAT_SEND, STR_NULL),
515 			EndContainer(),
516 		EndContainer(),
517 	EndContainer(),
518 };
519 
520 /** The description of the chat window. */
521 static WindowDesc _chat_window_desc(
522 	WDP_MANUAL, nullptr, 0, 0,
523 	WC_SEND_NETWORK_MSG, WC_NONE,
524 	0,
525 	_nested_chat_window_widgets, lengthof(_nested_chat_window_widgets)
526 );
527 
528 
529 /**
530  * Show the chat window.
531  * @param type The type of destination.
532  * @param dest The actual destination index.
533  */
ShowNetworkChatQueryWindow(DestType type,int dest)534 void ShowNetworkChatQueryWindow(DestType type, int dest)
535 {
536 	CloseWindowByClass(WC_SEND_NETWORK_MSG);
537 	new NetworkChatWindow(&_chat_window_desc, type, dest);
538 }
539