remove_detached_inventory: Fix segfault during mod load
[oweals/minetest.git] / src / chat.cpp
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
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
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
59 void ChatBuffer::clear()
60 {
61         m_unformatted.clear();
62         m_formatted.clear();
63         m_scroll = 0;
64 }
65
66 u32 ChatBuffer::getLineCount() const
67 {
68         return m_unformatted.size();
69 }
70
71 const ChatLine& ChatBuffer::getLine(u32 index) const
72 {
73         assert(index < getLineCount()); // pre-condition
74         return m_unformatted[index];
75 }
76
77 void ChatBuffer::step(f32 dtime)
78 {
79         for (ChatLine &line : m_unformatted) {
80                 line.age += dtime;
81         }
82 }
83
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
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
126 u32 ChatBuffer::getColumns() const
127 {
128         return m_cols;
129 }
130
131 u32 ChatBuffer::getRows() const
132 {
133         return m_rows;
134 }
135
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
192 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
201 void ChatBuffer::scroll(s32 rows)
202 {
203         scrollAbsolute(m_scroll + rows);
204 }
205
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
218 void ChatBuffer::scrollBottom()
219 {
220         m_scroll = getBottomScrollPos();
221 }
222
223 void ChatBuffer::scrollTop()
224 {
225         m_scroll = getTopScrollPos();
226 }
227
228 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
229                 std::vector<ChatFormattedLine>& destination) const
230 {
231         u32 num_added = 0;
232         std::vector<ChatFormattedFragment> next_frags;
233         ChatFormattedLine next_line;
234         ChatFormattedFragment temp_frag;
235         u32 out_column = 0;
236         u32 in_pos = 0;
237         u32 hanging_indentation = 0;
238
239         // Format the sender name and produce fragments
240         if (!line.name.empty()) {
241                 temp_frag.text = L"<";
242                 temp_frag.column = 0;
243                 //temp_frag.bold = 0;
244                 next_frags.push_back(temp_frag);
245                 temp_frag.text = line.name;
246                 temp_frag.column = 0;
247                 //temp_frag.bold = 1;
248                 next_frags.push_back(temp_frag);
249                 temp_frag.text = L"> ";
250                 temp_frag.column = 0;
251                 //temp_frag.bold = 0;
252                 next_frags.push_back(temp_frag);
253         }
254
255         std::wstring name_sanitized = line.name.c_str();
256
257         // Choose an indentation level
258         if (line.name.empty()) {
259                 // Server messages
260                 hanging_indentation = 0;
261         } else if (name_sanitized.size() + 3 <= cols/2) {
262                 // Names shorter than about half the console width
263                 hanging_indentation = line.name.size() + 3;
264         } else {
265                 // Very long names
266                 hanging_indentation = 2;
267         }
268         //EnrichedString line_text(line.text);
269
270         next_line.first = true;
271         bool text_processing = false;
272
273         // Produce fragments and layout them into lines
274         while (!next_frags.empty() || in_pos < line.text.size())
275         {
276                 // Layout fragments into lines
277                 while (!next_frags.empty())
278                 {
279                         ChatFormattedFragment& frag = next_frags[0];
280                         if (frag.text.size() <= cols - out_column)
281                         {
282                                 // Fragment fits into current line
283                                 frag.column = out_column;
284                                 next_line.fragments.push_back(frag);
285                                 out_column += frag.text.size();
286                                 next_frags.erase(next_frags.begin());
287                         }
288                         else
289                         {
290                                 // Fragment does not fit into current line
291                                 // So split it up
292                                 temp_frag.text = frag.text.substr(0, cols - out_column);
293                                 temp_frag.column = out_column;
294                                 //temp_frag.bold = frag.bold;
295                                 next_line.fragments.push_back(temp_frag);
296                                 frag.text = frag.text.substr(cols - out_column);
297                                 out_column = cols;
298                         }
299                         if (out_column == cols || text_processing)
300                         {
301                                 // End the current line
302                                 destination.push_back(next_line);
303                                 num_added++;
304                                 next_line.fragments.clear();
305                                 next_line.first = false;
306
307                                 out_column = text_processing ? hanging_indentation : 0;
308                         }
309                 }
310
311                 // Produce fragment
312                 if (in_pos < line.text.size())
313                 {
314                         u32 remaining_in_input = line.text.size() - in_pos;
315                         u32 remaining_in_output = cols - out_column;
316
317                         // Determine a fragment length <= the minimum of
318                         // remaining_in_{in,out}put. Try to end the fragment
319                         // on a word boundary.
320                         u32 frag_length = 1, space_pos = 0;
321                         while (frag_length < remaining_in_input &&
322                                         frag_length < remaining_in_output)
323                         {
324                                 if (iswspace(line.text.getString()[in_pos + frag_length]))
325                                         space_pos = frag_length;
326                                 ++frag_length;
327                         }
328                         if (space_pos != 0 && frag_length < remaining_in_input)
329                                 frag_length = space_pos + 1;
330
331                         temp_frag.text = line.text.substr(in_pos, frag_length);
332                         temp_frag.column = 0;
333                         //temp_frag.bold = 0;
334                         next_frags.push_back(temp_frag);
335                         in_pos += frag_length;
336                         text_processing = true;
337                 }
338         }
339
340         // End the last line
341         if (num_added == 0 || !next_line.fragments.empty())
342         {
343                 destination.push_back(next_line);
344                 num_added++;
345         }
346
347         return num_added;
348 }
349
350 s32 ChatBuffer::getTopScrollPos() const
351 {
352         s32 formatted_count = (s32) m_formatted.size();
353         s32 rows = (s32) m_rows;
354         if (rows == 0)
355                 return 0;
356
357         if (formatted_count <= rows)
358                 return formatted_count - rows;
359
360         return 0;
361 }
362
363 s32 ChatBuffer::getBottomScrollPos() const
364 {
365         s32 formatted_count = (s32) m_formatted.size();
366         s32 rows = (s32) m_rows;
367         if (rows == 0)
368                 return 0;
369
370         return formatted_count - rows;
371 }
372
373 void ChatBuffer::resize(u32 scrollback)
374 {
375         m_scrollback = scrollback;
376         if (m_unformatted.size() > m_scrollback)
377                 deleteOldest(m_unformatted.size() - m_scrollback);
378 }
379
380
381 ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
382         m_prompt(prompt),
383         m_history_limit(history_limit)
384 {
385 }
386
387 void ChatPrompt::input(wchar_t ch)
388 {
389         m_line.insert(m_cursor, 1, ch);
390         m_cursor++;
391         clampView();
392         m_nick_completion_start = 0;
393         m_nick_completion_end = 0;
394 }
395
396 void ChatPrompt::input(const std::wstring &str)
397 {
398         m_line.insert(m_cursor, str);
399         m_cursor += str.size();
400         clampView();
401         m_nick_completion_start = 0;
402         m_nick_completion_end = 0;
403 }
404
405 void ChatPrompt::addToHistory(const std::wstring &line)
406 {
407         if (!line.empty() &&
408                         (m_history.size() == 0 || m_history.back() != line)) {
409                 // Remove all duplicates
410                 m_history.erase(std::remove(m_history.begin(), m_history.end(),
411                         line), m_history.end());
412                 // Push unique line
413                 m_history.push_back(line);
414         }
415         if (m_history.size() > m_history_limit)
416                 m_history.erase(m_history.begin());
417         m_history_index = m_history.size();
418 }
419
420 void ChatPrompt::clear()
421 {
422         m_line.clear();
423         m_view = 0;
424         m_cursor = 0;
425         m_nick_completion_start = 0;
426         m_nick_completion_end = 0;
427 }
428
429 std::wstring ChatPrompt::replace(const std::wstring &line)
430 {
431         std::wstring old_line = m_line;
432         m_line =  line;
433         m_view = m_cursor = line.size();
434         clampView();
435         m_nick_completion_start = 0;
436         m_nick_completion_end = 0;
437         return old_line;
438 }
439
440 void ChatPrompt::historyPrev()
441 {
442         if (m_history_index != 0)
443         {
444                 --m_history_index;
445                 replace(m_history[m_history_index]);
446         }
447 }
448
449 void ChatPrompt::historyNext()
450 {
451         if (m_history_index + 1 >= m_history.size())
452         {
453                 m_history_index = m_history.size();
454                 replace(L"");
455         }
456         else
457         {
458                 ++m_history_index;
459                 replace(m_history[m_history_index]);
460         }
461 }
462
463 void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwards)
464 {
465         // Two cases:
466         // (a) m_nick_completion_start == m_nick_completion_end == 0
467         //     Then no previous nick completion is active.
468         //     Get the word around the cursor and replace with any nick
469         //     that has that word as a prefix.
470         // (b) else, continue a previous nick completion.
471         //     m_nick_completion_start..m_nick_completion_end are the
472         //     interval where the originally used prefix was. Cycle
473         //     through the list of completions of that prefix.
474         u32 prefix_start = m_nick_completion_start;
475         u32 prefix_end = m_nick_completion_end;
476         bool initial = (prefix_end == 0);
477         if (initial)
478         {
479                 // no previous nick completion is active
480                 prefix_start = prefix_end = m_cursor;
481                 while (prefix_start > 0 && !iswspace(m_line[prefix_start-1]))
482                         --prefix_start;
483                 while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end]))
484                         ++prefix_end;
485                 if (prefix_start == prefix_end)
486                         return;
487         }
488         std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
489
490         // find all names that start with the selected prefix
491         std::vector<std::wstring> completions;
492         for (const std::string &name : names) {
493                 if (str_starts_with(narrow_to_wide(name), prefix, true)) {
494                         std::wstring completion = narrow_to_wide(name);
495                         if (prefix_start == 0)
496                                 completion += L": ";
497                         completions.push_back(completion);
498                 }
499         }
500
501         if (completions.empty())
502                 return;
503
504         // find a replacement string and the word that will be replaced
505         u32 word_end = prefix_end;
506         u32 replacement_index = 0;
507         if (!initial)
508         {
509                 while (word_end < m_line.size() && !iswspace(m_line[word_end]))
510                         ++word_end;
511                 std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
512
513                 // cycle through completions
514                 for (u32 i = 0; i < completions.size(); ++i)
515                 {
516                         if (str_equal(word, completions[i], true))
517                         {
518                                 if (backwards)
519                                         replacement_index = i + completions.size() - 1;
520                                 else
521                                         replacement_index = i + 1;
522                                 replacement_index %= completions.size();
523                                 break;
524                         }
525                 }
526         }
527         std::wstring replacement = completions[replacement_index];
528         if (word_end < m_line.size() && iswspace(m_line[word_end]))
529                 ++word_end;
530
531         // replace existing word with replacement word,
532         // place the cursor at the end and record the completion prefix
533         m_line.replace(prefix_start, word_end - prefix_start, replacement);
534         m_cursor = prefix_start + replacement.size();
535         clampView();
536         m_nick_completion_start = prefix_start;
537         m_nick_completion_end = prefix_end;
538 }
539
540 void ChatPrompt::reformat(u32 cols)
541 {
542         if (cols <= m_prompt.size())
543         {
544                 m_cols = 0;
545                 m_view = m_cursor;
546         }
547         else
548         {
549                 s32 length = m_line.size();
550                 bool was_at_end = (m_view + m_cols >= length + 1);
551                 m_cols = cols - m_prompt.size();
552                 if (was_at_end)
553                         m_view = length;
554                 clampView();
555         }
556 }
557
558 std::wstring ChatPrompt::getVisiblePortion() const
559 {
560         return m_prompt + m_line.substr(m_view, m_cols);
561 }
562
563 s32 ChatPrompt::getVisibleCursorPosition() const
564 {
565         return m_cursor - m_view + m_prompt.size();
566 }
567
568 void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
569 {
570         s32 old_cursor = m_cursor;
571         s32 new_cursor = m_cursor;
572
573         s32 length = m_line.size();
574         s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
575
576         switch (scope) {
577         case CURSOROP_SCOPE_CHARACTER:
578                 new_cursor += increment;
579                 break;
580         case CURSOROP_SCOPE_WORD:
581                 if (dir == CURSOROP_DIR_RIGHT) {
582                         // skip one word to the right
583                         while (new_cursor < length && iswspace(m_line[new_cursor]))
584                                 new_cursor++;
585                         while (new_cursor < length && !iswspace(m_line[new_cursor]))
586                                 new_cursor++;
587                         while (new_cursor < length && iswspace(m_line[new_cursor]))
588                                 new_cursor++;
589                 } else {
590                         // skip one word to the left
591                         while (new_cursor >= 1 && iswspace(m_line[new_cursor - 1]))
592                                 new_cursor--;
593                         while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1]))
594                                 new_cursor--;
595                 }
596                 break;
597         case CURSOROP_SCOPE_LINE:
598                 new_cursor += increment * length;
599                 break;
600         case CURSOROP_SCOPE_SELECTION:
601                 break;
602         }
603
604         new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
605
606         switch (op) {
607         case CURSOROP_MOVE:
608                 m_cursor = new_cursor;
609                 m_cursor_len = 0;
610                 break;
611         case CURSOROP_DELETE:
612                 if (m_cursor_len > 0) { // Delete selected text first
613                         m_line.erase(m_cursor, m_cursor_len);
614                 } else {
615                         m_cursor = MYMIN(new_cursor, old_cursor);
616                         m_line.erase(m_cursor, abs(new_cursor - old_cursor));
617                 }
618                 m_cursor_len = 0;
619                 break;
620         case CURSOROP_SELECT:
621                 if (scope == CURSOROP_SCOPE_LINE) {
622                         m_cursor = 0;
623                         m_cursor_len = length;
624                 } else {
625                         m_cursor = MYMIN(new_cursor, old_cursor);
626                         m_cursor_len += abs(new_cursor - old_cursor);
627                         m_cursor_len = MYMIN(m_cursor_len, length - m_cursor);
628                 }
629                 break;
630         }
631
632         clampView();
633
634         m_nick_completion_start = 0;
635         m_nick_completion_end = 0;
636 }
637
638 void ChatPrompt::clampView()
639 {
640         s32 length = m_line.size();
641         if (length + 1 <= m_cols)
642         {
643                 m_view = 0;
644         }
645         else
646         {
647                 m_view = MYMIN(m_view, length + 1 - m_cols);
648                 m_view = MYMIN(m_view, m_cursor);
649                 m_view = MYMAX(m_view, m_cursor - m_cols + 1);
650                 m_view = MYMAX(m_view, 0);
651         }
652 }
653
654
655
656 ChatBackend::ChatBackend():
657         m_console_buffer(500),
658         m_recent_buffer(6),
659         m_prompt(L"]", 500)
660 {
661 }
662
663 void ChatBackend::addMessage(const std::wstring &name, std::wstring text)
664 {
665         // Note: A message may consist of multiple lines, for example the MOTD.
666         text = translate_string(text);
667         WStrfnd fnd(text);
668         while (!fnd.at_end())
669         {
670                 std::wstring line = fnd.next(L"\n");
671                 m_console_buffer.addLine(name, line);
672                 m_recent_buffer.addLine(name, line);
673         }
674 }
675
676 void ChatBackend::addUnparsedMessage(std::wstring message)
677 {
678         // TODO: Remove the need to parse chat messages client-side, by sending
679         // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
680
681         if (message.size() >= 2 && message[0] == L'<')
682         {
683                 std::size_t closing = message.find_first_of(L'>', 1);
684                 if (closing != std::wstring::npos &&
685                                 closing + 2 <= message.size() &&
686                                 message[closing+1] == L' ')
687                 {
688                         std::wstring name = message.substr(1, closing - 1);
689                         std::wstring text = message.substr(closing + 2);
690                         addMessage(name, text);
691                         return;
692                 }
693         }
694
695         // Unable to parse, probably a server message.
696         addMessage(L"", message);
697 }
698
699 ChatBuffer& ChatBackend::getConsoleBuffer()
700 {
701         return m_console_buffer;
702 }
703
704 ChatBuffer& ChatBackend::getRecentBuffer()
705 {
706         return m_recent_buffer;
707 }
708
709 EnrichedString ChatBackend::getRecentChat() const
710 {
711         EnrichedString result;
712         for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
713                 const ChatLine& line = m_recent_buffer.getLine(i);
714                 if (i != 0)
715                         result += L"\n";
716                 if (!line.name.empty()) {
717                         result += L"<";
718                         result += line.name;
719                         result += L"> ";
720                 }
721                 result += line.text;
722         }
723         return result;
724 }
725
726 ChatPrompt& ChatBackend::getPrompt()
727 {
728         return m_prompt;
729 }
730
731 void ChatBackend::reformat(u32 cols, u32 rows)
732 {
733         m_console_buffer.reformat(cols, rows);
734
735         // no need to reformat m_recent_buffer, its formatted lines
736         // are not used
737
738         m_prompt.reformat(cols);
739 }
740
741 void ChatBackend::clearRecentChat()
742 {
743         m_recent_buffer.clear();
744 }
745
746
747 void ChatBackend::applySettings()
748 {
749         u32 recent_lines = g_settings->getU32("recent_chat_messages");
750         recent_lines = rangelim(recent_lines, 2, 20);
751         m_recent_buffer.resize(recent_lines);
752 }
753
754 void ChatBackend::step(float dtime)
755 {
756         m_recent_buffer.step(dtime);
757         m_recent_buffer.deleteByAge(60.0);
758
759         // no need to age messages in anything but m_recent_buffer
760 }
761
762 void ChatBackend::scroll(s32 rows)
763 {
764         m_console_buffer.scroll(rows);
765 }
766
767 void ChatBackend::scrollPageDown()
768 {
769         m_console_buffer.scroll(m_console_buffer.getRows());
770 }
771
772 void ChatBackend::scrollPageUp()
773 {
774         m_console_buffer.scroll(-(s32)m_console_buffer.getRows());
775 }