The Battle for Wesnoth  1.19.13+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  const std::vector<std::string> toplevel_refs
152  = utils::quoted_split(toplevel["sections"]);
153  if (std::find(toplevel_refs.begin(), toplevel_refs.end(), section_id)
154  != toplevel_refs.end()) {
155  return true;
156  }
157  }
158 
159  for (const config &section : cfg.child_range("section"))
160  {
161  const std::vector<std::string> sections_refd
162  = utils::quoted_split(section["sections"]);
163  if (std::find(sections_refd.begin(), sections_refd.end(), section_id)
164  != sections_refd.end()) {
165  return true;
166  }
167  }
168  return false;
169 }
170 
171 bool topic_is_referenced(const std::string &topic_id, const config &cfg)
172 {
173  if (auto toplevel = cfg.optional_child("toplevel"))
174  {
175  const std::vector<std::string> toplevel_refs
176  = utils::quoted_split(toplevel["topics"]);
177  if (std::find(toplevel_refs.begin(), toplevel_refs.end(), topic_id)
178  != toplevel_refs.end()) {
179  return true;
180  }
181  }
182 
183  for (const config &section : cfg.child_range("section"))
184  {
185  const std::vector<std::string> topics_refd
186  = utils::quoted_split(section["topics"]);
187  if (std::find(topics_refd.begin(), topics_refd.end(), topic_id)
188  != topics_refd.end()) {
189  return true;
190  }
191  }
192  return false;
193 }
194 
195 void parse_config_internal(const config *help_cfg, const config *section_cfg,
196  section &sec, int level)
197 {
198  if (level > max_section_level) {
199  PLAIN_LOG << "Maximum section depth has been reached. Maybe circular dependency?";
200  }
201  else if (section_cfg != nullptr) {
202  const std::vector<std::string> sections = utils::quoted_split((*section_cfg)["sections"]);
203  std::string id = level == 0 ? "toplevel" : (*section_cfg)["id"].str();
204  if (level != 0) {
205  if (!is_valid_id(id)) {
206  std::stringstream ss;
207  ss << "Invalid ID, used for internal purpose: '" << id << "'";
208  throw parse_error(ss.str());
209  }
210  }
211  std::string title = level == 0 ? "" : (*section_cfg)["title"].str();
212  sec.id = id;
213  sec.title = title;
214  // Find all child sections.
215  for(const std::string& sec_id : sections) {
216  if (auto child_cfg = help_cfg->find_child("section", "id", sec_id))
217  {
218  section child_section;
219  parse_config_internal(help_cfg, child_cfg.ptr(), child_section, level + 1);
220  sec.add_section(child_section);
221  }
222  else {
223  std::stringstream ss;
224  ss << "Help-section '" << sec_id << "' referenced from '"
225  << id << "' but could not be found.";
226  throw parse_error(ss.str());
227  }
228  }
229 
230  generate_sections(help_cfg, (*section_cfg)["sections_generator"], sec, level);
231  if ((*section_cfg)["sort_sections"] == "yes") {
232  sec.sections.sort(section_less());
233  }
234 
235  bool sort_topics = false;
236  bool sort_generated = true;
237 
238  if ((*section_cfg)["sort_topics"] == "yes") {
239  sort_topics = true;
240  sort_generated = false;
241  } else if ((*section_cfg)["sort_topics"] == "no") {
242  sort_topics = false;
243  sort_generated = false;
244  } else if ((*section_cfg)["sort_topics"] == "generated") {
245  sort_topics = false;
246  sort_generated = true;
247  } else if (!(*section_cfg)["sort_topics"].empty()) {
248  std::stringstream ss;
249  ss << "Invalid sort option: '" << (*section_cfg)["sort_topics"] << "'";
250  throw parse_error(ss.str());
251  }
252 
253  std::vector<topic> generated_topics = generate_topics(sort_generated,(*section_cfg)["generator"]);
254  std::vector<topic> topics;
255 
256  // Find all topics in this section.
257  for(const std::string& topic_id : utils::quoted_split((*section_cfg)["topics"])) {
258  if (auto topic_cfg = help_cfg->find_child("topic", "id", topic_id))
259  {
260  std::string text = topic_cfg["text"];
261  text += generate_topic_text(topic_cfg["generator"], help_cfg, sec);
262  topic child_topic(topic_cfg["title"], topic_cfg["id"], text);
263  if (!is_valid_id(child_topic.id)) {
264  std::stringstream ss;
265  ss << "Invalid ID, used for internal purpose: '" << id << "'";
266  throw parse_error(ss.str());
267  }
268  topics.push_back(child_topic);
269  }
270  else {
271  std::stringstream ss;
272  ss << "Help-topic '" << topic_id << "' referenced from '" << id
273  << "' but could not be found." << std::endl;
274  throw parse_error(ss.str());
275  }
276  }
277 
278  if (sort_topics) {
279  std::sort(topics.begin(),topics.end(), title_less());
280  std::sort(generated_topics.begin(),
281  generated_topics.end(), title_less());
282  std::merge(generated_topics.begin(),
283  generated_topics.end(),topics.begin(),topics.end()
284  ,std::back_inserter(sec.topics),title_less());
285  }
286  else {
287  sec.topics.insert(sec.topics.end(),
288  topics.begin(), topics.end());
289  sec.topics.insert(sec.topics.end(),
290  generated_topics.begin(), generated_topics.end());
291  }
292  }
293 }
294 
296 {
297  section sec;
298  if (cfg != nullptr) {
299  auto toplevel_cfg = cfg->optional_child("toplevel");
300  parse_config_internal(cfg, toplevel_cfg.ptr(), sec);
301  }
302  return sec;
303 }
304 
305 std::vector<topic> generate_topics(const bool sort_generated,const std::string &generator)
306 {
307  std::vector<topic> res;
308  if (generator.empty()) {
309  return res;
310  }
311 
312  if (generator == "abilities") {
313  res = generate_ability_topics(sort_generated);
314  } else if (generator == "weapon_specials") {
315  res = generate_weapon_special_topics(sort_generated);
316  } else if (generator == "time_of_days") {
317  res = generate_time_of_day_topics(sort_generated);
318  } else if (generator == "traits") {
319  res = generate_trait_topics(sort_generated);
320  } else {
321  std::vector<std::string> parts = utils::split(generator, ':', utils::STRIP_SPACES);
322  if (parts.size() > 1 && parts[0] == "units") {
323  res = generate_unit_topics(sort_generated, parts[1]);
324  } else if (parts[0] == "era" && parts.size()>1) {
325  res = generate_era_topics(sort_generated, parts[1]);
326  } else {
327  WRN_HP << "Found a topic generator that I didn't recognize: " << generator;
328  }
329  }
330 
331  return res;
332 }
333 
334 void generate_sections(const config *help_cfg, const std::string &generator, section &sec, int level)
335 {
336  if (generator == "races") {
337  generate_races_sections(help_cfg, sec, level);
338  } else if (generator == "terrains") {
340  } else if (generator == "eras") {
341  DBG_HP << "Generating eras...";
342  generate_era_sections(help_cfg, sec, level);
343  } else {
344  std::vector<std::string> parts = utils::split(generator, ':', utils::STRIP_SPACES);
345  if (parts.size() > 1 && parts[0] == "units") {
346  generate_unit_sections(help_cfg, sec, level, true, parts[1]);
347  } else if (generator.size() > 0) {
348  WRN_HP << "Found a section generator that I didn't recognize: " << generator;
349  }
350  }
351 }
352 
353 std::string generate_topic_text(const std::string &generator, const config *help_cfg, const section &sec)
354 {
355  std::string empty_string = "";
356  if (generator.empty()) {
357  return empty_string;
358  } else {
359  std::vector<std::string> parts = utils::split(generator, ':');
360  if (parts.size() > 1 && parts[0] == "contents") {
361  if (parts[1] == "generated") {
362  return generate_contents_links(sec);
363  } else {
364  return generate_contents_links(parts[1], help_cfg);
365  }
366  }
367  }
368  return empty_string;
369 }
370 
371 topic_text& topic_text::operator=(std::shared_ptr<topic_generator> g)
372 {
373  generator_ = std::move(g);
374  return *this;
375 }
376 
378 {
379  if (generator_) {
381  // This caches the result, so doesn't need the generator any more
382  generator_.reset();
383  }
384  return parsed_text_;
385 }
386 
387 static std::string time_of_day_bonus_colored(const int time_of_day_bonus)
388 {
389  return markup::span_color((time_of_day_bonus > 0 ? "green" : (time_of_day_bonus < 0 ? "red" : "white")), time_of_day_bonus);
390 }
391 
392 std::vector<topic> generate_time_of_day_topics(const bool /*sort_generated*/)
393 {
394  std::vector<topic> topics;
395  std::stringstream toplevel;
396 
397  if (!resources::tod_manager) {
398  toplevel << _("Only available during a scenario.");
399  topics.emplace_back(_("Time of Day Schedule"), "..schedule", toplevel.str());
400  return topics;
401  }
402 
403  const std::vector<time_of_day>& times = resources::tod_manager->times();
404  for (const time_of_day& time : times)
405  {
406  const std::string id = "time_of_day_" + time.id;
407  const std::string image = markup::img(time.image);
408  const std::string image_lawful = markup::img("icons/alignments/alignment_lawful_30.png");
409  const std::string image_neutral = markup::img("icons/alignments/alignment_neutral_30.png");
410  const std::string image_chaotic = markup::img("icons/alignments/alignment_chaotic_30.png");
411  const std::string image_liminal = markup::img("icons/alignments/alignment_liminal_30.png");
412  std::stringstream text, row_ss;
413 
414  const int lawful_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::lawful, false, resources::tod_manager->get_max_liminal_bonus());
415  const int neutral_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::neutral, false, resources::tod_manager->get_max_liminal_bonus());
416  const int chaotic_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::chaotic, false, resources::tod_manager->get_max_liminal_bonus());
417  const int liminal_bonus = generic_combat_modifier(time.lawful_bonus, unit_alignments::type::liminal, false, resources::tod_manager->get_max_liminal_bonus());
418 
419  row_ss << markup::tag("col", markup::make_link(time.name.str(), id))
420  << markup::tag("col", image)
421  << markup::tag("col", image_lawful, time_of_day_bonus_colored(lawful_bonus))
422  << markup::tag("col", image_neutral, time_of_day_bonus_colored(neutral_bonus))
423  << markup::tag("col", image_chaotic, time_of_day_bonus_colored(chaotic_bonus))
424  << markup::tag("col", image_liminal, time_of_day_bonus_colored(liminal_bonus));
425  toplevel << markup::tag("row", row_ss.str());
426 
427  text << image << '\n'
428  << time.description.str() << '\n'
429  << image_lawful << _("Lawful Bonus:") << ' ' << time_of_day_bonus_colored(lawful_bonus) << '\n'
430  << image_neutral << _("Neutral Bonus:") << ' ' << time_of_day_bonus_colored(neutral_bonus) << '\n'
431  << image_chaotic << _("Chaotic Bonus:") << ' ' << time_of_day_bonus_colored(chaotic_bonus) << '\n'
432  << image_liminal << _("Liminal Bonus:") << ' ' << time_of_day_bonus_colored(liminal_bonus) << '\n' << '\n'
433  << markup::make_link(_("Schedule"), "..schedule");
434 
435  topics.emplace_back(time.name.str(), id, text.str());
436  }
437 
438  topics.emplace_back(_("Time of Day Schedule"), "..schedule", markup::tag("table", toplevel.str()));
439  return topics;
440 }
441 
442 std::vector<topic> generate_weapon_special_topics(const bool sort_generated)
443 {
444  std::vector<topic> topics;
445 
446  std::map<t_string, std::string> special_description;
447  std::map<t_string, std::set<std::string, string_less>> special_units;
448 
449  for(const auto& [type_id, type] : unit_types.types()) {
450  // Only show the weapon special if we find it on a unit that
451  // detailed description should be shown about.
453  continue;
454 
455  for(const attack_type& atk : type.attacks()) {
456  for(const auto& [name, description] : atk.special_tooltips()) {
457  special_description.emplace(name, description);
458 
459  if (!type.hide_help()) {
460  //add a link in the list of units having this special
461  std::string type_name = type.type_name();
462  //check for variations (walking corpse/soulless etc)
463  const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
464  std::string ref_id = section_prefix + unit_prefix + type.id();
465  //we put the translated name at the beginning of the hyperlink,
466  //so the automatic alphabetic sorting of std::set can use it
467  std::string link = markup::make_link(type_name, ref_id);
468  special_units[name].insert(link);
469  }
470  }
471  }
472 
473  for(const config& adv : type.modification_advancements()) {
474  for(const config& effect : adv.child_range("effect")) {
475  if(effect["apply_to"] == "new_attack" && effect.has_child("specials")) {
476  for(const auto [key, special] : effect.mandatory_child("specials").all_children_view()) {
477  if(!special["name"].empty()) {
478  special_description.emplace(special["name"].t_str(), special["description"].t_str());
479  if(!type.hide_help()) {
480  //add a link in the list of units having this special
481  std::string type_name = type.type_name();
482  //check for variations (walking corpse/soulless etc)
483  const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
484  std::string ref_id = section_prefix + unit_prefix + type.id();
485  //we put the translated name at the beginning of the hyperlink,
486  //so the automatic alphabetic sorting of std::set can use it
487  std::string link = markup::make_link(type_name, ref_id);
488  special_units[special["name"]].insert(link);
489  }
490  }
491  }
492  } else if(effect["apply_to"] == "attack" && effect.has_child("set_specials")) {
493  for(const auto [key, special] : effect.mandatory_child("set_specials").all_children_view()) {
494  if(!special["name"].empty()) {
495  special_description.emplace(special["name"].t_str(), special["description"].t_str());
496  if(!type.hide_help()) {
497  //add a link in the list of units having this special
498  std::string type_name = type.type_name();
499  //check for variations (walking corpse/soulless etc)
500  const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
501  std::string ref_id = section_prefix + unit_prefix + type.id();
502  //we put the translated name at the beginning of the hyperlink,
503  //so the automatic alphabetic sorting of std::set can use it
504  std::string link = markup::make_link(type_name, ref_id);
505  special_units[special["name"]].insert(link);
506  }
507  }
508  }
509  }
510  }
511  }
512  }
513 
514  for(const auto& [name, description] : special_description) {
515  // use untranslated name to have universal topic id
516  std::string id = "weaponspecial_" + name.base_str();
517  std::stringstream text;
518  text << description;
519  text << "\n\n" << markup::tag("header", _("Units with this special attack")) << "\n";
520  for(const std::string& type_id : special_units[name]) {
521  text << font::unicode_bullet << " " << type_id << "\n";
522  }
523 
524  topics.emplace_back(name, id, text.str());
525  }
526 
527  if (sort_generated)
528  std::sort(topics.begin(), topics.end(), title_less());
529  return topics;
530 }
531 
532 std::vector<topic> generate_ability_topics(const bool sort_generated)
533 {
534  std::vector<topic> topics;
535 
536  std::map<std::string, const unit_type::ability_metadata*> ability_topic_data;
537  std::map<std::string, std::set<std::string, string_less>> ability_units;
538 
539  const auto parse = [&](const unit_type& type, const unit_type::ability_metadata& ability) {
540  // NOTE: neither ability names nor ability ids are necessarily unique. Creating
541  // topics for either each unique name or each unique id means certain abilities
542  // will be excluded from help. So... the ability topic ref id is a combination
543  // of id and (untranslated) name. It's rather ugly, but it works.
544  const std::string topic_ref = ability.id + ability.name.base_str();
545 
546  ability_topic_data.emplace(topic_ref, &ability);
547 
548  if(!type.hide_help()) {
549  // Add a link in the list of units with this ability
550  // We put the translated name at the beginning of the hyperlink,
551  // so the automatic alphabetic sorting of std::set can use it
552  const std::string link = markup::make_link(type.type_name(), unit_prefix + type.id());
553  ability_units[topic_ref].insert(link);
554  }
555  };
556 
557  // Look through all the unit types. If a unit of that type would have a full
558  // description, add its abilities to the potential topic list. We don't want
559  // to show abilities that the user has not encountered yet.
560  for(const auto& [type_id, type] : unit_types.types()) {
562  continue;
563  }
564 
565  for(const unit_type::ability_metadata& ability : type.abilities_metadata()) {
566  parse(type, ability);
567  }
568 
569  for(const unit_type::ability_metadata& ability : type.adv_abilities_metadata()) {
570  parse(type, ability);
571  }
572  }
573 
574  for(const auto& a : ability_topic_data) {
575  if (a.second->name.empty()) {
576  continue;
577  }
578  std::ostringstream text;
579  text << a.second->description;
580  text << "\n\n" << markup::tag("header", _("Units with this ability")) << "\n";
581 
582  for(const auto& u : ability_units[a.first]) { // first is the topic ref id
583  text << font::unicode_bullet << " " << u << "\n";
584  }
585 
586  topics.emplace_back(a.second->name, ability_prefix + a.first, text.str());
587  }
588 
589  if(sort_generated) {
590  std::sort(topics.begin(), topics.end(), title_less());
591  }
592 
593  return topics;
594 }
595 
596 std::vector<topic> generate_era_topics(const bool sort_generated, const std::string & era_id)
597 {
598  std::vector<topic> topics;
599 
600  auto era = game_cfg->find_child("era","id", era_id);
601  if(era && !era["hide_help"].to_bool()) {
602  topics = generate_faction_topics(*era, sort_generated);
603 
604  std::vector<std::string> faction_links;
605  for (const topic & t : topics) {
606  faction_links.push_back(markup::make_link(t.title, t.id));
607  }
608 
609  std::stringstream text;
610  const config::attribute_value& description = era["description"];
611  if (!description.empty()) {
612  text << description.t_str() << "\n";
613  text << "\n";
614  }
615 
616  text << markup::tag("header", _("Factions")) << "\n";
617 
618  std::sort(faction_links.begin(), faction_links.end());
619  for (const std::string &link : faction_links) {
620  text << font::unicode_bullet << " " << link << "\n";
621  }
622 
623  topic era_topic(era["name"], ".." + era_prefix + era["id"].str(), text.str());
624 
625  topics.push_back( era_topic );
626  }
627  return topics;
628 }
629 
630 std::vector<topic> generate_faction_topics(const config & era, const bool sort_generated)
631 {
632  std::vector<topic> topics;
633  for (const config &f : era.child_range("multiplayer_side")) {
634  const std::string& id = f["id"];
635  if (id == "Random")
636  continue;
637 
638  std::stringstream text;
639 
640  const config::attribute_value& description = f["description"];
641  if (!description.empty()) {
642  text << description.t_str() << "\n";
643  text << "\n";
644  }
645 
646  const std::vector<std::string> recruit_ids = utils::split(f["recruit"]);
647  std::set<std::string> races;
648  std::set<std::string> alignments;
649 
650  for (const std::string & u_id : recruit_ids) {
651  if (const unit_type * t = unit_types.find(u_id, unit_type::HELP_INDEXED)) {
652  assert(t);
653  const unit_type & type = *t;
654 
655  if (const unit_race *r = unit_types.find_race(type.race_id())) {
656  races.insert(markup::make_link(r->plural_name(), std::string("..") + race_prefix + r->id()));
657  }
658  alignments.insert(markup::make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
659  }
660  }
661 
662  if (!races.empty()) {
663  std::set<std::string>::iterator it = races.begin();
664  text << _("Races: ") << *(it++);
665  while(it != races.end()) {
666  text << ", " << *(it++);
667  }
668  text << "\n\n";
669  }
670 
671  if (!alignments.empty()) {
672  std::set<std::string>::iterator it = alignments.begin();
673  text << _("Alignments: ") << *(it++);
674  while(it != alignments.end()) {
675  text << ", " << *(it++);
676  }
677  text << "\n\n";
678  }
679 
680  text << markup::tag("header", _("Leaders")) << "\n";
681  const std::vector<std::string> leaders =
682  make_unit_links_list( utils::split(f["leader"]), true );
683  for (const std::string &link : leaders) {
684  text << font::unicode_bullet << " " << link << "\n";
685  }
686 
687  text << "\n";
688 
689  text << markup::tag("header", _("Recruits")) << "\n";
690  const std::vector<std::string> recruit_links =
691  make_unit_links_list( recruit_ids, true );
692  for (const std::string &link : recruit_links) {
693  text << font::unicode_bullet << " " << link << "\n";
694  }
695 
696  const std::string name = f["name"];
697  const std::string ref_id = faction_prefix + era["id"] + "_" + id;
698  topics.emplace_back(name, ref_id, text.str());
699  }
700  if (sort_generated)
701  std::sort(topics.begin(), topics.end(), title_less());
702  return topics;
703 }
704 
705 std::vector<topic> generate_trait_topics(const bool sort_generated)
706 {
707  // All traits that could be assigned to at least one discovered or HIDDEN_BUT_SHOW_MACROS unit.
708  // This is collected from the [units][trait], [race][traits], and [unit_type][traits] tags. If
709  // there are duplicates with the same id, it takes the first one encountered.
710  std::map<std::string, const config> trait_list;
711 
712  // The global traits that are direct children of a [units] tag
713  for (const config & trait : unit_types.traits()) {
714  trait_list.emplace(trait["id"], trait);
715  }
716 
717  // Search for discovered unit types
718  std::set<std::string> races;
719  for(const auto& i : unit_types.types()) {
720  const unit_type& type = i.second;
722 
723  // Remember which races have been discovered.
724  //
725  // For unit types, unit_type::possible_traits() usually includes racial traits; however it's
726  // possible that all discovered units of a race have ignore_race_traits=yes, and so we still
727  // need to loop over the [race] tags looking for more traits.
728  if(desc_type == FULL_DESCRIPTION) {
729  races.insert(type.race_id());
730  }
731 
732  // Handle [unit_type][trait]s.
733  //
734  // It would be better if we only looked at the traits that are specific to the unit_type,
735  // but that unmerged unit_type_data.traits() isn't available. We're forced to use
736  // possible_traits() instead which returns all of the traits, including the ones that units
737  // with ignore_race_traits=no have inherited from their [race] tag.
738  if (desc_type == FULL_DESCRIPTION || desc_type == HIDDEN_BUT_SHOW_MACROS) {
739  for (const config& trait : type.possible_traits()) {
740  trait_list.emplace(trait["id"], trait);
741  }
742  }
743  }
744 
745  // Race traits, even those that duplicate a global trait (which will be dropped by emplace()).
746  //
747  // For traits, assume we don't discover additional races via the [race]help_taxonomy= links. The
748  // traits themselves don't propagate down those links, so if the trait is interesting w.r.t. the
749  // discovered units then their own race will already include it.
750  for(const auto& race_id : races) {
751  if(const unit_race *r = unit_types.find_race(race_id)) {
752  for(const config & trait : r->additional_traits()) {
753  trait_list.emplace(trait["id"], trait);
754  }
755  }
756  }
757 
758  std::vector<topic> topics;
759  for(auto& a : trait_list) {
760  std::string id = "traits_" + a.first;
761  const config& trait = a.second;
762 
763  std::string name = trait["male_name"].str();
764  if (name.empty()) name = trait["female_name"].str();
765  if (name.empty()) name = trait["name"].str();
766  if (name.empty()) continue; // Hidden trait
767 
768  std::stringstream text;
769  if (!trait["help_text"].empty()) {
770  text << trait["help_text"];
771  } else if (!trait["description"].empty()) {
772  text << trait["description"];
773  } else {
774  text << _("No description available.");
775  }
776  text << "\n\n";
777 
778  topics.emplace_back(name, id, text.str());
779  }
780 
781  if (sort_generated)
782  std::sort(topics.begin(), topics.end(), title_less());
783  return topics;
784 }
785 
786 
787 std::string make_unit_link(const std::string& type_id)
788 {
789  std::string link;
790 
792  if (!type) {
793  PLAIN_LOG << "Unknown unit type : " << type_id;
794  // don't return an hyperlink (no page)
795  // instead show the id (as hint)
796  link = type_id;
797  } else if (!type->hide_help()) {
798  std::string name = type->type_name();
799  std::string ref_id;
801  const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
802  ref_id = section_prefix + unit_prefix + type->id();
803  } else {
804  ref_id = unknown_unit_topic;
805  name += " (?)";
806  }
807  link = markup::make_link(name, ref_id);
808  } // if hide_help then link is an empty string
809 
810  return link;
811 }
812 
813 std::vector<std::string> make_unit_links_list(const std::vector<std::string>& type_id_list, bool ordered)
814 {
815  std::vector<std::string> links_list;
816  for (const std::string &type_id : type_id_list) {
817  std::string unit_link = make_unit_link(type_id);
818  if (!unit_link.empty())
819  links_list.push_back(unit_link);
820  }
821 
822  if (ordered)
823  std::sort(links_list.begin(), links_list.end());
824 
825  return links_list;
826 }
827 
828 void generate_races_sections(const config* help_cfg, section& sec, int level)
829 {
830  std::set<std::string, string_less> races;
831  std::set<std::string, string_less> visible_races;
832 
833  // Calculate which races have been discovered, from the list of discovered unit types.
834  for(const auto& i : unit_types.types()) {
835  const unit_type& type = i.second;
837  if(desc_type == FULL_DESCRIPTION) {
838  races.insert(type.race_id());
839  if(!type.hide_help())
840  visible_races.insert(type.race_id());
841  }
842  }
843 
844  // Propagate visibility up the help_taxonomy tree.
845  std::set<std::string, string_less> last_sweep = visible_races;
846  while(!last_sweep.empty()) {
847  std::set<std::string, string_less> current_sweep;
848  for(const auto& race_id : last_sweep) {
849  if(const unit_race* r = unit_types.find_race(race_id)) {
850  const auto& help_taxonomy = r->help_taxonomy();
851  if(!help_taxonomy.empty() && !visible_races.count(help_taxonomy) && unit_types.find_race(help_taxonomy)) {
852  current_sweep.insert(help_taxonomy);
853  races.insert(help_taxonomy);
854  visible_races.insert(help_taxonomy);
855  }
856  }
857  }
858  last_sweep = std::move(current_sweep);
859  }
860 
861  struct taxonomy_queue_type
862  {
863  std::string parent_id;
864  section content;
865  };
866  std::vector<taxonomy_queue_type> taxonomy_queue;
867 
868  // Add all races without a [race]help_taxonomy= to the documentation section, and queue the others.
869  // This avoids a race condition dependency on the order that races are encountered in help_cfg.
870  for(const auto& race_id : races) {
871  section race_section;
872  config section_cfg;
873 
874  bool hidden = (visible_races.count(race_id) == 0);
875 
876  section_cfg["id"] = hidden_symbol(hidden) + race_prefix + race_id;
877 
878  std::string title;
879  std::string help_taxonomy;
880  if(const unit_race* r = unit_types.find_race(race_id)) {
881  title = r->plural_name();
882  help_taxonomy = r->help_taxonomy();
883  } else {
884  title = _("race^Miscellaneous");
885  // leave help_taxonomy empty
886  }
887  section_cfg["title"] = title;
888 
889  section_cfg["sections_generator"] = "units:" + race_id;
890  section_cfg["generator"] = "units:" + race_id;
891 
892  parse_config_internal(help_cfg, &section_cfg, race_section, level + 1);
893 
894  if(help_taxonomy.empty()) {
895  sec.add_section(race_section);
896  } else {
897  bool parent_hidden = (visible_races.count(help_taxonomy) == 0);
898  auto parent_id = hidden_symbol(parent_hidden) + race_prefix + help_taxonomy;
899  taxonomy_queue.push_back({std::move(parent_id), std::move(race_section)});
900  }
901  }
902 
903  // Each run through this loop handles one level of nesting of [race]help_taxonomy=
904  bool process_queue_again = true;
905  while(process_queue_again && !taxonomy_queue.empty()) {
906  process_queue_again = false;
907  auto to_process = std::exchange(taxonomy_queue, {});
908 
909  for(auto& x : to_process) {
910  auto parent = find_section(sec, x.parent_id);
911  if(parent) {
912  parent->add_section(x.content);
913  process_queue_again = true;
914  } else {
915  taxonomy_queue.push_back(std::move(x));
916  }
917  }
918  }
919 
920  // Fallback to adding the new race at the top level, as if it had help_taxonomy.empty().
921  for(auto& x : taxonomy_queue) {
922  sec.add_section(x.content);
923  }
924 }
925 
926 void generate_era_sections(const config* help_cfg, section & sec, int level)
927 {
928  for (const config & era : game_cfg->child_range("era")) {
929  if (era["hide_help"].to_bool()) {
930  continue;
931  }
932 
933  DBG_HP << "Adding help section: " << era["id"].str();
934 
935  section era_section;
936  config section_cfg;
937  section_cfg["id"] = era_prefix + era["id"].str();
938  section_cfg["title"] = era["name"];
939 
940  section_cfg["generator"] = "era:" + era["id"].str();
941 
942  DBG_HP << section_cfg.debug();
943 
944  parse_config_internal(help_cfg, &section_cfg, era_section, level+1);
945  sec.add_section(era_section);
946  }
947 }
948 
949 void generate_terrain_sections(section& sec, int /*level*/)
950 {
951  std::shared_ptr<terrain_type_data> tdata = load_terrain_types_data();
952 
953  if (!tdata) {
954  WRN_HP << "When building terrain help sections, couldn't acquire terrain types data, aborting.";
955  return;
956  }
957 
958  std::map<std::string, section> base_map;
959 
960  const t_translation::ter_list& t_listi = tdata->list();
961 
962  for (const t_translation::terrain_code& t : t_listi) {
963 
964  const terrain_type& info = tdata->get_terrain_info(t);
965 
966  bool hidden = info.hide_help();
967 
968  if (prefs::get().encountered_terrains().find(t)
969  == prefs::get().encountered_terrains().end() && !info.is_overlay())
970  hidden = true;
971 
972  topic terrain_topic;
973  terrain_topic.title = info.editor_name();
974  terrain_topic.id = hidden_symbol(hidden) + terrain_prefix + info.id();
975  terrain_topic.text = std::make_shared<terrain_topic_generator>(info);
976 
977  t_translation::ter_list base_terrains = tdata->underlying_union_terrain(t);
978  if (info.has_default_base()) {
979  for (const auto base : tdata->underlying_union_terrain(info.default_base())) {
980  if (!utils::contains(base_terrains, base)) {
981  base_terrains.emplace_back(base);
982  }
983  }
984  }
985  for (const t_translation::terrain_code& base : base_terrains) {
986 
987  const terrain_type& base_info = tdata->get_terrain_info(base);
988 
989  if (!base_info.is_nonnull() || base_info.hide_help())
990  continue;
991 
992  section& base_section = base_map[base_info.id()];
993 
994  base_section.id = terrain_prefix + base_info.id();
995  base_section.title = base_info.editor_name();
996 
997  if (base_info.id() == info.id())
998  terrain_topic.id = ".." + terrain_prefix + info.id();
999  base_section.topics.push_back(terrain_topic);
1000  }
1001  }
1002 
1003  for (const auto& base : base_map) {
1004  sec.add_section(base.second);
1005  }
1006 }
1007 
1008 void generate_unit_sections(const config* /*help_cfg*/, section& sec, int /*level*/, const bool /*sort_generated*/, const std::string& race)
1009 {
1010  for (const unit_type_data::unit_type_map::value_type &i : unit_types.types()) {
1011  const unit_type &type = i.second;
1012 
1013  if (type.race_id() != race)
1014  continue;
1015 
1016  if (!type.show_variations_in_help())
1017  continue;
1018 
1019  section base_unit;
1020  for (const std::string &variation_id : type.variations()) {
1021  // TODO: Do we apply encountered stuff to variations?
1022  const unit_type &var_type = type.get_variation(variation_id);
1023  const std::string topic_name = var_type.variation_name();
1024  const std::string var_ref = hidden_symbol(var_type.hide_help()) + variation_prefix + var_type.id() + "_" + variation_id;
1025 
1026  topic var_topic(topic_name, var_ref, "");
1027  var_topic.text = std::make_shared<unit_topic_generator>(var_type, variation_id);
1028  base_unit.topics.push_back(var_topic);
1029  }
1030 
1031  const std::string type_name = type.type_name();
1032  const std::string ref_id = hidden_symbol(type.hide_help()) + unit_prefix + type.id();
1033 
1034  base_unit.id = ref_id;
1035  base_unit.title = type_name;
1036 
1037  sec.add_section(base_unit);
1038  }
1039 }
1040 
1041 std::vector<topic> generate_unit_topics(const bool sort_generated, const std::string& race)
1042 {
1043  std::vector<topic> topics;
1044  std::set<std::string, string_less> race_units;
1045  std::set<std::string, string_less> race_topics;
1046  std::set<std::string> alignments;
1047 
1048  for (const unit_type_data::unit_type_map::value_type &i : unit_types.types())
1049  {
1050  const unit_type &type = i.second;
1051 
1052  if (type.race_id() != race)
1053  continue;
1054 
1056  if (desc_type != FULL_DESCRIPTION)
1057  continue;
1058 
1059  const std::string debug_suffix = (game_config::debug ? " (" + type.id() + ")" : "");
1060  const std::string type_name = type.type_name() + (type.id() == type.type_name().str() ? "" : debug_suffix);
1061  const std::string real_prefix = type.show_variations_in_help() ? ".." : "";
1062  const std::string ref_id = hidden_symbol(type.hide_help()) + real_prefix + unit_prefix + type.id();
1063  topic unit_topic(type_name, ref_id, "");
1064  unit_topic.text = std::make_shared<unit_topic_generator>(type);
1065  topics.push_back(unit_topic);
1066 
1067  if (!type.hide_help()) {
1068  // we also record an hyperlink of this unit
1069  // in the list used for the race topic
1070  std::string link = markup::make_link(type_name, ref_id);
1071  race_units.insert(link);
1072 
1073  alignments.insert(markup::make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
1074  }
1075  }
1076 
1077  //generate the hidden race description topic
1078  std::string race_id = "..race_"+race;
1079  std::string race_name;
1080  std::string race_description;
1081  std::string race_help_taxonomy;
1082  if (const unit_race *r = unit_types.find_race(race)) {
1083  race_name = r->plural_name();
1084  race_description = r->description();
1085  race_help_taxonomy = r->help_taxonomy();
1086  // if (description.empty()) description = _("No description Available");
1087  for (const config &additional_topic : r->additional_topics())
1088  {
1089  std::string id = additional_topic["id"];
1090  std::string title = additional_topic["title"];
1091  std::string text = additional_topic["text"];
1092  //topic additional_topic(title, id, text);
1093  topics.emplace_back(title,id,text);
1094  std::string link = markup::make_link(title, id);
1095  race_topics.insert(link);
1096  }
1097  } else {
1098  race_name = _ ("race^Miscellaneous");
1099  // description = _("Here put the description of the Miscellaneous race");
1100  }
1101 
1102  // Find any other races whose [race]help_taxonomy points to the current race
1103  std::map<std::string, t_string> subgroups;
1104  for (const auto &r : unit_types.races()) {
1105  if (r.second.help_taxonomy() == race) {
1106  if (!r.second.plural_name().empty())
1107  subgroups[r.first] = r.second.plural_name();
1108  else
1109  subgroups[r.first] = r.first;
1110  }
1111  }
1112 
1113  std::stringstream text;
1114 
1115  if (!race_description.empty()) {
1116  text << race_description << "\n\n";
1117  }
1118 
1119  if (!alignments.empty()) {
1120  std::set<std::string>::iterator it = alignments.begin();
1121  text << (alignments.size() > 1 ? _("Alignments: ") : _("Alignment: ")) << *(it++);
1122  while(it != alignments.end()) {
1123  text << ", " << *(it++);
1124  }
1125  text << "\n\n";
1126  }
1127 
1128  if (!race_help_taxonomy.empty()) {
1129  utils::string_map symbols;
1130  symbols["topic_id"] = "..race_"+race_help_taxonomy;
1131  if (const unit_race *r = unit_types.find_race(race_help_taxonomy)) {
1132  symbols["help_taxonomy"] = r->plural_name();
1133  } else {
1134  // Fall back to using showing the race id for the race that we couldn't find.
1135  // Not great, but probably useful if UMC has a broken link.
1136  symbols["help_taxonomy"] = race_help_taxonomy;
1137  }
1138  // TRANSLATORS: this is expected to say "[Dunefolk are] a group of units, all of whom are Humans",
1139  // or "[Quenoth Elves are] a group of units, all of whom are Elves".
1140  text << VGETTEXT("This is a group of units, all of whom are <ref dst='$topic_id'>$help_taxonomy</ref>.", symbols) << "\n\n";
1141  }
1142 
1143  if (!subgroups.empty()) {
1144  if (!race_help_taxonomy.empty()) {
1145  text << markup::tag("header", _("Subgroups of units within this group")) << "\n";
1146  } else {
1147  text << markup::tag("header", _("Groups of units within this race")) << "\n";
1148  }
1149  for (const auto &sg : subgroups) {
1150  text << font::unicode_bullet << " " << markup::make_link(sg.second, "..race_" + sg.first) << "\n";
1151  }
1152  text << "\n";
1153  }
1154 
1155  if (!race_help_taxonomy.empty()) {
1156  text << markup::tag("header", _("Units of this group")) << "\n";
1157  } else {
1158  text << markup::tag("header", _("Units of this race")) << "\n";
1159  }
1160  for (const auto &u : race_units) {
1161  text << font::unicode_bullet << " " << u << "\n";
1162  }
1163 
1164  topics.emplace_back(race_name, race_id, text.str());
1165 
1166  if (sort_generated)
1167  std::sort(topics.begin(), topics.end(), title_less());
1168 
1169  return topics;
1170 }
1171 
1173 {
1174  if (game_config::debug || prefs::get().show_all_units_in_help() ||
1176  return FULL_DESCRIPTION;
1177  }
1178 
1179  const std::set<std::string> &encountered_units = prefs::get().encountered_units();
1180  if (encountered_units.find(type.id()) != encountered_units.end()) {
1181  return FULL_DESCRIPTION;
1182  }
1183 
1184  // See the docs of HIDDEN_BUT_SHOW_MACROS
1185  if (type.id() == "Fog Clearer") {
1186  return HIDDEN_BUT_SHOW_MACROS;
1187  }
1188 
1189  return NO_DESCRIPTION;
1190 }
1191 
1192 std::string generate_contents_links(const std::string& section_name, config const *help_cfg)
1193 {
1194  auto section_cfg = help_cfg->find_child("section", "id", section_name);
1195  if (!section_cfg) {
1196  return std::string();
1197  }
1198 
1199  std::ostringstream res;
1200 
1201  std::vector<std::string> topics = utils::quoted_split(section_cfg["topics"]);
1202 
1203  // we use an intermediate structure to allow a conditional sorting
1204  typedef std::pair<std::string,std::string> link;
1205  std::vector<link> topics_links;
1206 
1207  // Find all topics in this section.
1208  for(const std::string& topic : topics) {
1209  if (auto topic_cfg = help_cfg->find_child("topic", "id", topic)) {
1210  std::string id = topic_cfg["id"];
1211  if (is_visible_id(id))
1212  topics_links.emplace_back(topic_cfg["title"], id);
1213  }
1214  }
1215 
1216  if (section_cfg["sort_topics"] == "yes") {
1217  std::sort(topics_links.begin(),topics_links.end());
1218  }
1219 
1220  for(const auto& [text, target] : topics_links) {
1221  std::string link = markup::make_link(text, target);
1222  res << font::unicode_bullet << " " << link << "\n";
1223  }
1224 
1225  return res.str();
1226 }
1227 
1228 std::string generate_contents_links(const section &sec)
1229 {
1230  std::stringstream res;
1231 
1232  for (auto &s : sec.sections) {
1233  if (is_visible_id(s.id)) {
1234  std::string link = markup::make_link(s.title, ".."+s.id);
1235  res << font::unicode_bullet << " " << link << "\n";
1236  }
1237  }
1238 
1239  for(const topic& t : sec.topics) {
1240  if (is_visible_id(t.id)) {
1241  std::string link = markup::make_link(t.title, t.id);
1242  res << font::unicode_bullet << " " << link << "\n";
1243  }
1244  }
1245  return res.str();
1246 }
1247 
1248 bool topic::operator==(const topic &t) const
1249 {
1250  return t.id == id;
1251 }
1252 
1253 bool topic::operator<(const topic &t) const
1254 {
1255  return id < t.id;
1256 }
1257 
1258 bool section::operator==(const section &sec) const
1259 {
1260  return sec.id == id;
1261 }
1262 
1263 bool section::operator<(const section &sec) const
1264 {
1265  return id < sec.id;
1266 }
1267 
1269 {
1270  sections.emplace_back(s);
1271 }
1272 
1274 {
1275  topics.clear();
1276  sections.clear();
1277 }
1278 
1279 const topic *find_topic(const section &sec, const std::string &id)
1280 {
1281  topic_list::const_iterator tit =
1282  std::find_if(sec.topics.begin(), sec.topics.end(), has_id(id));
1283  if (tit != sec.topics.end()) {
1284  return &(*tit);
1285  }
1286  for (const auto &s : sec.sections) {
1287  const auto t = find_topic(s, id);
1288  if (t != nullptr) {
1289  return t;
1290  }
1291  }
1292  return nullptr;
1293 }
1294 
1295 const section *find_section(const section &sec, const std::string &id)
1296 {
1297  const auto sit =
1298  std::find_if(sec.sections.begin(), sec.sections.end(), has_id(id));
1299  if (sit != sec.sections.end()) {
1300  return &*sit;
1301  }
1302  for (const auto &subsection : sec.sections) {
1303  const auto s = find_section(subsection, id);
1304  if (s != nullptr) {
1305  return s;
1306  }
1307  }
1308  return nullptr;
1309 }
1310 
1311 section *find_section(section &sec, const std::string &id)
1312 {
1313  return const_cast<section *>(find_section(const_cast<const section &>(sec), id));
1314 }
1315 
1316 std::string remove_first_space(const std::string& text)
1317 {
1318  if (text.length() > 0 && text[0] == ' ') {
1319  return text.substr(1);
1320  }
1321  return text;
1322 }
1323 
1324 std::string get_first_word(const std::string &s)
1325 {
1326  std::size_t first_word_start = s.find_first_not_of(' ');
1327  if (first_word_start == std::string::npos) {
1328  return s;
1329  }
1330  std::size_t first_word_end = s.find_first_of(" \n", first_word_start);
1331  if( first_word_end == first_word_start ) {
1332  // This word is '\n'.
1333  first_word_end = first_word_start+1;
1334  }
1335 
1336  //if no gap(' ' or '\n') found, test if it is CJK character
1337  std::string re = s.substr(0, first_word_end);
1338 
1339  utf8::iterator ch(re);
1340  if (ch == utf8::iterator::end(re))
1341  return re;
1342 
1343  char32_t firstchar = *ch;
1344  if (is_cjk_char(firstchar)) {
1345  re = unicode_cast<std::string>(firstchar);
1346  }
1347  return re;
1348 }
1349 
1351 {
1354  if (game_cfg != nullptr) {
1355  const config *help_config = &game_cfg->child_or_empty("help");
1356  try {
1357  default_toplevel = parse_config(help_config);
1358  // Create a config object that contains everything that is
1359  // not referenced from the toplevel element. Read this
1360  // config and save these sections and topics so that they
1361  // can be referenced later on when showing help about
1362  // specified things, but that should not be shown when
1363  // opening the help browser in the default manner.
1364  config hidden_toplevel;
1365  std::stringstream ss;
1366  for (const config &section : help_config->child_range("section"))
1367  {
1368  const std::string id = section["id"];
1369  if (find_section(default_toplevel, id) == nullptr) {
1370  // This section does not exist referenced from the
1371  // toplevel. Hence, add it to the hidden ones if it
1372  // is not referenced from another section.
1373  if (!section_is_referenced(id, *help_config)) {
1374  if (!ss.str().empty()) {
1375  ss << ",";
1376  }
1377  ss << id;
1378  }
1379  }
1380  }
1381  hidden_toplevel["sections"] = ss.str();
1382  ss.str("");
1383  for (const config &topic : help_config->child_range("topic"))
1384  {
1385  const std::string id = topic["id"];
1386  if (find_topic(default_toplevel, id) == nullptr) {
1387  if (!topic_is_referenced(id, *help_config)) {
1388  if (!ss.str().empty()) {
1389  ss << ",";
1390  }
1391  ss << id;
1392  }
1393  }
1394  }
1395  hidden_toplevel["topics"] = ss.str();
1396  config hidden_cfg = *help_config;
1397  // Change the toplevel to our new, custom built one.
1398  hidden_cfg.clear_children("toplevel");
1399  hidden_cfg.add_child("toplevel", std::move(hidden_toplevel));
1400  hidden_sections = parse_config(&hidden_cfg);
1401  }
1402  catch (parse_error& e) {
1403  std::stringstream msg;
1404  msg << "Parse error when parsing help text: '" << e.message << "'";
1405  PLAIN_LOG << msg.str();
1406  }
1407  }
1408 }
1409 
1410 // id starting with '.' are hidden
1411 std::string hidden_symbol(bool hidden) {
1412  return (hidden ? "." : "");
1413 }
1414 
1415 bool is_visible_id(const std::string &id) {
1416  return (id.empty() || id[0] != '.');
1417 }
1418 
1419 /**
1420  * Return true if the id is valid for user defined topics and
1421  * sections. Some IDs are special, such as toplevel and may not be
1422  * be defined in the config.
1423  */
1424 bool is_valid_id(const std::string &id) {
1425  if (id == "toplevel") {
1426  return false;
1427  }
1428  if (id.compare(0, unit_prefix.length(), unit_prefix) == 0 || id.compare(hidden_symbol().length(), unit_prefix.length(), unit_prefix) == 0) {
1429  return false;
1430  }
1431  if (id.compare(0, 8, "ability_") == 0) {
1432  return false;
1433  }
1434  if (id.compare(0, 14, "weaponspecial_") == 0) {
1435  return false;
1436  }
1437  if (id == "hidden") {
1438  return false;
1439  }
1440  return true;
1441 }
1442 
1443 /** Load the appropriate terrain types data to use */
1444 std::shared_ptr<terrain_type_data> load_terrain_types_data()
1445 {
1446  if (game_config_manager::get()){
1448  } else {
1449  return {};
1450  }
1451 }
1452 
1453 
1454 } // 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:1620
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:1236
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:377
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 map_location &loc=map_location::null_location()) 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:1225
const race_map & races() const
Definition: types.hpp:406
const unit_race * find_race(const std::string &) const
Definition: types.cpp:1329
const unit_type_map & types() const
Definition: types.hpp:396
config_array_view traits() const
Definition: types.hpp:408
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:582
const t_string & variation_name() const
Definition: types.hpp:177
#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:1324
std::vector< topic > generate_unit_topics(const bool sort_generated, const std::string &race)
Definition: help_impl.cpp:1041
std::string hidden_symbol(bool hidden)
Definition: help_impl.cpp:1411
void generate_unit_sections(const config *, section &sec, int, const bool, const std::string &race)
Definition: help_impl.cpp:1008
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:828
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:171
std::vector< topic > generate_time_of_day_topics(const bool)
Definition: help_impl.cpp:392
int last_num_encountered_units
Definition: help_impl.cpp:67
void generate_terrain_sections(section &sec, int)
Definition: help_impl.cpp:949
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:787
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:195
bool is_visible_id(const std::string &id)
Definition: help_impl.cpp:1415
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:630
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:334
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:1172
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:813
std::vector< topic > generate_topics(const bool sort_generated, const std::string &generator)
Definition: help_impl.cpp:305
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:1295
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:387
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:1350
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:442
std::vector< topic > generate_ability_topics(const bool sort_generated)
Definition: help_impl.cpp:532
std::string remove_first_space(const std::string &text)
Definition: help_impl.cpp:1316
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:1444
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:1424
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:1192
std::string generate_topic_text(const std::string &generator, const config *help_cfg, const section &sec)
Definition: help_impl.cpp:353
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:1279
section parse_config(const config *cfg)
Parse a help config, return the top level section.
Definition: help_impl.cpp:295
std::vector< topic > generate_era_topics(const bool sort_generated, const std::string &era_id)
Definition: help_impl.cpp:596
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:705
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:926
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:86
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:140
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:1263
void add_section(const section &s)
Allocate memory for and add the section.
Definition: help_impl.cpp:1268
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:1258
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:1248
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:1253
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:1464
#define e
#define f