1 /**
2  * @file
3  * @brief Misc commands.
4 **/
5 
6 #include "AppHdr.h"
7 
8 #include "command.h"
9 
10 #include <cctype>
11 #include <cinttypes>
12 #include <cstdio>
13 #include <cstring>
14 #include <sstream>
15 
16 #include "chardump.h"
17 #include "database.h"
18 #include "describe.h"
19 #include "env.h"
20 #include "files.h"
21 #include "hints.h"
22 #include "invent.h"
23 #include "item-prop.h"
24 #include "items.h"
25 #include "libutil.h"
26 #include "lookup-help.h"
27 #include "macro.h"
28 #include "message.h"
29 #include "prompt.h"
30 #include "scroller.h"
31 #include "showsymb.h"
32 #include "state.h"
33 #include "stringutil.h"
34 #include "syscalls.h"
35 #include "unicode.h"
36 #include "version.h"
37 #include "viewchar.h"
38 
39 using namespace ui;
40 
41 #ifdef USE_TILE
42  #include "rltiles/tiledef-gui.h"
43 #endif
44 
45 static const char *features[] =
46 {
47 #ifdef CLUA_BINDINGS
48     "Lua user scripts",
49 #endif
50 
51 #ifdef USE_TILE_LOCAL
52     "Tile support",
53 #endif
54 
55 #ifdef USE_TILE_WEB
56     "Web Tile support",
57 #endif
58 
59 #ifdef WIZARD
60     "Wizard mode",
61 #endif
62 
63 #ifdef DEBUG
64     "Debug mode",
65 #endif
66 
67 #if defined(REGEX_POSIX)
68     "POSIX regexps",
69 #elif defined(REGEX_PCRE)
70     "PCRE regexps",
71 #else
72     "Glob patterns",
73 #endif
74 
75 #if defined(USE_SOUND) && defined(SOUND_BACKEND)
76     SOUND_BACKEND,
77 #endif
78 
79 #ifdef DGL_MILESTONES
80     "Milestones",
81 #endif
82 };
83 
_get_version_information()84 static string _get_version_information()
85 {
86     string result = string("This is <w>" CRAWL " ") + Version::Long + "</w>";
87     return result;
88 }
89 
_get_version_features()90 static string _get_version_features()
91 {
92     string result;
93     if (crawl_state.need_save
94 #ifdef DGAMELAUNCH
95         && (you.wizard || crawl_state.type == GAME_TYPE_CUSTOM_SEED)
96 #endif
97        )
98     {
99         if (you.fully_seeded)
100         {
101             result += seed_description();
102             if (Version::history_size() > 1)
103                 result += " (seed may be affected by game upgrades)";
104         }
105         else
106             result += "Game is not seeded.";
107         result += "\n\n";
108     }
109     if (Version::history_size() > 1)
110     {
111         result += "Version history for your current game:\n";
112         result += Version::history();
113         result += "\n\n";
114     }
115 
116     result += "<w>Features</w>\n"
117                  "--------\n";
118 
119     for (const char *feature : features)
120     {
121         result += " * ";
122         result += feature;
123         result += "\n";
124     }
125 
126     return result;
127 }
128 
_get_version_changes()129 static string _get_version_changes()
130 {
131     // Attempts to print "Highlights" of the latest version.
132     FILE* fp = fopen_u(datafile_path("changelog.txt", false).c_str(), "r");
133     if (!fp)
134         return "";
135 
136     string result = "";
137     string help;
138     char buf[200];
139     bool start = false;
140     while (fgets(buf, sizeof buf, fp))
141     {
142         // Remove trailing spaces.
143         for (int i = strlen(buf) - 1; i >= 0; i++)
144         {
145             if (isspace(buf[i]))
146                 buf[i] = 0;
147             else
148                 break;
149         }
150         help = buf;
151 
152         // Look for version headings
153         if (starts_with(help, "Stone Soup "))
154         {
155             // Stop if this is for an older major version; otherwise, highlight
156             if (help.find(string("Stone Soup ")+Version::Major) == string::npos)
157                 break;
158             else
159                 goto highlight;
160         }
161 
162         if (help.find("Highlights") != string::npos)
163         {
164         highlight:
165             // Highlight the Highlights, so to speak.
166             result += "<w>" + help + "</w>\n";
167             // And start printing from now on.
168             start = true;
169         }
170         else if (!start)
171             continue;
172         else
173         {
174             result += buf;
175             result += "\n";
176         }
177     }
178     fclose(fp);
179 
180     // Did we ever get to print the Highlights?
181     if (start)
182     {
183         result.erase(1+result.find_last_not_of('\n'));
184         result += "\n\n";
185         result += "For earlier changes, see changelog.txt "
186                   "in the docs/ directory.";
187     }
188     else
189     {
190         result += "For a list of changes, see changelog.txt in the docs/ "
191                   "directory.";
192     }
193 
194     return result;
195 }
196 
197 //#define DEBUG_FILES
_print_version()198 static void _print_version()
199 {
200     const string info = _get_version_information(),
201           feats = _get_version_features(),
202           changes = _get_version_changes();
203 
204     auto vbox = make_shared<Box>(Widget::VERT);
205 
206 #ifdef USE_TILE_LOCAL
207     vbox->max_size().width = tiles.get_crt_font()->char_width()*80;
208 #endif
209 
210     auto title_hbox = make_shared<Box>(Widget::HORZ);
211 #ifdef USE_TILE
212     auto icon = make_shared<Image>();
213     icon->set_tile(tile_def(TILEG_STARTUP_STONESOUP));
214     title_hbox->add_child(move(icon));
215 #endif
216 
217     auto title = make_shared<Text>(formatted_string::parse_string(info));
218     title->set_margin_for_sdl(0, 0, 0, 10);
219     title_hbox->add_child(move(title));
220 
221     title_hbox->set_cross_alignment(Widget::CENTER);
222     title_hbox->set_margin_for_crt(0, 0, 1, 0);
223     title_hbox->set_margin_for_sdl(0, 0, 20, 0);
224     vbox->add_child(move(title_hbox));
225 
226     auto scroller = make_shared<Scroller>();
227     auto content = formatted_string::parse_string(feats + "\n\n" + changes);
228     auto text = make_shared<Text>(move(content));
229     text->set_wrap_text(true);
230     scroller->set_child(move(text));
231     vbox->add_child(scroller);
232 
233     auto popup = make_shared<ui::Popup>(vbox);
234 
235     bool done = false;
236     popup->on_keydown_event([&](const KeyEvent& ev) {
237         done = !scroller->on_event(ev);
238         return true;
239     });
240 
241 #ifdef USE_TILE_WEB
242     tiles.json_open_object();
243     tiles.json_write_string("information", info);
244     tiles.json_write_string("features", feats);
245     tiles.json_write_string("changes", changes);
246     tiles.push_ui_layout("version", 0);
247     popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
248 #endif
249 
250     ui::run_layout(move(popup), done);
251 }
252 
list_armour()253 void list_armour()
254 {
255     ostringstream estr;
256     for (int j = EQ_MIN_ARMOUR; j <= EQ_MAX_ARMOUR; j++)
257     {
258         const equipment_type i = static_cast<equipment_type>(j);
259         const int armour_id = you.equip[i];
260         int       colour    = MSGCOL_BLACK;
261 
262         estr.str("");
263         estr.clear();
264 
265         estr << ((i == EQ_CLOAK)       ? "Cloak  " :
266                  (i == EQ_HELMET)      ? "Helmet " :
267                  (i == EQ_GLOVES)      ? "Gloves " :
268                  (i == EQ_SHIELD)      ? "Shield " :
269                  (i == EQ_BODY_ARMOUR) ? "Armour " :
270                  (i == EQ_BOOTS)       ?
271                    (you.wear_barding() ? "Barding"
272                                        : "Boots  ")
273                                        : "unknown")
274              << " : ";
275 
276         if (you_can_wear(i) == MB_FALSE)
277             estr << "    (unavailable)";
278         else if (you_can_wear(i, true) == MB_FALSE)
279             estr << "    (currently unavailable)";
280         else if (armour_id != -1)
281         {
282             estr << you.inv[armour_id].name(DESC_INVENTORY);
283             colour = menu_colour(estr.str(), item_prefix(you.inv[armour_id]),
284                                  "equip");
285         }
286         else if (you_can_wear(i) == MB_MAYBE)
287             estr << "    (restricted)";
288         else
289             estr << "    none";
290 
291         if (colour == MSGCOL_BLACK)
292             colour = menu_colour(estr.str(), "", "equip");
293 
294         mprf(MSGCH_EQUIPMENT, colour, "%s", estr.str().c_str());
295     }
296 }
297 
list_jewellery()298 void list_jewellery()
299 {
300     string jstr;
301     int cols = get_number_of_cols() - 1;
302     bool split = species::arm_count(you.species) > 2 && cols > 84;
303 
304     for (int j = EQ_LEFT_RING; j < NUM_EQUIP; j++)
305     {
306         const equipment_type i = static_cast<equipment_type>(j);
307         if (!you_can_wear(i))
308             continue;
309 
310         const int jewellery_id = you.equip[i];
311         int       colour       = MSGCOL_BLACK;
312 
313         const char *slot =
314                  (i == EQ_LEFT_RING)   ? "Left ring" :
315                  (i == EQ_RIGHT_RING)  ? "Right ring" :
316                  (i == EQ_AMULET)      ? "Amulet" :
317                  (i == EQ_RING_ONE)    ? "1st ring" :
318                  (i == EQ_RING_TWO)    ? "2nd ring" :
319                  (i == EQ_RING_THREE)  ? "3rd ring" :
320                  (i == EQ_RING_FOUR)   ? "4th ring" :
321                  (i == EQ_RING_FIVE)   ? "5th ring" :
322                  (i == EQ_RING_SIX)    ? "6th ring" :
323                  (i == EQ_RING_SEVEN)  ? "7th ring" :
324                  (i == EQ_RING_EIGHT)  ? "8th ring" :
325                  (i == EQ_RING_AMULET) ? "Amulet ring"
326                                        : "unknown";
327 
328         string item;
329         if (you_can_wear(i, true) == MB_FALSE)
330             item = "    (currently unavailable)";
331         else if (jewellery_id != -1)
332         {
333             item = you.inv[jewellery_id].name(DESC_INVENTORY);
334             string prefix = item_prefix(you.inv[jewellery_id]);
335             colour = menu_colour(item, prefix, "equip");
336         }
337         else
338             item = "    none";
339 
340         if (colour == MSGCOL_BLACK)
341             colour = menu_colour(item, "", "equip");
342 
343         item = chop_string(make_stringf("%-*s: %s",
344                                         split ? cols > 96 ? 9 : 8 : 11,
345                                         slot, item.c_str()),
346                            split && i > EQ_AMULET ? (cols - 1) / 2 : cols);
347         item = colour_string(item, colour);
348 
349         // doesn't handle arbitrary arm counts
350         if (i == EQ_RING_SEVEN && you.arm_count() == 7)
351             mprf(MSGCH_EQUIPMENT, "%s", item.c_str());
352         else if (split && i > EQ_AMULET && (i - EQ_AMULET) % 2)
353             jstr = item + " ";
354         else
355             mprf(MSGCH_EQUIPMENT, "%s%s", jstr.c_str(), item.c_str());
356     }
357 }
358 
359 static const char *targeting_help_1 =
360     "<h>Examine surroundings ('<w>x</w><h>' in main):\n"
361     "<w>Esc</w> : cancel (also <w>Space</w>, <w>x</w>)\n"
362     "<w>Ctrl-X</w> : list all things in view\n"
363     "<w>Dir.</w>: move cursor in that direction\n"
364     "<w>.</w> : move to cursor (also <w>Enter</w>, <w>Del</w>)\n"
365     "<w>g</w> : pick up item at cursor\n"
366     "<w>v</w> : describe monster under cursor\n"
367     "<w>+</w> : cycle monsters forward (also <w>=</w>)\n"
368     "<w>-</w> : cycle monsters backward\n"
369     "<w>*</w> : cycle objects forward (also <w>'</w>)\n"
370     "<w>/</w> : cycle objects backward (also <w>;</w>)\n"
371     "<w>^</w> : cycle through traps\n"
372     "<w>_</w> : cycle through altars\n"
373     "<w><<</w>/<w>></w> : cycle through up/down stairs\n"
374     "<w>Tab</w> : cycle through shops and portals\n"
375     "<w>r</w> : move cursor to you\n"
376     "<w>e</w> : create/remove travel exclusion\n"
377     "<w>Ctrl-P</w> : repeat prompt\n"
378 ;
379 #ifdef WIZARD
380 static const char *targeting_help_wiz =
381     "<h>Wizard targeting commands:</h>\n"
382     "<w>Ctrl-C</w> : cycle through beam paths\n"
383     "<w>D</w>: get debugging information about the monster\n"
384     "<w>o</w>: give item to monster\n"
385     "<w>F</w>: cycle monster friendly/good neutral/neutral/hostile\n"
386     "<w>G</w>: make monster gain experience\n"
387     "<w>Ctrl-H</w>: heal the monster fully\n"
388     "<w>P</w>: apply divine blessing to monster\n"
389     "<w>m</w>: move monster or player\n"
390     "<w>M</w>: cause spell miscast for monster or player\n"
391     "<w>s</w>: force monster to shout or speak\n"
392     "<w>S</w>: make monster a summoned monster\n"
393     "<w>w</w>: calculate shortest path to any point on the map\n"
394     "<w>\"</w>: get debugging information about a portal\n"
395     "<w>~</w>: polymorph monster to specific type\n"
396     "<w>,</w>: bring down the monster to 1 hp\n"
397     "<w>Ctrl-(</w>: place a mimic\n"
398     "<w>Ctrl-B</w>: banish monster\n"
399     "<w>Ctrl-K</w>: kill monster\n"
400 ;
401 #endif
402 
403 static const char *targeting_help_2 =
404     "<h>Targeting (zap wands, cast spells, etc.):\n"
405     "Most keys from examine surroundings work.\n"
406     "Some keys fire at the target. <w>Ctrl-X</w> only\n"
407     "lists eligible targets. By default,\n"
408     "range is respected and beams don't stop.\n"
409     "<w>Enter</w> : fire (<w>Space</w>, <w>Del</w>)\n"
410     "<w>.</w> : fire, stop at target\n"
411     "<w>@</w> : fire, stop at target, ignore range\n"
412     "<w>!</w> : fire, don't stop, ignore range\n"
413     "<w>p</w> : fire at Previous target (also <w>f</w>)\n"
414     "<w>:</w> : show/hide beam path\n"
415     "<w>Shift-Dir.</w> : fire straight-line beam\n"
416     "\n"
417     "<h>Firing mode ('<w>f</w><h>' in main):\n"
418     "<w>Q</w> : choose fire action.\n"
419     "<w>(</w> : cycle to previous suitable action\n"
420     "<w>)</w> : cycle to next suitable action.\n"
421 ;
422 
423 struct help_file
424 {
425     const char* name;
426     int hotkey;
427     bool auto_hotkey;
428 };
429 
430 static help_file help_files[] =
431 {
432     { "crawl_manual.txt",  '*', true },
433     { "aptitudes.txt",     '%', false },
434     { "quickstart.txt",     '^', false },
435     { "macros_guide.txt",  '~', false },
436     { "options_guide.txt", '&', false },
437 #ifdef USE_TILE_LOCAL
438     { "tiles_help.txt",    't', false },
439 #endif
440     { nullptr, 0, false }
441 };
442 
443 // Reads all questions from database/FAQ.txt, outputs them in the form of
444 // a selectable menu and prints the corresponding answer for a chosen question.
_handle_FAQ()445 static void _handle_FAQ()
446 {
447     vector<string> question_keys = getAllFAQKeys();
448     if (question_keys.empty())
449     {
450         mpr("No questions found in FAQ! Please submit a bug report!");
451         return;
452     }
453     Menu FAQmenu(MF_SINGLESELECT | MF_ANYPRINTABLE | MF_ALLOW_FORMATTING);
454     MenuEntry *title = new MenuEntry("Frequently Asked Questions");
455     title->colour = YELLOW;
456     FAQmenu.set_title(title);
457 
458     for (unsigned int i = 0, size = question_keys.size(); i < size; i++)
459     {
460         const char letter = index_to_letter(i);
461         string question = getFAQ_Question(question_keys[i]);
462         trim_string_right(question);
463         MenuEntry *me = new MenuEntry(question, MEL_ITEM, 1, letter);
464         me->data = &question_keys[i];
465         FAQmenu.add_entry(me);
466     }
467 
468     while (true)
469     {
470         vector<MenuEntry*> sel = FAQmenu.show();
471         if (sel.empty())
472             return;
473         else
474         {
475             ASSERT(sel.size() == 1);
476             ASSERT(sel[0]->hotkeys.size() == 1);
477 
478             string key = *((string*) sel[0]->data);
479             string answer = getFAQ_Answer(key);
480             if (answer.empty())
481             {
482                 answer = "No answer found in the FAQ! Please submit a "
483                          "bug report!";
484             }
485             answer = "Q: " + getFAQ_Question(key) + "\n" + answer;
486             show_description(answer);
487         }
488     }
489 
490     return;
491 }
492 
493 ////////////////////////////////////////////////////////////////////////////
494 
show_keyhelp_menu(const vector<formatted_string> & lines)495 int show_keyhelp_menu(const vector<formatted_string> &lines)
496 {
497     int flags = FS_PREWRAPPED_TEXT | FS_EASY_EXIT;
498     formatted_scroller cmd_help(flags);
499     cmd_help.set_tag("help");
500     cmd_help.set_more();
501 
502     for (unsigned i = 0; i < lines.size(); ++i)
503         cmd_help.add_formatted_string(lines[i], i < lines.size()-1);
504 
505     cmd_help.show();
506 
507     return cmd_help.get_lastch();
508 }
509 
show_specific_help(const string & key)510 void show_specific_help(const string &key)
511 {
512     string help = getHelpString(key);
513     trim_string_right(help);
514     vector<formatted_string> formatted_lines;
515     for (const string &line : split_string("\n", help, false, true))
516         formatted_lines.push_back(formatted_string::parse_string(line));
517     show_keyhelp_menu(formatted_lines);
518 }
519 
show_levelmap_help()520 void show_levelmap_help()
521 {
522     show_specific_help("level-map");
523 }
524 
show_targeting_help()525 void show_targeting_help()
526 {
527     column_composer cols(2, 40);
528     cols.add_formatted(0, targeting_help_1, true);
529 #ifdef WIZARD
530     if (you.wizard)
531         cols.add_formatted(0, targeting_help_wiz, true);
532 #endif
533     cols.add_formatted(1, targeting_help_2, true);
534     show_keyhelp_menu(cols.formatted_lines());
535 }
show_interlevel_travel_branch_help()536 void show_interlevel_travel_branch_help()
537 {
538     show_specific_help("interlevel-travel.branch.prompt");
539 }
540 
show_interlevel_travel_depth_help()541 void show_interlevel_travel_depth_help()
542 {
543     show_specific_help("interlevel-travel.depth.prompt");
544 }
545 
show_interlevel_travel_altar_help()546 void show_interlevel_travel_altar_help()
547 {
548     show_specific_help("interlevel-travel.altar.prompt");
549 }
550 
show_annotate_help()551 void show_annotate_help()
552 {
553     show_specific_help("annotate.prompt");
554 }
555 
show_stash_search_help()556 void show_stash_search_help()
557 {
558     show_specific_help("stash-search.prompt");
559 }
560 
show_skill_menu_help()561 void show_skill_menu_help()
562 {
563     show_specific_help("skill-menu");
564 }
565 
show_spell_library_help()566 void show_spell_library_help()
567 {
568     if (crawl_state.game_is_hints_tutorial())
569     {
570         const string help1 = hints_memorise_info() + "\n\n";
571         vector<formatted_string> formatted_lines;
572         for (const string &line : split_string("\n", help1, false, true))
573         {
574             formatted_lines.push_back(formatted_string::parse_string(line,
575                                         channel_to_colour(MSGCH_TUTORIAL)));
576         }
577         const string help2 = getHelpString("spell-library");
578         for (const string &line : split_string("\n", help2, false, true))
579             formatted_lines.push_back(formatted_string::parse_string(line));
580         show_keyhelp_menu(formatted_lines);
581     }
582     else
583         show_specific_help("spell-library");
584 }
585 
_add_command(column_composer & cols,const int column,const command_type cmd,const string & desc,const unsigned int space_to_colon=7)586 static void _add_command(column_composer &cols, const int column,
587                          const command_type cmd,
588                          const string &desc,
589                          const unsigned int space_to_colon = 7)
590 {
591     string command_name = command_to_string(cmd);
592     if (strcmp(command_name.c_str(), "<") == 0)
593         command_name += "<";
594 
595     const int cmd_len = strwidth(command_name);
596     string line = "<w>" + command_name + "</w>";
597     for (unsigned int i = cmd_len; i < space_to_colon; ++i)
598         line += " ";
599     line += ": " + untag_tiles_console(desc) + "\n";
600 
601     cols.add_formatted(
602             column,
603             line.c_str(),
604             false);
605 }
606 
_add_insert_commands(column_composer & cols,const int column,const unsigned int space_to_colon,command_type lead_cmd,string desc,const vector<command_type> & cmd_vector)607 static void _add_insert_commands(column_composer &cols, const int column,
608                                  const unsigned int space_to_colon,
609                                  command_type lead_cmd, string desc,
610                                  const vector<command_type> &cmd_vector)
611 {
612     insert_commands(desc, cmd_vector);
613     desc += "\n";
614     _add_command(cols, column, lead_cmd, desc, space_to_colon);
615 }
616 
_add_insert_commands(column_composer & cols,const int column,string desc,const vector<command_type> & cmd_vector)617 static void _add_insert_commands(column_composer &cols, const int column,
618                                  string desc,
619                                  const vector<command_type> &cmd_vector)
620 {
621     insert_commands(desc, cmd_vector);
622     desc += "\n";
623     cols.add_formatted(column, desc.c_str(), false);
624 }
625 
_add_formatted_help_menu(column_composer & cols)626 static void _add_formatted_help_menu(column_composer &cols)
627 {
628     cols.add_formatted(
629         0,
630         "<h>Dungeon Crawl Help\n"
631         "\n"
632         "Press one of the following keys to\n"
633         "obtain more information on a certain\n"
634         "aspect of Dungeon Crawl.\n"
635 
636         "<w>?</w>: List of commands\n"
637         "<w>^</w>: Quickstart Guide\n"
638         "<w>:</w>: Browse character notes\n"
639         "<w>#</w>: Browse character dump\n"
640         "<w>~</w>: Macros help\n"
641         "<w>&</w>: Options help\n"
642         "<w>%</w>: Table of aptitudes\n"
643         "<w>/</w>: Lookup description\n"
644         "<w>Q</w>: FAQ\n"
645 #ifdef USE_TILE_LOCAL
646         "<w>T</w>: Tiles key help\n"
647 #endif
648         "<w>V</w>: Version information\n"
649         "<w>Home</w>: This screen\n");
650 
651     // TODO: generate this from the manual somehow
652     cols.add_formatted(
653         1,
654         "<h>Manual Contents\n\n"
655         "<w>*</w>       Table of contents\n"
656         "<w>A</w>.      Overview\n"
657         "<w>B</w>.      Starting Screen\n"
658         "<w>C</w>.      Attributes and Stats\n"
659         "<w>D</w>.      Exploring the Dungeon\n"
660         "<w>E</w>.      Experience and Skills\n"
661         "<w>F</w>.      Monsters\n"
662         "<w>G</w>.      Items\n"
663         "<w>H</w>.      Spellcasting\n"
664         "<w>I</w>.      Targeting\n"
665         "<w>J</w>.      Religion\n"
666         "<w>K</w>.      Mutations\n"
667         "<w>L</w>.      Licence, Contact, History\n"
668         "<w>M</w>.      Macros, Options, Performance\n"
669         "<w>N</w>.      Philosophy\n"
670         "<w>1</w>.      List of Character Species\n"
671         "<w>2</w>.      List of Character Backgrounds\n"
672         "<w>3</w>.      List of Skills\n"
673         "<w>4</w>.      List of Keys and Commands\n"
674         "<w>5</w>.      Inscriptions\n"
675         "<w>6</w>.      Dungeon sprint modes\n");
676 }
677 
_add_formatted_keyhelp(column_composer & cols)678 static void _add_formatted_keyhelp(column_composer &cols)
679 {
680     cols.add_formatted(
681             0,
682             "<h>Movement:\n"
683             "To move in a direction or to attack, \n"
684             "use the numpad (try Numlock off and \n"
685             "on) or vi keys:\n");
686 
687     _add_insert_commands(cols, 0, "                 <w>7 8 9      % % %",
688                          { CMD_MOVE_UP_LEFT, CMD_MOVE_UP, CMD_MOVE_UP_RIGHT });
689     _add_insert_commands(cols, 0, "                  \\|/        \\|/", {});
690     _add_insert_commands(cols, 0, "                 <w>4</w>-<w>5</w>-<w>6</w>"
691                                   "      <w>%</w>-<w>%</w>-<w>%</w>",
692                          { CMD_MOVE_LEFT, CMD_WAIT, CMD_MOVE_RIGHT });
693     _add_insert_commands(cols, 0, "                  /|\\        /|\\", {});
694     _add_insert_commands(cols, 0, "                 <w>1 2 3      % % %",
695                          { CMD_MOVE_DOWN_LEFT, CMD_MOVE_DOWN,
696                            CMD_MOVE_DOWN_RIGHT });
697 
698     cols.add_formatted(
699             0,
700             "<h>Rest:\n");
701 
702     _add_command(cols, 0, CMD_WAIT, "wait a turn (also <w>s</w>, <w>Del</w>)", 2);
703     _add_command(cols, 0, CMD_REST, "rest and long wait; stops when", 2);
704     cols.add_formatted(
705             0,
706             "    Health or Magic become full or\n"
707             "    something is detected. If Health\n"
708             "    and Magic are already full, stops\n"
709             "    when 100 turns over (<w>numpad-5</w>)\n",
710             false);
711 
712     cols.add_formatted(
713             0,
714             "<h>Extended Movement:\n");
715 
716     _add_command(cols, 0, CMD_EXPLORE, "auto-explore");
717     _add_command(cols, 0, CMD_INTERLEVEL_TRAVEL, "interlevel travel");
718     _add_command(cols, 0, CMD_SEARCH_STASHES, "Find items");
719     _add_command(cols, 0, CMD_FIX_WAYPOINT, "set Waypoint");
720 
721     cols.add_formatted(
722             0,
723             "<w>/ Dir.</w>, <w>Shift-Dir.</w>: long walk\n"
724             "<w>* Dir.</w>, <w>Ctrl-Dir.</w> : attack without move \n",
725             false);
726 
727     cols.add_formatted(
728             0,
729             "<h>Autofight:\n"
730             "<w>Tab</w>          : attack nearest monster,\n"
731             "               moving if necessary\n"
732             "<w>Shift-Tab</w>, <w>p</w> : trigger quivered action;\n"
733             "               if targeted, aims at\n"
734             "               nearest monster\n");
735 
736     cols.add_formatted(
737             0,
738             "<h>Item types (and common commands)\n");
739 
740     _add_insert_commands(cols, 0, "<cyan>)</cyan> : hand weapons (<w>%</w>ield)",
741                          { CMD_WIELD_WEAPON });
742     _add_insert_commands(cols, 0, "<brown>(</brown> : missiles (<w>%</w>uiver, "
743                                   "<w>%</w>ire, <w>%</w>/<w>%</w> cycle)",
744                          { CMD_QUIVER_ITEM, CMD_FIRE, CMD_CYCLE_QUIVER_FORWARD,
745                            CMD_CYCLE_QUIVER_BACKWARD });
746     _add_insert_commands(cols, 0, "<cyan>[</cyan> : armour (<w>%</w>ear and <w>%</w>ake off)",
747                          { CMD_WEAR_ARMOUR, CMD_REMOVE_ARMOUR });
748     _add_insert_commands(cols, 0, "<w>?</w> : scrolls (<w>%</w>ead)",
749                          { CMD_READ });
750     _add_insert_commands(cols, 0, "<magenta>!</magenta> : potions (<w>%</w>uaff)",
751                          { CMD_QUAFF });
752     _add_insert_commands(cols, 0, "<blue>=</blue> : rings (<w>%</w>ut on and <w>%</w>emove)",
753                          { CMD_WEAR_JEWELLERY, CMD_REMOVE_JEWELLERY });
754     _add_insert_commands(cols, 0, "<red>\"</red> : amulets (<w>%</w>ut on and <w>%</w>emove)",
755                          { CMD_WEAR_JEWELLERY, CMD_REMOVE_JEWELLERY });
756     _add_insert_commands(cols, 0, "<lightgrey>/</lightgrey> : wands (e<w>%</w>oke)",
757                          { CMD_EVOKE });
758 
759     string item_types = "<lightcyan>";
760     item_types += stringize_glyph(get_item_symbol(SHOW_ITEM_BOOK));
761     item_types +=
762         "</lightcyan> : books (<w>%</w>emorise, <w>%</w>ap, <w>%</w>ap,\n"
763         "    pick up to add to library)";
764     _add_insert_commands(cols, 0, item_types,
765                          { CMD_MEMORISE_SPELL, CMD_CAST_SPELL,
766                            CMD_FORCE_CAST_SPELL });
767     _add_insert_commands(cols, 0, "<brown>|</brown> : staves (<w>%</w>ield)",
768                          { CMD_WIELD_WEAPON});
769     _add_insert_commands(cols, 0, "<lightgreen>}</lightgreen> : miscellaneous items (e<w>%</w>oke)",
770                          { CMD_EVOKE });
771     _add_insert_commands(cols, 0, "<yellow>$</yellow> : gold (<w>%</w> counts gold)",
772                          { CMD_LIST_GOLD });
773 
774     cols.add_formatted(
775             0,
776             "<lightmagenta>0</lightmagenta> : the Orb of Zot\n"
777             "    Carry it to the surface and win!\n",
778             false);
779 
780     cols.add_formatted(
781             0,
782             "<h>Other Gameplay Actions:\n");
783 
784     _add_insert_commands(cols, 0, 2, CMD_USE_ABILITY,
785                          "use special Ability (<w>%!</w> for help)",
786                          { CMD_USE_ABILITY });
787     _add_command(cols, 0, CMD_CAST_SPELL, "cast spell, abort without targets", 2);
788     _add_command(cols, 0, CMD_FORCE_CAST_SPELL, "cast spell, no matter what", 2);
789     _add_command(cols, 0, CMD_DISPLAY_SPELLS, "list all memorised spells", 2);
790     _add_command(cols, 0, CMD_MEMORISE_SPELL, "Memorise a spell from your library", 2);
791 
792     _add_insert_commands(cols, 0, 2, CMD_SHOUT,
793                          "tell allies (<w>%t</w> to shout)",
794                          { CMD_SHOUT });
795     _add_command(cols, 0, CMD_PREV_CMD_AGAIN, "re-do previous command", 2);
796     _add_command(cols, 0, CMD_REPEAT_CMD, "repeat next command # of times", 2);
797 
798     cols.add_formatted(
799             0,
800             "<h>Non-Gameplay Commands / Info\n");
801 
802     _add_command(cols, 0, CMD_GAME_MENU, "game menu", 2);
803     _add_command(cols, 0, CMD_REPLAY_MESSAGES, "show Previous messages");
804     _add_command(cols, 0, CMD_REDRAW_SCREEN, "Redraw screen");
805     _add_command(cols, 0, CMD_CLEAR_MAP, "Clear main and level maps");
806     _add_command(cols, 0, CMD_MACRO_ADD, "quick add macro");
807     _add_command(cols, 0, CMD_MACRO_MENU, "edit macros");
808     _add_command(cols, 0, CMD_ANNOTATE_LEVEL, "annotate the dungeon level", 2);
809     _add_command(cols, 0, CMD_CHARACTER_DUMP, "dump character to file", 2);
810     _add_insert_commands(cols, 0, 2, CMD_MAKE_NOTE,
811                          "add note (use <w>%:</w> to read notes)",
812                          { CMD_DISPLAY_COMMANDS });
813     _add_command(cols, 0, CMD_ADJUST_INVENTORY, "reassign inventory/spell letters", 2);
814 #ifdef USE_TILE_LOCAL
815     _add_command(cols, 0, CMD_EDIT_PLAYER_TILE, "edit player doll", 2);
816 #else
817 #ifdef USE_TILE_WEB
818     if (tiles.is_controlled_from_web())
819     {
820         cols.add_formatted(0, "<w>F12</w> : read messages (online play only)",
821                            false);
822     }
823     else
824 #endif
825     _add_command(cols, 0, CMD_READ_MESSAGES, "read messages (online play only)", 2);
826 #endif
827 
828     cols.add_formatted(
829             1,
830             "<h>Game Saving and Quitting:\n");
831 
832     _add_command(cols, 1, CMD_SAVE_GAME, "Save game and exit");
833     _add_command(cols, 1, CMD_SAVE_GAME_NOW, "Save and exit without query");
834     _add_command(cols, 1, CMD_QUIT, "Abandon the current character");
835     cols.add_formatted(1, "         and quit the game\n",
836                        false);
837 
838     cols.add_formatted(
839             1,
840             "<h>Player Character Information:\n");
841 
842     _add_command(cols, 1, CMD_DISPLAY_CHARACTER_STATUS, "display character status", 2);
843     _add_command(cols, 1, CMD_DISPLAY_SKILLS, "show skill screen", 2);
844     _add_command(cols, 1, CMD_RESISTS_SCREEN, "character overview", 2);
845     _add_command(cols, 1, CMD_DISPLAY_RELIGION, "show religion screen", 2);
846     _add_command(cols, 1, CMD_DISPLAY_MUTATIONS, "show Abilities/mutations", 2);
847     _add_command(cols, 1, CMD_DISPLAY_KNOWN_OBJECTS, "show item knowledge", 2);
848     _add_command(cols, 1, CMD_MEMORISE_SPELL, "show your spell library", 2);
849     _add_command(cols, 1, CMD_DISPLAY_RUNES, "show runes collected", 2);
850     _add_command(cols, 1, CMD_LIST_ARMOUR, "display worn armour", 2);
851     _add_command(cols, 1, CMD_LIST_JEWELLERY, "display worn jewellery", 2);
852     _add_command(cols, 1, CMD_LIST_GOLD, "display gold in possession", 2);
853     _add_command(cols, 1, CMD_EXPERIENCE_CHECK, "display experience info", 2);
854 
855     cols.add_formatted(
856             1,
857             "<h>Dungeon Interaction and Information:\n");
858 
859     _add_insert_commands(cols, 1, "<w>%</w>/<w>%</w>    : Open/Close door",
860                          { CMD_OPEN_DOOR, CMD_CLOSE_DOOR });
861     _add_insert_commands(cols, 1, "<w>%</w>/<w>%</w>    : use staircase",
862                          { CMD_GO_UPSTAIRS, CMD_GO_DOWNSTAIRS });
863 
864     _add_command(cols, 1, CMD_INSPECT_FLOOR, "examine occupied tile and");
865     cols.add_formatted(1, "         pickup part of a single stack\n",
866                        false);
867 
868     _add_command(cols, 1, CMD_LOOK_AROUND, "eXamine surroundings/targets");
869     _add_insert_commands(cols, 1, 7, CMD_DISPLAY_MAP,
870                          "eXamine level map (<w>%?</w> for help)",
871                          { CMD_DISPLAY_MAP });
872     _add_command(cols, 1, CMD_FULL_VIEW, "list monsters, items, features");
873     cols.add_formatted(1, "         in view\n",
874                        false);
875     _add_command(cols, 1, CMD_SHOW_TERRAIN, "toggle view layers");
876     _add_command(cols, 1, CMD_DISPLAY_OVERMAP, "show dungeon Overview");
877     _add_command(cols, 1, CMD_TOGGLE_AUTOPICKUP, "toggle auto-pickup");
878 #ifdef USE_SOUND
879     _add_command(cols, 1, CMD_TOGGLE_SOUND, "mute/unmute sound effects");
880 #endif
881     _add_command(cols, 1, CMD_TOGGLE_TRAVEL_SPEED, "set your travel speed to your");
882     cols.add_formatted(1, "         slowest ally\n",
883                            false);
884 #ifdef USE_TILE_LOCAL
885     _add_insert_commands(cols, 1, "<w>%</w>/<w>%</w> : zoom out/in",
886                         { CMD_ZOOM_OUT, CMD_ZOOM_IN });
887 #endif
888 
889     cols.add_formatted(
890             1,
891             "<h>Inventory management:\n");
892 
893     _add_command(cols, 1, CMD_DISPLAY_INVENTORY, "show Inventory list", 2);
894     _add_command(cols, 1, CMD_PICKUP, "pick up items (also <w>g</w>)", 2);
895     cols.add_formatted(
896             1,
897             "    (press twice for pick up menu)\n",
898             false);
899 
900     _add_command(cols, 1, CMD_DROP, "Drop an item", 2);
901     _add_insert_commands(cols, 1, "<w>%#</w>: Drop exact number of items",
902                          { CMD_DROP });
903     _add_command(cols, 1, CMD_DROP_LAST, "Drop the last item(s) you picked up", 2);
904 
905     cols.add_formatted(
906             1,
907             "<h>Item Interaction:\n");
908 
909     _add_command(cols, 1, CMD_INSCRIBE_ITEM, "inscribe item", 2);
910     _add_command(cols, 1, CMD_FIRE, "Fire the currently quivered action", 2);
911     _add_command(cols, 1, CMD_THROW_ITEM_NO_QUIVER, "select an item and Fire it", 2);
912     _add_command(cols, 1, CMD_QUIVER_ITEM, "select action to be Quivered", 2);
913     _add_command(cols, 1, CMD_SWAP_QUIVER_RECENT, "swap between most recent quiver actions", 2);
914     _add_command(cols, 1, CMD_QUAFF, "Quaff a potion", 2);
915     _add_command(cols, 1, CMD_READ, "Read a scroll", 2);
916     _add_command(cols, 1, CMD_WIELD_WEAPON, "Wield an item (<w>-</w> for none)", 2);
917     _add_command(cols, 1, CMD_WEAPON_SWAP, "wield item a, or switch to b", 2);
918 
919     _add_insert_commands(cols, 1, "    (use <w>%</w> to assign slots)",
920                          { CMD_ADJUST_INVENTORY });
921 
922     _add_command(cols, 1, CMD_PRIMARY_ATTACK, "attack with wielded item", 2);
923     _add_command(cols, 1, CMD_EVOKE, "eVoke wand and miscellaneous item", 2);
924 
925     _add_insert_commands(cols, 1, "<w>%</w>/<w>%</w> : Wear or Take off armour",
926                          { CMD_WEAR_ARMOUR, CMD_REMOVE_ARMOUR });
927     _add_insert_commands(cols, 1, "<w>%</w>/<w>%</w> : Put on or Remove jewellery",
928                          { CMD_WEAR_JEWELLERY, CMD_REMOVE_JEWELLERY });
929 
930     cols.add_formatted(
931             1,
932             "<h>Additional help:\n");
933 
934     string text =
935             "Many commands have context sensitive "
936             "help, among them <w>%</w>, <w>%</w>, <w>%</w> (or any "
937             "form of targeting), <w>%</w>, and <w>%</w>.\n"
938             "You can read descriptions of your "
939             "current spells (<w>%</w>), skills (<w>%?</w>) and "
940             "abilities (<w>%!</w>).";
941     insert_commands(text, { CMD_DISPLAY_MAP, CMD_LOOK_AROUND, CMD_FIRE,
942                             CMD_SEARCH_STASHES, CMD_INTERLEVEL_TRAVEL,
943                             CMD_DISPLAY_SPELLS, CMD_DISPLAY_SKILLS,
944                             CMD_USE_ABILITY
945                           });
946     linebreak_string(text, 40);
947 
948     cols.add_formatted(
949             1, text,
950             false);
951 }
952 
_add_formatted_hints_help(column_composer & cols)953 static void _add_formatted_hints_help(column_composer &cols)
954 {
955     // First column.
956     cols.add_formatted(
957             0,
958             "<h>Movement:\n"
959             "To move in a direction or to attack, \n"
960             "use the numpad (try Numlock off and \n"
961             "on) or vi keys:\n",
962             false);
963 
964     _add_insert_commands(cols, 0, "                 <w>7 8 9      % % %",
965                          { CMD_MOVE_UP_LEFT, CMD_MOVE_UP, CMD_MOVE_UP_RIGHT });
966     _add_insert_commands(cols, 0, "                  \\|/        \\|/", {});
967     _add_insert_commands(cols, 0, "                 <w>4</w>-<w>5</w>-<w>6</w>"
968                                   "      <w>%</w>-<w>%</w>-<w>%</w>",
969                          { CMD_MOVE_LEFT, CMD_WAIT, CMD_MOVE_RIGHT });
970     _add_insert_commands(cols, 0, "                  /|\\        /|\\", {});
971     _add_insert_commands(cols, 0, "                 <w>1 2 3      % % %",
972                          { CMD_MOVE_DOWN_LEFT, CMD_MOVE_DOWN,
973                            CMD_MOVE_DOWN_RIGHT });
974 
975     cols.add_formatted(0, " ", false);
976     cols.add_formatted(0, "<w>Shift-Dir.</w> runs into one direction",
977                        false);
978     _add_insert_commands(cols, 0, "<w>%</w> or <w>%</w> : ascend/descend the stairs",
979                          { CMD_GO_UPSTAIRS, CMD_GO_DOWNSTAIRS });
980     _add_command(cols, 0, CMD_EXPLORE, "autoexplore", 2);
981 
982     cols.add_formatted(
983             0,
984             "<h>Rest:\n");
985 
986     _add_command(cols, 0, CMD_WAIT, "wait a turn (also <w>s</w>, <w>Del</w>)", 2);
987     _add_command(cols, 0, CMD_REST, "rest and long wait; stops when", 2);
988     cols.add_formatted(
989             0,
990             "    Health or Magic become full or\n"
991             "    something is detected. If Health\n"
992             "    and Magic are already full, stops\n"
993             "    when 100 turns over (<w>numpad-5</w>)\n",
994             false);
995 
996     cols.add_formatted(
997             0,
998             "\n<h>Attacking monsters\n"
999             "Walking into a monster will attack it\n"
1000             "with the wielded weapon or barehanded.",
1001             false);
1002 
1003     cols.add_formatted(
1004             0,
1005             "\n<h>Ranged combat and magic\n",
1006             false);
1007 
1008     _add_insert_commands(cols, 0, "<w>%</w> to throw/fire missiles",
1009                          { CMD_FIRE });
1010     _add_insert_commands(cols, 0, "<w>%</w>/<w>%</w> to cast spells "
1011                                   "(<w>%?/%</w> lists spells)",
1012                          { CMD_CAST_SPELL, CMD_FORCE_CAST_SPELL, CMD_CAST_SPELL,
1013                            CMD_DISPLAY_SPELLS });
1014     _add_command(cols, 0, CMD_MEMORISE_SPELL, "Memorise spells and view spell\n"
1015                                               "    library (get books to add to it)", 2);
1016 
1017     // Second column.
1018     cols.add_formatted(
1019             1, "<h>Item types and inventory management\n",
1020             false);
1021 
1022     _add_insert_commands(cols, 1,
1023                          "<console><cyan>)</cyan> : </console>"
1024                          "hand weapons (<w>%</w>ield)",
1025                          { CMD_WIELD_WEAPON });
1026     _add_insert_commands(cols, 1,
1027                          "<console><brown>(</brown> : </console>"
1028                          "missiles (<w>%</w>uiver, <w>%</w>ire, <w>%</w>/<w>%</w> cycle)",
1029                          { CMD_QUIVER_ITEM, CMD_FIRE, CMD_CYCLE_QUIVER_FORWARD,
1030                            CMD_CYCLE_QUIVER_BACKWARD });
1031     _add_insert_commands(cols, 1,
1032                          "<console><cyan>[</cyan> : </console>"
1033                          "armour (<w>%</w>ear and <w>%</w>ake off)",
1034                          { CMD_WEAR_ARMOUR, CMD_REMOVE_ARMOUR });
1035     _add_insert_commands(cols, 1,
1036                          "<console><w>?</w> : </console>"
1037                          "scrolls (<w>%</w>ead)",
1038                          { CMD_READ });
1039     _add_insert_commands(cols, 1,
1040                          "<console><magenta>!</magenta> : </console>"
1041                          "potions (<w>%</w>uaff)",
1042                          { CMD_QUAFF });
1043     _add_insert_commands(cols, 1,
1044                          "<console><blue>=</blue> : </console>"
1045                          "rings (<w>%</w>ut on and <w>%</w>emove)",
1046                          { CMD_WEAR_JEWELLERY, CMD_REMOVE_JEWELLERY });
1047     _add_insert_commands(cols, 1,
1048                          "<console><red>\"</red> : </console>"
1049                          "amulets (<w>%</w>ut on and <w>%</w>emove)",
1050                          { CMD_WEAR_JEWELLERY, CMD_REMOVE_JEWELLERY });
1051     _add_insert_commands(cols, 1,
1052                          "<console><lightgrey>/</lightgrey> : </console>"
1053                          "wands (e<w>%</w>oke)",
1054                          { CMD_EVOKE });
1055 
1056     string item_types =
1057                   "<console><lightcyan>";
1058     item_types += stringize_glyph(get_item_symbol(SHOW_ITEM_BOOK));
1059     item_types +=
1060         "</lightcyan> : </console>"
1061         "books (<w>%</w>emorise, <w>%</w>ap, <w>%</w>ap,\n"
1062         "    pick up to add to spell library)";
1063     _add_insert_commands(cols, 1, item_types,
1064                          { CMD_MEMORISE_SPELL, CMD_CAST_SPELL,
1065                            CMD_FORCE_CAST_SPELL });
1066 
1067     item_types =
1068                   "<console><brown>";
1069     item_types += stringize_glyph(get_item_symbol(SHOW_ITEM_STAFF));
1070     item_types +=
1071         "</brown> : </console>"
1072         "staves (<w>%</w>ield)";
1073     _add_insert_commands(cols, 1, item_types,
1074                          { CMD_WIELD_WEAPON });
1075 
1076     cols.add_formatted(1, " ", false);
1077     _add_command(cols, 1, CMD_DISPLAY_INVENTORY, "inventory (select item to view)", 2);
1078     _add_command(cols, 1, CMD_PICKUP, "pick up item from ground (also <w>g</w>)", 2);
1079     _add_command(cols, 1, CMD_DROP, "drop item", 2);
1080     _add_command(cols, 1, CMD_DROP_LAST, "drop the last item(s) you picked up", 2);
1081 
1082     cols.add_formatted(
1083             1,
1084             "<h>Additional important commands\n");
1085 
1086     _add_command(cols, 1, CMD_SAVE_GAME_NOW, "Save the game and exit", 2);
1087     _add_command(cols, 1, CMD_REPLAY_MESSAGES, "show previous messages", 2);
1088     _add_command(cols, 1, CMD_USE_ABILITY, "use an ability", 2);
1089     _add_command(cols, 1, CMD_RESISTS_SCREEN, "show character overview", 2);
1090     _add_command(cols, 1, CMD_DISPLAY_RELIGION, "show religion overview", 2);
1091     _add_command(cols, 1, CMD_DISPLAY_MAP, "show map of the whole level", 2);
1092     _add_command(cols, 1, CMD_DISPLAY_OVERMAP, "show dungeon overview", 2);
1093 
1094     cols.add_formatted(
1095             1,
1096             "\n<h>Targeting\n"
1097             "<w>Enter</w> or <w>.</w> or <w>Del</w> : confirm target\n"
1098             "<w>+</w> and <w>-</w> : cycle between targets\n"
1099             "<w>f</w> or <w>p</w> : shoot at previous target\n"
1100             "         if still alive and in sight\n",
1101             false);
1102 }
1103 
_col_conv(void (* func)(column_composer &))1104 static formatted_string _col_conv(void (*func)(column_composer &))
1105 {
1106     column_composer cols(2, 42);
1107     func(cols);
1108     formatted_string contents;
1109     for (const auto& line : cols.formatted_lines())
1110     {
1111         contents += line;
1112         contents += "\n";
1113     }
1114     contents.ops.pop_back();
1115     return contents;
1116 }
1117 
_get_help_section(int section,formatted_string & header_out,formatted_string & text_out,int & scroll_out)1118 static int _get_help_section(int section, formatted_string &header_out, formatted_string &text_out, int &scroll_out)
1119 {
1120     static map<int, int> hotkeys;
1121     static map<int, formatted_string> page_text;
1122     static map<int, string> headers = {
1123         {'*', "Manual"}, {'%', "Aptitudes"}, {'^', "Quickstart"},
1124         {'~', "Macros"}, {'&', "Options"}, {'t', "Tiles"},
1125         {'?', "Key help"}
1126     };
1127 
1128     if (!page_text.size())
1129     {
1130         for (int i = 0; help_files[i].name != nullptr; ++i)
1131         {
1132             formatted_string text;
1133             bool next_is_hotkey = false;
1134             char buf[200];
1135             string fname = canonicalise_file_separator(help_files[i].name);
1136             FILE* fp = fopen_u(datafile_path(fname, false).c_str(), "r");
1137             ASSERTM(fp, "Failed to open '%s'!", fname.c_str());
1138             while (fgets(buf, sizeof buf, fp))
1139             {
1140                 text += string(buf);
1141                 if (next_is_hotkey && (isaupper(buf[0]) || isadigit(buf[0])))
1142                 {
1143                     int hotkey = tolower_safe(buf[0]);
1144                     hotkeys[hotkey] = count_occurrences(text.tostring(), "\n");
1145                 }
1146 
1147                 next_is_hotkey =
1148                     strstr(buf, "------------------------------------------"
1149                                         "------------------------------") == buf;
1150             }
1151             trim_string_right(text.ops.back().text);
1152             page_text[help_files[i].hotkey] = text;
1153         }
1154     }
1155 
1156     // All hotkeys are currently on *-page
1157     const int page = hotkeys.count(section) ? '*' : section;
1158 
1159     string header = headers.count(page) ? ": "+headers[page] : "";
1160     header_out = formatted_string::parse_string(
1161                     "<yellow>Dungeon Crawl Help"+header+"</yellow>");
1162     scroll_out = 0;
1163     switch (section)
1164     {
1165         case '?':
1166             if (crawl_state.game_is_hints_tutorial())
1167                 text_out = _col_conv(_add_formatted_hints_help);
1168             else
1169                 text_out = _col_conv(_add_formatted_keyhelp);
1170             return page;
1171         case CK_HOME:
1172             text_out = _col_conv(_add_formatted_help_menu);
1173             return page;
1174         default:
1175             if (hotkeys.count(section))
1176                 scroll_out = hotkeys[section];
1177             if (page_text.count(page))
1178             {
1179                 text_out = page_text[page];
1180                 return page;
1181             }
1182             break;
1183     }
1184     return 0;
1185 }
1186 
1187 class help_popup : public formatted_scroller
1188 {
1189 public:
help_popup(int key)1190     help_popup(int key) : formatted_scroller(FS_PREWRAPPED_TEXT) {
1191         set_tag("help");
1192         process_key(key);
1193     };
1194 private:
process_key(int ch)1195     bool process_key(int ch) override
1196     {
1197         int key = toalower(ch);
1198 
1199 #ifdef USE_TILE_LOCAL
1200         const int line_height = tiles.get_crt_font()->char_height();
1201 #else
1202         const int line_height = 1;
1203 #endif
1204 
1205         int scroll, page;
1206         formatted_string header_text, help_text;
1207         switch (key)
1208         {
1209             case CK_ESCAPE: case ':': case '#': case '/': case 'q': case 'v':
1210                 return false;
1211             default:
1212                 if (!(page = _get_help_section(key, header_text, help_text, scroll)))
1213                     break;
1214                 if (page != prev_page)
1215                 {
1216                     contents = help_text;
1217                     m_contents_dirty = true;
1218                     prev_page = page;
1219                 }
1220                 scroll = scroll ? (scroll-2)*line_height : 0;
1221                 set_scroll(scroll);
1222                 return true;
1223         }
1224 
1225         return formatted_scroller::process_key(ch);
1226     };
1227     int prev_page{0};
1228 };
1229 
_show_help_special(int key)1230 static bool _show_help_special(int key)
1231 {
1232     switch (key)
1233     {
1234         case ':':
1235             // If the game has begun, show notes.
1236             if (crawl_state.need_save)
1237                 display_notes();
1238             return true;
1239         case '#':
1240             // If the game has begun, show dump.
1241             if (crawl_state.need_save)
1242                 display_char_dump();
1243             return true;
1244         case '/':
1245             keyhelp_query_descriptions();
1246             return true;
1247         case 'q':
1248             _handle_FAQ();
1249             return true;
1250         case 'v':
1251             _print_version();
1252             return true;
1253         default:
1254             return false;
1255     }
1256 }
1257 
show_help(int section,string highlight_string)1258 void show_help(int section, string highlight_string)
1259 {
1260     // if `section` is a special case, don't instantiate a help popup at all.
1261     if (_show_help_special(toalower(section)))
1262         return;
1263     help_popup help(section);
1264     help.highlight = highlight_string;
1265     int key = toalower(help.show());
1266     // handle the case where one of the special case help sections is triggered
1267     // from the help main menu.
1268     _show_help_special(key);
1269 }
1270