The Battle for Wesnoth  1.19.14+dev
help_impl.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #include "help/help_impl.hpp"
17 
18 #include "actions/attack.hpp" // for time_of_day bonus
19 #include "color.hpp"
20 #include "formula/string_utils.hpp" // for VGETTEXT
21 #include "font/standard_colors.hpp" // for NORMAL_COLOR
22 #include "game_config.hpp" // for debug, menu_contract, etc
23 #include "game_config_manager.hpp" // for game_config_manager
24 #include "gettext.hpp" // for _, gettext, N_
26 #include "hotkey/hotkey_command.hpp" // for is_scope_active, etc
27 #include "log.hpp" // for LOG_STREAM, logger, etc
28 #include "map/map.hpp" // for gamemap
29 #include "picture.hpp" // for get_image, locator
30 #include "preferences/preferences.hpp" // for encountered_terrains, etc
31 #include "resources.hpp" // for tod_manager, config_manager
32 #include "serialization/markup.hpp" // for markup utility functions
33 #include "serialization/parser.hpp"
34 #include "serialization/string_utils.hpp" // for split, quoted_split, etc
35 #include "serialization/unicode.hpp" // for iterator
36 #include "serialization/utf8_exception.hpp" // for char_t, etc
37 #include "terrain/terrain.hpp" // for terrain_type
38 #include "terrain/translation.hpp" // for operator==, ter_list, etc
39 #include "terrain/type_data.hpp" // for terrain_type_data, etc
40 #include "time_of_day.hpp" // for time_of_day
41 #include "tod_manager.hpp" // for tod_manager
42 #include "tstring.hpp" // for t_string, operator<<
43 #include "units/race.hpp" // for unit_race, etc
44 #include "units/types.hpp" // for unit_type, unit_type_data, etc
45 #include "utils/general.hpp" // for contains
46 
47 #include <algorithm> // for sort, find, transform, etc
48 #include <boost/algorithm/string.hpp>
49 #include <cassert> // for assert
50 #include <iterator> // for back_insert_iterator, etc
51 #include <map> // for map, etc
52 #include <set>
53 #include <utility>
54 
55 static lg::log_domain log_help("help");
56 #define WRN_HP LOG_STREAM(warn, log_help)
57 #define DBG_HP LOG_STREAM(debug, log_help)
58 
59 namespace help {
60 
61 const game_config_view *game_cfg = nullptr;
62 // The default toplevel.
64 // All sections and topics not referenced from the default toplevel.
66 
69 boost::tribool last_debug_state = boost::indeterminate;
70 
71 std::vector<std::string> empty_string_vector;
72 const int max_section_level = 15;
75 const int box_width = 2;
77 const unsigned max_history = 100;
78 const std::string topic_img = "help/topic.png";
79 const std::string closed_section_img = "help/closed_section.png";
80 const std::string open_section_img = "help/open_section.png";
81 // The topic to open by default when opening the help dialog.
82 const std::string default_show_topic = "..introduction";
83 const std::string unknown_unit_topic = ".unknown_unit";
84 const std::string unit_prefix = "unit_";
85 const std::string terrain_prefix = "terrain_";
86 const std::string race_prefix = "race_";
87 const std::string faction_prefix = "faction_";
88 const std::string era_prefix = "era_";
89 const std::string variation_prefix = "variation_";
90 const std::string ability_prefix = "ability_";
91 
92 static bool is_cjk_char(const char32_t ch)
93 {
94  /**
95  * You can check these range at http://unicode.org/charts/
96  * see the "East Asian Scripts" part.
97  * Notice that not all characters in that part is still in use today, so don't list them all here.
98  * Below are characters that I guess may be used in wesnoth translations.
99  */
100 
101  //FIXME add range from Japanese-specific and Korean-specific section if you know the characters are used today.
102 
103  if (ch < 0x2e80) return false; // shortcut for common non-CJK
104 
105  return
106  //Han Ideographs: all except Supplement
107  (ch >= 0x4e00 && ch < 0x9fcf) ||
108  (ch >= 0x3400 && ch < 0x4dbf) ||
109  (ch >= 0x20000 && ch < 0x2a6df) ||
110  (ch >= 0xf900 && ch < 0xfaff) ||
111  (ch >= 0x3190 && ch < 0x319f) ||
112 
113  //Radicals: all except Ideographic Description
114  (ch >= 0x2e80 && ch < 0x2eff) ||
115  (ch >= 0x2f00 && ch < 0x2fdf) ||
116  (ch >= 0x31c0 && ch < 0x31ef) ||
117 
118  //Chinese-specific: Bopomofo and Bopomofo Extended
119  (ch >= 0x3104 && ch < 0x312e) ||
120  (ch >= 0x31a0 && ch < 0x31bb) ||
121 
122  //Yi-specific: Yi Radicals, Yi Syllables
123  (ch >= 0xa490 && ch < 0xa4c7) ||
124  (ch >= 0xa000 && ch < 0xa48d) ||
125 
126  //Japanese-specific: Hiragana, Katakana, Kana Supplement
127  (ch >= 0x3040 && ch <= 0x309f) ||
128  (ch >= 0x30a0 && ch <= 0x30ff) ||
129  (ch >= 0x1b000 && ch <= 0x1b001) ||
130 
131  //Ainu-specific: Katakana Phonetic Extensions
132  (ch >= 0x31f0 && ch <= 0x31ff) ||
133 
134  //Korean-specific: Hangul Syllables, Hangul Jamo, Hangul Jamo Extended-A, Hangul Jamo Extended-B
135  (ch >= 0xac00 && ch < 0xd7af) ||
136  (ch >= 0x1100 && ch <= 0x11ff) ||
137  (ch >= 0xa960 && ch <= 0xa97c) ||
138  (ch >= 0xd7b0 && ch <= 0xd7fb) ||
139 
140  //CJK Symbols and Punctuation
141  (ch >= 0x3000 && ch < 0x303f) ||
142 
143  //Halfwidth and Fullwidth Forms
144  (ch >= 0xff00 && ch < 0xffef);
145 }
146 
147 bool section_is_referenced(const std::string &section_id, const config &cfg)
148 {
149  if (auto toplevel = cfg.optional_child("toplevel"))
150  {
151  if(utils::contains(utils::quoted_split(toplevel["sections"]), section_id)) {
152  return true;
153  }
154  }
155 
156  for (const config &section : cfg.child_range("section"))
157  {
158  if(utils::contains(utils::quoted_split(section["sections"]), section_id)) {
159  return true;
160  }
161  }
162  return false;
163 }
164 
165 bool topic_is_referenced(const std::string &topic_id, const config &cfg)
166 {
167  if (auto toplevel = cfg.optional_child("toplevel"))
168  {
169  if(utils::contains(utils::quoted_split(toplevel["topics"]), topic_id)) {
170  return true;
171  }
172  }
173 
174  for (const config &section : cfg.child_range("section"))
175  {
176  if(utils::contains(utils::quoted_split(section["topics"]), topic_id)) {
177  return true;
178  }
179  }
180  return false;
181 }
182 
183 void parse_config_internal(const config *help_cfg, const config *section_cfg,
184  section &sec, int level)
185 {
186  if (level > max_section_level) {
187  PLAIN_LOG << "Maximum section depth has been reached. Maybe circular dependency?";
188  }
189  else if (section_cfg != nullptr) {
190  const std::vector<std::string> sections = utils::quoted_split((*section_cfg)["sections"]);
191  std::string id = level == 0 ? "toplevel" : (*section_cfg)["id"].str();
192  if (level != 0) {
193  if (!is_valid_id(id)) {
194  std::stringstream ss;
195  ss << "Invalid ID, used for internal purpose: '" << id << "'";
196  throw parse_error(ss.str());
197  }
198  }
199  std::string title = level == 0 ? "" : (*section_cfg)["title"].str();
200  sec.id = id;
201  sec.title = title;
202  // Find all child sections.
203  for(const std::string& sec_id : sections) {
204  if (auto child_cfg = help_cfg->find_child("section", "id", sec_id))
205  {
206  section child_section;
207  parse_config_internal(help_cfg, child_cfg.ptr(), child_section, level + 1);
208  sec.add_section(child_section);
209  }
210  else {
211  std::stringstream ss;
212  ss << "Help-section '" << sec_id << "' referenced from '"
213  << id << "' but could not be found.";
214  throw parse_error(ss.str());
215  }
216  }
217 
218  generate_sections(help_cfg, (*section_cfg)["sections_generator"], sec, level);
219  if ((*section_cfg)["sort_sections"] == "yes") {
220  sec.sections.sort(section_less());
221  }
222 
223  bool sort_topics = false;
224  bool sort_generated = true;
225 
226  if ((*section_cfg)["sort_topics"] == "yes") {
227  sort_topics = true;
228  sort_generated = false;
229  } else if ((*section_cfg)["sort_topics"] == "no") {
230  sort_topics = false;
231  sort_generated = false;
232  } else if ((*section_cfg)["sort_topics"] == "generated") {
233  sort_topics = false;
234  sort_generated = true;
235  } else if (!(*section_cfg)["sort_topics"].empty()) {
236  std::stringstream ss;
237  ss << "Invalid sort option: '" << (*section_cfg)["sort_topics"] << "'";
238  throw parse_error(ss.str());
239  }
240 
241  std::vector<topic> generated_topics = generate_topics(sort_generated,(*section_cfg)["generator"]);
242  std::vector<topic> topics;
243 
244  // Find all topics in this section.
245  for(const std::string& topic_id : utils::quoted_split((*section_cfg)["topics"])) {
246  if (auto topic_cfg = help_cfg->find_child("topic", "id", topic_id))
247  {
248  std::string text = topic_cfg["text"];
249  text += generate_topic_text(topic_cfg["generator"], help_cfg, sec);
250  topic child_topic(topic_cfg["title"], topic_cfg["id"], text);
251  if (!is_valid_id(child_topic.id)) {
252  std::stringstream ss;
253  ss << "Invalid ID, used for internal purpose: '" << id << "'";
254  throw parse_error(ss.str());
255  }
256  topics.push_back(child_topic);
257  }
258  else {
259  std::stringstream ss;
260  ss << "Help-topic '" << topic_id << "' referenced from '" << id
261  << "' but could not be found." << std::endl;
262  throw parse_error(ss.str());
263  }
264  }
265 
266  if (sort_topics) {
267  std::sort(topics.begin(),topics.end(), title_less());
268  std::sort(generated_topics.begin(),
269  generated_topics.end(), title_less());
270  std::merge(generated_topics.begin(),
271  generated_topics.end(),topics.begin(),topics.end()
272  ,std::back_inserter(sec.topics),title_less());
273  }
274  else {
275  sec.topics.insert(sec.topics.end(),
276  topics.begin(), topics.end());
277  sec.topics.insert(sec.topics.end(),
278  generated_topics.begin(), generated_topics.end());
279  }
280  }
281 }
282 
284 {
285  section sec;
286  if (cfg != nullptr) {
287  auto toplevel_cfg = cfg->optional_child("toplevel");
288  parse_config_internal(cfg, toplevel_cfg.ptr(), sec);
289  }
290  return sec;
291 }
292 
293 std::vector<topic> generate_topics(const bool sort_generated,const std::string &generator)
294 {
295  std::vector<topic> res;
296  if (generator.empty()) {
297  return res;
298  }
299 
300  if (generator == "abilities") {
301  res = generate_ability_topics(sort_generated);
302  } else if (generator == "weapon_specials") {
303  res = generate_weapon_special_topics(sort_generated);
304  } else if (generator == "time_of_days") {
305  res = generate_time_of_day_topics(sort_generated);
306  } else if (generator == "traits") {
307  res = generate_trait_topics(sort_generated);
308  } else {
309  std::vector<std::string> parts = utils::split(generator, ':', utils::STRIP_SPACES);
310  if (parts.size() > 1 && parts[0] == "units") {
311  res = generate_unit_topics(sort_generated, parts[1]);
312  } else if (parts[0] == "era" && parts.size()>1) {
313  res = generate_era_topics(sort_generated, parts[1]);
314  } else {
315  WRN_HP << "Found a topic generator that I didn't recognize: " << generator;
316  }
317  }
318 
319  return res;
320 }
321 
322 void generate_sections(const config *help_cfg, const std::string &generator, section &sec, int level)
323 {
324  if (generator == "races") {
325  generate_races_sections(help_cfg, sec, level);
326  } else if (generator == "terrains") {
328  } else if (generator == "eras") {
329  DBG_HP << "Generating eras...";
330  generate_era_sections(help_cfg, sec, level);
331  } else {
332  std::vector<std::string> parts = utils::split(generator, ':', utils::STRIP_SPACES);
333  if (parts.size() > 1 && parts[0] == "units") {
334  generate_unit_sections(help_cfg, sec, level, true, parts[1]);
335  } else if (generator.size() > 0) {
336  WRN_HP << "Found a section generator that I didn't recognize: " << generator;
337  }
338  }
339 }
340 
341 std::string generate_topic_text(const std::string &generator, const config *help_cfg, const section &sec)
342 {
343  std::string empty_string = "";
344  if (generator.empty()) {
345  return empty_string;
346  } else {
347  std::vector<std::string> parts = utils::split(generator, ':');
348  if (parts.size() > 1 && parts[0] == "contents") {
349  if (parts[1] == "generated") {
350  return generate_contents_links(sec);
351  } else {
352  return generate_contents_links(parts[1], help_cfg);
353  }
354  }
355  }
356  return empty_string;
357 }
358 
359 topic_text& topic_text::operator=(std::shared_ptr<topic_generator> g)
360 {
361  generator_ = std::move(g);
362  return *this;
363 }
364 
366 {
367  if (generator_) {
369  // This caches the result, so doesn't need the generator any more
370  generator_.reset();
371  }
372  return parsed_text_;
373 }
374 
375 static std::string time_of_day_bonus_colored(const int time_of_day_bonus)
376 {
377  return markup::span_color((time_of_day_bonus > 0 ? "green" : (time_of_day_bonus < 0 ? "red" : "white")), time_of_day_bonus);
378 }
379 
380 std::vector<topic> generate_time_of_day_topics(const bool /*sort_generated*/)
381 {
382  std::vector<topic> topics;
383  std::stringstream toplevel;
384 
385  if (!resources::tod_manager) {
386  toplevel << _("Only available during a scenario.");
387  topics.emplace_back(_("Time of Day Schedule"), "..schedule", toplevel.str());
388  return topics;
389  }
390 
391  const std::vector<time_of_day>& times = resources::tod_manager->times();
392  for (const time_of_day& time : times)
393  {
394  const std::string id = "time_of_day_" + time.id;
395  const std::string image = markup::img(time.image);
396  const std::string image_lawful = markup::img("icons/alignments/alignment_lawful_30.png");
397  const std::string image_neutral = markup::img("icons/alignments/alignment_neutral_30.png");
398  const std::string image_chaotic = markup::img("icons/alignments/alignment_chaotic_30.png");
399  const std::string image_liminal = markup::img("icons/alignments/alignment_liminal_30.png");
400  std::stringstream text, row_ss;
401 
402  const int lawful_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::lawful, false, resources::tod_manager->get_max_liminal_bonus());
403  const int neutral_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::neutral, false, resources::tod_manager->get_max_liminal_bonus());
404  const int chaotic_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::chaotic, false, resources::tod_manager->get_max_liminal_bonus());
405  const int liminal_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::liminal, false, resources::tod_manager->get_max_liminal_bonus());
406 
407  row_ss << markup::tag("col", markup::make_link(time.name.str(), id))
408  << markup::tag("col", image)
409  << markup::tag("col", image_lawful, time_of_day_bonus_colored(lawful_bonus))
410  << markup::tag("col", image_neutral, time_of_day_bonus_colored(neutral_bonus))
411  << markup::tag("col", image_chaotic, time_of_day_bonus_colored(chaotic_bonus))
412  << markup::tag("col", image_liminal, time_of_day_bonus_colored(liminal_bonus));
413  toplevel << markup::tag("row", row_ss.str());
414 
415  text << image << '\n'
416  << time.description.str() << '\n'
417  << image_lawful << _("Lawful Bonus:") << ' ' << time_of_day_bonus_colored(lawful_bonus) << '\n'
418  << image_neutral << _("Neutral Bonus:") << ' ' << time_of_day_bonus_colored(neutral_bonus) << '\n'
419  << image_chaotic << _("Chaotic Bonus:") << ' ' << time_of_day_bonus_colored(chaotic_bonus) << '\n'
420  << image_liminal << _("Liminal Bonus:") << ' ' << time_of_day_bonus_colored(liminal_bonus) << '\n' << '\n'
421  << markup::make_link(_("Schedule"), "..schedule");
422 
423  topics.emplace_back(time.name.str(), id, text.str());
424  }
425 
426  topics.emplace_back(_("Time of Day Schedule"), "..schedule", markup::tag("table", toplevel.str()));
427  return topics;
428 }
429 
430 std::vector<topic> generate_weapon_special_topics(const bool sort_generated)
431 {
432  std::vector<topic> topics;
433 
434  std::map<t_string, std::string> special_description;
435  std::map<t_string, std::set<std::string, string_less>> special_units;
436 
437  for(const auto& [type_id, type] : unit_types.types()) {
438  // Only show the weapon special if we find it on a unit that
439  // detailed description should be shown about.
441  continue;
442 
443  for(const attack_type& atk : type.attacks()) {
444  for(const auto& [name, description] : atk.special_tooltips()) {
445  special_description.emplace(name, description);
446 
447  if (!type.hide_help()) {
448  //add a link in the list of units having this special
449  std::string type_name = type.type_name();
450  //check for variations (walking corpse/soulless etc)
451  const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
452  std::string ref_id = section_prefix + unit_prefix + type.id();
453  //we put the translated name at the beginning of the hyperlink,
454  //so the automatic alphabetic sorting of std::set can use it
455  std::string link = markup::make_link(type_name, ref_id);
456  special_units[name].insert(link);
457  }
458  }
459  }
460 
461  for(const config& adv : type.modification_advancements()) {
462  for(const config& effect : adv.child_range("effect")) {
463  if(effect["apply_to"] == "new_attack" && effect.has_child("specials")) {
464  for(const auto [key, special] : effect.mandatory_child("specials").all_children_view()) {
465  if(!special["name"].empty()) {
466  special_description.emplace(special["name"].t_str(), special["description"].t_str());
467  if(!type.hide_help()) {
468  //add a link in the list of units having this special
469  std::string type_name = type.type_name();
470  //check for variations (walking corpse/soulless etc)
471  const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
472  std::string ref_id = section_prefix + unit_prefix + type.id();
473  //we put the translated name at the beginning of the hyperlink,
474  //so the automatic alphabetic sorting of std::set can use it
475  std::string link = markup::make_link(type_name, ref_id);
476  special_units[special["name"]].insert(link);
477  }
478  }
479  }
480  } else if(effect["apply_to"] == "attack" && effect.has_child("set_specials")) {
481  for(const auto [key, special] : effect.mandatory_child("set_specials").all_children_view()) {
482  if(!special["name"].empty()) {
483  special_description.emplace(special["name"].t_str(), special["description"].t_str());
484  if(!type.hide_help()) {
485  //add a link in the list of units having this special
486  std::string type_name = type.type_name();
487  //check for variations (walking corpse/soulless etc)
488  const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
489  std::string ref_id = section_prefix + unit_prefix + type.id();
490  //we put the translated name at the beginning of the hyperlink,
491  //so the automatic alphabetic sorting of std::set can use it
492  std::string link = markup::make_link(type_name, ref_id);
493  special_units[special["name"]].insert(link);
494  }
495  }
496  }
497  }
498  }
499  }
500  }
501 
502  for(const auto& [name, description] : special_description) {
503  // use untranslated name to have universal topic id
504  std::string id = "weaponspecial_" + name.base_str();
505  std::stringstream text;
506  text << description;
507  text << "\n\n" << markup::tag("header", _("Units with this special attack")) << "\n";
508  for(const std::string& type_id : special_units[name]) {
509  text << font::unicode_bullet << " " << type_id << "\n";
510  }
511 
512  topics.emplace_back(name, id, text.str());
513  }
514 
515  if (sort_generated)
516  std::sort(topics.begin(), topics.end(), title_less());
517  return topics;
518 }
519 
520 std::vector<topic> generate_ability_topics(const bool sort_generated)
521 {
522  std::vector<topic> topics;
523 
524  std::map<std::string, const unit_type::ability_metadata*> ability_topic_data;
525  std::map<std::string, std::set<std::string, string_less>> ability_units;
526 
527  const auto parse = [&](const unit_type& type, const unit_type::ability_metadata& ability) {
528  // NOTE: neither ability names nor ability ids are necessarily unique. Creating
529  // topics for either each unique name or each unique id means certain abilities
530  // will be excluded from help. So... the ability topic ref id is a combination
531  // of id and (untranslated) name. It's rather ugly, but it works.
532  const std::string topic_ref = ability.id + ability.name.base_str();
533 
534  ability_topic_data.emplace(topic_ref, &ability);
535 
536  if(!type.hide_help()) {
537  // Add a link in the list of units with this ability
538  // We put the translated name at the beginning of the hyperlink,
539  // so the automatic alphabetic sorting of std::set can use it
540  const std::string link = markup::make_link(type.type_name(), unit_prefix + type.id());
541  ability_units[topic_ref].insert(link);
542  }
543  };
544 
545  // Look through all the unit types. If a unit of that type would have a full
546  // description, add its abilities to the potential topic list. We don't want
547  // to show abilities that the user has not encountered yet.
548  for(const auto& [type_id, type] : unit_types.types()) {
550  continue;
551  }
552 
553  for(const unit_type::ability_metadata& ability : type.abilities_metadata()) {
554  parse(type, ability);
555  }
556 
557  for(const unit_type::ability_metadata& ability : type.adv_abilities_metadata()) {
558  parse(type, ability);
559  }
560  }
561 
562  for(const auto& a : ability_topic_data) {
563  if (a.second->name.empty()) {
564  continue;
565  }
566  std::ostringstream text;
567  text << a.second->description;
568  text << "\n\n" << markup::tag("header", _("Units with this ability")) << "\n";
569 
570  for(const auto& u : ability_units[a.first]) { // first is the topic ref id
571  text << font::unicode_bullet << " " << u << "\n";
572  }
573 
574  topics.emplace_back(a.second->name, ability_prefix + a.first, text.str());
575  }
576 
577  if(sort_generated) {
578  std::sort(topics.begin(), topics.end(), title_less());
579  }
580 
581  return topics;
582 }
583 
584 std::vector<topic> generate_era_topics(const bool sort_generated, const std::string & era_id)
585 {
586  std::vector<topic> topics;
587 
588  auto era = game_cfg->find_child("era","id", era_id);
589  if(era && !era["hide_help"].to_bool()) {
590  topics = generate_faction_topics(*era, sort_generated);
591 
592  std::vector<std::string> faction_links;
593  for (const topic & t : topics) {
594  faction_links.push_back(markup::make_link(t.title, t.id));
595  }
596 
597  std::stringstream text;
598  const config::attribute_value& description = era["description"];
599  if (!description.empty()) {
600  text << description.t_str() << "\n";
601  text << "\n";
602  }
603 
604  text << markup::tag("header", _("Factions")) << "\n";
605 
606  std::sort(faction_links.begin(), faction_links.end());
607  for (const std::string &link : faction_links) {
608  text << font::unicode_bullet << " " << link << "\n";
609  }
610 
611  topic era_topic(era["name"], ".." + era_prefix + era["id"].str(), text.str());
612 
613  topics.push_back( era_topic );
614  }
615  return topics;
616 }
617 
618 std::vector<topic> generate_faction_topics(const config & era, const bool sort_generated)
619 {
620  std::vector<topic> topics;
621  for (const config &f : era.child_range("multiplayer_side")) {
622  const std::string& id = f["id"];
623  if (id == "Random")
624  continue;
625 
626  std::stringstream text;
627 
628  const config::attribute_value& description = f["description"];
629  if (!description.empty()) {
630  text << description.t_str() << "\n";
631  text << "\n";
632  }
633 
634  const std::vector<std::string> recruit_ids = utils::split(f["recruit"]);
635  std::set<std::string> races;
636  std::set<std::string> alignments;
637 
638  for (const std::string & u_id : recruit_ids) {
639  if (const unit_type * t = unit_types.find(u_id, unit_type::HELP_INDEXED)) {
640  assert(t);
641  const unit_type & type = *t;
642 
643  if (const unit_race *r = unit_types.find_race(type.race_id())) {
644  races.insert(markup::make_link(r->plural_name(), std::string("..") + race_prefix + r->id()));
645  }
646  alignments.insert(markup::make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
647  }
648  }
649 
650  if (!races.empty()) {
651  std::set<std::string>::iterator it = races.begin();
652  text << _("Races: ") << *(it++);
653  while(it != races.end()) {
654  text << ", " << *(it++);
655  }
656  text << "\n\n";
657  }
658 
659  if (!alignments.empty()) {
660  std::set<std::string>::iterator it = alignments.begin();
661  text << _("Alignments: ") << *(it++);
662  while(it != alignments.end()) {
663  text << ", " << *(it++);
664  }
665  text << "\n\n";
666  }
667 
668  text << markup::tag("header", _("Leaders")) << "\n";
669  const std::vector<std::string> leaders =
670  make_unit_links_list( utils::split(f["leader"]), true );
671  for (const std::string &link : leaders) {
672  text << font::unicode_bullet << " " << link << "\n";
673  }
674 
675  text << "\n";
676 
677  text << markup::tag("header", _("Recruits")) << "\n";
678  const std::vector<std::string> recruit_links =
679  make_unit_links_list( recruit_ids, true );
680  for (const std::string &link : recruit_links) {
681  text << font::unicode_bullet << " " << link << "\n";
682  }
683 
684  const std::string name = f["name"];
685  const std::string ref_id = faction_prefix + era["id"] + "_" + id;
686  topics.emplace_back(name, ref_id, text.str());
687  }
688  if (sort_generated)
689  std::sort(topics.begin(), topics.end(), title_less());
690  return topics;
691 }
692 
693 std::vector<topic> generate_trait_topics(const bool sort_generated)
694 {
695  // All traits that could be assigned to at least one discovered or HIDDEN_BUT_SHOW_MACROS unit.
696  // This is collected from the [units][trait], [race][traits], and [unit_type][traits] tags. If
697  // there are duplicates with the same id, it takes the first one encountered.
698  std::map<std::string, const config> trait_list;
699 
700  // The global traits that are direct children of a [units] tag
701  for (const config & trait : unit_types.traits()) {
702  trait_list.emplace(trait["id"], trait);
703  }
704 
705  // Search for discovered unit types
706  std::set<std::string> races;
707  for(const auto& i : unit_types.types()) {
708  const unit_type& type = i.second;
710 
711  // Remember which races have been discovered.
712  //
713  // For unit types, unit_type::possible_traits() usually includes racial traits; however it's
714  // possible that all discovered units of a race have ignore_race_traits=yes, and so we still
715  // need to loop over the [race] tags looking for more traits.
716  if(desc_type == FULL_DESCRIPTION) {
717  races.insert(type.race_id());
718  }
719 
720  // Handle [unit_type][trait]s.
721  //
722  // It would be better if we only looked at the traits that are specific to the unit_type,
723  // but that unmerged unit_type_data.traits() isn't available. We're forced to use
724  // possible_traits() instead which returns all of the traits, including the ones that units
725  // with ignore_race_traits=no have inherited from their [race] tag.
726  if (desc_type == FULL_DESCRIPTION || desc_type == HIDDEN_BUT_SHOW_MACROS) {
727  for (const config& trait : type.possible_traits()) {
728  trait_list.emplace(trait["id"], trait);
729  }
730  }
731  }
732 
733  // Race traits, even those that duplicate a global trait (which will be dropped by emplace()).
734  //
735  // For traits, assume we don't discover additional races via the [race]help_taxonomy= links. The
736  // traits themselves don't propagate down those links, so if the trait is interesting w.r.t. the
737  // discovered units then their own race will already include it.
738  for(const auto& race_id : races) {
739  if(const unit_race *r = unit_types.find_race(race_id)) {
740  for(const config & trait : r->additional_traits()) {
741  trait_list.emplace(trait["id"], trait);
742  }
743  }
744  }
745 
746  std::vector<topic> topics;
747  for(auto& a : trait_list) {
748  std::string id = "traits_" + a.first;
749  const config& trait = a.second;
750 
751  std::string name = trait["male_name"].str();
752  if (name.empty()) name = trait["female_name"].str();
753  if (name.empty()) name = trait["name"].str();
754  if (name.empty()) continue; // Hidden trait
755 
756  std::stringstream text;
757  if (!trait["help_text"].empty()) {
758  text << trait["help_text"];
759  } else if (!trait["description"].empty()) {
760  text << trait["description"];
761  } else {
762  text << _("No description available.");
763  }
764  text << "\n\n";
765 
766  topics.emplace_back(name, id, text.str());
767  }
768 
769  if (sort_generated)
770  std::sort(topics.begin(), topics.end(), title_less());
771  return topics;
772 }
773 
774 
775 std::string make_unit_link(const std::string& type_id)
776 {
777  std::string link;
778 
780  if (!type) {
781  PLAIN_LOG << "Unknown unit type : " << type_id;
782  // don't return an hyperlink (no page)
783  // instead show the id (as hint)
784  link = type_id;
785  } else if (!type->hide_help()) {
786  std::string name = type->type_name();
787  std::string ref_id;
789  const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
790  ref_id = section_prefix + unit_prefix + type->id();
791  } else {
792  ref_id = unknown_unit_topic;
793  name += " (?)";
794  }
795  link = markup::make_link(name, ref_id);
796  } // if hide_help then link is an empty string
797 
798  return link;
799 }
800 
801 std::vector<std::string> make_unit_links_list(const std::vector<std::string>& type_id_list, bool ordered)
802 {
803  std::vector<std::string> links_list;
804  for (const std::string &type_id : type_id_list) {
805  std::string unit_link = make_unit_link(type_id);
806  if (!unit_link.empty())
807  links_list.push_back(unit_link);
808  }
809 
810  if (ordered)
811  std::sort(links_list.begin(), links_list.end());
812 
813  return links_list;
814 }
815 
816 void generate_races_sections(const config* help_cfg, section& sec, int level)
817 {
818  std::set<std::string, string_less> races;
819  std::set<std::string, string_less> visible_races;
820 
821  // Calculate which races have been discovered, from the list of discovered unit types.
822  for(const auto& i : unit_types.types()) {
823  const unit_type& type = i.second;
825  if(desc_type == FULL_DESCRIPTION) {
826  races.insert(type.race_id());
827  if(!type.hide_help())
828  visible_races.insert(type.race_id());
829  }
830  }
831 
832  // Propagate visibility up the help_taxonomy tree.
833  std::set<std::string, string_less> last_sweep = visible_races;
834  while(!last_sweep.empty()) {
835  std::set<std::string, string_less> current_sweep;
836  for(const auto& race_id : last_sweep) {
837  if(const unit_race* r = unit_types.find_race(race_id)) {
838  const auto& help_taxonomy = r->help_taxonomy();
839  if(!help_taxonomy.empty() && !visible_races.count(help_taxonomy) && unit_types.find_race(help_taxonomy)) {
840  current_sweep.insert(help_taxonomy);
841  races.insert(help_taxonomy);
842  visible_races.insert(help_taxonomy);
843  }
844  }
845  }
846  last_sweep = std::move(current_sweep);
847  }
848 
849  struct taxonomy_queue_type
850  {
851  std::string parent_id;
852  section content;
853  };
854  std::vector<taxonomy_queue_type> taxonomy_queue;
855 
856  // Add all races without a [race]help_taxonomy= to the documentation section, and queue the others.
857  // This avoids a race condition dependency on the order that races are encountered in help_cfg.
858  for(const auto& race_id : races) {
859  section race_section;
860  config section_cfg;
861 
862  bool hidden = (visible_races.count(race_id) == 0);
863 
864  section_cfg["id"] = hidden_symbol(hidden) + race_prefix + race_id;
865 
866  std::string title;
867  std::string help_taxonomy;
868  if(const unit_race* r = unit_types.find_race(race_id)) {
869  title = r->plural_name();
870  help_taxonomy = r->help_taxonomy();
871  } else {
872  title = _("race^Miscellaneous");
873  // leave help_taxonomy empty
874  }
875  section_cfg["title"] = title;
876 
877  section_cfg["sections_generator"] = "units:" + race_id;
878  section_cfg["generator"] = "units:" + race_id;
879 
880  parse_config_internal(help_cfg, &section_cfg, race_section, level + 1);
881 
882  if(help_taxonomy.empty()) {
883  sec.add_section(race_section);
884  } else {
885  bool parent_hidden = (visible_races.count(help_taxonomy) == 0);
886  auto parent_id = hidden_symbol(parent_hidden) + race_prefix + help_taxonomy;
887  taxonomy_queue.push_back({std::move(parent_id), std::move(race_section)});
888  }
889  }
890 
891  // Each run through this loop handles one level of nesting of [race]help_taxonomy=
892  bool process_queue_again = true;
893  while(process_queue_again && !taxonomy_queue.empty()) {
894  process_queue_again = false;
895  auto to_process = std::exchange(taxonomy_queue, {});
896 
897  for(auto& x : to_process) {
898  auto parent = find_section(sec, x.parent_id);
899  if(parent) {
900  parent->add_section(x.content);
901  process_queue_again = true;
902  } else {
903  taxonomy_queue.push_back(std::move(x));
904  }
905  }
906  }
907 
908  // Fallback to adding the new race at the top level, as if it had help_taxonomy.empty().
909  for(auto& x : taxonomy_queue) {
910  sec.add_section(x.content);
911  }
912 }
913 
914 void generate_era_sections(const config* help_cfg, section & sec, int level)
915 {
916  for (const config & era : game_cfg->child_range("era")) {
917  if (era["hide_help"].to_bool()) {
918  continue;
919  }
920 
921  DBG_HP << "Adding help section: " << era["id"].str();
922 
923  section era_section;
924  config section_cfg;
925  section_cfg["id"] = era_prefix + era["id"].str();
926  section_cfg["title"] = era["name"];
927 
928  section_cfg["generator"] = "era:" + era["id"].str();
929 
930  DBG_HP << section_cfg.debug();
931 
932  parse_config_internal(help_cfg, &section_cfg, era_section, level+1);
933  sec.add_section(era_section);
934  }
935 }
936 
937 void generate_terrain_sections(section& sec, int /*level*/)
938 {
939  std::shared_ptr<terrain_type_data> tdata = load_terrain_types_data();
940 
941  if (!tdata) {
942  WRN_HP << "When building terrain help sections, couldn't acquire terrain types data, aborting.";
943  return;
944  }
945 
946  std::map<std::string, section> base_map;
947 
948  const t_translation::ter_list& t_listi = tdata->list();
949 
950  for (const t_translation::terrain_code& t : t_listi) {
951 
952  const terrain_type& info = tdata->get_terrain_info(t);
953 
954  bool hidden = info.hide_help();
955 
956  if (prefs::get().encountered_terrains().find(t)
957  == prefs::get().encountered_terrains().end() && !info.is_overlay())
958  hidden = true;
959 
960  topic terrain_topic;
961  terrain_topic.title = info.editor_name();
962  terrain_topic.id = hidden_symbol(hidden) + terrain_prefix + info.id();
963  terrain_topic.text = std::make_shared<terrain_topic_generator>(info);
964 
965  t_translation::ter_list base_terrains = tdata->underlying_union_terrain(t);
966  if (info.has_default_base()) {
967  for (const auto base : tdata->underlying_union_terrain(info.default_base())) {
968  if (!utils::contains(base_terrains, base)) {
969  base_terrains.emplace_back(base);
970  }
971  }
972  }
973  for (const t_translation::terrain_code& base : base_terrains) {
974 
975  const terrain_type& base_info = tdata->get_terrain_info(base);
976 
977  if (!base_info.is_nonnull() || base_info.hide_help())
978  continue;
979 
980  section& base_section = base_map[base_info.id()];
981 
982  base_section.id = terrain_prefix + base_info.id();
983  base_section.title = base_info.editor_name();
984 
985  if (base_info.id() == info.id())
986  terrain_topic.id = ".." + terrain_prefix + info.id();
987  base_section.topics.push_back(terrain_topic);
988  }
989  }
990 
991  for (const auto& base : base_map) {
992  sec.add_section(base.second);
993  }
994 }
995 
996 void generate_unit_sections(const config* /*help_cfg*/, section& sec, int /*level*/, const bool /*sort_generated*/, const std::string& race)
997 {
998  for (const unit_type_data::unit_type_map::value_type &i : unit_types.types()) {
999  const unit_type &type = i.second;
1000 
1001  if (type.race_id() != race)
1002  continue;
1003 
1004  if (!type.show_variations_in_help())
1005  continue;
1006 
1007  section base_unit;
1008  for (const std::string &variation_id : type.variations()) {
1009  // TODO: Do we apply encountered stuff to variations?
1010  const unit_type &var_type = type.get_variation(variation_id);
1011  const std::string topic_name = var_type.variation_name();
1012  const std::string var_ref = hidden_symbol(var_type.hide_help()) + variation_prefix + var_type.id() + "_" + variation_id;
1013 
1014  topic var_topic(topic_name, var_ref, "");
1015  var_topic.text = std::make_shared<unit_topic_generator>(var_type, variation_id);
1016  base_unit.topics.push_back(var_topic);
1017  }
1018 
1019  const std::string type_name = type.type_name();
1020  const std::string ref_id = hidden_symbol(type.hide_help()) + unit_prefix + type.id();
1021 
1022  base_unit.id = ref_id;
1023  base_unit.title = type_name;
1024 
1025  sec.add_section(base_unit);
1026  }
1027 }
1028 
1029 std::vector<topic> generate_unit_topics(const bool sort_generated, const std::string& race)
1030 {
1031  std::vector<topic> topics;
1032  std::set<std::string, string_less> race_units;
1033  std::set<std::string, string_less> race_topics;
1034  std::set<std::string> alignments;
1035 
1036  for (const unit_type_data::unit_type_map::value_type &i : unit_types.types())
1037  {
1038  const unit_type &type = i.second;
1039 
1040  if (type.race_id() != race)
1041  continue;
1042 
1044  if (desc_type != FULL_DESCRIPTION)
1045  continue;
1046 
1047  const std::string debug_suffix = (game_config::debug ? " (" + type.id() + ")" : "");
1048  const std::string type_name = type.type_name() + (type.id() == type.type_name().str() ? "" : debug_suffix);
1049  const std::string real_prefix = type.show_variations_in_help() ? ".." : "";
1050  const std::string ref_id = hidden_symbol(type.hide_help()) + real_prefix + unit_prefix + type.id();
1051  topic unit_topic(type_name, ref_id, "");
1052  unit_topic.text = std::make_shared<unit_topic_generator>(type);
1053  topics.push_back(unit_topic);
1054 
1055  if (!type.hide_help()) {
1056  // we also record an hyperlink of this unit
1057  // in the list used for the race topic
1058  std::string link = markup::make_link(type_name, ref_id);
1059  race_units.insert(link);
1060 
1061  alignments.insert(markup::make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
1062  }
1063  }
1064 
1065  //generate the hidden race description topic
1066  std::string race_id = "..race_"+race;
1067  std::string race_name;
1068  std::string race_description;
1069  std::string race_help_taxonomy;
1070  if (const unit_race *r = unit_types.find_race(race)) {
1071  race_name = r->plural_name();
1072  race_description = r->description();
1073  race_help_taxonomy = r->help_taxonomy();
1074  // if (description.empty()) description = _("No description Available");
1075  for (const config &additional_topic : r->additional_topics())
1076  {
1077  std::string id = additional_topic["id"];
1078  std::string title = additional_topic["title"];
1079  std::string text = additional_topic["text"];
1080  //topic additional_topic(title, id, text);
1081  topics.emplace_back(title,id,text);
1082  std::string link = markup::make_link(title, id);
1083  race_topics.insert(link);
1084  }
1085  } else {
1086  race_name = _ ("race^Miscellaneous");
1087  // description = _("Here put the description of the Miscellaneous race");
1088  }
1089 
1090  // Find any other races whose [race]help_taxonomy points to the current race
1091  std::map<std::string, t_string> subgroups;
1092  for (const auto &r : unit_types.races()) {
1093  if (r.second.help_taxonomy() == race) {
1094  if (!r.second.plural_name().empty())
1095  subgroups[r.first] = r.second.plural_name();
1096  else
1097  subgroups[r.first] = r.first;
1098  }
1099  }
1100 
1101  std::stringstream text;
1102 
1103  if (!race_description.empty()) {
1104  text << race_description << "\n\n";
1105  }
1106 
1107  if (!alignments.empty()) {
1108  std::set<std::string>::iterator it = alignments.begin();
1109  text << (alignments.size() > 1 ? _("Alignments: ") : _("Alignment: ")) << *(it++);
1110  while(it != alignments.end()) {
1111  text << ", " << *(it++);
1112  }
1113  text << "\n\n";
1114  }
1115 
1116  if (!race_help_taxonomy.empty()) {
1117  utils::string_map symbols;
1118  symbols["topic_id"] = "..race_"+race_help_taxonomy;
1119  if (const unit_race *r = unit_types.find_race(race_help_taxonomy)) {
1120  symbols["help_taxonomy"] = r->plural_name();
1121  } else {
1122  // Fall back to using showing the race id for the race that we couldn't find.
1123  // Not great, but probably useful if UMC has a broken link.
1124  symbols["help_taxonomy"] = race_help_taxonomy;
1125  }
1126  // TRANSLATORS: this is expected to say "[Dunefolk are] a group of units, all of whom are Humans",
1127  // or "[Quenoth Elves are] a group of units, all of whom are Elves".
1128  text << VGETTEXT("This is a group of units, all of whom are <ref dst='$topic_id'>$help_taxonomy</ref>.", symbols) << "\n\n";
1129  }
1130 
1131  if (!subgroups.empty()) {
1132  if (!race_help_taxonomy.empty()) {
1133  text << markup::tag("header", _("Subgroups of units within this group")) << "\n";
1134  } else {
1135  text << markup::tag("header", _("Groups of units within this race")) << "\n";
1136  }
1137  for (const auto &sg : subgroups) {
1138  text << font::unicode_bullet << " " << markup::make_link(sg.second, "..race_" + sg.first) << "\n";
1139  }
1140  text << "\n";
1141  }
1142 
1143  if (!race_help_taxonomy.empty()) {
1144  text << markup::tag("header", _("Units of this group")) << "\n";
1145  } else {
1146  text << markup::tag("header", _("Units of this race")) << "\n";
1147  }
1148  for (const auto &u : race_units) {
1149  text << font::unicode_bullet << " " << u << "\n";
1150  }
1151 
1152  topics.emplace_back(race_name, race_id, text.str());
1153 
1154  if (sort_generated)
1155  std::sort(topics.begin(), topics.end(), title_less());
1156 
1157  return topics;
1158 }
1159 
1161 {
1162  if (game_config::debug || prefs::get().show_all_units_in_help() ||
1164  return FULL_DESCRIPTION;
1165  }
1166 
1167  const std::set<std::string> &encountered_units = prefs::get().encountered_units();
1168  if (encountered_units.find(type.id()) != encountered_units.end()) {
1169  return FULL_DESCRIPTION;
1170  }
1171 
1172  // See the docs of HIDDEN_BUT_SHOW_MACROS
1173  if (type.id() == "Fog Clearer") {
1174  return HIDDEN_BUT_SHOW_MACROS;
1175  }
1176 
1177  return NO_DESCRIPTION;
1178 }
1179 
1180 std::string generate_contents_links(const std::string& section_name, config const *help_cfg)
1181 {
1182  auto section_cfg = help_cfg->find_child("section", "id", section_name);
1183  if (!section_cfg) {
1184  return std::string();
1185  }
1186 
1187  std::ostringstream res;
1188 
1189  std::vector<std::string> topics = utils::quoted_split(section_cfg["topics"]);
1190 
1191  // we use an intermediate structure to allow a conditional sorting
1192  typedef std::pair<std::string,std::string> link;
1193  std::vector<link> topics_links;
1194 
1195  // Find all topics in this section.
1196  for(const std::string& topic : topics) {
1197  if (auto topic_cfg = help_cfg->find_child("topic", "id", topic)) {
1198  std::string id = topic_cfg["id"];
1199  if (is_visible_id(id))
1200  topics_links.emplace_back(topic_cfg["title"], id);
1201  }
1202  }
1203 
1204  if (section_cfg["sort_topics"] == "yes") {
1205  std::sort(topics_links.begin(),topics_links.end());
1206  }
1207 
1208  for(const auto& [text, target] : topics_links) {
1209  std::string link = markup::make_link(text, target);
1210  res << font::unicode_bullet << " " << link << "\n";
1211  }
1212 
1213  return res.str();
1214 }
1215 
1216 std::string generate_contents_links(const section &sec)
1217 {
1218  std::stringstream res;
1219 
1220  for (auto &s : sec.sections) {
1221  if (is_visible_id(s.id)) {
1222  std::string link = markup::make_link(s.title, ".."+s.id);
1223  res << font::unicode_bullet << " " << link << "\n";
1224  }
1225  }
1226 
1227  for(const topic& t : sec.topics) {
1228  if (is_visible_id(t.id)) {
1229  std::string link = markup::make_link(t.title, t.id);
1230  res << font::unicode_bullet << " " << link << "\n";
1231  }
1232  }
1233  return res.str();
1234 }
1235 
1236 bool topic::operator==(const topic &t) const
1237 {
1238  return t.id == id;
1239 }
1240 
1241 bool topic::operator<(const topic &t) const
1242 {
1243  return id < t.id;
1244 }
1245 
1246 bool section::operator==(const section &sec) const
1247 {
1248  return sec.id == id;
1249 }
1250 
1251 bool section::operator<(const section &sec) const
1252 {
1253  return id < sec.id;
1254 }
1255 
1257 {
1258  sections.emplace_back(s);
1259 }
1260 
1262 {
1263  topics.clear();
1264  sections.clear();
1265 }
1266 
1267 const topic *find_topic(const section &sec, const std::string &id)
1268 {
1269  topic_list::const_iterator tit =
1270  std::find_if(sec.topics.begin(), sec.topics.end(), has_id(id));
1271  if (tit != sec.topics.end()) {
1272  return &(*tit);
1273  }
1274  for (const auto &s : sec.sections) {
1275  const auto t = find_topic(s, id);
1276  if (t != nullptr) {
1277  return t;
1278  }
1279  }
1280  return nullptr;
1281 }
1282 
1283 const section *find_section(const section &sec, const std::string &id)
1284 {
1285  const auto sit =
1286  std::find_if(sec.sections.begin(), sec.sections.end(), has_id(id));
1287  if (sit != sec.sections.end()) {
1288  return &*sit;
1289  }
1290  for (const auto &subsection : sec.sections) {
1291  const auto s = find_section(subsection, id);
1292  if (s != nullptr) {
1293  return s;
1294  }
1295  }
1296  return nullptr;
1297 }
1298 
1299 section *find_section(section &sec, const std::string &id)
1300 {
1301  return const_cast<section *>(find_section(const_cast<const section &>(sec), id));
1302 }
1303 
1304 std::string remove_first_space(const std::string& text)
1305 {
1306  if (text.length() > 0 && text[0] == ' ') {
1307  return text.substr(1);
1308  }
1309  return text;
1310 }
1311 
1312 std::string get_first_word(const std::string &s)
1313 {
1314  std::size_t first_word_start = s.find_first_not_of(' ');
1315  if (first_word_start == std::string::npos) {
1316  return s;
1317  }
1318  std::size_t first_word_end = s.find_first_of(" \n", first_word_start);
1319  if( first_word_end == first_word_start ) {
1320  // This word is '\n'.
1321  first_word_end = first_word_start+1;
1322  }
1323 
1324  //if no gap(' ' or '\n') found, test if it is CJK character
1325  std::string re = s.substr(0, first_word_end);
1326 
1327  utf8::iterator ch(re);
1328  if (ch == utf8::iterator::end(re))
1329  return re;
1330 
1331  char32_t firstchar = *ch;
1332  if (is_cjk_char(firstchar)) {
1333  re = unicode_cast<std::string>(firstchar);
1334  }
1335  return re;
1336 }
1337 
1339 {
1342  if (game_cfg != nullptr) {
1343  const config *help_config = &game_cfg->child_or_empty("help");
1344  try {
1345  default_toplevel = parse_config(help_config);
1346  // Create a config object that contains everything that is
1347  // not referenced from the toplevel element. Read this
1348  // config and save these sections and topics so that they
1349  // can be referenced later on when showing help about
1350  // specified things, but that should not be shown when
1351  // opening the help browser in the default manner.
1352  config hidden_toplevel;
1353  std::stringstream ss;
1354  for (const config &section : help_config->child_range("section"))
1355  {
1356  const std::string id = section["id"];
1357  if (find_section(default_toplevel, id) == nullptr) {
1358  // This section does not exist referenced from the
1359  // toplevel. Hence, add it to the hidden ones if it
1360  // is not referenced from another section.
1361  if (!section_is_referenced(id, *help_config)) {
1362  if (!ss.str().empty()) {
1363  ss << ",";
1364  }
1365  ss << id;
1366  }
1367  }
1368  }
1369  hidden_toplevel["sections"] = ss.str();
1370  ss.str("");
1371  for (const config &topic : help_config->child_range("topic"))
1372  {
1373  const std::string id = topic["id"];
1374  if (find_topic(default_toplevel, id) == nullptr) {
1375  if (!topic_is_referenced(id, *help_config)) {
1376  if (!ss.str().empty()) {
1377  ss << ",";
1378  }
1379  ss << id;
1380  }
1381  }
1382  }
1383  hidden_toplevel["topics"] = ss.str();
1384  config hidden_cfg = *help_config;
1385  // Change the toplevel to our new, custom built one.
1386  hidden_cfg.clear_children("toplevel");
1387  hidden_cfg.add_child("toplevel", std::move(hidden_toplevel));
1388  hidden_sections = parse_config(&hidden_cfg);
1389  }
1390  catch (parse_error& e) {
1391  std::stringstream msg;
1392  msg << "Parse error when parsing help text: '" << e.message << "'";
1393  PLAIN_LOG << msg.str();
1394  }
1395  }
1396 }
1397 
1398 // id starting with '.' are hidden
1399 std::string hidden_symbol(bool hidden) {
1400  return (hidden ? "." : "");
1401 }
1402 
1403 bool is_visible_id(const std::string &id) {
1404  return (id.empty() || id[0] != '.');
1405 }
1406 
1407 /**
1408  * Return true if the id is valid for user defined topics and
1409  * sections. Some IDs are special, such as toplevel and may not be
1410  * be defined in the config.
1411  */
1412 bool is_valid_id(const std::string &id) {
1413  if (id == "toplevel") {
1414  return false;
1415  }
1416  if (id.compare(0, unit_prefix.length(), unit_prefix) == 0 || id.compare(hidden_symbol().length(), unit_prefix.length(), unit_prefix) == 0) {
1417  return false;
1418  }
1419  if (id.compare(0, 8, "ability_") == 0) {
1420  return false;
1421  }
1422  if (id.compare(0, 14, "weaponspecial_") == 0) {
1423  return false;
1424  }
1425  if (id == "hidden") {
1426  return false;
1427  }
1428  return true;
1429 }
1430 
1431 /** Load the appropriate terrain types data to use */
1432 std::shared_ptr<terrain_type_data> load_terrain_types_data()
1433 {
1434  if (game_config_manager::get()){
1436  } else {
1437  return {};
1438  }
1439 }
1440 
1441 
1442 } // end namespace help
int generic_combat_modifier(int lawful_bonus, unit_alignments::type alignment, bool is_fearless, int max_liminal_bonus)
Returns the amount that a unit's damage should be multiplied by due to a given lawful_bonus.
Definition: attack.cpp:1615
Various functions that implement attacks and attack calculations.
double t
Definition: astarsearch.cpp:63
double g
Definition: astarsearch.cpp:63
Variant for storing WML attributes.
bool empty() const
Tests for an attribute that either was never set or was set to "".
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
optional_config_impl< config > find_child(config_key_type key, const std::string &name, const std::string &value)
Returns the first child of tag key with a name attribute containing value.
Definition: config.cpp:780
void clear_children(T... keys)
Definition: config.hpp:602
child_itors child_range(config_key_type key)
Definition: config.cpp:268
std::string debug() const
Definition: config.cpp:1230
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:380
config & add_child(config_key_type key)
Definition: config.cpp:436
static game_config_manager * get()
const std::shared_ptr< terrain_type_data > & terrain_types() const
A class grating read only view to a vector of config objects, viewed as one config with all children ...
const config & child_or_empty(config_key_type key) const
optional_const_config find_child(config_key_type key, const std::string &name, const std::string &value) const
config_array_view child_range(config_key_type key) const
To be used as a function object to locate sections and topics with a specified ID.
Definition: help_impl.hpp:176
To be used as a function object when sorting section lists on the title.
Definition: help_impl.hpp:198
To be used as a function object when sorting topic lists on the title.
Definition: help_impl.hpp:188
The text displayed in a topic.
Definition: help_impl.hpp:85
std::shared_ptr< topic_generator > generator_
Definition: help_impl.hpp:87
topic_text & operator=(topic_text &&t)=default
const config & parsed_text() const
Definition: help_impl.cpp:365
config parsed_text_
Definition: help_impl.hpp:86
std::set< std::string > & encountered_units()
static prefs & get()
bool hide_help() const
For instances created from a [terrain_type] tag, the value in the tag (with default false).
Definition: terrain.hpp:61
bool is_nonnull() const
True if this object represents some sentinel values.
Definition: terrain.hpp:129
const std::string & id() const
Definition: terrain.hpp:52
const t_string & editor_name() const
Definition: terrain.hpp:49
int get_max_liminal_bonus() const
const std::vector< time_of_day > & times() const
static iterator_base end(const string_type &str)
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition: types.cpp:1224
const race_map & races() const
Definition: types.hpp:403
const unit_race * find_race(const std::string &) const
Definition: types.cpp:1328
const unit_type_map & types() const
Definition: types.hpp:393
config_array_view traits() const
Definition: types.hpp:405
A single unit type that the player may recruit.
Definition: types.hpp:43
const std::string & id() const
The id for this unit_type.
Definition: types.hpp:144
@ HELP_INDEXED
Definition: types.hpp:77
bool hide_help() const
Definition: types.cpp:580
const t_string & variation_name() const
Definition: types.hpp:177
const config * cfg
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
#define DBG_HP
Definition: help_impl.cpp:57
#define WRN_HP
Definition: help_impl.cpp:56
static lg::log_domain log_help("help")
Standard logging facilities (interface).
#define PLAIN_LOG
Definition: log.hpp:296
const int SIZE_LARGE
Definition: constants.cpp:30
const int SIZE_PLUS
Definition: constants.cpp:29
const std::string unicode_bullet
Definition: constants.cpp:47
const int SIZE_NORMAL
Definition: constants.cpp:20
const bool & debug
Definition: game_config.cpp:95
std::string get_first_word(const std::string &s)
Return the first word in s, not removing any spaces in the start of it.
Definition: help_impl.cpp:1312
std::vector< topic > generate_unit_topics(const bool sort_generated, const std::string &race)
Definition: help_impl.cpp:1029
std::string hidden_symbol(bool hidden)
Definition: help_impl.cpp:1399
void generate_unit_sections(const config *, section &sec, int, const bool, const std::string &race)
Definition: help_impl.cpp:996
UNIT_DESCRIPTION_TYPE
Definition: help_impl.hpp:244
@ FULL_DESCRIPTION
Definition: help_impl.hpp:245
@ HIDDEN_BUT_SHOW_MACROS
Although the unit itself is hidden, traits reachable via this unit are not hidden.
Definition: help_impl.hpp:256
@ NO_DESCRIPTION
Ignore this unit for documentation purposes.
Definition: help_impl.hpp:247
void generate_races_sections(const config *help_cfg, section &sec, int level)
Definition: help_impl.cpp:816
bool topic_is_referenced(const std::string &topic_id, const config &cfg)
Return true if the topic with id topic_id is referenced from another section in the config,...
Definition: help_impl.cpp:165
std::vector< topic > generate_time_of_day_topics(const bool)
Definition: help_impl.cpp:380
int last_num_encountered_units
Definition: help_impl.cpp:67
void generate_terrain_sections(section &sec, int)
Definition: help_impl.cpp:937
std::string make_unit_link(const std::string &type_id)
return a hyperlink with the unit's name and pointing to the unit page return empty string if this uni...
Definition: help_impl.cpp:775
const std::string open_section_img
Definition: help_impl.cpp:80
const std::string unit_prefix
Definition: help_impl.cpp:84
const std::string variation_prefix
Definition: help_impl.cpp:89
void parse_config_internal(const config *help_cfg, const config *section_cfg, section &sec, int level)
Recursive function used by parse_config.
Definition: help_impl.cpp:183
bool is_visible_id(const std::string &id)
Definition: help_impl.cpp:1403
const std::string closed_section_img
Definition: help_impl.cpp:79
std::vector< topic > generate_faction_topics(const config &era, const bool sort_generated)
Definition: help_impl.cpp:618
const int box_width
Definition: help_impl.cpp:75
void generate_sections(const config *help_cfg, const std::string &generator, section &sec, int level)
Dispatch generators to their appropriate functions.
Definition: help_impl.cpp:322
const std::string topic_img
Definition: help_impl.cpp:78
UNIT_DESCRIPTION_TYPE description_type(const unit_type &type)
Return the type of description that should be shown for a unit of the given kind.
Definition: help_impl.cpp:1160
const std::string race_prefix
Definition: help_impl.cpp:86
const std::string ability_prefix
Definition: help_impl.cpp:90
std::vector< std::string > make_unit_links_list(const std::vector< std::string > &type_id_list, bool ordered)
return a list of hyperlinks to unit's pages (ordered or not)
Definition: help_impl.cpp:801
std::vector< topic > generate_topics(const bool sort_generated, const std::string &generator)
Definition: help_impl.cpp:293
const section * find_section(const section &sec, const std::string &id)
Search for the section with the specified identifier in the section and its subsections.
Definition: help_impl.cpp:1283
const int title2_size
Definition: help_impl.cpp:74
static std::string time_of_day_bonus_colored(const int time_of_day_bonus)
Definition: help_impl.cpp:375
const int normal_font_size
Definition: help_impl.cpp:76
void generate_contents()
Generate the help contents from the configurations given to the manager.
Definition: help_impl.cpp:1338
const std::string terrain_prefix
Definition: help_impl.cpp:85
std::vector< topic > generate_weapon_special_topics(const bool sort_generated)
Definition: help_impl.cpp:430
std::vector< topic > generate_ability_topics(const bool sort_generated)
Definition: help_impl.cpp:520
std::string remove_first_space(const std::string &text)
Definition: help_impl.cpp:1304
const std::string unknown_unit_topic
Definition: help_impl.cpp:83
const int max_section_level
Definition: help_impl.cpp:72
const unsigned max_history
Definition: help_impl.cpp:77
std::vector< std::string > empty_string_vector
Definition: help_impl.cpp:71
bool section_is_referenced(const std::string &section_id, const config &cfg)
Return true if the section with id section_id is referenced from another section in the config,...
Definition: help_impl.cpp:147
std::shared_ptr< terrain_type_data > load_terrain_types_data()
Load the appropriate terrain types data to use.
Definition: help_impl.cpp:1432
bool is_valid_id(const std::string &id)
Return true if the id is valid for user defined topics and sections.
Definition: help_impl.cpp:1412
boost::tribool last_debug_state
Definition: help_impl.cpp:69
std::string generate_contents_links(const std::string &section_name, config const *help_cfg)
Definition: help_impl.cpp:1180
std::string generate_topic_text(const std::string &generator, const config *help_cfg, const section &sec)
Definition: help_impl.cpp:341
help::section default_toplevel
Definition: help_impl.cpp:63
const topic * find_topic(const section &sec, const std::string &id)
Search for the topic with the specified identifier in the section and its subsections.
Definition: help_impl.cpp:1267
section parse_config(const config *cfg)
Parse a help config, return the top level section.
Definition: help_impl.cpp:283
std::vector< topic > generate_era_topics(const bool sort_generated, const std::string &era_id)
Definition: help_impl.cpp:584
const game_config_view * game_cfg
Definition: help_impl.cpp:61
int last_num_encountered_terrains
Definition: help_impl.cpp:68
help::section hidden_sections
Definition: help_impl.cpp:65
std::vector< topic > generate_trait_topics(const bool sort_generated)
Definition: help_impl.cpp:693
const std::string default_show_topic
Definition: help_impl.cpp:82
void generate_era_sections(const config *help_cfg, section &sec, int level)
Definition: help_impl.cpp:914
static bool is_cjk_char(const char32_t ch)
Definition: help_impl.cpp:92
const std::string era_prefix
Definition: help_impl.cpp:88
const int title_size
Definition: help_impl.cpp:73
const std::string faction_prefix
Definition: help_impl.cpp:87
bool is_scope_active(scope s)
Functions to load and save images from/to disk.
logger & info()
Definition: log.cpp:351
std::string img(const std::string &src, const std::string &align, bool floating)
Generates a Help markup tag corresponding to an image.
Definition: markup.cpp:31
std::string make_link(const std::string &text, const std::string &dst)
Generates a Help markup tag corresponding to a reference or link.
Definition: markup.cpp:25
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
std::string tag(std::string_view tag, Args &&... data)
Wraps the given data in the specified tag.
Definition: markup.hpp:45
config parse_text(const std::string &text)
Parse a xml style marked up text string.
Definition: markup.cpp:403
rng * generator
This generator is automatically synced during synced context.
Definition: random.cpp:60
::tod_manager * tod_manager
Definition: resources.cpp:29
std::vector< terrain_code > ter_list
Definition: translation.hpp:77
int compare(const std::string &s1, const std::string &s2)
Case-sensitive lexicographical comparison.
Definition: gettext.cpp:502
@ STRIP_SPACES
REMOVE_EMPTY: remove empty elements.
std::vector< std::string > quoted_split(const std::string &val, char c, int flags, char quote)
This function is identical to split(), except it does not split when it otherwise would if the previo...
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:87
std::map< std::string, t_string > string_map
std::vector< std::string > split(const config_attribute_value &val)
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition: general.hpp:141
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
Thrown when the help system fails to parse something.
Definition: help_impl.hpp:227
A section contains topics and sections along with title and ID.
Definition: help_impl.hpp:148
section_list sections
Definition: help_impl.hpp:168
bool operator<(const section &) const
Comparison on the ID.
Definition: help_impl.cpp:1251
void add_section(const section &s)
Allocate memory for and add the section.
Definition: help_impl.cpp:1256
std::string id
Definition: help_impl.hpp:166
std::string title
Definition: help_impl.hpp:166
bool operator==(const section &) const
Two sections are equal if their IDs are equal.
Definition: help_impl.cpp:1246
topic_list topics
Definition: help_impl.hpp:167
A topic contains a title, an id and some text.
Definition: help_impl.hpp:115
bool operator==(const topic &) const
Two topics are equal if their IDs are equal.
Definition: help_impl.cpp:1236
std::string id
Definition: help_impl.hpp:139
topic_text text
Definition: help_impl.hpp:140
bool operator<(const topic &) const
Comparison on the ID.
Definition: help_impl.cpp:1241
std::string title
Definition: help_impl.hpp:139
A terrain string which is converted to a terrain is a string with 1 or 2 layers the layers are separa...
Definition: translation.hpp:49
Object which defines a time of day with associated bonuses, image, sounds etc.
Definition: time_of_day.hpp:57
static map_location::direction s
unit_type_data unit_types
Definition: types.cpp:1463
#define e
#define f