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