1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4 
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14 
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19 
20 #include "chat.h"
21 
22 #include <algorithm>
23 #include <cctype>
24 #include <sstream>
25 
26 #include "config.h"
27 #include "debug.h"
28 #include "util/strfnd.h"
29 #include "util/string.h"
30 #include "util/numeric.h"
31 
ChatBuffer(u32 scrollback)32 ChatBuffer::ChatBuffer(u32 scrollback):
33 	m_scrollback(scrollback)
34 {
35 	if (m_scrollback == 0)
36 		m_scrollback = 1;
37 	m_empty_formatted_line.first = true;
38 }
39 
addLine(const std::wstring & name,const std::wstring & text)40 void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
41 {
42 	ChatLine line(name, text);
43 	m_unformatted.push_back(line);
44 
45 	if (m_rows > 0) {
46 		// m_formatted is valid and must be kept valid
47 		bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
48 		u32 num_added = formatChatLine(line, m_cols, m_formatted);
49 		if (scrolled_at_bottom)
50 			m_scroll += num_added;
51 	}
52 
53 	// Limit number of lines by m_scrollback
54 	if (m_unformatted.size() > m_scrollback) {
55 		deleteOldest(m_unformatted.size() - m_scrollback);
56 	}
57 }
58 
clear()59 void ChatBuffer::clear()
60 {
61 	m_unformatted.clear();
62 	m_formatted.clear();
63 	m_scroll = 0;
64 }
65 
getLineCount() const66 u32 ChatBuffer::getLineCount() const
67 {
68 	return m_unformatted.size();
69 }
70 
getLine(u32 index) const71 const ChatLine& ChatBuffer::getLine(u32 index) const
72 {
73 	assert(index < getLineCount());	// pre-condition
74 	return m_unformatted[index];
75 }
76 
step(f32 dtime)77 void ChatBuffer::step(f32 dtime)
78 {
79 	for (ChatLine &line : m_unformatted) {
80 		line.age += dtime;
81 	}
82 }
83 
deleteOldest(u32 count)84 void ChatBuffer::deleteOldest(u32 count)
85 {
86 	bool at_bottom = (m_scroll == getBottomScrollPos());
87 
88 	u32 del_unformatted = 0;
89 	u32 del_formatted = 0;
90 
91 	while (count > 0 && del_unformatted < m_unformatted.size())
92 	{
93 		++del_unformatted;
94 
95 		// keep m_formatted in sync
96 		if (del_formatted < m_formatted.size())
97 		{
98 
99 			sanity_check(m_formatted[del_formatted].first);
100 			++del_formatted;
101 			while (del_formatted < m_formatted.size() &&
102 					!m_formatted[del_formatted].first)
103 				++del_formatted;
104 		}
105 
106 		--count;
107 	}
108 
109 	m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted);
110 	m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted);
111 
112 	if (at_bottom)
113 		m_scroll = getBottomScrollPos();
114 	else
115 		scrollAbsolute(m_scroll - del_formatted);
116 }
117 
deleteByAge(f32 maxAge)118 void ChatBuffer::deleteByAge(f32 maxAge)
119 {
120 	u32 count = 0;
121 	while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
122 		++count;
123 	deleteOldest(count);
124 }
125 
getRows() const126 u32 ChatBuffer::getRows() const
127 {
128 	return m_rows;
129 }
130 
scrollTop()131 void ChatBuffer::scrollTop()
132 {
133 	m_scroll = getTopScrollPos();
134 }
135 
reformat(u32 cols,u32 rows)136 void ChatBuffer::reformat(u32 cols, u32 rows)
137 {
138 	if (cols == 0 || rows == 0)
139 	{
140 		// Clear formatted buffer
141 		m_cols = 0;
142 		m_rows = 0;
143 		m_scroll = 0;
144 		m_formatted.clear();
145 	}
146 	else if (cols != m_cols || rows != m_rows)
147 	{
148 		// TODO: Avoid reformatting ALL lines (even invisible ones)
149 		// each time the console size changes.
150 
151 		// Find out the scroll position in *unformatted* lines
152 		u32 restore_scroll_unformatted = 0;
153 		u32 restore_scroll_formatted = 0;
154 		bool at_bottom = (m_scroll == getBottomScrollPos());
155 		if (!at_bottom)
156 		{
157 			for (s32 i = 0; i < m_scroll; ++i)
158 			{
159 				if (m_formatted[i].first)
160 					++restore_scroll_unformatted;
161 			}
162 		}
163 
164 		// If number of columns change, reformat everything
165 		if (cols != m_cols)
166 		{
167 			m_formatted.clear();
168 			for (u32 i = 0; i < m_unformatted.size(); ++i)
169 			{
170 				if (i == restore_scroll_unformatted)
171 					restore_scroll_formatted = m_formatted.size();
172 				formatChatLine(m_unformatted[i], cols, m_formatted);
173 			}
174 		}
175 
176 		// Update the console size
177 		m_cols = cols;
178 		m_rows = rows;
179 
180 		// Restore the scroll position
181 		if (at_bottom)
182 		{
183 			scrollBottom();
184 		}
185 		else
186 		{
187 			scrollAbsolute(restore_scroll_formatted);
188 		}
189 	}
190 }
191 
getFormattedLine(u32 row) const192 const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
193 {
194 	s32 index = m_scroll + (s32) row;
195 	if (index >= 0 && index < (s32) m_formatted.size())
196 		return m_formatted[index];
197 
198 	return m_empty_formatted_line;
199 }
200 
scroll(s32 rows)201 void ChatBuffer::scroll(s32 rows)
202 {
203 	scrollAbsolute(m_scroll + rows);
204 }
205 
scrollAbsolute(s32 scroll)206 void ChatBuffer::scrollAbsolute(s32 scroll)
207 {
208 	s32 top = getTopScrollPos();
209 	s32 bottom = getBottomScrollPos();
210 
211 	m_scroll = scroll;
212 	if (m_scroll < top)
213 		m_scroll = top;
214 	if (m_scroll > bottom)
215 		m_scroll = bottom;
216 }
217 
scrollBottom()218 void ChatBuffer::scrollBottom()
219 {
220 	m_scroll = getBottomScrollPos();
221 }
222 
formatChatLine(const ChatLine & line,u32 cols,std::vector<ChatFormattedLine> & destination) const223 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
224 		std::vector<ChatFormattedLine>& destination) const
225 {
226 	u32 num_added = 0;
227 	std::vector<ChatFormattedFragment> next_frags;
228 	ChatFormattedLine next_line;
229 	ChatFormattedFragment temp_frag;
230 	u32 out_column = 0;
231 	u32 in_pos = 0;
232 	u32 hanging_indentation = 0;
233 
234 	// Format the sender name and produce fragments
235 	if (!line.name.empty()) {
236 		temp_frag.text = L"<";
237 		temp_frag.column = 0;
238 		//temp_frag.bold = 0;
239 		next_frags.push_back(temp_frag);
240 		temp_frag.text = line.name;
241 		temp_frag.column = 0;
242 		//temp_frag.bold = 1;
243 		next_frags.push_back(temp_frag);
244 		temp_frag.text = L"> ";
245 		temp_frag.column = 0;
246 		//temp_frag.bold = 0;
247 		next_frags.push_back(temp_frag);
248 	}
249 
250 	std::wstring name_sanitized = line.name.c_str();
251 
252 	// Choose an indentation level
253 	if (line.name.empty()) {
254 		// Server messages
255 		hanging_indentation = 0;
256 	} else if (name_sanitized.size() + 3 <= cols/2) {
257 		// Names shorter than about half the console width
258 		hanging_indentation = line.name.size() + 3;
259 	} else {
260 		// Very long names
261 		hanging_indentation = 2;
262 	}
263 	//EnrichedString line_text(line.text);
264 
265 	next_line.first = true;
266 	bool text_processing = false;
267 
268 	// Produce fragments and layout them into lines
269 	while (!next_frags.empty() || in_pos < line.text.size())
270 	{
271 		// Layout fragments into lines
272 		while (!next_frags.empty())
273 		{
274 			ChatFormattedFragment& frag = next_frags[0];
275 			if (frag.text.size() <= cols - out_column)
276 			{
277 				// Fragment fits into current line
278 				frag.column = out_column;
279 				next_line.fragments.push_back(frag);
280 				out_column += frag.text.size();
281 				next_frags.erase(next_frags.begin());
282 			}
283 			else
284 			{
285 				// Fragment does not fit into current line
286 				// So split it up
287 				temp_frag.text = frag.text.substr(0, cols - out_column);
288 				temp_frag.column = out_column;
289 				//temp_frag.bold = frag.bold;
290 				next_line.fragments.push_back(temp_frag);
291 				frag.text = frag.text.substr(cols - out_column);
292 				out_column = cols;
293 			}
294 			if (out_column == cols || text_processing)
295 			{
296 				// End the current line
297 				destination.push_back(next_line);
298 				num_added++;
299 				next_line.fragments.clear();
300 				next_line.first = false;
301 
302 				out_column = text_processing ? hanging_indentation : 0;
303 			}
304 		}
305 
306 		// Produce fragment
307 		if (in_pos < line.text.size())
308 		{
309 			u32 remaining_in_input = line.text.size() - in_pos;
310 			u32 remaining_in_output = cols - out_column;
311 
312 			// Determine a fragment length <= the minimum of
313 			// remaining_in_{in,out}put. Try to end the fragment
314 			// on a word boundary.
315 			u32 frag_length = 1, space_pos = 0;
316 			while (frag_length < remaining_in_input &&
317 					frag_length < remaining_in_output)
318 			{
319 				if (iswspace(line.text.getString()[in_pos + frag_length]))
320 					space_pos = frag_length;
321 				++frag_length;
322 			}
323 			if (space_pos != 0 && frag_length < remaining_in_input)
324 				frag_length = space_pos + 1;
325 
326 			temp_frag.text = line.text.substr(in_pos, frag_length);
327 			temp_frag.column = 0;
328 			//temp_frag.bold = 0;
329 			next_frags.push_back(temp_frag);
330 			in_pos += frag_length;
331 			text_processing = true;
332 		}
333 	}
334 
335 	// End the last line
336 	if (num_added == 0 || !next_line.fragments.empty())
337 	{
338 		destination.push_back(next_line);
339 		num_added++;
340 	}
341 
342 	return num_added;
343 }
344 
getTopScrollPos() const345 s32 ChatBuffer::getTopScrollPos() const
346 {
347 	s32 formatted_count = (s32) m_formatted.size();
348 	s32 rows = (s32) m_rows;
349 	if (rows == 0)
350 		return 0;
351 
352 	if (formatted_count <= rows)
353 		return formatted_count - rows;
354 
355 	return 0;
356 }
357 
getBottomScrollPos() const358 s32 ChatBuffer::getBottomScrollPos() const
359 {
360 	s32 formatted_count = (s32) m_formatted.size();
361 	s32 rows = (s32) m_rows;
362 	if (rows == 0)
363 		return 0;
364 
365 	return formatted_count - rows;
366 }
367 
resize(u32 scrollback)368 void ChatBuffer::resize(u32 scrollback)
369 {
370 	m_scrollback = scrollback;
371 	if (m_unformatted.size() > m_scrollback)
372 		deleteOldest(m_unformatted.size() - m_scrollback);
373 }
374 
375 
ChatPrompt(const std::wstring & prompt,u32 history_limit)376 ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
377 	m_prompt(prompt),
378 	m_history_limit(history_limit)
379 {
380 }
381 
input(wchar_t ch)382 void ChatPrompt::input(wchar_t ch)
383 {
384 	m_line.insert(m_cursor, 1, ch);
385 	m_cursor++;
386 	clampView();
387 	m_nick_completion_start = 0;
388 	m_nick_completion_end = 0;
389 }
390 
input(const std::wstring & str)391 void ChatPrompt::input(const std::wstring &str)
392 {
393 	m_line.insert(m_cursor, str);
394 	m_cursor += str.size();
395 	clampView();
396 	m_nick_completion_start = 0;
397 	m_nick_completion_end = 0;
398 }
399 
addToHistory(const std::wstring & line)400 void ChatPrompt::addToHistory(const std::wstring &line)
401 {
402 	if (!line.empty() &&
403 			(m_history.size() == 0 || m_history.back() != line)) {
404 		// Remove all duplicates
405 		m_history.erase(std::remove(m_history.begin(), m_history.end(),
406 			line), m_history.end());
407 		// Push unique line
408 		m_history.push_back(line);
409 	}
410 	if (m_history.size() > m_history_limit)
411 		m_history.erase(m_history.begin());
412 	m_history_index = m_history.size();
413 }
414 
clear()415 void ChatPrompt::clear()
416 {
417 	m_line.clear();
418 	m_view = 0;
419 	m_cursor = 0;
420 	m_nick_completion_start = 0;
421 	m_nick_completion_end = 0;
422 }
423 
replace(const std::wstring & line)424 std::wstring ChatPrompt::replace(const std::wstring &line)
425 {
426 	std::wstring old_line = m_line;
427 	m_line =  line;
428 	m_view = m_cursor = line.size();
429 	clampView();
430 	m_nick_completion_start = 0;
431 	m_nick_completion_end = 0;
432 	return old_line;
433 }
434 
historyPrev()435 void ChatPrompt::historyPrev()
436 {
437 	if (m_history_index != 0)
438 	{
439 		--m_history_index;
440 		replace(m_history[m_history_index]);
441 	}
442 }
443 
historyNext()444 void ChatPrompt::historyNext()
445 {
446 	if (m_history_index + 1 >= m_history.size())
447 	{
448 		m_history_index = m_history.size();
449 		replace(L"");
450 	}
451 	else
452 	{
453 		++m_history_index;
454 		replace(m_history[m_history_index]);
455 	}
456 }
457 
nickCompletion(const std::list<std::string> & names,bool backwards)458 void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwards)
459 {
460 	// Two cases:
461 	// (a) m_nick_completion_start == m_nick_completion_end == 0
462 	//     Then no previous nick completion is active.
463 	//     Get the word around the cursor and replace with any nick
464 	//     that has that word as a prefix.
465 	// (b) else, continue a previous nick completion.
466 	//     m_nick_completion_start..m_nick_completion_end are the
467 	//     interval where the originally used prefix was. Cycle
468 	//     through the list of completions of that prefix.
469 	u32 prefix_start = m_nick_completion_start;
470 	u32 prefix_end = m_nick_completion_end;
471 	bool initial = (prefix_end == 0);
472 	if (initial)
473 	{
474 		// no previous nick completion is active
475 		prefix_start = prefix_end = m_cursor;
476 		while (prefix_start > 0 && !iswspace(m_line[prefix_start-1]))
477 			--prefix_start;
478 		while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end]))
479 			++prefix_end;
480 		if (prefix_start == prefix_end)
481 			return;
482 	}
483 	std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
484 
485 	// find all names that start with the selected prefix
486 	std::vector<std::wstring> completions;
487 	for (const std::string &name : names) {
488 		std::wstring completion = utf8_to_wide(name);
489 		if (str_starts_with(completion, prefix, true)) {
490 			if (prefix_start == 0)
491 				completion += L": ";
492 			completions.push_back(completion);
493 		}
494 	}
495 
496 	if (completions.empty())
497 		return;
498 
499 	// find a replacement string and the word that will be replaced
500 	u32 word_end = prefix_end;
501 	u32 replacement_index = 0;
502 	if (!initial)
503 	{
504 		while (word_end < m_line.size() && !iswspace(m_line[word_end]))
505 			++word_end;
506 		std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
507 
508 		// cycle through completions
509 		for (u32 i = 0; i < completions.size(); ++i)
510 		{
511 			if (str_equal(word, completions[i], true))
512 			{
513 				if (backwards)
514 					replacement_index = i + completions.size() - 1;
515 				else
516 					replacement_index = i + 1;
517 				replacement_index %= completions.size();
518 				break;
519 			}
520 		}
521 	}
522 	std::wstring replacement = completions[replacement_index];
523 	if (word_end < m_line.size() && iswspace(m_line[word_end]))
524 		++word_end;
525 
526 	// replace existing word with replacement word,
527 	// place the cursor at the end and record the completion prefix
528 	m_line.replace(prefix_start, word_end - prefix_start, replacement);
529 	m_cursor = prefix_start + replacement.size();
530 	clampView();
531 	m_nick_completion_start = prefix_start;
532 	m_nick_completion_end = prefix_end;
533 }
534 
reformat(u32 cols)535 void ChatPrompt::reformat(u32 cols)
536 {
537 	if (cols <= m_prompt.size())
538 	{
539 		m_cols = 0;
540 		m_view = m_cursor;
541 	}
542 	else
543 	{
544 		s32 length = m_line.size();
545 		bool was_at_end = (m_view + m_cols >= length + 1);
546 		m_cols = cols - m_prompt.size();
547 		if (was_at_end)
548 			m_view = length;
549 		clampView();
550 	}
551 }
552 
getVisiblePortion() const553 std::wstring ChatPrompt::getVisiblePortion() const
554 {
555 	return m_prompt + m_line.substr(m_view, m_cols);
556 }
557 
getVisibleCursorPosition() const558 s32 ChatPrompt::getVisibleCursorPosition() const
559 {
560 	return m_cursor - m_view + m_prompt.size();
561 }
562 
cursorOperation(CursorOp op,CursorOpDir dir,CursorOpScope scope)563 void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
564 {
565 	s32 old_cursor = m_cursor;
566 	s32 new_cursor = m_cursor;
567 
568 	s32 length = m_line.size();
569 	s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
570 
571 	switch (scope) {
572 	case CURSOROP_SCOPE_CHARACTER:
573 		new_cursor += increment;
574 		break;
575 	case CURSOROP_SCOPE_WORD:
576 		if (dir == CURSOROP_DIR_RIGHT) {
577 			// skip one word to the right
578 			while (new_cursor < length && iswspace(m_line[new_cursor]))
579 				new_cursor++;
580 			while (new_cursor < length && !iswspace(m_line[new_cursor]))
581 				new_cursor++;
582 			while (new_cursor < length && iswspace(m_line[new_cursor]))
583 				new_cursor++;
584 		} else {
585 			// skip one word to the left
586 			while (new_cursor >= 1 && iswspace(m_line[new_cursor - 1]))
587 				new_cursor--;
588 			while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1]))
589 				new_cursor--;
590 		}
591 		break;
592 	case CURSOROP_SCOPE_LINE:
593 		new_cursor += increment * length;
594 		break;
595 	case CURSOROP_SCOPE_SELECTION:
596 		break;
597 	}
598 
599 	new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
600 
601 	switch (op) {
602 	case CURSOROP_MOVE:
603 		m_cursor = new_cursor;
604 		m_cursor_len = 0;
605 		break;
606 	case CURSOROP_DELETE:
607 		if (m_cursor_len > 0) { // Delete selected text first
608 			m_line.erase(m_cursor, m_cursor_len);
609 		} else {
610 			m_cursor = MYMIN(new_cursor, old_cursor);
611 			m_line.erase(m_cursor, abs(new_cursor - old_cursor));
612 		}
613 		m_cursor_len = 0;
614 		break;
615 	case CURSOROP_SELECT:
616 		if (scope == CURSOROP_SCOPE_LINE) {
617 			m_cursor = 0;
618 			m_cursor_len = length;
619 		} else {
620 			m_cursor = MYMIN(new_cursor, old_cursor);
621 			m_cursor_len += abs(new_cursor - old_cursor);
622 			m_cursor_len = MYMIN(m_cursor_len, length - m_cursor);
623 		}
624 		break;
625 	}
626 
627 	clampView();
628 
629 	m_nick_completion_start = 0;
630 	m_nick_completion_end = 0;
631 }
632 
clampView()633 void ChatPrompt::clampView()
634 {
635 	s32 length = m_line.size();
636 	if (length + 1 <= m_cols)
637 	{
638 		m_view = 0;
639 	}
640 	else
641 	{
642 		m_view = MYMIN(m_view, length + 1 - m_cols);
643 		m_view = MYMIN(m_view, m_cursor);
644 		m_view = MYMAX(m_view, m_cursor - m_cols + 1);
645 		m_view = MYMAX(m_view, 0);
646 	}
647 }
648 
649 
650 
ChatBackend()651 ChatBackend::ChatBackend():
652 	m_console_buffer(500),
653 	m_recent_buffer(6),
654 	m_prompt(L"]", 500)
655 {
656 }
657 
addMessage(const std::wstring & name,std::wstring text)658 void ChatBackend::addMessage(const std::wstring &name, std::wstring text)
659 {
660 	// Note: A message may consist of multiple lines, for example the MOTD.
661 	text = translate_string(text);
662 	WStrfnd fnd(text);
663 	while (!fnd.at_end())
664 	{
665 		std::wstring line = fnd.next(L"\n");
666 		m_console_buffer.addLine(name, line);
667 		m_recent_buffer.addLine(name, line);
668 	}
669 }
670 
addUnparsedMessage(std::wstring message)671 void ChatBackend::addUnparsedMessage(std::wstring message)
672 {
673 	// TODO: Remove the need to parse chat messages client-side, by sending
674 	// separate name and text fields in TOCLIENT_CHAT_MESSAGE.
675 
676 	if (message.size() >= 2 && message[0] == L'<')
677 	{
678 		std::size_t closing = message.find_first_of(L'>', 1);
679 		if (closing != std::wstring::npos &&
680 				closing + 2 <= message.size() &&
681 				message[closing+1] == L' ')
682 		{
683 			std::wstring name = message.substr(1, closing - 1);
684 			std::wstring text = message.substr(closing + 2);
685 			addMessage(name, text);
686 			return;
687 		}
688 	}
689 
690 	// Unable to parse, probably a server message.
691 	addMessage(L"", message);
692 }
693 
getConsoleBuffer()694 ChatBuffer& ChatBackend::getConsoleBuffer()
695 {
696 	return m_console_buffer;
697 }
698 
getRecentBuffer()699 ChatBuffer& ChatBackend::getRecentBuffer()
700 {
701 	return m_recent_buffer;
702 }
703 
getRecentChat() const704 EnrichedString ChatBackend::getRecentChat() const
705 {
706 	EnrichedString result;
707 	for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
708 		const ChatLine& line = m_recent_buffer.getLine(i);
709 		if (i != 0)
710 			result += L"\n";
711 		if (!line.name.empty()) {
712 			result += L"<";
713 			result += line.name;
714 			result += L"> ";
715 		}
716 		result += line.text;
717 	}
718 	return result;
719 }
720 
getPrompt()721 ChatPrompt& ChatBackend::getPrompt()
722 {
723 	return m_prompt;
724 }
725 
reformat(u32 cols,u32 rows)726 void ChatBackend::reformat(u32 cols, u32 rows)
727 {
728 	m_console_buffer.reformat(cols, rows);
729 
730 	// no need to reformat m_recent_buffer, its formatted lines
731 	// are not used
732 
733 	m_prompt.reformat(cols);
734 }
735 
clearRecentChat()736 void ChatBackend::clearRecentChat()
737 {
738 	m_recent_buffer.clear();
739 }
740 
741 
applySettings()742 void ChatBackend::applySettings()
743 {
744 	u32 recent_lines = g_settings->getU32("recent_chat_messages");
745 	recent_lines = rangelim(recent_lines, 2, 20);
746 	m_recent_buffer.resize(recent_lines);
747 }
748 
step(float dtime)749 void ChatBackend::step(float dtime)
750 {
751 	m_recent_buffer.step(dtime);
752 	m_recent_buffer.deleteByAge(60.0);
753 
754 	// no need to age messages in anything but m_recent_buffer
755 }
756 
scroll(s32 rows)757 void ChatBackend::scroll(s32 rows)
758 {
759 	m_console_buffer.scroll(rows);
760 }
761 
scrollPageDown()762 void ChatBackend::scrollPageDown()
763 {
764 	m_console_buffer.scroll(m_console_buffer.getRows());
765 }
766 
scrollPageUp()767 void ChatBackend::scrollPageUp()
768 {
769 	m_console_buffer.scroll(-(s32)m_console_buffer.getRows());
770 }
771