The Battle for Wesnoth  1.19.17+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"].t_str()].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"].t_str()].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"].str() + "_" + 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  std::vector<section> sorted_sections;
992  for (const auto& pair : base_map) {
993  sorted_sections.push_back(pair.second);
994  }
995 
996  std::sort(sorted_sections.begin(), sorted_sections.end(), section_less());
997 
998  for (const section& s : sorted_sections) {
999  sec.add_section(s);
1000  }
1001 }
1002 
1003 void generate_unit_sections(const config* /*help_cfg*/, section& sec, int /*level*/, const bool /*sort_generated*/, const std::string& race)
1004 {
1005  for (const unit_type_data::unit_type_map::value_type &i : unit_types.types()) {
1006  const unit_type &type = i.second;
1007 
1008  if (type.race_id() != race)
1009  continue;
1010 
1011  if (!type.show_variations_in_help())
1012  continue;
1013 
1014  section base_unit;
1015  for (const std::string &variation_id : type.variations()) {
1016  // TODO: Do we apply encountered stuff to variations?
1017  const unit_type &var_type = type.get_variation(variation_id);
1018  const std::string topic_name = var_type.variation_name();
1019  const std::string var_ref = hidden_symbol(var_type.hide_help()) + variation_prefix + var_type.id() + "_" + variation_id;
1020 
1021  topic var_topic(topic_name, var_ref, "");
1022  var_topic.text = std::make_shared<unit_topic_generator>(var_type, variation_id);
1023  base_unit.topics.push_back(var_topic);
1024  }
1025 
1026  const std::string type_name = type.type_name();
1027  const std::string ref_id = hidden_symbol(type.hide_help()) + unit_prefix + type.id();
1028 
1029  base_unit.id = ref_id;
1030  base_unit.title = type_name;
1031 
1032  sec.add_section(base_unit);
1033  }
1034 }
1035 
1036 std::vector<topic> generate_unit_topics(const bool sort_generated, const std::string& race)
1037 {
1038  std::vector<topic> topics;
1039  std::set<std::string, string_less> race_units;
1040  std::set<std::string, string_less> race_topics;
1041  std::set<std::string> alignments;
1042 
1043  for (const unit_type_data::unit_type_map::value_type &i : unit_types.types())
1044  {
1045  const unit_type &type = i.second;
1046 
1047  if (type.race_id() != race)
1048  continue;
1049 
1051  if (desc_type != FULL_DESCRIPTION)
1052  continue;
1053 
1054  const std::string debug_suffix = (game_config::debug ? " (" + type.id() + ")" : "");
1055  const std::string type_name = type.type_name() + (type.id() == type.type_name().str() ? "" : debug_suffix);
1056  const std::string real_prefix = type.show_variations_in_help() ? ".." : "";
1057  const std::string ref_id = hidden_symbol(type.hide_help()) + real_prefix + unit_prefix + type.id();
1058  topic unit_topic(type_name, ref_id, "");
1059  unit_topic.text = std::make_shared<unit_topic_generator>(type);
1060  topics.push_back(unit_topic);
1061 
1062  if (!type.hide_help()) {
1063  // we also record an hyperlink of this unit
1064  // in the list used for the race topic
1065  std::string link = markup::make_link(type_name, ref_id);
1066  race_units.insert(link);
1067 
1068  alignments.insert(markup::make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
1069  }
1070  }
1071 
1072  //generate the hidden race description topic
1073  std::string race_id = "..race_"+race;
1074  std::string race_name;
1075  std::string race_description;
1076  std::string race_help_taxonomy;
1077  if (const unit_race *r = unit_types.find_race(race)) {
1078  race_name = r->plural_name();
1079  race_description = r->description();
1080  race_help_taxonomy = r->help_taxonomy();
1081  // if (description.empty()) description = _("No description Available");
1082  for (const config &additional_topic : r->additional_topics())
1083  {
1084  std::string id = additional_topic["id"];
1085  std::string title = additional_topic["title"];
1086  std::string text = additional_topic["text"];
1087  //topic additional_topic(title, id, text);
1088  topics.emplace_back(title,id,text);
1089  std::string link = markup::make_link(title, id);
1090  race_topics.insert(link);
1091  }
1092  } else {
1093  race_name = _ ("race^Miscellaneous");
1094  // description = _("Here put the description of the Miscellaneous race");
1095  }
1096 
1097  // Find any other races whose [race]help_taxonomy points to the current race
1098  std::map<std::string, t_string> subgroups;
1099  for (const auto &r : unit_types.races()) {
1100  if (r.second.help_taxonomy() == race) {
1101  if (!r.second.plural_name().empty())
1102  subgroups[r.first] = r.second.plural_name();
1103  else
1104  subgroups[r.first] = r.first;
1105  }
1106  }
1107 
1108  std::stringstream text;
1109 
1110  if (!race_description.empty()) {
1111  text << race_description << "\n\n";
1112  }
1113 
1114  if (!alignments.empty()) {
1115  std::set<std::string>::iterator it = alignments.begin();
1116  text << (alignments.size() > 1 ? _("Alignments: ") : _("Alignment: ")) << *(it++);
1117  while(it != alignments.end()) {
1118  text << ", " << *(it++);
1119  }
1120  text << "\n\n";
1121  }
1122 
1123  if (!race_help_taxonomy.empty()) {
1124  utils::string_map symbols;
1125  symbols["topic_id"] = "..race_"+race_help_taxonomy;
1126  if (const unit_race *r = unit_types.find_race(race_help_taxonomy)) {
1127  symbols["help_taxonomy"] = r->plural_name();
1128  } else {
1129  // Fall back to using showing the race id for the race that we couldn't find.
1130  // Not great, but probably useful if UMC has a broken link.
1131  symbols["help_taxonomy"] = race_help_taxonomy;
1132  }
1133  // TRANSLATORS: this is expected to say "[Dunefolk are] a group of units, all of whom are Humans",
1134  // or "[Quenoth Elves are] a group of units, all of whom are Elves".
1135  text << VGETTEXT("This is a group of units, all of whom are <ref dst='$topic_id'>$help_taxonomy</ref>.", symbols) << "\n\n";
1136  }
1137 
1138  if (!subgroups.empty()) {
1139  if (!race_help_taxonomy.empty()) {
1140  text << markup::tag("header", _("Subgroups of units within this group")) << "\n";
1141  } else {
1142  text << markup::tag("header", _("Groups of units within this race")) << "\n";
1143  }
1144  for (const auto &sg : subgroups) {
1145  text << font::unicode_bullet << " " << markup::make_link(sg.second, "..race_" + sg.first) << "\n";
1146  }
1147  text << "\n";
1148  }
1149 
1150  if (!race_help_taxonomy.empty()) {
1151  text << markup::tag("header", _("Units of this group")) << "\n";
1152  } else {
1153  text << markup::tag("header", _("Units of this race")) << "\n";
1154  }
1155  for (const auto &u : race_units) {
1156  text << font::unicode_bullet << " " << u << "\n";
1157  }
1158 
1159  topics.emplace_back(race_name, race_id, text.str());
1160 
1161  if (sort_generated)
1162  std::sort(topics.begin(), topics.end(), title_less());
1163 
1164  return topics;
1165 }
1166 
1168 {
1169  if (game_config::debug || prefs::get().show_all_units_in_help() ||
1171  return FULL_DESCRIPTION;
1172  }
1173 
1174  const std::set<std::string> &encountered_units = prefs::get().encountered_units();
1175  if (encountered_units.find(type.id()) != encountered_units.end()) {
1176  return FULL_DESCRIPTION;
1177  }
1178 
1179  // See the docs of HIDDEN_BUT_SHOW_MACROS
1180  if (type.id() == "Fog Clearer") {
1181  return HIDDEN_BUT_SHOW_MACROS;
1182  }
1183 
1184  return NO_DESCRIPTION;
1185 }
1186 
1187 std::string generate_contents_links(const std::string& section_name, config const *help_cfg)
1188 {
1189  auto section_cfg = help_cfg->find_child("section", "id", section_name);
1190  if (!section_cfg) {
1191  return std::string();
1192  }
1193 
1194  std::ostringstream res;
1195 
1196  std::vector<std::string> topics = utils::quoted_split(section_cfg["topics"]);
1197 
1198  // we use an intermediate structure to allow a conditional sorting
1199  typedef std::pair<std::string,std::string> link;
1200  std::vector<link> topics_links;
1201 
1202  // Find all topics in this section.
1203  for(const std::string& topic : topics) {
1204  if (auto topic_cfg = help_cfg->find_child("topic", "id", topic)) {
1205  std::string id = topic_cfg["id"];
1206  if (is_visible_id(id))
1207  topics_links.emplace_back(topic_cfg["title"], id);
1208  }
1209  }
1210 
1211  if (section_cfg["sort_topics"] == "yes") {
1212  std::sort(topics_links.begin(),topics_links.end());
1213  }
1214 
1215  for(const auto& [text, target] : topics_links) {
1216  std::string link = markup::make_link(text, target);
1217  res << font::unicode_bullet << " " << link << "\n";
1218  }
1219 
1220  return res.str();
1221 }
1222 
1223 std::string generate_contents_links(const section &sec)
1224 {
1225  std::stringstream res;
1226 
1227  for (auto &s : sec.sections) {
1228  if (is_visible_id(s.id)) {
1229  std::string link = markup::make_link(s.title, ".."+s.id);
1230  res << font::unicode_bullet << " " << link << "\n";
1231  }
1232  }
1233 
1234  for(const topic& t : sec.topics) {
1235  if (is_visible_id(t.id)) {
1236  std::string link = markup::make_link(t.title, t.id);
1237  res << font::unicode_bullet << " " << link << "\n";
1238  }
1239  }
1240  return res.str();
1241 }
1242 
1243 bool topic::operator==(const topic &t) const
1244 {
1245  return t.id == id;
1246 }
1247 
1248 bool topic::operator<(const topic &t) const
1249 {
1250  return id < t.id;
1251 }
1252 
1253 bool section::operator==(const section &sec) const
1254 {
1255  return sec.id == id;
1256 }
1257 
1258 bool section::operator<(const section &sec) const
1259 {
1260  return id < sec.id;
1261 }
1262 
1264 {
1265  sections.emplace_back(s);
1266 }
1267 
1269 {
1270  topics.clear();
1271  sections.clear();
1272 }
1273 
1274 const topic *find_topic(const section &sec, const std::string &id)
1275 {
1276  topic_list::const_iterator tit =
1277  std::find_if(sec.topics.begin(), sec.topics.end(), has_id(id));
1278  if (tit != sec.topics.end()) {
1279  return &(*tit);
1280  }
1281  for (const auto &s : sec.sections) {
1282  const auto t = find_topic(s, id);
1283  if (t != nullptr) {
1284  return t;
1285  }
1286  }
1287  return nullptr;
1288 }
1289 
1290 const section *find_section(const section &sec, const std::string &id)
1291 {
1292  const auto sit =
1293  std::find_if(sec.sections.begin(), sec.sections.end(), has_id(id));
1294  if (sit != sec.sections.end()) {
1295  return &*sit;
1296  }
1297  for (const auto &subsection : sec.sections) {
1298  const auto s = find_section(subsection, id);
1299  if (s != nullptr) {
1300  return s;
1301  }
1302  }
1303  return nullptr;
1304 }
1305 
1306 section *find_section(section &sec, const std::string &id)
1307 {
1308  return const_cast<section *>(find_section(const_cast<const section &>(sec), id));
1309 }
1310 
1311 std::string remove_first_space(const std::string& text)
1312 {
1313  if (text.length() > 0 && text[0] == ' ') {
1314  return text.substr(1);
1315  }
1316  return text;
1317 }
1318 
1319 std::string get_first_word(const std::string &s)
1320 {
1321  std::size_t first_word_start = s.find_first_not_of(' ');
1322  if (first_word_start == std::string::npos) {
1323  return s;
1324  }
1325  std::size_t first_word_end = s.find_first_of(" \n", first_word_start);
1326  if( first_word_end == first_word_start ) {
1327  // This word is '\n'.
1328  first_word_end = first_word_start+1;
1329  }
1330 
1331  //if no gap(' ' or '\n') found, test if it is CJK character
1332  std::string re = s.substr(0, first_word_end);
1333 
1334  utf8::iterator ch(re);
1335  if (ch == utf8::iterator::end(re))
1336  return re;
1337 
1338  char32_t firstchar = *ch;
1339  if (is_cjk_char(firstchar)) {
1340  re = unicode_cast<std::string>(firstchar);
1341  }
1342  return re;
1343 }
1344 
1346 {
1349  if (game_cfg != nullptr) {
1350  const config *help_config = &game_cfg->child_or_empty("help");
1351  try {
1352  default_toplevel = parse_config(help_config);
1353  // Create a config object that contains everything that is
1354  // not referenced from the toplevel element. Read this
1355  // config and save these sections and topics so that they
1356  // can be referenced later on when showing help about
1357  // specified things, but that should not be shown when
1358  // opening the help browser in the default manner.
1359  config hidden_toplevel;
1360  std::stringstream ss;
1361  for (const config &section : help_config->child_range("section"))
1362  {
1363  const std::string id = section["id"];
1364  if (find_section(default_toplevel, id) == nullptr) {
1365  // This section does not exist referenced from the
1366  // toplevel. Hence, add it to the hidden ones if it
1367  // is not referenced from another section.
1368  if (!section_is_referenced(id, *help_config)) {
1369  if (!ss.str().empty()) {
1370  ss << ",";
1371  }
1372  ss << id;
1373  }
1374  }
1375  }
1376  hidden_toplevel["sections"] = ss.str();
1377  ss.str("");
1378  for (const config &topic : help_config->child_range("topic"))
1379  {
1380  const std::string id = topic["id"];
1381  if (find_topic(default_toplevel, id) == nullptr) {
1382  if (!topic_is_referenced(id, *help_config)) {
1383  if (!ss.str().empty()) {
1384  ss << ",";
1385  }
1386  ss << id;
1387  }
1388  }
1389  }
1390  hidden_toplevel["topics"] = ss.str();
1391  config hidden_cfg = *help_config;
1392  // Change the toplevel to our new, custom built one.
1393  hidden_cfg.clear_children("toplevel");
1394  hidden_cfg.add_child("toplevel", std::move(hidden_toplevel));
1395  hidden_sections = parse_config(&hidden_cfg);
1396  }
1397  catch (parse_error& e) {
1398  std::stringstream msg;
1399  msg << "Parse error when parsing help text: '" << e.message << "'";
1400  PLAIN_LOG << msg.str();
1401  }
1402  }
1403 }
1404 
1405 // id starting with '.' are hidden
1406 std::string hidden_symbol(bool hidden) {
1407  return (hidden ? "." : "");
1408 }
1409 
1410 bool is_visible_id(const std::string &id) {
1411  return (id.empty() || id[0] != '.');
1412 }
1413 
1414 /**
1415  * Return true if the id is valid for user defined topics and
1416  * sections. Some IDs are special, such as toplevel and may not be
1417  * be defined in the config.
1418  */
1419 bool is_valid_id(const std::string &id) {
1420  if (id == "toplevel") {
1421  return false;
1422  }
1423  if (id.compare(0, unit_prefix.length(), unit_prefix) == 0 || id.compare(hidden_symbol().length(), unit_prefix.length(), unit_prefix) == 0) {
1424  return false;
1425  }
1426  if (id.compare(0, 8, "ability_") == 0) {
1427  return false;
1428  }
1429  if (id.compare(0, 14, "weaponspecial_") == 0) {
1430  return false;
1431  }
1432  if (id == "hidden") {
1433  return false;
1434  }
1435  return true;
1436 }
1437 
1438 /** Load the appropriate terrain types data to use */
1439 std::shared_ptr<terrain_type_data> load_terrain_types_data()
1440 {
1441  if (game_config_manager::get()){
1443  } else {
1444  return {};
1445  }
1446 }
1447 
1448 
1449 } // 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:157
config & add_child(std::string_view key)
Definition: config.cpp:435
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:379
child_itors child_range(std::string_view key)
Definition: config.cpp:267
void clear_children(T... keys)
Definition: config.hpp:601
optional_config_impl< config > find_child(std::string_view 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:763
std::string debug() const
Definition: config.cpp:1213
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(std::string_view key) const
config_array_view child_range(std::string_view key) const
optional_const_config find_child(std::string_view key, const std::string &name, const std::string &value) 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:1319
std::vector< topic > generate_unit_topics(const bool sort_generated, const std::string &race)
Definition: help_impl.cpp:1036
std::string hidden_symbol(bool hidden)
Definition: help_impl.cpp:1406
void generate_unit_sections(const config *, section &sec, int, const bool, const std::string &race)
Definition: help_impl.cpp:1003
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:1410
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:1167
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:1290
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:1345
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:1311
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:1439
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:1419
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:1187
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:1274
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:36
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:30
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:443
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:1258
void add_section(const section &s)
Allocate memory for and add the section.
Definition: help_impl.cpp:1263
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:1253
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:1243
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:1248
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