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