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