Trim some extra spaces (ChrisOelmueller)
[crawl:crawl.git] / crawl-ref / source / message.cc
1 /**
2  * @file
3  * @brief Functions used to print messages.
4 **/
5
6 #include "AppHdr.h"
7
8 #include "message.h"
9
10 #include "cio.h"
11 #include "colour.h"
12 #include "delay.h"
13 #include "format.h"
14 #include "initfile.h"
15 #include "libutil.h"
16 #include "menu.h"
17 #include "mon-stuff.h"
18 #include "notes.h"
19 #include "options.h"
20 #include "player.h"
21 #include "religion.h"
22 #include "stash.h"
23 #include "state.h"
24 #include "areas.h"
25 #include "tags.h"
26 #include "travel.h"
27 #include "hints.h"
28 #include "unwind.h"
29 #include "view.h"
30 #include "shout.h"
31 #include "viewgeom.h"
32
33 #include <sstream>
34
35 #ifdef WIZARD
36 #include "luaterp.h"
37 #endif
38
39 #ifdef USE_TILE_WEB
40 #include "tileweb.h"
41 #endif
42
43 static void _mpr(string text, msg_channel_type channel=MSGCH_PLAIN, int param=0,
44                  bool nojoin=false, bool cap=true);
45
46 void mpr(const char *text)
47 {
48     _mpr(text);
49 }
50
51 void mpr_nojoin(msg_channel_type channel, string text)
52 {
53     _mpr(text, channel, 0, true);
54 }
55
56 static bool _ends_in_punctuation(const string& text)
57 {
58     switch (text[text.size() - 1])
59     {
60     case '.':
61     case '!':
62     case '?':
63     case ',':
64     case ';':
65     case ':':
66         return true;
67     default:
68         return false;
69     }
70 }
71
72 struct message_item
73 {
74     msg_channel_type    channel;        // message channel
75     int                 param;          // param for channel (god, enchantment)
76     string              text;           // text of message (tagged string...)
77     int                 repeats;
78     int                 turn;
79     bool                join;           // may this message be joined with
80                                         // others?
81
82     message_item() : channel(NUM_MESSAGE_CHANNELS), param(0),
83                      text(""), repeats(0), turn(-1), join(true)
84     {
85     }
86
87     message_item(string msg, msg_channel_type chan, int par, bool jn)
88         : channel(chan), param(par), text(msg), repeats(1),
89           turn(you.num_turns)
90     {
91          // Don't join long messages.
92          join = jn && strwidth(pure_text()) < 40;
93     }
94
95     // Constructor for restored messages.
96     message_item(string msg, msg_channel_type chan, int par, int rep, int trn)
97         : channel(chan), param(par), text(msg), repeats(rep),
98           turn(trn), join(false)
99     {
100     }
101
102     operator bool() const
103     {
104         return repeats > 0;
105     }
106
107     string pure_text() const
108     {
109         return formatted_string::parse_string(text).tostring();
110     }
111
112     string with_repeats() const
113     {
114         // TODO: colour the repeats indicator?
115         string rep = "";
116         if (repeats > 1)
117             rep = make_stringf(" x%d", repeats);
118         return text + rep;
119     }
120
121     // Tries to condense the argument into this message.
122     // Either *this needs to be an empty item, or it must be the
123     // same as the argument.
124     bool merge(const message_item& other)
125     {
126         if (! *this)
127         {
128             *this = other;
129             return true;
130         }
131
132         if (!Options.msg_condense_repeats)
133             return false;
134         if (other.channel == channel && other.param == param)
135         {
136             if (Options.msg_condense_repeats && other.text == text)
137             {
138                 repeats += other.repeats;
139                 return true;
140             }
141             else if (Options.msg_condense_short
142                      && turn == other.turn
143                      && repeats == 1 && other.repeats == 1
144                      && join && other.join
145                      && _ends_in_punctuation(pure_text())
146                         == _ends_in_punctuation(other.pure_text()))
147             {
148                 // Note that join stays true.
149
150                 string sep = "<lightgrey>";
151                 int seplen = 1;
152                 if (!_ends_in_punctuation(pure_text()))
153                 {
154                     sep += ";";
155                     seplen++;
156                 }
157                 sep += " </lightgrey>";
158                 if (strwidth(pure_text()) + seplen + strwidth(other.pure_text())
159                     > (int)msgwin_line_length())
160                 {
161                     return false;
162                 }
163
164                 text += sep;
165                 text += other.text;
166                 return true;
167             }
168         }
169         return false;
170     }
171 };
172
173 static int _mod(int num, int denom)
174 {
175     ASSERT(denom > 0);
176     div_t res = div(num, denom);
177     return res.rem >= 0 ? res.rem : res.rem + denom;
178 }
179
180 template <typename T, int SIZE>
181 class circ_vec
182 {
183     T data[SIZE];
184
185     int end;   // first unfilled index
186
187     static void inc(int* index)
188     {
189         ASSERT_RANGE(*index, 0, SIZE);
190         *index = _mod(*index + 1, SIZE);
191     }
192
193     static void dec(int* index)
194     {
195         ASSERT_RANGE(*index, 0, SIZE);
196         *index = _mod(*index - 1, SIZE);
197     }
198
199 public:
200     circ_vec() : end(0) {}
201
202     void clear()
203     {
204         end = 0;
205         for (int i = 0; i < SIZE; ++i)
206             data[i] = T();
207     }
208
209     int size() const
210     {
211         return SIZE;
212     }
213
214     T& operator[](int i)
215     {
216         ASSERT(_mod(i, SIZE) < size());
217         return data[_mod(end + i, SIZE)];
218     }
219
220     const T& operator[](int i) const
221     {
222         ASSERT(_mod(i, SIZE) < size());
223         return data[_mod(end + i, SIZE)];
224     }
225
226     void push_back(const T& item)
227     {
228         data[end] = item;
229         inc(&end);
230     }
231
232     void roll_back(int n)
233     {
234         for (int i = 0; i < n; ++i)
235         {
236             dec(&end);
237             data[end] = T();
238         }
239     }
240 };
241
242 static void readkey_more(bool user_forced=false);
243
244 // Types of message prefixes.
245 // Higher values override lower.
246 enum prefix_type
247 {
248     P_NONE,
249     P_TURN_START,
250     P_TURN_END,
251     P_NEW_CMD, // new command, but no new turn
252     P_NEW_TURN,
253     P_FULL_MORE,   // single-character more prompt (full window)
254     P_OTHER_MORE,  // the other type of --more-- prompt
255 };
256
257 // Could also go with coloured glyphs.
258 static cglyph_t _prefix_glyph(prefix_type p)
259 {
260     cglyph_t g;
261     switch (p)
262     {
263     case P_TURN_START:
264         g.ch = Options.show_newturn_mark ? '-' : ' ';
265         g.col = LIGHTGRAY;
266         break;
267     case P_TURN_END:
268     case P_NEW_TURN:
269         g.ch = Options.show_newturn_mark ? '_' : ' ';
270         g.col = LIGHTGRAY;
271         break;
272     case P_NEW_CMD:
273         g.ch = Options.show_newturn_mark ? '_' : ' ';
274         g.col = DARKGRAY;
275         break;
276     case P_FULL_MORE:
277         g.ch = '+';
278         g.col = channel_to_colour(MSGCH_PROMPT);
279         break;
280     case P_OTHER_MORE:
281         g.ch = '+';
282         g.col = LIGHTRED;
283         break;
284     default:
285         g.ch = ' ';
286         g.col = LIGHTGRAY;
287         break;
288     }
289     return g;
290 }
291
292 static bool _pre_more();
293
294 static bool _temporary = false;
295
296 class message_window
297 {
298     int next_line;
299     int temp_line;     // starting point of temporary messages
300     int input_line;    // last line-after-input
301     vector<formatted_string> lines;
302     prefix_type prompt; // current prefix prompt
303
304     int height() const
305     {
306         return crawl_view.msgsz.y;
307     }
308
309     int use_last_line() const
310     {
311         return first_col_more();
312     }
313
314     int width() const
315     {
316         return crawl_view.msgsz.x;
317     }
318
319     void out_line(const formatted_string& line, int n) const
320     {
321         cgotoxy(1, n + 1, GOTO_MSG);
322         line.display();
323         cprintf("%*s", width() - line.width(), "");
324     }
325
326     // Place cursor at end of last non-empty line to handle prompts.
327     // TODO: might get rid of this by clearing the whole window when writing,
328     //       and then just writing the actual non-empty lines.
329     void place_cursor() const
330     {
331         // XXX: the screen may have resized since the last time we
332         //  called lines.resize().  We can't actually resize lines
333         //  here because this is a const method.  Consider only the
334         //  last height() lines if this has happened.
335         const int diff = max(int(lines.size()) - height(), 0);
336
337         int i;
338         for (i = lines.size() - 1; i >= diff && lines[i].width() == 0; --i);
339         if (i >= diff && (int) lines[i].width() < crawl_view.msgsz.x)
340             cgotoxy(lines[i].width() + 1, i - diff + 1, GOTO_MSG);
341         else if (i < diff)
342         {
343             // If there were no lines, put the cursor at the upper left.
344             cgotoxy(1, 1, GOTO_MSG);
345         }
346     }
347
348     // Whether to show msgwin-full more prompts.
349     bool more_enabled() const
350     {
351         return crawl_state.show_more_prompt
352                && (Options.clear_messages || Options.show_more);
353     }
354
355     int make_space(int n)
356     {
357         int space = out_height() - next_line;
358
359         if (space >= n)
360             return 0;
361
362         int s = 0;
363         if (input_line > 0)
364         {
365             s = min(input_line, n - space);
366             scroll(s);
367             space += s;
368         }
369
370         if (space >= n)
371             return s;
372
373         if (more_enabled())
374             more(true);
375
376         // We could consider just scrolling off after --more--;
377         // that would require marking the last message before
378         // the prompt.
379         if (!Options.clear_messages && !more_enabled())
380         {
381             scroll(n - space);
382             return s + n - space;
383         }
384         else
385         {
386             clear();
387             return height();
388         }
389     }
390
391     void add_line(const formatted_string& line)
392     {
393         resize(); // TODO: get rid of this
394         lines[next_line] = line;
395         next_line++;
396     }
397
398     void output_prefix(prefix_type p)
399     {
400         if (!use_first_col())
401             return;
402         if (p <= prompt)
403             return;
404         prompt = p;
405         if (next_line > 0)
406         {
407             formatted_string line;
408             line.add_glyph(_prefix_glyph(prompt));
409             lines[next_line-1].del_char();
410             line += lines[next_line-1];
411             lines[next_line-1] = line;
412         }
413         show();
414     }
415
416 public:
417     message_window()
418         : next_line(0), temp_line(0), input_line(0), prompt(P_NONE)
419     {
420         clear_lines(); // initialize this->lines
421     }
422
423     void resize()
424     {
425         // XXX: broken (why?)
426         lines.resize(height());
427     }
428
429     unsigned int out_width() const
430     {
431         return width() - (use_first_col() ? 1 : 0);
432     }
433
434     unsigned int out_height() const
435     {
436         return height() - (use_last_line() ? 0 : 1);
437     }
438
439     void clear_lines()
440     {
441         lines.clear();
442         lines.resize(height());
443     }
444
445     bool first_col_more() const
446     {
447         return use_first_col() && Options.small_more;
448     }
449
450     bool use_first_col() const
451     {
452         return !Options.clear_messages;
453     }
454
455     void set_starting_line()
456     {
457         // TODO: start at end (sometimes?)
458         next_line = 0;
459         input_line = 0;
460         temp_line = 0;
461     }
462
463     void clear()
464     {
465         clear_lines();
466         set_starting_line();
467         show();
468     }
469
470     void scroll(int n)
471     {
472         ASSERT(next_line >= n);
473         int i;
474         for (i = 0; i < height() - n; ++i)
475             lines[i] = lines[i + n];
476         for (; i < height(); ++i)
477             lines[i].clear();
478         next_line -= n;
479         temp_line -= n;
480         input_line -= n;
481     }
482
483     // write to screen (without refresh)
484     void show() const
485     {
486         // XXX: this should not be necessary as formatted_string should
487         //      already do it
488         textcolor(LIGHTGREY);
489
490         // XXX: the screen may have resized since the last time we
491         //  called lines.resize().  We can't actually resize lines
492         //  here because this is a const method.  Display the last
493         //  height() lines if this has happened.
494         const int diff = max(int(lines.size()) - height(), 0);
495
496         for (size_t i = diff; i < lines.size(); ++i)
497             out_line(lines[i], i - diff);
498         place_cursor();
499 #ifdef USE_TILE
500         tiles.set_need_redraw();
501 #endif
502     }
503
504     // temporary: to be overwritten with next item, e.g. new turn
505     //            leading dash or prompt without response
506     void add_item(string text, prefix_type first_col = P_NONE,
507                   bool temporary = false)
508     {
509         prompt = P_NONE; // reset prompt
510
511         vector<formatted_string> newlines;
512         linebreak_string(text, out_width());
513         formatted_string::parse_string_to_multiple(text, newlines);
514
515         for (size_t i = 0; i < newlines.size(); ++i)
516         {
517             make_space(1);
518             formatted_string line;
519             if (use_first_col())
520                 line.add_glyph(_prefix_glyph(first_col));
521             line += newlines[i];
522             add_line(line);
523         }
524
525         if (!temporary)
526             reset_temp();
527
528         show();
529     }
530
531     void roll_back()
532     {
533         temp_line = max(temp_line, 0);
534         for (int i = temp_line; i < next_line; ++i)
535             lines[i].clear();
536         next_line = temp_line;
537     }
538
539     /**
540      * Consider any formerly-temporary messages permanent.
541      */
542     void reset_temp()
543     {
544         temp_line = next_line;
545     }
546
547     void got_input()
548     {
549         input_line = next_line;
550     }
551
552     void new_cmdturn(bool new_turn)
553     {
554         output_prefix(new_turn ? P_NEW_TURN : P_NEW_CMD);
555     }
556
557     bool any_messages()
558     {
559         return next_line > input_line;
560     }
561
562     /*
563      * Handling of more prompts (both types).
564      */
565     void more(bool full, bool user=false)
566     {
567         if (_pre_more())
568             return;
569
570         show();
571         int last_row = crawl_view.msgsz.y;
572         if (first_col_more())
573         {
574             cgotoxy(1, last_row, GOTO_MSG);
575             cglyph_t g = _prefix_glyph(full ? P_FULL_MORE : P_OTHER_MORE);
576             formatted_string f;
577             f.add_glyph(g);
578             f.display();
579             // Move cursor back for nicer display.
580             cgotoxy(1, last_row, GOTO_MSG);
581             // Need to read_key while cursor_control in scope.
582             cursor_control con(true);
583             readkey_more();
584         }
585         else
586         {
587             cgotoxy(use_first_col() ? 2 : 1, last_row, GOTO_MSG);
588             textcolor(channel_to_colour(MSGCH_PROMPT));
589             if (crawl_state.game_is_hints())
590             {
591                 string more_str = "--more-- Press Space ";
592                 if (is_tiles())
593                     more_str += "or click ";
594                 more_str += "to continue. You can later reread messages with "
595                             "Ctrl-P.";
596                 cprintf(more_str.c_str());
597             }
598             else
599                 cprintf("--more--");
600
601             readkey_more(user);
602         }
603     }
604 };
605
606 message_window msgwin;
607
608 void display_message_window()
609 {
610     msgwin.show();
611 }
612
613 void clear_message_window()
614 {
615     msgwin = message_window();
616 }
617
618 void scroll_message_window(int n)
619 {
620     msgwin.scroll(n);
621     msgwin.show();
622 }
623
624 bool any_messages()
625 {
626     return msgwin.any_messages();
627 }
628
629 typedef circ_vec<message_item, NUM_STORED_MESSAGES> store_t;
630
631 class message_store
632 {
633     store_t msgs;
634     message_item prev_msg;
635     bool last_of_turn;
636     int temp; // number of temporary messages
637
638     int unsent; // number of messages not yet sent to the webtiles client
639     bool prev_unsent;
640     int client_rollback;
641 #ifdef USE_TILE_WEB
642     bool send_ignore_one;
643 #endif
644
645 public:
646     message_store() : last_of_turn(false), temp(0),
647                       unsent(0), prev_unsent(false),
648                       client_rollback(0)
649 #ifdef USE_TILE_WEB
650                       , send_ignore_one(false)
651 #endif
652     {}
653
654     void add(const message_item& msg)
655     {
656         if (msg.channel != MSGCH_PROMPT && prev_msg.merge(msg))
657         {
658             prev_unsent = true;
659             return;
660         }
661         flush_prev();
662         prev_msg = msg;
663         prev_unsent = true;
664         if (msg.channel == MSGCH_PROMPT || _temporary)
665             flush_prev();
666     }
667
668     bool have_prev()
669     {
670         return prev_msg;
671     }
672
673     void store_msg(const message_item& msg)
674     {
675         prefix_type p = P_NONE;
676         msgs.push_back(msg);
677         if (_temporary)
678             temp++;
679         else
680             reset_temp();
681 #ifdef USE_TILE_WEB
682         // ignore this message until it's actually displayed in case we run out
683         // of space and have to display --more-- instead
684         send_ignore_one = true;
685 #endif
686         msgwin.add_item(msg.with_repeats(), p, _temporary);
687 #ifdef USE_TILE_WEB
688         send_ignore_one = false;
689 #endif
690     }
691
692     void roll_back()
693     {
694         client_rollback = max(0, temp - unsent);
695         msgs.roll_back(temp);
696         temp = 0;
697     }
698
699     void reset_temp()
700     {
701         temp = 0;
702     }
703
704     void flush_prev()
705     {
706         if (!prev_msg)
707             return;
708         message_item msg = prev_msg;
709         // Clear prev_msg before storing it, since
710         // writing out to the message window might
711         // in turn result in a recursive flush_prev.
712         prev_msg = message_item();
713         if (prev_unsent)
714         {
715             unsent++;
716             prev_unsent = false;
717         }
718         store_msg(msg);
719         if (last_of_turn)
720         {
721             msgwin.new_cmdturn(true);
722             last_of_turn = false;
723         }
724     }
725
726     void new_turn()
727     {
728         if (prev_msg)
729             last_of_turn = true;
730         else
731             msgwin.new_cmdturn(true);
732     }
733
734     // XXX: this should not need to exist
735     const store_t& get_store()
736     {
737         return msgs;
738     }
739
740     void clear()
741     {
742         msgs.clear();
743         prev_msg = message_item();
744         last_of_turn = false;
745         temp = 0;
746     }
747
748 #ifdef USE_TILE_WEB
749     void send(int old_msgs = 0)
750     {
751         unsent += old_msgs;
752         if (unsent == 0 || (send_ignore_one && unsent == 1)) return;
753
754         if (client_rollback > 0)
755         {
756             tiles.json_write_int("rollback", client_rollback);
757             client_rollback = 0;
758         }
759         if (old_msgs > 0)
760             tiles.json_write_int("old_msgs", old_msgs);
761         tiles.json_open_array("messages");
762         for (int i = -unsent; i < (send_ignore_one ? -1 : 0); ++i)
763         {
764             message_item& msg = msgs[i];
765             tiles.json_open_object();
766             tiles.json_write_string("text", msg.text);
767             tiles.json_write_int("turn", msg.turn);
768             tiles.json_write_int("channel", msg.channel);
769             if (msg.repeats > 1)
770                 tiles.json_write_int("repeats", msg.repeats);
771             tiles.json_close_object();
772         }
773         if (prev_unsent && have_prev())
774         {
775             tiles.json_open_object();
776             tiles.json_write_string("text", prev_msg.text);
777             tiles.json_write_int("turn", prev_msg.turn);
778             tiles.json_write_int("channel", prev_msg.channel);
779             if (prev_msg.repeats > 1)
780                 tiles.json_write_int("repeats", prev_msg.repeats);
781             tiles.json_close_object();
782         }
783         tiles.json_close_array();
784         unsent = send_ignore_one ? 1 : 0;
785         prev_unsent = false;
786     }
787 #endif
788 };
789
790 // Circular buffer for keeping past messages.
791 message_store buffer;
792
793 #ifdef USE_TILE_WEB
794 bool _more = false, _last_more = false;
795
796 void webtiles_send_messages()
797 {
798     webtiles_send_last_messages(0);
799 }
800 void webtiles_send_last_messages(int n)
801 {
802     tiles.json_open_object();
803     tiles.json_write_string("msg", "msgs");
804     tiles.json_treat_as_empty();
805     if (_more != _last_more)
806     {
807         tiles.json_write_bool("more", _more);
808         _last_more = _more;
809     }
810     buffer.send(n);
811     tiles.json_close_object(true);
812     tiles.finish_message();
813 }
814 #endif
815
816 static FILE* _msg_dump_file = NULL;
817
818 static bool suppress_messages = false;
819 static msg_colour_type prepare_message(const string& imsg,
820                                        msg_channel_type channel,
821                                        int param);
822
823 no_messages::no_messages() : msuppressed(suppress_messages)
824 {
825     suppress_messages = true;
826 }
827
828 no_messages::~no_messages()
829 {
830     suppress_messages = msuppressed;
831 }
832
833 msg_colour_type msg_colour(int col)
834 {
835     return static_cast<msg_colour_type>(col);
836 }
837
838 static int colour_msg(msg_colour_type col)
839 {
840     if (col == MSGCOL_MUTED)
841         return DARKGREY;
842     else
843         return static_cast<int>(col);
844 }
845
846 // Returns a colour or MSGCOL_MUTED.
847 static msg_colour_type channel_to_msgcol(msg_channel_type channel, int param)
848 {
849     msg_colour_type ret;
850
851     switch (Options.channels[channel])
852     {
853     case MSGCOL_PLAIN:
854         // Note that if the plain channel is muted, then we're protecting
855         // the player from having that spread to other channels here.
856         // The intent of plain is to give non-coloured messages, not to
857         // suppress them.
858         if (Options.channels[MSGCH_PLAIN] >= MSGCOL_DEFAULT)
859             ret = MSGCOL_LIGHTGREY;
860         else
861             ret = Options.channels[MSGCH_PLAIN];
862         break;
863
864     case MSGCOL_DEFAULT:
865     case MSGCOL_ALTERNATE:
866         switch (channel)
867         {
868         case MSGCH_GOD:
869         case MSGCH_PRAY:
870             ret = (Options.channels[channel] == MSGCOL_DEFAULT)
871                    ? msg_colour(god_colour(static_cast<god_type>(param)))
872                    : msg_colour(god_message_altar_colour(static_cast<god_type>(param)));
873             break;
874
875         case MSGCH_DURATION:
876             ret = MSGCOL_LIGHTBLUE;
877             break;
878
879         case MSGCH_DANGER:
880             ret = MSGCOL_RED;
881             break;
882
883         case MSGCH_WARN:
884         case MSGCH_ERROR:
885             ret = MSGCOL_LIGHTRED;
886             break;
887
888         case MSGCH_FOOD:
889             if (param) // positive change
890                 ret = MSGCOL_GREEN;
891             else
892                 ret = MSGCOL_YELLOW;
893             break;
894
895         case MSGCH_INTRINSIC_GAIN:
896             ret = MSGCOL_GREEN;
897             break;
898
899         case MSGCH_RECOVERY:
900             ret = MSGCOL_LIGHTGREEN;
901             break;
902
903         case MSGCH_TALK:
904         case MSGCH_TALK_VISUAL:
905         case MSGCH_HELL_EFFECT:
906             ret = MSGCOL_WHITE;
907             break;
908
909         case MSGCH_MUTATION:
910             ret = MSGCOL_LIGHTRED;
911             break;
912
913         case MSGCH_MONSTER_SPELL:
914         case MSGCH_MONSTER_ENCHANT:
915         case MSGCH_FRIEND_SPELL:
916         case MSGCH_FRIEND_ENCHANT:
917             ret = MSGCOL_LIGHTMAGENTA;
918             break;
919
920         case MSGCH_TUTORIAL:
921         case MSGCH_ORB:
922         case MSGCH_BANISHMENT:
923             ret = MSGCOL_MAGENTA;
924             break;
925
926         case MSGCH_MONSTER_DAMAGE:
927             ret =  ((param == MDAM_DEAD)               ? MSGCOL_RED :
928                     (param >= MDAM_SEVERELY_DAMAGED)   ? MSGCOL_LIGHTRED :
929                     (param >= MDAM_MODERATELY_DAMAGED) ? MSGCOL_YELLOW
930                                                        : MSGCOL_LIGHTGREY);
931             break;
932
933         case MSGCH_PROMPT:
934             ret = MSGCOL_CYAN;
935             break;
936
937         case MSGCH_DIAGNOSTICS:
938         case MSGCH_MULTITURN_ACTION:
939             ret = MSGCOL_DARKGREY; // makes it easier to ignore at times -- bwr
940             break;
941
942         case MSGCH_PLAIN:
943         case MSGCH_FRIEND_ACTION:
944         case MSGCH_ROTTEN_MEAT:
945         case MSGCH_EQUIPMENT:
946         case MSGCH_EXAMINE:
947         case MSGCH_EXAMINE_FILTER:
948         default:
949             ret = param > 0 ? msg_colour(param) : MSGCOL_LIGHTGREY;
950             break;
951         }
952         break;
953
954     case MSGCOL_MUTED:
955         ret = MSGCOL_MUTED;
956         break;
957
958     default:
959         // Setting to a specific colour is handled here, special
960         // cases should be handled above.
961         if (channel == MSGCH_MONSTER_DAMAGE)
962         {
963             // A special case right now for monster damage (at least until
964             // the init system is improved)... selecting a specific
965             // colour here will result in only the death messages coloured.
966             if (param == MDAM_DEAD)
967                 ret = Options.channels[channel];
968             else if (Options.channels[MSGCH_PLAIN] >= MSGCOL_DEFAULT)
969                 ret = MSGCOL_LIGHTGREY;
970             else
971                 ret = Options.channels[MSGCH_PLAIN];
972         }
973         else
974             ret = Options.channels[channel];
975         break;
976     }
977
978     return ret;
979 }
980
981 int channel_to_colour(msg_channel_type channel, int param)
982 {
983     return colour_msg(channel_to_msgcol(channel, param));
984 }
985
986 static void do_message_print(msg_channel_type channel, int param, bool cap,
987                              bool nojoin, const char *format, va_list argp)
988 {
989     va_list ap;
990     va_copy(ap, argp);
991     char buff[200];
992     size_t len = vsnprintf(buff, sizeof(buff), format, argp);
993     if (len < sizeof(buff))
994         _mpr(buff, channel, param, nojoin, cap);
995     else
996     {
997         char *heapbuf = (char*)malloc(len + 1);
998         vsnprintf(heapbuf, len + 1, format, ap);
999         _mpr(heapbuf, channel, param, nojoin, cap);
1000         free(heapbuf);
1001     }
1002     va_end(ap);
1003 }
1004
1005 void mprf_nocap(msg_channel_type channel, int param, const char *format, ...)
1006 {
1007     va_list argp;
1008     va_start(argp, format);
1009     do_message_print(channel, param, false, false, format, argp);
1010     va_end(argp);
1011 }
1012
1013 void mprf_nocap(msg_channel_type channel, const char *format, ...)
1014 {
1015     va_list argp;
1016     va_start(argp, format);
1017     do_message_print(channel, channel == MSGCH_GOD ? you.religion : 0,
1018                      false, false, format, argp);
1019     va_end(argp);
1020 }
1021
1022 void mprf_nocap(const char *format, ...)
1023 {
1024     va_list argp;
1025     va_start(argp, format);
1026     do_message_print(MSGCH_PLAIN, 0, false, false, format, argp);
1027     va_end(argp);
1028 }
1029
1030 void mprf(msg_channel_type channel, int param, const char *format, ...)
1031 {
1032     va_list argp;
1033     va_start(argp, format);
1034     do_message_print(channel, param, true, false, format, argp);
1035     va_end(argp);
1036 }
1037
1038 void mprf(msg_channel_type channel, const char *format, ...)
1039 {
1040     va_list argp;
1041     va_start(argp, format);
1042     do_message_print(channel, channel == MSGCH_GOD ? you.religion : 0,
1043                      true, false, format, argp);
1044     va_end(argp);
1045 }
1046
1047 void mprf(const char *format, ...)
1048 {
1049     va_list argp;
1050     va_start(argp, format);
1051     do_message_print(MSGCH_PLAIN, 0, true, false, format, argp);
1052     va_end(argp);
1053 }
1054
1055 void mprf_nojoin(msg_channel_type channel, const char *format, ...)
1056 {
1057     va_list argp;
1058     va_start(argp, format);
1059     do_message_print(channel, channel == MSGCH_GOD ? you.religion : 0,
1060                      true, true, format, argp);
1061     va_end(argp);
1062 }
1063
1064 void mprf_nojoin(const char *format, ...)
1065 {
1066     va_list argp;
1067     va_start(argp, format);
1068     do_message_print(MSGCH_PLAIN, 0, true, true, format, argp);
1069     va_end(argp);
1070 }
1071
1072 #ifdef DEBUG_DIAGNOSTICS
1073 void dprf(const char *format, ...)
1074 {
1075     va_list argp;
1076     va_start(argp, format);
1077     do_message_print(MSGCH_DIAGNOSTICS, 0, false, false, format, argp);
1078     va_end(argp);
1079 }
1080
1081 void dprf(diag_type param, const char *format, ...)
1082 {
1083     if (Options.quiet_debug_messages[param])
1084         return;
1085
1086     va_list argp;
1087     va_start(argp, format);
1088     do_message_print(MSGCH_DIAGNOSTICS, param, false, false, format, argp);
1089     va_end(argp);
1090 }
1091 #endif
1092
1093 static bool _updating_view = false;
1094
1095 static bool check_more(const string& line, msg_channel_type channel)
1096 {
1097     for (unsigned i = 0; i < Options.force_more_message.size(); ++i)
1098         if (Options.force_more_message[i].is_filtered(channel, line))
1099             return true;
1100     return false;
1101 }
1102
1103 static bool check_join(const string& line, msg_channel_type channel)
1104 {
1105     switch (channel)
1106     {
1107     case MSGCH_EQUIPMENT:
1108         return false;
1109     default:
1110         break;
1111     }
1112     return true;
1113 }
1114
1115 static void debug_channel_arena(msg_channel_type channel)
1116 {
1117     switch (channel)
1118     {
1119     case MSGCH_PROMPT:
1120     case MSGCH_GOD:
1121     case MSGCH_PRAY:
1122     case MSGCH_DURATION:
1123     case MSGCH_FOOD:
1124     case MSGCH_RECOVERY:
1125     case MSGCH_INTRINSIC_GAIN:
1126     case MSGCH_MUTATION:
1127     case MSGCH_ROTTEN_MEAT:
1128     case MSGCH_EQUIPMENT:
1129     case MSGCH_FLOOR_ITEMS:
1130     case MSGCH_MULTITURN_ACTION:
1131     case MSGCH_EXAMINE:
1132     case MSGCH_EXAMINE_FILTER:
1133     case MSGCH_ORB:
1134     case MSGCH_TUTORIAL:
1135         die("Invalid channel '%s' in arena mode",
1136                  channel_to_str(channel).c_str());
1137         break;
1138     default:
1139         break;
1140     }
1141 }
1142
1143 bool strip_channel_prefix(string &text, msg_channel_type &channel, bool silence)
1144 {
1145     string::size_type pos = text.find(":");
1146     if (pos == string::npos)
1147         return false;
1148
1149     string param = text.substr(0, pos);
1150     bool sound = false;
1151
1152     if (param == "WARN")
1153         channel = MSGCH_WARN, sound = true;
1154     else if (param == "VISUAL WARN")
1155         channel = MSGCH_WARN;
1156     else if (param == "SOUND")
1157         channel = MSGCH_SOUND, sound = true;
1158     else if (param == "VISUAL")
1159         channel = MSGCH_TALK_VISUAL;
1160     else if (param == "SPELL")
1161         channel = MSGCH_MONSTER_SPELL, sound = true;
1162     else if (param == "VISUAL SPELL")
1163         channel = MSGCH_MONSTER_SPELL;
1164     else if (param == "ENCHANT")
1165         channel = MSGCH_MONSTER_ENCHANT, sound = true;
1166     else if (param == "VISUAL ENCHANT")
1167         channel = MSGCH_MONSTER_ENCHANT;
1168     else
1169     {
1170         param = replace_all(param, " ", "_");
1171         lowercase(param);
1172         int ch = str_to_channel(param);
1173         if (ch == -1)
1174             return false;
1175         channel = static_cast<msg_channel_type>(ch);
1176     }
1177
1178     if (sound && silence)
1179         text = "";
1180     else
1181         text = text.substr(pos + 1);
1182     return true;
1183 }
1184
1185 void msgwin_set_temporary(bool temp)
1186 {
1187     flush_prev_message();
1188     _temporary = temp;
1189     if (!temp)
1190     {
1191         buffer.reset_temp();
1192         msgwin.reset_temp();
1193     }
1194 }
1195
1196 void msgwin_clear_temporary()
1197 {
1198     buffer.roll_back();
1199     msgwin.roll_back();
1200 }
1201
1202 static int _last_msg_turn = -1; // Turn of last message.
1203
1204 static void _mpr(string text, msg_channel_type channel, int param, bool nojoin, bool cap)
1205 {
1206     if (_msg_dump_file != NULL)
1207         fprintf(_msg_dump_file, "%s\n", text.c_str());
1208
1209     if (crawl_state.game_crashed)
1210         return;
1211
1212     if (crawl_state.game_is_arena())
1213         debug_channel_arena(channel);
1214
1215 #ifdef DEBUG_FATAL
1216     if (channel == MSGCH_ERROR)
1217         die_noline("%s", text.c_str());
1218 #endif
1219
1220     if (!crawl_state.io_inited)
1221     {
1222         if (channel == MSGCH_ERROR)
1223             fprintf(stderr, "%s\n", text.c_str());
1224         return;
1225     }
1226
1227     // Flush out any "comes into view" monster announcements before the
1228     // monster has a chance to give any other messages.
1229     if (!_updating_view)
1230     {
1231         _updating_view = true;
1232         flush_comes_into_view();
1233         _updating_view = false;
1234     }
1235
1236     if (channel == MSGCH_GOD && param == 0)
1237         param = you.religion;
1238
1239     // Ugly hack.
1240     if (channel == MSGCH_DIAGNOSTICS || channel == MSGCH_ERROR)
1241         cap = false;
1242
1243     msg_colour_type colour = prepare_message(text, channel, param);
1244
1245     if (colour == MSGCOL_MUTED)
1246     {
1247         if (channel == MSGCH_PROMPT)
1248             msgwin.show();
1249         return;
1250     }
1251
1252     bool domore = check_more(text, channel);
1253     bool join = !domore && !nojoin && check_join(text, channel);
1254
1255     // Must do this before converting to formatted string and back;
1256     // that doesn't preserve close tags!
1257     string col = colour_to_str(colour_msg(colour));
1258     text = "<" + col + ">" + text + "</" + col + ">"; // XXX
1259
1260     formatted_string fs = formatted_string::parse_string(text);
1261     if (you.duration[DUR_QUAD_DAMAGE])
1262         fs.all_caps(); // No sound, so we simulate the reverb with all caps.
1263     else if (cap)
1264         fs.capitalise();
1265     if (channel != MSGCH_ERROR && channel != MSGCH_DIAGNOSTICS)
1266         fs.filter_lang();
1267     text = fs.to_colour_string();
1268
1269     message_item msg = message_item(text, channel, param, join);
1270     buffer.add(msg);
1271     _last_msg_turn = msg.turn;
1272
1273     if (channel == MSGCH_ERROR)
1274         interrupt_activity(AI_FORCE_INTERRUPT);
1275
1276     if (channel == MSGCH_PROMPT || channel == MSGCH_ERROR)
1277         set_more_autoclear(false);
1278
1279     if (domore)
1280         more(true);
1281 }
1282
1283 static string show_prompt(string prompt)
1284 {
1285     mprf(MSGCH_PROMPT, "%s", prompt.c_str());
1286
1287     // FIXME: duplicating mpr code.
1288     msg_colour_type colour = prepare_message(prompt, MSGCH_PROMPT, 0);
1289     return colour_string(prompt, colour_msg(colour));
1290 }
1291
1292 static string _prompt;
1293 void msgwin_prompt(string prompt)
1294 {
1295     msgwin_set_temporary(true);
1296     _prompt = show_prompt(prompt);
1297 }
1298
1299 void msgwin_reply(string reply)
1300 {
1301     msgwin_clear_temporary();
1302     msgwin_set_temporary(false);
1303     reply = replace_all(reply, "<", "<<");
1304     mprf(MSGCH_PROMPT, "%s<lightgrey>%s</lightgrey>", _prompt.c_str(), reply.c_str());
1305     msgwin.got_input();
1306 }
1307
1308 void msgwin_got_input()
1309 {
1310     msgwin.got_input();
1311 }
1312
1313 int msgwin_get_line(string prompt, char *buf, int len,
1314                     input_history *mh, const string &fill)
1315 {
1316     if (prompt != "")
1317         msgwin_prompt(prompt);
1318
1319     int ret = cancellable_get_line(buf, len, mh, NULL, fill);
1320     msgwin_reply(buf);
1321     return ret;
1322 }
1323
1324 void msgwin_new_turn()
1325 {
1326     buffer.new_turn();
1327 }
1328
1329 void msgwin_new_cmd()
1330 {
1331     flush_prev_message();
1332     bool new_turn = (you.num_turns > _last_msg_turn);
1333     msgwin.new_cmdturn(new_turn);
1334 }
1335
1336 unsigned int msgwin_line_length()
1337 {
1338     return msgwin.out_width();
1339 }
1340
1341 unsigned int msgwin_lines()
1342 {
1343     return msgwin.out_height();
1344 }
1345
1346 // mpr() an arbitrarily long list of strings without truncation or risk
1347 // of overflow.
1348 void mpr_comma_separated_list(const string &prefix,
1349                               const vector<string> &list,
1350                               const string &andc,
1351                               const string &comma,
1352                               const msg_channel_type channel,
1353                               const int param)
1354 {
1355     string out = prefix;
1356
1357     for (int i = 0, size = list.size(); i < size; i++)
1358     {
1359         out += list[i];
1360
1361         if (size > 0 && i < (size - 2))
1362             out += comma;
1363         else if (i == (size - 2))
1364             out += andc;
1365         else if (i == (size - 1))
1366             out += ".";
1367     }
1368     _mpr(out, channel, param);
1369 }
1370
1371
1372 // Checks whether a given message contains patterns relevant for
1373 // notes, stop_running or sounds and handles these cases.
1374 static void mpr_check_patterns(const string& message,
1375                                msg_channel_type channel,
1376                                int param)
1377 {
1378     for (unsigned i = 0; i < Options.note_messages.size(); ++i)
1379     {
1380         if (channel == MSGCH_EQUIPMENT || channel == MSGCH_FLOOR_ITEMS
1381             || channel == MSGCH_MULTITURN_ACTION
1382             || channel == MSGCH_EXAMINE || channel == MSGCH_EXAMINE_FILTER
1383             || channel == MSGCH_TUTORIAL)
1384         {
1385             continue;
1386         }
1387
1388         if (Options.note_messages[i].matches(message))
1389         {
1390             take_note(Note(NOTE_MESSAGE, channel, param, message.c_str()));
1391             break;
1392         }
1393     }
1394
1395     if (channel != MSGCH_DIAGNOSTICS && channel != MSGCH_EQUIPMENT)
1396         interrupt_activity(AI_MESSAGE, channel_to_str(channel) + ":" + message);
1397
1398     // Any sound has a chance of waking the PC if the PC is asleep.
1399     if (channel == MSGCH_SOUND)
1400         you.check_awaken(5);
1401
1402     if (!Options.sound_mappings.empty())
1403         for (unsigned i = 0; i < Options.sound_mappings.size(); i++)
1404         {
1405             // Maybe we should allow message channel matching as for
1406             // force_more_message?
1407             if (Options.sound_mappings[i].pattern.matches(message))
1408             {
1409                 play_sound(Options.sound_mappings[i].soundfile.c_str());
1410                 break;
1411             }
1412         }
1413 }
1414
1415 static bool channel_message_history(msg_channel_type channel)
1416 {
1417     switch (channel)
1418     {
1419     case MSGCH_PROMPT:
1420     case MSGCH_EQUIPMENT:
1421     case MSGCH_EXAMINE_FILTER:
1422         return false;
1423     default:
1424         return true;
1425     }
1426 }
1427
1428 // Returns the default colour of the message, or MSGCOL_MUTED if
1429 // the message should be suppressed.
1430 static msg_colour_type prepare_message(const string& imsg,
1431                                        msg_channel_type channel,
1432                                        int param)
1433 {
1434     if (suppress_messages)
1435         return MSGCOL_MUTED;
1436
1437     if (silenced(you.pos())
1438         && (channel == MSGCH_SOUND || channel == MSGCH_TALK))
1439     {
1440         return MSGCOL_MUTED;
1441     }
1442
1443     msg_colour_type colour = channel_to_msgcol(channel, param);
1444
1445     if (colour != MSGCOL_MUTED)
1446         mpr_check_patterns(imsg, channel, param);
1447
1448     const vector<message_colour_mapping>& mcm
1449                = Options.message_colour_mappings;
1450     typedef vector<message_colour_mapping>::const_iterator mcmci;
1451
1452     for (mcmci ci = mcm.begin(); ci != mcm.end(); ++ci)
1453     {
1454         if (ci->message.is_filtered(channel, imsg))
1455         {
1456             colour = ci->colour;
1457             break;
1458         }
1459     }
1460
1461     return colour;
1462 }
1463
1464 void flush_prev_message()
1465 {
1466     buffer.flush_prev();
1467 }
1468
1469 void mesclr(bool force)
1470 {
1471     if (!crawl_state.io_inited)
1472         return;
1473     // Unflushed message will be lost with clear_messages,
1474     // so they shouldn't really exist, but some of the delay
1475     // code appears to do this intentionally.
1476     // ASSERT(!buffer.have_prev());
1477     flush_prev_message();
1478
1479     msgwin.got_input(); // Consider old messages as read.
1480
1481     if (Options.clear_messages || force)
1482         msgwin.clear();
1483
1484     // TODO: we could indicate indicate mesclr with a different
1485     //       leading character than '-'.
1486 }
1487
1488 static bool autoclear_more = false;
1489
1490 void set_more_autoclear(bool on)
1491 {
1492     autoclear_more = on;
1493 }
1494
1495 static void readkey_more(bool user_forced)
1496 {
1497     if (autoclear_more)
1498         return;
1499     int keypress;
1500 #ifdef USE_TILE_WEB
1501     unwind_bool unwind_more(_more, true);
1502 #endif
1503     mouse_control mc(MOUSE_MODE_MORE);
1504
1505     do
1506         keypress = getch_ck();
1507     while (keypress != ' ' && keypress != '\r' && keypress != '\n'
1508            && !key_is_escape(keypress)
1509 #ifdef TOUCH_UI
1510            && (keypress != CK_MOUSE_CLICK));
1511 #else
1512            && (user_forced || keypress != CK_MOUSE_CLICK));
1513 #endif
1514
1515     if (key_is_escape(keypress))
1516         set_more_autoclear(true);
1517 }
1518
1519 /*
1520  * more() preprocessing.
1521  *
1522  * @return Whether the more prompt should be skipped.
1523  */
1524 static bool _pre_more()
1525 {
1526     if (crawl_state.game_crashed || crawl_state.seen_hups)
1527         return true;
1528
1529 #ifdef DEBUG_DIAGNOSTICS
1530     if (you.running)
1531         return true;
1532 #endif
1533
1534     if (crawl_state.game_is_arena())
1535     {
1536         delay(Options.arena_delay);
1537         return true;
1538     }
1539
1540     if (crawl_state.is_replaying_keys())
1541         return true;
1542
1543 #ifdef WIZARD
1544     if (luaterp_running())
1545         return true;
1546 #endif
1547
1548     if (!crawl_state.show_more_prompt || suppress_messages)
1549         return true;
1550
1551     return false;
1552 }
1553
1554 void more(bool user_forced)
1555 {
1556     if (!crawl_state.io_inited)
1557         return;
1558     flush_prev_message();
1559     msgwin.more(false, user_forced);
1560     mesclr();
1561 }
1562
1563 static bool is_channel_dumpworthy(msg_channel_type channel)
1564 {
1565     return channel != MSGCH_EQUIPMENT
1566            && channel != MSGCH_DIAGNOSTICS
1567            && channel != MSGCH_TUTORIAL;
1568 }
1569
1570 void clear_message_store()
1571 {
1572     buffer.clear();
1573 }
1574
1575 string get_last_messages(int mcount, bool full)
1576 {
1577     flush_prev_message();
1578
1579     string text;
1580     // XXX: should use some message_history iterator here
1581     const store_t& msgs = buffer.get_store();
1582     // XXX: loop wraps around otherwise. This could be done better.
1583     mcount = min(mcount, NUM_STORED_MESSAGES);
1584     for (int i = -1; mcount > 0; --i)
1585     {
1586         const message_item msg = msgs[i];
1587         if (!msg)
1588             break;
1589         if (full || is_channel_dumpworthy(msg.channel))
1590             text = msg.pure_text() + "\n" + text;
1591         mcount--;
1592     }
1593
1594     // An extra line of clearance.
1595     if (!text.empty())
1596         text += "\n";
1597     return text;
1598 }
1599
1600 void get_recent_messages(vector<string> &mess,
1601                          vector<msg_channel_type> &chan)
1602 {
1603     flush_prev_message();
1604
1605     const store_t& msgs = buffer.get_store();
1606     int mcount = NUM_STORED_MESSAGES;
1607     for (int i = -1; mcount > 0; --i, --mcount)
1608     {
1609         const message_item msg = msgs[i];
1610         if (!msg)
1611             break;
1612         mess.push_back(msg.pure_text());
1613         chan.push_back(msg.channel);
1614     }
1615 }
1616
1617 // We just write out the whole message store including empty/unused
1618 // messages. They'll be ignored when restoring.
1619 void save_messages(writer& outf)
1620 {
1621     store_t msgs = buffer.get_store();
1622     marshallInt(outf, msgs.size());
1623     for (int i = 0; i < msgs.size(); ++i)
1624     {
1625         marshallString4(outf, msgs[i].text);
1626         marshallInt(outf, msgs[i].channel);
1627         marshallInt(outf, msgs[i].param);
1628         marshallInt(outf, msgs[i].repeats);
1629         marshallInt(outf, msgs[i].turn);
1630     }
1631 }
1632
1633 void load_messages(reader& inf)
1634 {
1635     unwind_var<bool> save_more(crawl_state.show_more_prompt, false);
1636
1637     int num = unmarshallInt(inf);
1638     for (int i = 0; i < num; ++i)
1639     {
1640         string text;
1641         unmarshallString4(inf, text);
1642
1643         msg_channel_type channel = (msg_channel_type) unmarshallInt(inf);
1644         int           param      = unmarshallInt(inf);
1645         int           repeats    = unmarshallInt(inf);
1646         int           turn       = unmarshallInt(inf);
1647
1648         message_item msg(message_item(text, channel, param, repeats, turn));
1649         if (msg)
1650             buffer.store_msg(msg);
1651     }
1652     // With Options.message_clear, we don't want the message window
1653     // pre-filled.
1654     mesclr();
1655 }
1656
1657 void replay_messages(void)
1658 {
1659     formatted_scroller hist(MF_START_AT_END | MF_ALWAYS_SHOW_MORE, "");
1660     hist.set_more();
1661
1662     const store_t msgs = buffer.get_store();
1663     for (int i = 0; i < msgs.size(); ++i)
1664         if (channel_message_history(msgs[i].channel))
1665         {
1666             string text = msgs[i].with_repeats();
1667             linebreak_string(text, cgetsize(GOTO_CRT).x - 1);
1668             vector<formatted_string> parts;
1669             formatted_string::parse_string_to_multiple(text, parts);
1670             for (unsigned int j = 0; j < parts.size(); ++j)
1671             {
1672                 formatted_string line;
1673                 prefix_type p = P_NONE;
1674                 if (j == parts.size() - 1 && i + 1 < msgs.size()
1675                     && msgs[i+1].turn > msgs[i].turn)
1676                 {
1677                     p = P_TURN_END;
1678                 }
1679                 line.add_glyph(_prefix_glyph(p));
1680                 line += parts[j];
1681                 hist.add_item_formatted_string(line);
1682             }
1683         }
1684
1685     hist.show();
1686 }
1687
1688 void set_msg_dump_file(FILE* file)
1689 {
1690     _msg_dump_file = file;
1691 }
1692
1693
1694 void formatted_mpr(const formatted_string& fs,
1695                    msg_channel_type channel, int param)
1696 {
1697     _mpr(fs.to_colour_string(), channel, param);
1698 }