1 /**
2 * @file
3 * @brief Functions used to print messages.
4 **/
5
6 #include "AppHdr.h"
7
8 #include "message.h"
9
10 #include <sstream>
11
12 #include "areas.h"
13 #include "colour.h"
14 #include "delay.h"
15 #include "hints.h"
16 #include "initfile.h"
17 #include "libutil.h"
18 #ifdef WIZARD
19 #include "luaterp.h"
20 #endif
21 #include "menu.h"
22 #include "monster.h"
23 #include "mon-util.h"
24 #include "notes.h"
25 #include "output.h"
26 #include "religion.h"
27 #include "scroller.h"
28 #include "sound.h"
29 #include "state.h"
30 #include "stringutil.h"
31 #include "tiles-build-specific.h"
32 #include "tag-version.h"
33 #include "unwind.h"
34 #include "view.h"
35
36 static void _mpr(string text, msg_channel_type channel=MSGCH_PLAIN, int param=0,
37 bool nojoin=false, bool cap=true);
38
mpr(const string & text)39 void mpr(const string &text)
40 {
41 _mpr(text);
42 }
43
mpr_nojoin(msg_channel_type channel,string text)44 void mpr_nojoin(msg_channel_type channel, string text)
45 {
46 _mpr(text, channel, 0, true);
47 }
48
_ends_in_punctuation(const string & text)49 static bool _ends_in_punctuation(const string& text)
50 {
51 if (text.size() == 0)
52 return false;
53 switch (text[text.size() - 1])
54 {
55 case '.':
56 case '!':
57 case '?':
58 case ',':
59 case ';':
60 case ':':
61 return true;
62 default:
63 return false;
64 }
65 }
66
67 struct message_particle
68 {
69 string text; /// text of message (tagged string...)
70 int repeats; /// Number of times the message is in succession (x2)
71
pure_textmessage_particle72 string pure_text() const
73 {
74 return formatted_string::parse_string(text).tostring();
75 }
76
with_repeatsmessage_particle77 string with_repeats() const
78 {
79 // TODO: colour the repeats indicator?
80 string rep = "";
81 if (repeats > 1)
82 rep = make_stringf(" x%d", repeats);
83 return text + rep;
84 }
85
pure_text_with_repeatsmessage_particle86 string pure_text_with_repeats() const
87 {
88 string rep = "";
89 if (repeats > 1)
90 rep = make_stringf(" x%d", repeats);
91 return pure_text() + rep;
92 }
93
94 /**
95 * If this is followed by another message particle on the same line,
96 * should there be a semicolon between them?
97 */
needs_semicolonmessage_particle98 bool needs_semicolon() const
99 {
100 return repeats > 1 || !_ends_in_punctuation(pure_text());
101 }
102 };
103
104 struct message_line
105 {
106 msg_channel_type channel; // message channel
107 int param; // param for channel (god, enchantment)
108 vector<message_particle> messages; // a set of possibly-repeated messages
109 int turn;
110 bool join; /// may we merge this message w/others?
111
message_linemessage_line112 message_line() : channel(NUM_MESSAGE_CHANNELS), param(0), turn(-1),
113 join(true)
114 {
115 }
116
message_linemessage_line117 message_line(string msg, msg_channel_type chan, int par, bool jn)
118 : channel(chan), param(par), turn(you.num_turns)
119 {
120 messages = { { msg, 1 } };
121 // Don't join long messages.
122 join = jn && strwidth(last_msg().pure_text()) < 40;
123 }
124
125 // Constructor for restored messages.
message_linemessage_line126 message_line(string text, msg_channel_type chan, int par, int trn)
127 : channel(chan), param(par), messages({{ text, 1 }}), turn(trn),
128 join(false)
129 {
130 }
131
operator boolmessage_line132 operator bool() const
133 {
134 return !messages.empty();
135 }
136
last_msgmessage_line137 const message_particle& last_msg() const
138 {
139 return messages.back();
140 }
141
142 // Tries to condense the argument into this message.
143 // Either *this needs to be an empty item, or it must be the
144 // same as the argument.
mergemessage_line145 bool merge(const message_line& other)
146 {
147 if (! *this)
148 {
149 *this = other;
150 return true;
151 }
152 if (!other)
153 return true;
154
155
156 if (crawl_state.game_is_arena())
157 return false; // dangerous for hacky code (looks at EOL for '!'...)
158 if (!Options.msg_condense_repeats)
159 return false;
160 if (other.channel != channel || other.param != param)
161 return false;
162 if (other.messages.size() > 1)
163 {
164 return false; // not gonna try to handle this complexity
165 // shouldn't come up...
166 }
167
168 if (Options.msg_condense_repeats
169 && other.last_msg().text == last_msg().text)
170 {
171 messages.back().repeats += other.last_msg().repeats;
172 return true;
173 }
174 else if (Options.msg_condense_short
175 && turn == other.turn
176 && join && other.join
177 && _ends_in_punctuation(last_msg().pure_text())
178 == _ends_in_punctuation(other.last_msg().pure_text()))
179 // punct check is a hack to avoid pickup messages merging with
180 // combat on the same turn - should find a nicer heuristic
181 {
182 // "; " or " "?
183 const int seplen = last_msg().needs_semicolon() ? 2 : 1;
184 const int total_len = pure_len() + seplen + other.pure_len();
185 if (total_len > (int)msgwin_line_length())
186 return false;
187
188 // merge in other's messages; they'll be delimited when printing.
189 messages.insert(messages.end(),
190 other.messages.begin(), other.messages.end());
191 return true;
192 }
193
194 return false;
195 }
196
197 /// What's the length of the actual combined text of the particles, not
198 /// including non-rendering text (<red> etc)?
pure_lenmessage_line199 int pure_len() const
200 {
201 // could we do this more functionally?
202 int len = 0;
203 for (auto &msg : messages)
204 {
205 if (len > 0) // not first msg
206 len += msg.needs_semicolon() ? 2 : 1; // " " vs "; "
207 len += strwidth(msg.pure_text_with_repeats());
208 }
209 return len;
210 }
211
212 /// The full string, with elements joined as appropriate.
full_textmessage_line213 string full_text() const
214 {
215 string text = "";
216 bool needs_semicolon = false;
217 for (auto &msg : messages)
218 {
219 if (!text.empty())
220 {
221 text += make_stringf("<lightgrey>%s </lightgrey>",
222 needs_semicolon ? ";" : "");
223 }
224 text += msg.with_repeats();
225 needs_semicolon = msg.needs_semicolon();
226 }
227 return text;
228 }
229
pure_text_with_repeatsmessage_line230 string pure_text_with_repeats() const
231 {
232 return formatted_string::parse_string(full_text()).tostring();
233 }
234 };
235
_mod(int num,int denom)236 static int _mod(int num, int denom)
237 {
238 ASSERT(denom > 0);
239 div_t res = div(num, denom);
240 return res.rem >= 0 ? res.rem : res.rem + denom;
241 }
242
243 template <typename T, int SIZE>
244 class circ_vec
245 {
246 T data[SIZE];
247
248 int end; // first unfilled index
249 bool has_circled;
250 // TODO: properly track the tail, and make this into a real data
251 // structure with an iterator and whatnot
252
inc(int * index)253 static void inc(int* index)
254 {
255 ASSERT_RANGE(*index, 0, SIZE);
256 *index = _mod(*index + 1, SIZE);
257 }
258
dec(int * index)259 static void dec(int* index)
260 {
261 ASSERT_RANGE(*index, 0, SIZE);
262 *index = _mod(*index - 1, SIZE);
263 }
264
265 public:
circ_vec()266 circ_vec() : end(0), has_circled(false) {}
267
clear()268 void clear()
269 {
270 end = 0;
271 has_circled = false;
272 for (int i = 0; i < SIZE; ++i)
273 data[i] = T();
274 }
275
size() const276 int size() const
277 {
278 return SIZE;
279 }
280
filled_size() const281 int filled_size() const
282 {
283 if (has_circled)
284 return SIZE;
285 else
286 return end;
287 }
288
operator [](int i)289 T& operator[](int i)
290 {
291 ASSERT(_mod(i, SIZE) < size());
292 return data[_mod(end + i, SIZE)];
293 }
294
operator [](int i) const295 const T& operator[](int i) const
296 {
297 ASSERT(_mod(i, SIZE) < size());
298 return data[_mod(end + i, SIZE)];
299 }
300
push_back(const T & item)301 void push_back(const T& item)
302 {
303 data[end] = item;
304 inc(&end);
305 if (end == 0)
306 has_circled = true;
307 }
308
roll_back(int n)309 void roll_back(int n)
310 {
311 for (int i = 0; i < n; ++i)
312 {
313 dec(&end);
314 data[end] = T();
315 }
316 // don't bother to worry about has_circled in this case
317 // TODO: properly track the tail
318 }
319
320 /**
321 * Append the contents of `buf` to the current buffer.
322 * If `buf` has cycled, this will overwrite the entire contents of `this`.
323 */
append(const circ_vec<T,SIZE> buf)324 void append(const circ_vec<T, SIZE> buf)
325 {
326 const int buf_size = buf.filled_size();
327 for (int i = 0; i < buf_size; i++)
328 push_back(buf[i - buf_size]);
329 }
330 };
331
332 static void readkey_more(bool user_forced=false);
333
334 // Types of message prefixes.
335 // Higher values override lower.
336 enum class prefix_type
337 {
338 none,
339 turn_start,
340 turn_end,
341 new_cmd, // new command, but no new turn
342 new_turn,
343 full_more, // single-character more prompt (full window)
344 other_more, // the other type of --more-- prompt
345 };
346
347 // Could also go with coloured glyphs.
_prefix_glyph(prefix_type p)348 static cglyph_t _prefix_glyph(prefix_type p)
349 {
350 cglyph_t g;
351 switch (p)
352 {
353 case prefix_type::turn_start:
354 g.ch = Options.show_newturn_mark ? '-' : ' ';
355 g.col = LIGHTGRAY;
356 break;
357 case prefix_type::turn_end:
358 case prefix_type::new_turn:
359 g.ch = Options.show_newturn_mark ? '_' : ' ';
360 g.col = LIGHTGRAY;
361 break;
362 case prefix_type::new_cmd:
363 g.ch = Options.show_newturn_mark ? '_' : ' ';
364 g.col = DARKGRAY;
365 break;
366 case prefix_type::full_more:
367 g.ch = '+';
368 g.col = channel_to_colour(MSGCH_PROMPT);
369 break;
370 case prefix_type::other_more:
371 g.ch = '+';
372 g.col = LIGHTRED;
373 break;
374 default:
375 g.ch = ' ';
376 g.col = LIGHTGRAY;
377 break;
378 }
379 return g;
380 }
381
382 static bool _pre_more();
383
384 static bool _temporary = false;
385
386 class message_window
387 {
388 int next_line;
389 int temp_line; // starting point of temporary messages
390 int input_line; // last line-after-input
391 vector<formatted_string> lines;
392 prefix_type prompt; // current prefix prompt
393
height() const394 int height() const
395 {
396 return crawl_view.msgsz.y;
397 }
398
use_last_line() const399 int use_last_line() const
400 {
401 return first_col_more();
402 }
403
width() const404 int width() const
405 {
406 return crawl_view.msgsz.x;
407 }
408
out_line(const formatted_string & line,int n) const409 void out_line(const formatted_string& line, int n) const
410 {
411 cgotoxy(1, n + 1, GOTO_MSG);
412 line.display();
413 cprintf("%*s", width() - line.width(), "");
414 }
415
416 // Place cursor at end of last non-empty line to handle prompts.
417 // TODO: might get rid of this by clearing the whole window when writing,
418 // and then just writing the actual non-empty lines.
place_cursor()419 void place_cursor()
420 {
421 // XXX: the screen may have resized since the last time we
422 // called lines.resize(). Consider only the last height()
423 // lines if this has happened.
424 const int diff = max(int(lines.size()) - height(), 0);
425
426 int i;
427 for (i = lines.size() - 1; i >= diff && lines[i].width() == 0; --i)
428 ;
429 if (i >= diff)
430 {
431 // If there was room, put the cursor at the end of that line.
432 // Otherwise, put it at the beginning of the next line.
433 if ((int) lines[i].width() < crawl_view.msgsz.x)
434 cgotoxy(lines[i].width() + 1, i - diff + 1, GOTO_MSG);
435 else if (i - diff + 2 <= height())
436 cgotoxy(1, i - diff + 2, GOTO_MSG);
437 else
438 {
439 // Scroll to make room for the next line, then redraw.
440 scroll(1);
441 // Results in a recursive call to place_cursor! But scroll()
442 // made lines[height()] empty, so that recursive call shouldn't
443 // hit this case again.
444 show();
445 return;
446 }
447 }
448 else
449 {
450 // If there were no lines, put the cursor at the upper left.
451 cgotoxy(1, 1, GOTO_MSG);
452 }
453 }
454
455 // Whether to show msgwin-full more prompts.
more_enabled() const456 bool more_enabled() const
457 {
458 return crawl_state.show_more_prompt
459 && (Options.clear_messages || Options.show_more);
460 }
461
make_space(int n)462 int make_space(int n)
463 {
464 int space = out_height() - next_line;
465
466 if (space >= n)
467 return 0;
468
469 int s = 0;
470 if (input_line > 0)
471 {
472 s = min(input_line, n - space);
473 scroll(s);
474 space += s;
475 }
476
477 if (space >= n)
478 return s;
479
480 if (more_enabled())
481 more(true);
482
483 // We could consider just scrolling off after --more--;
484 // that would require marking the last message before
485 // the prompt.
486 if (!Options.clear_messages && !more_enabled())
487 {
488 scroll(n - space);
489 return s + n - space;
490 }
491 else
492 {
493 clear();
494 return height();
495 }
496 }
497
add_line(const formatted_string & line)498 void add_line(const formatted_string& line)
499 {
500 resize(); // TODO: get rid of this
501 lines[next_line] = line;
502 next_line++;
503 }
504
output_prefix(prefix_type p)505 void output_prefix(prefix_type p)
506 {
507 if (!use_first_col())
508 return;
509 if (p <= prompt)
510 return;
511 prompt = p;
512 if (next_line > 0)
513 {
514 formatted_string line;
515 line.add_glyph(_prefix_glyph(prompt));
516 lines[next_line-1].del_char();
517 line += lines[next_line-1];
518 lines[next_line-1] = line;
519 }
520 show();
521 }
522
523 public:
message_window()524 message_window()
525 : next_line(0), temp_line(0), input_line(0), prompt(prefix_type::none)
526 {
527 clear_lines(); // initialize this->lines
528 }
529
resize()530 void resize()
531 {
532 // if this is resized to 0, bad crashes will happen. N.b. I have no idea
533 // if this issue is what the following note is about:
534 // XXX: broken (why?)
535 lines.resize(max(height(), 1));
536 }
537
out_width() const538 unsigned int out_width() const
539 {
540 return width() - (use_first_col() ? 1 : 0);
541 }
542
out_height() const543 unsigned int out_height() const
544 {
545 return height() - (use_last_line() ? 0 : 1);
546 }
547
clear_lines()548 void clear_lines()
549 {
550 lines.clear();
551 lines.resize(height());
552 }
553
first_col_more() const554 bool first_col_more() const
555 {
556 return Options.small_more;
557 }
558
use_first_col() const559 bool use_first_col() const
560 {
561 return !Options.clear_messages;
562 }
563
set_starting_line()564 void set_starting_line()
565 {
566 // TODO: start at end (sometimes?)
567 next_line = 0;
568 input_line = 0;
569 temp_line = 0;
570 }
571
clear()572 void clear()
573 {
574 clear_lines();
575 set_starting_line();
576 show();
577 }
578
scroll(int n)579 void scroll(int n)
580 {
581 // We might be asked to scroll off everything by the line reader.
582 if (next_line < n)
583 n = next_line;
584
585 int i;
586 for (i = 0; i < height() - n; ++i)
587 lines[i] = lines[i + n];
588 for (; i < height(); ++i)
589 lines[i].clear();
590 next_line -= n;
591 temp_line -= n;
592 input_line -= n;
593 }
594
595 // write to screen (without refresh)
show()596 void show()
597 {
598 // skip if there is no layout yet
599 if (width() <= 0)
600 return;
601
602 // XXX: this should not be necessary as formatted_string should
603 // already do it
604 textcolour(LIGHTGREY);
605
606 // XXX: the screen may have resized since the last time we
607 // called lines.resize(). Consider only the last height()
608 // lines if this has happened.
609 const int diff = max(int(lines.size()) - height(), 0);
610
611 for (size_t i = diff; i < lines.size(); ++i)
612 out_line(lines[i], i - diff);
613 place_cursor();
614 #ifdef USE_TILE
615 tiles.set_need_redraw();
616 #endif
617 }
618
619 // temporary: to be overwritten with next item, e.g. new turn
620 // leading dash or prompt without response
add_item(string text,prefix_type first_col=prefix_type::none,bool temporary=false)621 void add_item(string text, prefix_type first_col = prefix_type::none,
622 bool temporary = false)
623 {
624 prompt = prefix_type::none; // reset prompt
625
626 vector<formatted_string> newlines;
627 linebreak_string(text, out_width());
628 formatted_string::parse_string_to_multiple(text, newlines);
629
630 for (const formatted_string &nl : newlines)
631 {
632 make_space(1);
633 formatted_string line;
634 if (use_first_col())
635 line.add_glyph(_prefix_glyph(first_col));
636 line += nl;
637 add_line(line);
638 }
639
640 if (!temporary)
641 reset_temp();
642
643 show();
644 }
645
roll_back()646 void roll_back()
647 {
648 temp_line = max(temp_line, 0);
649 for (int i = temp_line; i < next_line; ++i)
650 lines[i].clear();
651 next_line = temp_line;
652 }
653
654 /**
655 * Consider any formerly-temporary messages permanent.
656 */
reset_temp()657 void reset_temp()
658 {
659 temp_line = next_line;
660 }
661
got_input()662 void got_input()
663 {
664 input_line = next_line;
665 }
666
new_cmdturn(bool new_turn)667 void new_cmdturn(bool new_turn)
668 {
669 output_prefix(new_turn ? prefix_type::new_turn : prefix_type::new_cmd);
670 }
671
any_messages()672 bool any_messages()
673 {
674 return next_line > input_line;
675 }
676
677 /*
678 * Handling of more prompts (both types).
679 */
more(bool full,bool user=false)680 void more(bool full, bool user=false)
681 {
682 rng::generator rng(rng::UI);
683
684 if (_pre_more())
685 return;
686
687 if (you.running)
688 {
689 mouse_control mc(MOUSE_MODE_MORE);
690 redraw_screen();
691 update_screen();
692 }
693 else
694 {
695 print_stats();
696 update_screen();
697 show();
698 }
699
700 int last_row = crawl_view.msgsz.y;
701 if (first_col_more())
702 {
703 cgotoxy(1, last_row, GOTO_MSG);
704 cglyph_t g = _prefix_glyph(full ? prefix_type::full_more : prefix_type::other_more);
705 formatted_string f;
706 f.add_glyph(g);
707 f.display();
708 // Move cursor back for nicer display.
709 cgotoxy(1, last_row, GOTO_MSG);
710 // Need to read_key while cursor_control in scope.
711 cursor_control con(true);
712 readkey_more();
713 }
714 else
715 {
716 cgotoxy(use_first_col() ? 2 : 1, last_row, GOTO_MSG);
717 textcolour(channel_to_colour(MSGCH_PROMPT));
718 if (crawl_state.game_is_hints())
719 {
720 string more_str = "--more-- Press Space ";
721 if (is_tiles())
722 more_str += "or click ";
723 more_str += "to continue. You can later reread messages with "
724 "Ctrl-P.";
725 cprintf(more_str.c_str());
726 }
727 else
728 cprintf("--more--");
729
730 readkey_more(user);
731 }
732 }
733 };
734
735 message_window msgwin;
736
display_message_window()737 void display_message_window()
738 {
739 msgwin.show();
740 }
741
clear_message_window()742 void clear_message_window()
743 {
744 msgwin = message_window();
745 }
746
scroll_message_window(int n)747 void scroll_message_window(int n)
748 {
749 msgwin.scroll(n);
750 msgwin.show();
751 }
752
any_messages()753 bool any_messages()
754 {
755 return msgwin.any_messages();
756 }
757
758 typedef circ_vec<message_line, NUM_STORED_MESSAGES> store_t;
759
760 class message_store
761 {
762 store_t msgs;
763 message_line prev_msg;
764 bool last_of_turn;
765 int temp; // number of temporary messages
766
767 #ifdef USE_TILE_WEB
768 int unsent; // number of messages not yet sent to the webtiles client
769 int client_rollback;
770 bool send_ignore_one;
771 #endif
772
773 public:
message_store()774 message_store() : last_of_turn(false), temp(0)
775 #ifdef USE_TILE_WEB
776 , unsent(0), client_rollback(0), send_ignore_one(false)
777 #endif
778 {}
779
add(const message_line & msg)780 void add(const message_line& msg)
781 {
782 string orig_full_text = msg.full_text();
783
784 if (!(msg.channel != MSGCH_PROMPT && prev_msg.merge(msg)))
785 {
786 flush_prev();
787 prev_msg = msg;
788 if (msg.channel == MSGCH_PROMPT || _temporary)
789 flush_prev();
790 }
791
792 // If we play sound, wait until the corresponding message is printed
793 // in case we intend on holding up output that comes after.
794 //
795 // FIXME This doesn't work yet, and causes the game to play the sound,
796 // THEN display the text. This appears to only be solvable by reworking
797 // the way the game outputs messages, as the game it prints messages
798 // one line at a time, not one message at a time.
799 //
800 // However, it should only print one message at a time when it really
801 // needs to, i.e. an sound that interrupts the game. Otherwise it is
802 // more efficent to print text together.
803 #ifdef USE_SOUND
804 play_sound(check_sound_patterns(orig_full_text));
805 #endif
806 }
807
store_msg(const message_line & msg)808 void store_msg(const message_line& msg)
809 {
810 prefix_type p = prefix_type::none;
811 msgs.push_back(msg);
812 if (_temporary)
813 temp++;
814 else
815 reset_temp();
816 #ifdef USE_TILE_WEB
817 // ignore this message until it's actually displayed in case we run out
818 // of space and have to display --more-- instead
819 unwind_bool dontsend(send_ignore_one, true);
820 #endif
821 if (crawl_state.io_inited && crawl_state.game_started)
822 msgwin.add_item(msg.full_text(), p, _temporary);
823 }
824
roll_back()825 void roll_back()
826 {
827 #ifdef USE_TILE_WEB
828 client_rollback = max(0, temp - unsent);
829 unsent = max(0, unsent - temp);
830 #endif
831 msgs.roll_back(temp);
832 temp = 0;
833 }
834
reset_temp()835 void reset_temp()
836 {
837 temp = 0;
838 }
839
flush_prev()840 void flush_prev()
841 {
842 if (!prev_msg)
843 return;
844 message_line msg = prev_msg;
845 // Clear prev_msg before storing it, since
846 // writing out to the message window might
847 // in turn result in a recursive flush_prev.
848 prev_msg = message_line();
849 #ifdef USE_TILE_WEB
850 unsent++;
851 #endif
852 store_msg(msg);
853 if (last_of_turn)
854 {
855 msgwin.new_cmdturn(true);
856 last_of_turn = false;
857 }
858 }
859
new_turn()860 void new_turn()
861 {
862 if (prev_msg)
863 last_of_turn = true;
864 else
865 msgwin.new_cmdturn(true);
866 }
867
868 // XXX: this should not need to exist
get_store()869 const store_t& get_store()
870 {
871 return msgs;
872 }
873
append_store(store_t store)874 void append_store(store_t store)
875 {
876 msgs.append(store);
877 const int msgs_to_print = store.filled_size();
878 #ifdef USE_TILE_WEB
879 unwind_bool dontsend(send_ignore_one, true);
880 #endif
881 for (int i = 0; i < msgs_to_print; i++)
882 msgwin.add_item(msgs[i - msgs_to_print].full_text(), prefix_type::none, false);
883 }
884
clear()885 void clear()
886 {
887 msgs.clear();
888 prev_msg = message_line();
889 last_of_turn = false;
890 temp = 0;
891 #ifdef USE_TILE_WEB
892 unsent = 0;
893 #endif
894 }
895
896 #ifdef USE_TILE_WEB
send()897 void send()
898 {
899 if (unsent == 0 || (send_ignore_one && unsent == 1)) return;
900
901 if (client_rollback > 0)
902 {
903 tiles.json_write_int("rollback", client_rollback);
904 client_rollback = 0;
905 }
906 tiles.json_open_array("messages");
907 for (int i = -unsent; i < (send_ignore_one ? -1 : 0); ++i)
908 {
909 message_line& msg = msgs[i];
910 tiles.json_open_object();
911 tiles.json_write_string("text", msg.full_text());
912 tiles.json_write_int("turn", msg.turn);
913 tiles.json_write_int("channel", msg.channel);
914 tiles.json_close_object();
915 }
916 tiles.json_close_array();
917 unsent = send_ignore_one ? 1 : 0;
918 }
919 #endif
920 };
921
922 // Circular buffer for keeping past messages.
923 message_store buffer;
924
925 #ifdef USE_TILE_WEB
926 bool _more = false, _last_more = false;
927
webtiles_send_messages()928 void webtiles_send_messages()
929 {
930 // defer sending any messages to client in this form until a game is
931 // started up. It's still possible to send them as a popup. When this is
932 // eventually called, it'll send any queued messages.
933 if (!crawl_state.io_inited || !crawl_state.game_started)
934 return;
935 tiles.json_open_object();
936 tiles.json_write_string("msg", "msgs");
937 tiles.json_treat_as_empty();
938 if (_more != _last_more)
939 {
940 tiles.json_write_bool("more", _more);
941 _last_more = _more;
942 }
943 buffer.send();
944 tiles.json_close_object(true);
945 tiles.finish_message();
946 }
947 #else
webtiles_send_messages()948 void webtiles_send_messages() { }
949 #endif
950
951 static FILE* _msg_dump_file = nullptr;
952
953 static msg_colour_type prepare_message(const string& imsg,
954 msg_channel_type channel,
955 int param,
956 bool allow_suppress=true);
957
958
959 namespace msg
960 {
961 static bool suppress_messages = false;
962 static unordered_set<tee *> current_message_tees;
963 static maybe_bool _msgs_to_stderr = MB_MAYBE;
964
_suppressed()965 static bool _suppressed()
966 {
967 return suppress_messages;
968 }
969
970 /**
971 * RAII logic for controlling echoing to stderr.
972 * @param f the new state:
973 * MB_TRUE: always echo to stderr (mainly used for debugging)
974 * MB_MAYBE: use default logic, based on mode, io state, etc
975 * MB_FALSE: never echo to stderr (for suppressing error echoing during
976 * startup, e.g. for first-pass initfile processing)
977 */
force_stderr(maybe_bool f)978 force_stderr::force_stderr(maybe_bool f)
979 : prev_state(_msgs_to_stderr)
980 {
981 _msgs_to_stderr = f;
982 }
983
~force_stderr()984 force_stderr::~force_stderr()
985 {
986 _msgs_to_stderr = prev_state;
987 }
988
989
uses_stderr(msg_channel_type channel)990 bool uses_stderr(msg_channel_type channel)
991 {
992 if (_msgs_to_stderr == MB_TRUE)
993 return true;
994 else if (_msgs_to_stderr == MB_FALSE)
995 return false;
996 // else, MB_MAYBE:
997
998 if (channel == MSGCH_ERROR)
999 {
1000 return !crawl_state.io_inited // one of these is not like the others
1001 || crawl_state.test || crawl_state.script
1002 || crawl_state.build_db
1003 || crawl_state.map_stat_gen || crawl_state.obj_stat_gen;
1004 }
1005 return false;
1006 }
1007
tee()1008 tee::tee()
1009 : target(nullptr)
1010 {
1011 current_message_tees.insert(this);
1012 }
1013
tee(string & _target)1014 tee::tee(string &_target)
1015 : target(&_target)
1016 {
1017 current_message_tees.insert(this);
1018 }
1019
force_update()1020 void tee::force_update()
1021 {
1022 if (target)
1023 *target += get_store();
1024 store.clear();
1025 }
1026
~tee()1027 tee::~tee()
1028 {
1029 force_update();
1030 current_message_tees.erase(this);
1031 }
1032
append(const string & s,msg_channel_type)1033 void tee::append(const string &s, msg_channel_type /*ch*/)
1034 {
1035 // could use a more c++y external interface -- but that just complicates things
1036 store << s;
1037 }
1038
append_line(const string & s,msg_channel_type ch)1039 void tee::append_line(const string &s, msg_channel_type ch)
1040 {
1041 append(s + "\n", ch);
1042 }
1043
get_store() const1044 string tee::get_store() const
1045 {
1046 return store.str();
1047 }
1048
_append_to_tees(const string & s,msg_channel_type ch)1049 static void _append_to_tees(const string &s, msg_channel_type ch)
1050 {
1051 for (auto tee : current_message_tees)
1052 tee->append(s, ch);
1053 }
1054
suppress()1055 suppress::suppress()
1056 : msuppressed(suppress_messages),
1057 channel(NUM_MESSAGE_CHANNELS),
1058 prev_colour(MSGCOL_NONE)
1059 {
1060 suppress_messages = true;
1061 }
1062
1063 // Push useful RAII conditional logic into a constructor
1064 // Won't override an outer suppressing msg::suppress
suppress(bool really_suppress)1065 suppress::suppress(bool really_suppress)
1066 : msuppressed(suppress_messages),
1067 channel(NUM_MESSAGE_CHANNELS),
1068 prev_colour(MSGCOL_NONE)
1069 {
1070 suppress_messages = suppress_messages || really_suppress;
1071 }
1072
1073 // Mute just one channel. Mainly useful for hiding debug spam in various
1074 // circumstances.
suppress(msg_channel_type _channel)1075 suppress::suppress(msg_channel_type _channel)
1076 : msuppressed(suppress_messages),
1077 channel(_channel),
1078 prev_colour(Options.channels[channel])
1079 {
1080 // don't change global suppress_messages for this case
1081 ASSERT(channel < NUM_MESSAGE_CHANNELS);
1082 Options.channels[channel] = MSGCOL_MUTED;
1083 }
1084
~suppress()1085 suppress::~suppress()
1086 {
1087 suppress_messages = msuppressed;
1088 if (channel < NUM_MESSAGE_CHANNELS)
1089 Options.channels[channel] = prev_colour;
1090 }
1091 }
1092
msg_colour(int col)1093 msg_colour_type msg_colour(int col)
1094 {
1095 return static_cast<msg_colour_type>(col);
1096 }
1097
colour_msg(msg_colour_type col)1098 static int colour_msg(msg_colour_type col)
1099 {
1100 if (col == MSGCOL_MUTED)
1101 return DARKGREY;
1102 else
1103 return static_cast<int>(col);
1104 }
1105
1106 // Returns a colour or MSGCOL_MUTED.
channel_to_msgcol(msg_channel_type channel,int param)1107 static msg_colour_type channel_to_msgcol(msg_channel_type channel, int param)
1108 {
1109 msg_colour_type ret;
1110
1111 switch (Options.channels[channel])
1112 {
1113 case MSGCOL_PLAIN:
1114 // Note that if the plain channel is muted, then we're protecting
1115 // the player from having that spread to other channels here.
1116 // The intent of plain is to give non-coloured messages, not to
1117 // suppress them.
1118 if (Options.channels[MSGCH_PLAIN] >= MSGCOL_DEFAULT)
1119 ret = MSGCOL_LIGHTGREY;
1120 else
1121 ret = Options.channels[MSGCH_PLAIN];
1122 break;
1123
1124 case MSGCOL_DEFAULT:
1125 case MSGCOL_ALTERNATE:
1126 switch (channel)
1127 {
1128 case MSGCH_GOD:
1129 ret = (Options.channels[channel] == MSGCOL_DEFAULT)
1130 ? msg_colour(god_colour(static_cast<god_type>(param)))
1131 : msg_colour(god_message_altar_colour(static_cast<god_type>(param)));
1132 break;
1133
1134 case MSGCH_DURATION:
1135 ret = MSGCOL_LIGHTBLUE;
1136 break;
1137
1138 case MSGCH_DANGER:
1139 ret = MSGCOL_RED;
1140 break;
1141
1142 case MSGCH_WARN:
1143 case MSGCH_ERROR:
1144 ret = MSGCOL_LIGHTRED;
1145 break;
1146
1147 case MSGCH_INTRINSIC_GAIN:
1148 ret = MSGCOL_GREEN;
1149 break;
1150
1151 case MSGCH_RECOVERY:
1152 ret = MSGCOL_LIGHTGREEN;
1153 break;
1154
1155 case MSGCH_TALK:
1156 case MSGCH_TALK_VISUAL:
1157 case MSGCH_HELL_EFFECT:
1158 ret = MSGCOL_WHITE;
1159 break;
1160
1161 case MSGCH_MUTATION:
1162 case MSGCH_MONSTER_WARNING:
1163 ret = MSGCOL_LIGHTRED;
1164 break;
1165
1166 case MSGCH_MONSTER_SPELL:
1167 case MSGCH_MONSTER_ENCHANT:
1168 case MSGCH_FRIEND_SPELL:
1169 case MSGCH_FRIEND_ENCHANT:
1170 ret = MSGCOL_LIGHTMAGENTA;
1171 break;
1172
1173 case MSGCH_TUTORIAL:
1174 case MSGCH_ORB:
1175 case MSGCH_BANISHMENT:
1176 ret = MSGCOL_MAGENTA;
1177 break;
1178
1179 case MSGCH_MONSTER_DAMAGE:
1180 ret = ((param == MDAM_DEAD) ? MSGCOL_RED :
1181 (param >= MDAM_SEVERELY_DAMAGED) ? MSGCOL_LIGHTRED :
1182 (param >= MDAM_MODERATELY_DAMAGED) ? MSGCOL_YELLOW
1183 : MSGCOL_LIGHTGREY);
1184 break;
1185
1186 case MSGCH_PROMPT:
1187 ret = MSGCOL_CYAN;
1188 break;
1189
1190 case MSGCH_DIAGNOSTICS:
1191 case MSGCH_MULTITURN_ACTION:
1192 ret = MSGCOL_DARKGREY; // makes it easier to ignore at times -- bwr
1193 break;
1194
1195 case MSGCH_PLAIN:
1196 case MSGCH_FRIEND_ACTION:
1197 case MSGCH_EQUIPMENT:
1198 case MSGCH_EXAMINE:
1199 case MSGCH_EXAMINE_FILTER:
1200 case MSGCH_DGL_MESSAGE:
1201 default:
1202 ret = param > 0 ? msg_colour(param) : MSGCOL_LIGHTGREY;
1203 break;
1204 }
1205 break;
1206
1207 case MSGCOL_MUTED:
1208 ret = MSGCOL_MUTED;
1209 break;
1210
1211 default:
1212 // Setting to a specific colour is handled here, special
1213 // cases should be handled above.
1214 if (channel == MSGCH_MONSTER_DAMAGE)
1215 {
1216 // A special case right now for monster damage (at least until
1217 // the init system is improved)... selecting a specific
1218 // colour here will result in only the death messages coloured.
1219 if (param == MDAM_DEAD)
1220 ret = Options.channels[channel];
1221 else if (Options.channels[MSGCH_PLAIN] >= MSGCOL_DEFAULT)
1222 ret = MSGCOL_LIGHTGREY;
1223 else
1224 ret = Options.channels[MSGCH_PLAIN];
1225 }
1226 else
1227 ret = Options.channels[channel];
1228 break;
1229 }
1230
1231 return ret;
1232 }
1233
channel_to_colour(msg_channel_type channel,int param)1234 int channel_to_colour(msg_channel_type channel, int param)
1235 {
1236 return colour_msg(channel_to_msgcol(channel, param));
1237 }
1238
do_message_print(msg_channel_type channel,int param,bool cap,bool nojoin,const char * format,va_list argp)1239 void do_message_print(msg_channel_type channel, int param, bool cap,
1240 bool nojoin, const char *format, va_list argp)
1241 {
1242 va_list ap;
1243 va_copy(ap, argp);
1244 char buff[200];
1245 size_t len = vsnprintf(buff, sizeof(buff), format, argp);
1246 if (len < sizeof(buff))
1247 _mpr(buff, channel, param, nojoin, cap);
1248 else
1249 {
1250 char *heapbuf = (char*)malloc(len + 1);
1251 vsnprintf(heapbuf, len + 1, format, ap);
1252 _mpr(heapbuf, channel, param, nojoin, cap);
1253 free(heapbuf);
1254 }
1255 va_end(ap);
1256 }
1257
mprf_nocap(msg_channel_type channel,int param,const char * format,...)1258 void mprf_nocap(msg_channel_type channel, int param, const char *format, ...)
1259 {
1260 va_list argp;
1261 va_start(argp, format);
1262 do_message_print(channel, param, false, false, format, argp);
1263 va_end(argp);
1264 }
1265
mprf_nocap(msg_channel_type channel,const char * format,...)1266 void mprf_nocap(msg_channel_type channel, const char *format, ...)
1267 {
1268 va_list argp;
1269 va_start(argp, format);
1270 do_message_print(channel, channel == MSGCH_GOD ? you.religion : 0,
1271 false, false, format, argp);
1272 va_end(argp);
1273 }
1274
mprf_nocap(const char * format,...)1275 void mprf_nocap(const char *format, ...)
1276 {
1277 va_list argp;
1278 va_start(argp, format);
1279 do_message_print(MSGCH_PLAIN, 0, false, false, format, argp);
1280 va_end(argp);
1281 }
1282
mprf(msg_channel_type channel,int param,const char * format,...)1283 void mprf(msg_channel_type channel, int param, const char *format, ...)
1284 {
1285 va_list argp;
1286 va_start(argp, format);
1287 do_message_print(channel, param, true, false, format, argp);
1288 va_end(argp);
1289 }
1290
mprf(msg_channel_type channel,const char * format,...)1291 void mprf(msg_channel_type channel, const char *format, ...)
1292 {
1293 va_list argp;
1294 va_start(argp, format);
1295 do_message_print(channel, channel == MSGCH_GOD ? you.religion : 0,
1296 true, false, format, argp);
1297 va_end(argp);
1298 }
1299
mprf(const char * format,...)1300 void mprf(const char *format, ...)
1301 {
1302 va_list argp;
1303 va_start(argp, format);
1304 do_message_print(MSGCH_PLAIN, 0, true, false, format, argp);
1305 va_end(argp);
1306 }
1307
mprf_nojoin(msg_channel_type channel,const char * format,...)1308 void mprf_nojoin(msg_channel_type channel, const char *format, ...)
1309 {
1310 va_list argp;
1311 va_start(argp, format);
1312 do_message_print(channel, channel == MSGCH_GOD ? you.religion : 0,
1313 true, true, format, argp);
1314 va_end(argp);
1315 }
1316
mprf_nojoin(const char * format,...)1317 void mprf_nojoin(const char *format, ...)
1318 {
1319 va_list argp;
1320 va_start(argp, format);
1321 do_message_print(MSGCH_PLAIN, 0, true, true, format, argp);
1322 va_end(argp);
1323 }
1324
1325 #ifdef DEBUG_DIAGNOSTICS
dprf(const char * format,...)1326 void dprf(const char *format, ...)
1327 {
1328 if (Options.quiet_debug_messages[DIAG_NORMAL] || you.suppress_wizard)
1329 return;
1330
1331 va_list argp;
1332 va_start(argp, format);
1333 do_message_print(MSGCH_DIAGNOSTICS, 0, false, false, format, argp);
1334 va_end(argp);
1335 }
1336
dprf(diag_type param,const char * format,...)1337 void dprf(diag_type param, const char *format, ...)
1338 {
1339 if (Options.quiet_debug_messages[param] || you.suppress_wizard)
1340 return;
1341
1342 va_list argp;
1343 va_start(argp, format);
1344 do_message_print(MSGCH_DIAGNOSTICS, param, false, false, format, argp);
1345 va_end(argp);
1346 }
1347 #endif
1348
1349 static bool _updating_view = false;
1350
_check_option(const string & line,msg_channel_type channel,const vector<message_filter> & option)1351 static bool _check_option(const string& line, msg_channel_type channel,
1352 const vector<message_filter>& option)
1353 {
1354 if (crawl_state.generating_level)
1355 return false;
1356 return any_of(begin(option),
1357 end(option),
1358 bind(mem_fn(&message_filter::is_filtered),
1359 placeholders::_1, channel, line));
1360 }
1361
_check_more(const string & line,msg_channel_type channel)1362 static bool _check_more(const string& line, msg_channel_type channel)
1363 {
1364 // Try to avoid mores during level excursions, they are glitchy at best.
1365 // TODO: this is sort of an emergency check, possibly it should
1366 // crash here in order to find the real bug?
1367 if (!you.on_current_level)
1368 return false;
1369 return _check_option(line, channel, Options.force_more_message);
1370 }
1371
_check_flash_screen(const string & line,msg_channel_type channel)1372 static bool _check_flash_screen(const string& line, msg_channel_type channel)
1373 {
1374 // absolutely never flash during a level excursion, things will go very
1375 // badly. TODO: this is sort of an emergency check, possibly it should
1376 // crash here in order to find the real bug?
1377 if (!you.on_current_level)
1378 return false;
1379 return _check_option(line, channel, Options.flash_screen_message);
1380 }
1381
_check_join(const string &,msg_channel_type channel)1382 static bool _check_join(const string& /*line*/, msg_channel_type channel)
1383 {
1384 switch (channel)
1385 {
1386 case MSGCH_EQUIPMENT:
1387 return false;
1388 default:
1389 break;
1390 }
1391 return true;
1392 }
1393
_debug_channel_arena(msg_channel_type channel)1394 static void _debug_channel_arena(msg_channel_type channel)
1395 {
1396 switch (channel)
1397 {
1398 case MSGCH_PROMPT:
1399 case MSGCH_GOD:
1400 case MSGCH_DURATION:
1401 case MSGCH_RECOVERY:
1402 case MSGCH_INTRINSIC_GAIN:
1403 case MSGCH_MUTATION:
1404 case MSGCH_EQUIPMENT:
1405 case MSGCH_FLOOR_ITEMS:
1406 case MSGCH_MULTITURN_ACTION:
1407 case MSGCH_EXAMINE:
1408 case MSGCH_EXAMINE_FILTER:
1409 case MSGCH_ORB:
1410 case MSGCH_TUTORIAL:
1411 die("Invalid channel '%s' in arena mode",
1412 channel_to_str(channel).c_str());
1413 break;
1414 default:
1415 break;
1416 }
1417 }
1418
strip_channel_prefix(string & text,msg_channel_type & channel,bool silence)1419 bool strip_channel_prefix(string &text, msg_channel_type &channel, bool silence)
1420 {
1421 string::size_type pos = text.find(":");
1422 if (pos == string::npos)
1423 return false;
1424
1425 string param = text.substr(0, pos);
1426 bool sound = false;
1427
1428 if (param == "WARN")
1429 channel = MSGCH_WARN, sound = true;
1430 else if (param == "VISUAL WARN")
1431 channel = MSGCH_WARN;
1432 else if (param == "SOUND")
1433 channel = MSGCH_SOUND, sound = true;
1434 else if (param == "VISUAL")
1435 channel = MSGCH_TALK_VISUAL;
1436 else if (param == "SPELL")
1437 channel = MSGCH_MONSTER_SPELL, sound = true;
1438 else if (param == "VISUAL SPELL")
1439 channel = MSGCH_MONSTER_SPELL;
1440 else if (param == "ENCHANT")
1441 channel = MSGCH_MONSTER_ENCHANT, sound = true;
1442 else if (param == "VISUAL ENCHANT")
1443 channel = MSGCH_MONSTER_ENCHANT;
1444 else
1445 {
1446 param = replace_all(param, " ", "_");
1447 lowercase(param);
1448 int ch = str_to_channel(param);
1449 if (ch == -1)
1450 return false;
1451 channel = static_cast<msg_channel_type>(ch);
1452 }
1453
1454 if (sound && silence)
1455 text = "";
1456 else
1457 text = text.substr(pos + 1);
1458 return true;
1459 }
1460
msgwin_set_temporary(bool temp)1461 void msgwin_set_temporary(bool temp)
1462 {
1463 flush_prev_message();
1464 _temporary = temp;
1465 if (!temp)
1466 {
1467 buffer.reset_temp();
1468 msgwin.reset_temp();
1469 }
1470 }
1471
msgwin_temporary_mode()1472 msgwin_temporary_mode::msgwin_temporary_mode()
1473 : previous(_temporary)
1474 {
1475 msgwin_set_temporary(true);
1476 }
1477
~msgwin_temporary_mode()1478 msgwin_temporary_mode::~msgwin_temporary_mode()
1479 {
1480 // RAII behaviour: embedding instances of this class within each other
1481 // will only reset the mode once they are all cleared.
1482 msgwin_set_temporary(previous);
1483 }
1484
msgwin_clear_temporary()1485 void msgwin_clear_temporary()
1486 {
1487 buffer.roll_back();
1488 msgwin.roll_back();
1489 }
1490
1491 static int _last_msg_turn = -1; // Turn of last message.
1492
_mpr(string text,msg_channel_type channel,int param,bool nojoin,bool cap)1493 static void _mpr(string text, msg_channel_type channel, int param, bool nojoin,
1494 bool cap)
1495 {
1496 rng::generator rng(rng::UI);
1497
1498 if (_msg_dump_file != nullptr)
1499 fprintf(_msg_dump_file, "%s\n", text.c_str()); // should this strip color tags?
1500
1501 if (crawl_state.game_crashed)
1502 return;
1503
1504 if (crawl_state.game_is_valid_type() && crawl_state.game_is_arena())
1505 _debug_channel_arena(channel);
1506
1507 #ifdef DEBUG_FATAL
1508 if (channel == MSGCH_ERROR)
1509 die_noline("%s", formatted_string::parse_string(text).tostring().c_str());
1510 #endif
1511
1512 if (msg::uses_stderr(channel))
1513 fprintf(stderr, "%s\n", formatted_string::parse_string(text).tostring().c_str());
1514
1515 // Flush out any "comes into view" monster announcements before the
1516 // monster has a chance to give any other messages.
1517 if (!_updating_view && crawl_state.io_inited)
1518 {
1519 _updating_view = true;
1520 flush_comes_into_view();
1521 _updating_view = false;
1522 }
1523
1524 if (channel == MSGCH_GOD && param == 0)
1525 param = you.religion;
1526
1527 // Ugly hack.
1528 if (channel == MSGCH_DIAGNOSTICS || channel == MSGCH_ERROR)
1529 cap = false;
1530
1531 // if the message would be muted, handle any tees before bailing. The
1532 // actual color for MSGCOL_MUTED ends up as darkgrey in any tees.
1533 msg_colour_type colour = prepare_message(text, channel, param);
1534
1535 string col = colour_to_str(colour_msg(colour));
1536 text = "<" + col + ">" + text + "</" + col + ">"; // XXX
1537
1538 msg::_append_to_tees(text + "\n", channel);
1539
1540 if (colour == MSGCOL_MUTED && crawl_state.io_inited)
1541 {
1542 if (channel == MSGCH_PROMPT)
1543 msgwin.show();
1544 return;
1545 }
1546
1547 clua.callfn("c_message", "ss", text.c_str(), channel_to_str(channel).c_str());
1548
1549 bool domore = _check_more(text, channel);
1550 bool do_flash_screen = _check_flash_screen(text, channel);
1551 bool join = !domore && !nojoin && _check_join(text, channel);
1552
1553 // Must do this before converting to formatted string and back;
1554 // that doesn't preserve close tags!
1555
1556 formatted_string fs = formatted_string::parse_string(text);
1557
1558 // TODO: this kind of check doesn't really belong in logging code...
1559 if (you.duration[DUR_QUAD_DAMAGE])
1560 fs.all_caps(); // No sound, so we simulate the reverb with all caps.
1561 else if (cap)
1562 fs.capitalise();
1563 if (channel != MSGCH_ERROR && channel != MSGCH_DIAGNOSTICS)
1564 fs.filter_lang();
1565 text = fs.to_colour_string();
1566
1567 message_line msg = message_line(text, channel, param, join);
1568 buffer.add(msg);
1569
1570 if (!crawl_state.io_inited)
1571 return;
1572
1573 _last_msg_turn = msg.turn;
1574
1575 if (channel == MSGCH_ERROR)
1576 interrupt_activity(activity_interrupt::force);
1577
1578 if (channel == MSGCH_PROMPT || channel == MSGCH_ERROR)
1579 set_more_autoclear(false);
1580
1581 if (domore)
1582 more(true);
1583
1584 if (do_flash_screen)
1585 flash_view_delay(UA_ALWAYS_ON, YELLOW, 50);
1586
1587 }
1588
show_prompt(string prompt)1589 static string show_prompt(string prompt)
1590 {
1591 mprf(MSGCH_PROMPT, "%s", prompt.c_str());
1592
1593 // FIXME: duplicating mpr code.
1594 msg_colour_type colour = prepare_message(prompt, MSGCH_PROMPT, 0);
1595 return colour_string(prompt, colour_msg(colour));
1596 }
1597
1598 static string _prompt;
msgwin_prompt(string prompt)1599 void msgwin_prompt(string prompt)
1600 {
1601 msgwin_set_temporary(true);
1602 _prompt = show_prompt(prompt);
1603 }
1604
msgwin_reply(string reply)1605 void msgwin_reply(string reply)
1606 {
1607 msgwin_clear_temporary();
1608 msgwin_set_temporary(false);
1609 reply = replace_all(reply, "<", "<<");
1610 mprf(MSGCH_PROMPT, "%s<lightgrey>%s</lightgrey>", _prompt.c_str(), reply.c_str());
1611 msgwin.got_input();
1612 }
1613
msgwin_got_input()1614 void msgwin_got_input()
1615 {
1616 msgwin.got_input();
1617 }
1618
msgwin_get_line(string prompt,char * buf,int len,input_history * mh,const string & fill)1619 int msgwin_get_line(string prompt, char *buf, int len,
1620 input_history *mh, const string &fill)
1621 {
1622 #ifdef TOUCH_UI
1623 bool use_popup = true;
1624 #else
1625 bool use_popup = !crawl_state.need_save || ui::has_layout();
1626 #endif
1627
1628 int ret;
1629 if (use_popup)
1630 {
1631 mouse_control mc(MOUSE_MODE_PROMPT);
1632
1633 linebreak_string(prompt, 79);
1634 msg_colour_type colour = prepare_message(prompt, MSGCH_PROMPT, 0);
1635 const auto colour_prompt = formatted_string(prompt, colour_msg(colour));
1636
1637 bool done = false;
1638 auto vbox = make_shared<ui::Box>(ui::Widget::VERT);
1639 auto popup = make_shared<ui::Popup>(vbox);
1640
1641 vbox->add_child(make_shared<ui::Text>(colour_prompt + "\n"));
1642
1643 auto input = make_shared<ui::TextEntry>();
1644 input->set_sync_id("input");
1645 input->set_text(fill);
1646 input->set_input_history(mh);
1647 #ifndef USE_TILE_LOCAL
1648 input->max_size().width = 20;
1649 #endif
1650 vbox->add_child(input);
1651
1652 popup->on_hotkey_event([&](const ui::KeyEvent& ev) {
1653 switch (ev.key())
1654 {
1655 CASE_ESCAPE
1656 ret = CK_ESCAPE;
1657 return done = true;
1658 case CK_ENTER:
1659 ret = 0;
1660 return done = true;
1661 default:
1662 return done = false;
1663 }
1664 });
1665
1666 #ifdef USE_TILE_WEB
1667 tiles.json_open_object();
1668 tiles.json_write_string("prompt", colour_prompt.to_colour_string());
1669 tiles.push_ui_layout("msgwin-get-line", 0);
1670 popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
1671 #endif
1672 ui::run_layout(move(popup), done, input);
1673
1674 strncpy(buf, input->get_text().c_str(), len - 1);
1675 buf[len - 1] = '\0';
1676 }
1677 else
1678 {
1679 if (!prompt.empty())
1680 msgwin_prompt(prompt);
1681 ret = cancellable_get_line(buf, len, mh, nullptr, fill);
1682 msgwin_reply(buf);
1683 }
1684
1685 return ret;
1686 }
1687
msgwin_new_turn()1688 void msgwin_new_turn()
1689 {
1690 buffer.new_turn();
1691 }
1692
msgwin_new_cmd()1693 void msgwin_new_cmd()
1694 {
1695 #ifndef USE_TILE_LOCAL
1696 if (crawl_state.smallterm)
1697 return;
1698 #endif
1699
1700 flush_prev_message();
1701 bool new_turn = (you.num_turns > _last_msg_turn);
1702 msgwin.new_cmdturn(new_turn);
1703 }
1704
msgwin_line_length()1705 unsigned int msgwin_line_length()
1706 {
1707 return msgwin.out_width();
1708 }
1709
msgwin_lines()1710 unsigned int msgwin_lines()
1711 {
1712 return msgwin.out_height();
1713 }
1714
1715 // mpr() an arbitrarily long list of strings without truncation or risk
1716 // of overflow.
mpr_comma_separated_list(const string & prefix,const vector<string> & list,const string & andc,const string & comma,const msg_channel_type channel,const int param)1717 void mpr_comma_separated_list(const string &prefix,
1718 const vector<string> &list,
1719 const string &andc,
1720 const string &comma,
1721 const msg_channel_type channel,
1722 const int param)
1723 {
1724 string out = prefix;
1725
1726 for (int i = 0, size = list.size(); i < size; i++)
1727 {
1728 out += list[i];
1729
1730 if (size > 0 && i < (size - 2))
1731 out += comma;
1732 else if (i == (size - 2))
1733 out += andc;
1734 else if (i == (size - 1))
1735 out += ".";
1736 }
1737 _mpr(out, channel, param);
1738 }
1739
1740 // Checks whether a given message contains patterns relevant for
1741 // notes, stop_running or sounds and handles these cases.
mpr_check_patterns(const string & message,msg_channel_type channel,int param)1742 static void mpr_check_patterns(const string& message,
1743 msg_channel_type channel,
1744 int param)
1745 {
1746 if (crawl_state.generating_level)
1747 return;
1748 for (const text_pattern &pat : Options.note_messages)
1749 {
1750 if (channel == MSGCH_EQUIPMENT || channel == MSGCH_FLOOR_ITEMS
1751 || channel == MSGCH_MULTITURN_ACTION
1752 || channel == MSGCH_EXAMINE || channel == MSGCH_EXAMINE_FILTER
1753 || channel == MSGCH_TUTORIAL || channel == MSGCH_DGL_MESSAGE)
1754 {
1755 continue;
1756 }
1757
1758 if (pat.matches(message))
1759 {
1760 take_note(Note(NOTE_MESSAGE, channel, param, message));
1761 break;
1762 }
1763 }
1764
1765 if (channel != MSGCH_DIAGNOSTICS && channel != MSGCH_EQUIPMENT)
1766 {
1767 interrupt_activity(activity_interrupt::message,
1768 channel_to_str(channel) + ":" + message);
1769 }
1770 }
1771
channel_message_history(msg_channel_type channel)1772 static bool channel_message_history(msg_channel_type channel)
1773 {
1774 switch (channel)
1775 {
1776 case MSGCH_PROMPT:
1777 case MSGCH_EQUIPMENT:
1778 case MSGCH_EXAMINE_FILTER:
1779 return false;
1780 default:
1781 return true;
1782 }
1783 }
1784
1785 // Returns the default colour of the message, or MSGCOL_MUTED if
1786 // the message should be suppressed.
prepare_message(const string & imsg,msg_channel_type channel,int param,bool allow_suppress)1787 static msg_colour_type prepare_message(const string& imsg,
1788 msg_channel_type channel,
1789 int param,
1790 bool allow_suppress)
1791 {
1792 if (allow_suppress && msg::_suppressed())
1793 return MSGCOL_MUTED;
1794
1795 if (you.num_turns > 0 && silenced(you.pos())
1796 && (channel == MSGCH_SOUND || channel == MSGCH_TALK))
1797 {
1798 return MSGCOL_MUTED;
1799 }
1800
1801 msg_colour_type colour = channel_to_msgcol(channel, param);
1802
1803 if (colour != MSGCOL_MUTED)
1804 mpr_check_patterns(imsg, channel, param);
1805
1806 if (!crawl_state.generating_level)
1807 {
1808 for (const message_colour_mapping &mcm : Options.message_colour_mappings)
1809 {
1810 if (mcm.message.is_filtered(channel, imsg))
1811 {
1812 colour = mcm.colour;
1813 break;
1814 }
1815 }
1816 }
1817
1818 return colour;
1819 }
1820
flush_prev_message()1821 void flush_prev_message()
1822 {
1823 buffer.flush_prev();
1824 }
1825
clear_messages(bool force)1826 void clear_messages(bool force)
1827 {
1828 if (!crawl_state.io_inited)
1829 return;
1830 // Unflushed message will be lost with clear_messages,
1831 // so they shouldn't really exist, but some of the delay
1832 // code appears to do this intentionally.
1833 // ASSERT(!buffer.have_prev());
1834 flush_prev_message();
1835
1836 msgwin.got_input(); // Consider old messages as read.
1837
1838 if (Options.clear_messages || force)
1839 msgwin.clear();
1840
1841 // TODO: we could indicate indicate clear_messages with a different
1842 // leading character than '-'.
1843 }
1844
1845 static bool autoclear_more = false;
1846
set_more_autoclear(bool on)1847 void set_more_autoclear(bool on)
1848 {
1849 autoclear_more = on;
1850 }
1851
readkey_more(bool user_forced)1852 static void readkey_more(bool user_forced)
1853 {
1854 if (autoclear_more)
1855 return;
1856 int keypress = 0;
1857 #ifdef USE_TILE_WEB
1858 unwind_bool unwind_more(_more, true);
1859 #endif
1860 mouse_control mc(MOUSE_MODE_MORE);
1861
1862 do
1863 {
1864 keypress = getch_ck();
1865 if (keypress == CK_REDRAW)
1866 {
1867 redraw_screen();
1868 update_screen();
1869 continue;
1870 }
1871 }
1872 while (keypress != ' ' && keypress != '\r' && keypress != '\n'
1873 && !key_is_escape(keypress)
1874 #ifdef TOUCH_UI
1875 && keypress != CK_MOUSE_CLICK);
1876 #else
1877 && (user_forced || keypress != CK_MOUSE_CLICK));
1878 #endif
1879
1880 if (key_is_escape(keypress))
1881 set_more_autoclear(true);
1882 }
1883
1884 /**
1885 * more() preprocessing.
1886 *
1887 * @return Whether the more prompt should be skipped.
1888 */
_pre_more()1889 static bool _pre_more()
1890 {
1891 if (crawl_state.game_crashed || crawl_state.seen_hups)
1892 return true;
1893
1894 #ifdef DEBUG_DIAGNOSTICS
1895 if (you.running)
1896 return true;
1897 #endif
1898
1899 if (crawl_state.game_is_arena())
1900 {
1901 delay(Options.view_delay);
1902 return true;
1903 }
1904
1905 if (crawl_state.is_replaying_keys())
1906 return true;
1907
1908 #ifdef WIZARD
1909 if (luaterp_running())
1910 return true;
1911 #endif
1912
1913 if (!crawl_state.show_more_prompt || msg::_suppressed())
1914 return true;
1915
1916 return false;
1917 }
1918
more(bool user_forced)1919 void more(bool user_forced)
1920 {
1921 rng::generator rng(rng::UI);
1922
1923 if (!crawl_state.io_inited)
1924 return;
1925 flush_prev_message();
1926 msgwin.more(false, user_forced);
1927 clear_messages();
1928 }
1929
canned_msg(canned_message_type which_message)1930 void canned_msg(canned_message_type which_message)
1931 {
1932 switch (which_message)
1933 {
1934 case MSG_SOMETHING_APPEARS:
1935 mprf("Something appears %s!",
1936 player_has_feet() ? "at your feet" : "before you");
1937 break;
1938 case MSG_NOTHING_HAPPENS:
1939 mpr("Nothing appears to happen.");
1940 break;
1941 case MSG_YOU_UNAFFECTED:
1942 mpr("You are unaffected.");
1943 break;
1944 case MSG_YOU_RESIST:
1945 mpr("You resist.");
1946 learned_something_new(HINT_YOU_RESIST);
1947 break;
1948 case MSG_YOU_PARTIALLY_RESIST:
1949 mpr("You partially resist.");
1950 break;
1951 case MSG_TOO_BERSERK:
1952 mpr("You are too berserk!");
1953 crawl_state.cancel_cmd_repeat();
1954 break;
1955 case MSG_TOO_CONFUSED:
1956 mpr("You are too confused!");
1957 break;
1958 case MSG_PRESENT_FORM:
1959 mpr("You can't do that in your present form.");
1960 crawl_state.cancel_cmd_repeat();
1961 break;
1962 case MSG_NOTHING_CARRIED:
1963 mpr("You aren't carrying anything.");
1964 crawl_state.cancel_cmd_repeat();
1965 break;
1966 case MSG_CANNOT_DO_YET:
1967 mpr("You can't do that yet.");
1968 crawl_state.cancel_cmd_repeat();
1969 break;
1970 case MSG_OK:
1971 mprf(MSGCH_PROMPT, "Okay, then.");
1972 crawl_state.cancel_cmd_repeat();
1973 break;
1974 case MSG_UNTHINKING_ACT:
1975 mpr("Why would you want to do that?");
1976 crawl_state.cancel_cmd_repeat();
1977 break;
1978 case MSG_NOTHING_THERE:
1979 mpr("There's nothing there!");
1980 crawl_state.cancel_cmd_repeat();
1981 break;
1982 case MSG_NOTHING_CLOSE_ENOUGH:
1983 mpr("There's nothing close enough!");
1984 crawl_state.cancel_cmd_repeat();
1985 break;
1986 case MSG_SPELL_FIZZLES:
1987 mpr("The spell fizzles.");
1988 break;
1989 case MSG_HUH:
1990 mprf(MSGCH_EXAMINE_FILTER, "Huh?");
1991 crawl_state.cancel_cmd_repeat();
1992 break;
1993 case MSG_EMPTY_HANDED_ALREADY:
1994 case MSG_EMPTY_HANDED_NOW:
1995 {
1996 const char* when =
1997 (which_message == MSG_EMPTY_HANDED_ALREADY ? "already" : "now");
1998 if (you.has_mutation(MUT_NO_GRASPING))
1999 mprf("Your mouth is %s empty.", when);
2000 else if (you.has_usable_claws(true))
2001 mprf("You are %s empty-clawed.", when);
2002 else if (you.has_usable_tentacles(true))
2003 mprf("You are %s empty-tentacled.", when);
2004 else
2005 mprf("You are %s empty-handed.", when);
2006 break;
2007 }
2008 case MSG_YOU_BLINK:
2009 mpr("You blink.");
2010 break;
2011 case MSG_STRANGE_STASIS:
2012 mpr("You feel a strange sense of stasis.");
2013 break;
2014 case MSG_NO_SPELLS:
2015 mpr("You don't know any spells.");
2016 break;
2017 case MSG_MANA_INCREASE:
2018 mpr("You feel your magic capacity increase.");
2019 break;
2020 case MSG_MANA_DECREASE:
2021 mpr("You feel your magic capacity decrease.");
2022 break;
2023 case MSG_DISORIENTED:
2024 mpr("You feel momentarily disoriented.");
2025 break;
2026 case MSG_DETECT_NOTHING:
2027 mpr("You detect nothing.");
2028 break;
2029 case MSG_CALL_DEAD:
2030 mpr("You call on the dead to rise...");
2031 break;
2032 case MSG_ANIMATE_REMAINS:
2033 mpr("You attempt to give life to the dead...");
2034 break;
2035 case MSG_CANNOT_MOVE:
2036 mpr("You cannot move.");
2037 break;
2038 case MSG_YOU_DIE:
2039 mpr_nojoin(MSGCH_PLAIN, "You die...");
2040 break;
2041 case MSG_GHOSTLY_OUTLINE:
2042 mpr("You see a ghostly outline there, and the spell fizzles.");
2043 break;
2044 case MSG_FULL_HEALTH:
2045 mpr("Your health is already full.");
2046 break;
2047 case MSG_FULL_MAGIC:
2048 mpr("Your reserves of magic are already full.");
2049 break;
2050 case MSG_GAIN_HEALTH:
2051 mpr("You feel better.");
2052 break;
2053 case MSG_GAIN_MAGIC:
2054 mpr("You feel your power returning.");
2055 break;
2056 case MSG_MAGIC_DRAIN:
2057 {
2058 if (you.has_mutation(MUT_HP_CASTING))
2059 mpr("You feel momentarily drained.");
2060 else
2061 mprf(MSGCH_WARN, "You suddenly feel drained of magical energy!");
2062 break;
2063 }
2064 case MSG_SOMETHING_IN_WAY:
2065 mpr("There's something in the way.");
2066 break;
2067 case MSG_CANNOT_SEE:
2068 mpr("You can't see that place.");
2069 break;
2070 case MSG_GOD_DECLINES:
2071 mpr("Your god isn't willing to do this for you now.");
2072 break;
2073 }
2074 }
2075
2076 // Note that this function *completely* blocks messaging for monsters
2077 // distant or invisible to the player ... look elsewhere for a function
2078 // permitting output of "It" messages for the invisible {dlb}
2079 // Intentionally avoids info and str_pass now. - bwr
simple_monster_message(const monster & mons,const char * event,msg_channel_type channel,int param,description_level_type descrip)2080 bool simple_monster_message(const monster& mons, const char *event,
2081 msg_channel_type channel,
2082 int param,
2083 description_level_type descrip)
2084 {
2085 if (you.see_cell(mons.pos())
2086 && (channel == MSGCH_MONSTER_SPELL || channel == MSGCH_FRIEND_SPELL
2087 || mons.visible_to(&you)))
2088 {
2089 string msg = mons.name(descrip);
2090 msg += event;
2091
2092 if (channel == MSGCH_PLAIN && mons.wont_attack())
2093 channel = MSGCH_FRIEND_ACTION;
2094
2095 mprf(channel, param, "%s", msg.c_str());
2096 return true;
2097 }
2098
2099 return false;
2100 }
2101
god_speaker(god_type which_deity)2102 string god_speaker(god_type which_deity)
2103 {
2104 if (which_deity == GOD_WU_JIAN)
2105 return "The Council";
2106 else
2107 return uppercase_first(god_name(which_deity));
2108 }
2109
2110 // yet another wrapper for mpr() {dlb}:
simple_god_message(const char * event,god_type which_deity)2111 void simple_god_message(const char *event, god_type which_deity)
2112 {
2113 string msg = god_speaker(which_deity) + event;
2114
2115 god_speaks(which_deity, msg.c_str());
2116 }
2117
wu_jian_sifu_message(const char * event)2118 void wu_jian_sifu_message(const char *event)
2119 {
2120 string msg;
2121 msg = uppercase_first(string("Sifu ") + wu_jian_random_sifu_name() + event);
2122 god_speaks(GOD_WU_JIAN, msg.c_str());
2123 }
2124
is_channel_dumpworthy(msg_channel_type channel)2125 static bool is_channel_dumpworthy(msg_channel_type channel)
2126 {
2127 return channel != MSGCH_EQUIPMENT
2128 && channel != MSGCH_DIAGNOSTICS
2129 && channel != MSGCH_TUTORIAL;
2130 }
2131
clear_message_store()2132 void clear_message_store()
2133 {
2134 buffer.clear();
2135 }
2136
get_last_messages(int mcount,bool full)2137 string get_last_messages(int mcount, bool full)
2138 {
2139 flush_prev_message();
2140
2141 string text;
2142 // XXX: should use some message_history iterator here
2143 const store_t& msgs = buffer.get_store();
2144 // XXX: loop wraps around otherwise. This could be done better.
2145 mcount = min(mcount, NUM_STORED_MESSAGES);
2146 for (int i = -1; mcount > 0; --i)
2147 {
2148 const message_line msg = msgs[i];
2149 if (!msg)
2150 break;
2151 if (full || is_channel_dumpworthy(msg.channel))
2152 {
2153 string line = msg.pure_text_with_repeats();
2154 string wrapped;
2155 while (!line.empty())
2156 wrapped += wordwrap_line(line, 79, false, true) + "\n";
2157 text = wrapped + text;
2158 }
2159 mcount--;
2160 }
2161
2162 // An extra line of clearance.
2163 if (!text.empty())
2164 text += "\n";
2165 return text;
2166 }
2167
get_recent_messages(vector<string> & mess,vector<msg_channel_type> & chan)2168 void get_recent_messages(vector<string> &mess,
2169 vector<msg_channel_type> &chan)
2170 {
2171 flush_prev_message();
2172
2173 const store_t& msgs = buffer.get_store();
2174 int mcount = NUM_STORED_MESSAGES;
2175 for (int i = -1; mcount > 0; --i, --mcount)
2176 {
2177 const message_line msg = msgs[i];
2178 if (!msg)
2179 break;
2180 mess.push_back(msg.pure_text_with_repeats());
2181 chan.push_back(msg.channel);
2182 }
2183 }
2184
recent_error_messages()2185 bool recent_error_messages()
2186 {
2187 // TODO: track whether player has seen error messages so this can be
2188 // more generally useful?
2189 flush_prev_message();
2190
2191 const store_t& msgs = buffer.get_store();
2192 int mcount = NUM_STORED_MESSAGES;
2193 for (int i = -1; mcount > 0; --i, --mcount)
2194 {
2195 const message_line msg = msgs[i];
2196 if (!msg)
2197 break;
2198 if (msg.channel == MSGCH_ERROR)
2199 return true;
2200 }
2201 return false;
2202 }
2203
2204 // We just write out the whole message store including empty/unused
2205 // messages. They'll be ignored when restoring.
save_messages(writer & outf)2206 void save_messages(writer& outf)
2207 {
2208 store_t msgs = buffer.get_store();
2209 marshallInt(outf, msgs.size());
2210 for (int i = 0; i < msgs.size(); ++i)
2211 {
2212 marshallString4(outf, msgs[i].full_text());
2213 marshallInt(outf, msgs[i].channel);
2214 marshallInt(outf, msgs[i].param);
2215 marshallInt(outf, msgs[i].turn);
2216 }
2217 }
2218
load_messages(reader & inf)2219 void load_messages(reader& inf)
2220 {
2221 unwind_bool save_more(crawl_state.show_more_prompt, false);
2222
2223 // assumes that the store was cleared at the beginning of _restore_game!
2224 flush_prev_message();
2225 store_t load_msgs = buffer.get_store(); // copy of messages during loading
2226 clear_message_store();
2227
2228 int num = unmarshallInt(inf);
2229 for (int i = 0; i < num; ++i)
2230 {
2231 string text;
2232 unmarshallString4(inf, text);
2233
2234 msg_channel_type channel = (msg_channel_type) unmarshallInt(inf);
2235 int param = unmarshallInt(inf);
2236 #if TAG_MAJOR_VERSION == 34
2237 if (inf.getMinorVersion() < TAG_MINOR_MESSAGE_REPEATS)
2238 unmarshallInt(inf); // was 'repeats'
2239 #endif
2240 int turn = unmarshallInt(inf);
2241
2242 message_line msg(message_line(text, channel, param, turn));
2243 if (msg)
2244 buffer.store_msg(msg);
2245 }
2246 flush_prev_message();
2247 buffer.append_store(load_msgs);
2248 clear_messages(); // check for Options.message_clear
2249 }
2250
_replay_messages_core(formatted_scroller & hist)2251 static void _replay_messages_core(formatted_scroller &hist)
2252 {
2253 flush_prev_message();
2254
2255 const store_t msgs = buffer.get_store();
2256 formatted_string lines;
2257 for (int i = 0; i < msgs.size(); ++i)
2258 if (channel_message_history(msgs[i].channel))
2259 {
2260 string text = msgs[i].full_text();
2261 if (!text.size())
2262 continue;
2263 linebreak_string(text, cgetsize(GOTO_CRT).x - 1);
2264 vector<formatted_string> parts;
2265 formatted_string::parse_string_to_multiple(text, parts, 80);
2266 for (unsigned int j = 0; j < parts.size(); ++j)
2267 {
2268 prefix_type p = prefix_type::none;
2269 if (j == parts.size() - 1 && i + 1 < msgs.size()
2270 && msgs[i+1].turn > msgs[i].turn)
2271 {
2272 p = prefix_type::turn_end;
2273 }
2274 if (!lines.empty())
2275 lines.add_glyph('\n');
2276 lines.add_glyph(_prefix_glyph(p));
2277 lines += parts[j];
2278 }
2279 }
2280
2281 hist.add_formatted_string(lines);
2282 hist.show();
2283 }
2284
replay_messages()2285 void replay_messages()
2286 {
2287 formatted_scroller hist(FS_START_AT_END | FS_PREWRAPPED_TEXT);
2288 hist.set_more();
2289
2290 _replay_messages_core(hist);
2291 }
2292
replay_messages_during_startup()2293 void replay_messages_during_startup()
2294 {
2295 formatted_scroller hist(FS_PREWRAPPED_TEXT);
2296 hist.set_more();
2297 hist.set_more(formatted_string::parse_string(
2298 "<cyan>Press Esc to close, arrows/pgup/pgdn to scroll.</cyan>"));
2299 hist.set_title(formatted_string::parse_string(recent_error_messages()
2300 ? "<yellow>Crawl encountered errors during initialization:</yellow>"
2301 : "<yellow>Initialization log:</yellow>"));
2302 _replay_messages_core(hist);
2303 }
2304
set_msg_dump_file(FILE * file)2305 void set_msg_dump_file(FILE* file)
2306 {
2307 _msg_dump_file = file;
2308 }
2309
formatted_mpr(const formatted_string & fs,msg_channel_type channel,int param)2310 void formatted_mpr(const formatted_string& fs,
2311 msg_channel_type channel, int param)
2312 {
2313 _mpr(fs.to_colour_string(), channel, param);
2314 }
2315