The Battle for Wesnoth  1.19.21+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  return make_unit_link(type);
814 }
815 
816 std::string make_unit_link(const unit_type* type)
817 {
818  if(!type) {
819  PLAIN_LOG << "Unknown unit type: " << type->id();
820  // don't return an hyperlink (no page)
821  // instead show the id (as hint)
822  return type->id();
823  } else if(!type->hide_help()) {
824  std::string name = type->type_name();
825  std::string ref_id;
827  const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
828  ref_id = section_prefix + unit_prefix + type->id();
829  } else {
830  ref_id = unknown_unit_topic;
831  name += " (?)";
832  }
833  return markup::make_link(name, ref_id);
834  }
835  return "";
836 }
837 
838 std::vector<std::string> make_unit_links_list(const std::vector<std::string>& type_id_list, bool ordered)
839 {
840  std::vector<std::string> links_list;
841  for(const std::string& type_id : type_id_list) {
842  std::string unit_link = make_unit_link(type_id);
843  if(!unit_link.empty())
844  links_list.push_back(unit_link);
845  }
846 
847  if(ordered)
848  std::sort(links_list.begin(), links_list.end());
849 
850  return links_list;
851 }
852 
853 void generate_races_sections(const config& help_cfg, section& sec, int level)
854 {
855  std::set<std::string, string_less> races;
856  std::set<std::string, string_less> visible_races;
857 
858  // Calculate which races have been discovered, from the list of discovered unit types.
859  for(const auto& [_, type] : unit_types.types()) {
861  if(desc_type == FULL_DESCRIPTION) {
862  races.insert(type.race_id());
863  if(!type.hide_help())
864  visible_races.insert(type.race_id());
865  }
866  }
867 
868  // Propagate visibility up the help_taxonomy tree.
869  std::set<std::string, string_less> last_sweep = visible_races;
870  while(!last_sweep.empty()) {
871  std::set<std::string, string_less> current_sweep;
872  for(const auto& race_id : last_sweep) {
873  if(const unit_race* r = unit_types.find_race(race_id)) {
874  const auto& help_taxonomy = r->help_taxonomy();
875  if(!help_taxonomy.empty() && !visible_races.count(help_taxonomy) && unit_types.find_race(help_taxonomy)) {
876  current_sweep.insert(help_taxonomy);
877  races.insert(help_taxonomy);
878  visible_races.insert(help_taxonomy);
879  }
880  }
881  }
882  last_sweep = std::move(current_sweep);
883  }
884 
885  struct taxonomy_queue_type
886  {
887  std::string parent_id;
888  section content;
889  };
890  std::vector<taxonomy_queue_type> taxonomy_queue;
891 
892  // Add all races without a [race]help_taxonomy= to the documentation section, and queue the others.
893  // This avoids a race condition dependency on the order that races are encountered in help_cfg.
894  for(const auto& race_id : races) {
895  config section_cfg;
896 
897  bool hidden = (visible_races.count(race_id) == 0);
898 
899  section_cfg["id"] = hidden_symbol(hidden) + race_prefix + race_id;
900 
901  std::string title;
902  std::string help_taxonomy;
903  if(const unit_race* r = unit_types.find_race(race_id)) {
904  title = r->plural_name();
905  help_taxonomy = r->help_taxonomy();
906  } else {
907  title = _("race^Miscellaneous");
908  // leave help_taxonomy empty
909  }
910  section_cfg["title"] = title;
911 
912  section_cfg["sections_generator"] = "units:" + race_id;
913  section_cfg["generator"] = "units:" + race_id;
914 
915  section race_section = parse_config_internal(help_cfg, section_cfg, level + 1);
916 
917  if(help_taxonomy.empty()) {
918  sec.add_section(race_section);
919  } else {
920  bool parent_hidden = (visible_races.count(help_taxonomy) == 0);
921  auto parent_id = hidden_symbol(parent_hidden) + race_prefix + help_taxonomy;
922  taxonomy_queue.push_back({std::move(parent_id), std::move(race_section)});
923  }
924  }
925 
926  // Each run through this loop handles one level of nesting of [race]help_taxonomy=
927  bool process_queue_again = true;
928  while(process_queue_again && !taxonomy_queue.empty()) {
929  process_queue_again = false;
930  auto to_process = std::exchange(taxonomy_queue, {});
931 
932  for(auto& x : to_process) {
933  auto parent = find_section(sec, x.parent_id);
934  if(parent) {
935  parent->add_section(x.content);
936  process_queue_again = true;
937  } else {
938  taxonomy_queue.push_back(std::move(x));
939  }
940  }
941  }
942 
943  // Fallback to adding the new race at the top level, as if it had help_taxonomy.empty().
944  for(auto& x : taxonomy_queue) {
945  sec.add_section(x.content);
946  }
947 }
948 
949 void generate_era_sections(const config& help_cfg, section& sec, int level)
950 {
951  std::set<std::string> era_ids;
952  for(const config& era : game_config_manager::get()->game_config().child_range("era")) {
953  if(era["hide_help"].to_bool()) {
954  continue;
955  }
956 
957  config section_cfg;
958  section_cfg["id"] = era_prefix + era["id"].str();
959  section_cfg["title"] = era["name"];
960  section_cfg["generator"] = "era:" + era["id"].str();
961 
962  const bool is_added = era_ids.insert(section_cfg["id"]).second;
963  if(is_added) {
964  DBG_HP << "Adding help section: " << era["id"].str();
965  } else {
966  WRN_HP << "Duplicate era with id ‘" << era["id"] << "’, (help id ‘" << section_cfg["id"] << "’) detected";
967  }
968  DBG_HP << section_cfg.debug();
969  sec.add_section(parse_config_internal(help_cfg, section_cfg, level + 1));
970  }
971 }
972 
973 void generate_terrain_sections(section& sec, int /*level*/)
974 {
975  auto tdata = terrain_type_data::get();
976  if(!tdata) {
977  WRN_HP << "When building terrain help sections, couldn't acquire terrain types data, aborting.";
978  return;
979  }
980 
981  std::map<std::string, section> base_map;
982 
983  const t_translation::ter_list& t_listi = tdata->list();
984 
985  for(const t_translation::terrain_code& t : t_listi) {
986 
987  const terrain_type& info = tdata->get_terrain_info(t);
988 
989  bool hidden = info.hide_help();
990 
991  if(prefs::get().encountered_terrains().find(t)
992  == prefs::get().encountered_terrains().end() && !info.is_overlay())
993  hidden = true;
994 
995  topic terrain_topic{
996  info.editor_name(),
997  hidden_symbol(hidden) + terrain_prefix + info.id(),
998  std::make_shared<terrain_topic_generator>(info)
999  };
1000 
1001  t_translation::ter_list base_terrains = info.union_type();
1002 
1003  if(info.has_default_base()) {
1004  for(const auto& base : tdata->get_terrain_info(info.default_base()).union_type()) {
1005  if(!utils::contains(base_terrains, base)) {
1006  base_terrains.emplace_back(base);
1007  }
1008  }
1009  }
1010 
1011  for(const t_translation::terrain_code& base : base_terrains) {
1012 
1013  const terrain_type& base_info = tdata->get_terrain_info(base);
1014 
1015  if(!base_info.is_nonnull() || base_info.hide_help())
1016  continue;
1017 
1018  section& base_section = base_map[base_info.id()];
1019 
1020  base_section.id = terrain_prefix + base_info.id();
1021  base_section.title = base_info.editor_name();
1022 
1023  if(base_info.id() == info.id())
1024  terrain_topic.id = ".." + terrain_prefix + info.id();
1025  base_section.topics.push_back(terrain_topic);
1026  }
1027  }
1028 
1029  std::vector<section> sorted_sections;
1030  for(const auto& pair : base_map) {
1031  sorted_sections.push_back(pair.second);
1032  }
1033 
1034  std::sort(sorted_sections.begin(), sorted_sections.end(), section_less());
1035 
1036  for(const section& s : sorted_sections) {
1037  sec.add_section(s);
1038  }
1039 }
1040 
1041 void generate_unit_sections(const config& /*help_cfg*/, section& sec, int /*level*/, const bool /*sort_generated*/, const std::string& race)
1042 {
1043  for(const unit_type_data::unit_type_map::value_type& i : unit_types.types()) {
1044  const unit_type& type = i.second;
1045 
1046  if(type.race_id() != race)
1047  continue;
1048 
1049  if(!type.show_variations_in_help())
1050  continue;
1051 
1052  section base_unit;
1053  for(const std::string& variation_id : type.variations()) {
1054  // TODO: Do we apply encountered stuff to variations?
1055  const unit_type& var_type = type.get_variation(variation_id);
1056  const std::string topic_name = var_type.variation_name();
1057  const std::string var_ref = hidden_symbol(var_type.hide_help()) + variation_prefix + var_type.id() + "_" + variation_id;
1058 
1059  base_unit.topics.emplace_back(topic_name, var_ref, std::make_shared<unit_topic_generator>(var_type, variation_id));
1060  }
1061 
1062  const std::string type_name = type.type_name();
1063  const std::string ref_id = hidden_symbol(type.hide_help()) + unit_prefix + type.id();
1064 
1065  base_unit.id = ref_id;
1066  base_unit.title = type_name;
1067 
1068  sec.add_section(base_unit);
1069  }
1070 }
1071 
1072 std::vector<topic> generate_unit_topics(const std::string& race, const bool sort_generated)
1073 {
1074  std::vector<topic> topics;
1075  std::set<std::string, string_less> race_units;
1076  std::set<std::string, string_less> race_topics;
1077  std::set<std::string> alignments;
1078 
1079  for(const auto& [_, type] : unit_types.types())
1080  {
1081  if(type.race_id() != race)
1082  continue;
1083 
1085  if(desc_type != FULL_DESCRIPTION)
1086  continue;
1087 
1088  const std::string debug_suffix = (game_config::debug ? " (" + type.id() + ")" : "");
1089  const std::string type_name = type.type_name() + (type.id() == type.type_name().str() ? "" : debug_suffix);
1090  const std::string real_prefix = type.show_variations_in_help() ? ".." : "";
1091  const std::string ref_id = hidden_symbol(type.hide_help()) + real_prefix + unit_prefix + type.id();
1092  topics.emplace_back(type_name, ref_id, std::make_shared<unit_topic_generator>(type));
1093 
1094  if(!type.hide_help()) {
1095  // we also record an hyperlink of this unit
1096  // in the list used for the race topic
1097  std::string link = markup::make_link(type_name, ref_id);
1098  race_units.insert(link);
1099 
1100  alignments.insert(markup::make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
1101  }
1102  }
1103 
1104  // generate the hidden race description topic
1105  std::string race_id = "..race_"+race;
1106  std::string race_name;
1107  std::string race_description;
1108  std::string race_help_taxonomy;
1109  if(const unit_race* r = unit_types.find_race(race)) {
1110  race_name = r->plural_name();
1111  race_description = r->description();
1112  race_help_taxonomy = r->help_taxonomy();
1113  // if(description.empty()) description = _("No description Available");
1114  for(const config& additional_topic : r->additional_topics())
1115  {
1116  std::string id = additional_topic["id"];
1117  std::string title = additional_topic["title"];
1118  std::string text = additional_topic["text"];
1119  //topic additional_topic(title, id, text);
1120  topics.emplace_back(title, id, text);
1121  std::string link = markup::make_link(title, id);
1122  race_topics.insert(link);
1123  }
1124  } else {
1125  race_name = _ ("race^Miscellaneous");
1126  // description = _("Here put the description of the Miscellaneous race");
1127  }
1128 
1129  // Find any other races whose [race]help_taxonomy points to the current race
1130  std::map<std::string, t_string> subgroups;
1131  for(const auto& r : unit_types.races()) {
1132  if(r.second.help_taxonomy() == race) {
1133  if(!r.second.plural_name().empty())
1134  subgroups[r.first] = r.second.plural_name();
1135  else
1136  subgroups[r.first] = r.first;
1137  }
1138  }
1139 
1140  std::stringstream text;
1141 
1142  if(!race_description.empty()) {
1143  text << race_description << "\n\n";
1144  }
1145 
1146  if(!alignments.empty()) {
1147  std::set<std::string>::iterator it = alignments.begin();
1148  text << (alignments.size() > 1 ? _("Alignments: ") : _("Alignment: ")) << *(it++);
1149  while(it != alignments.end()) {
1150  text << ", " << *(it++);
1151  }
1152  text << "\n\n";
1153  }
1154 
1155  if(!race_help_taxonomy.empty()) {
1156  utils::string_map symbols;
1157  symbols["topic_id"] = "..race_"+race_help_taxonomy;
1158  if(const unit_race* r = unit_types.find_race(race_help_taxonomy)) {
1159  symbols["help_taxonomy"] = r->plural_name();
1160  } else {
1161  // Fall back to using showing the race id for the race that we couldn't find.
1162  // Not great, but probably useful if UMC has a broken link.
1163  symbols["help_taxonomy"] = race_help_taxonomy;
1164  }
1165  // TRANSLATORS: this is expected to say "[Dunefolk are] a group of units, all of whom are Humans",
1166  // or "[Quenoth Elves are] a group of units, all of whom are Elves".
1167  text << VGETTEXT("This is a group of units, all of whom are <ref dst='$topic_id'>$help_taxonomy</ref>.", symbols) << "\n\n";
1168  }
1169 
1170  if(!subgroups.empty()) {
1171  if(!race_help_taxonomy.empty()) {
1172  text << markup::tag("header", _("Subgroups of units within this group")) << "\n";
1173  } else {
1174  text << markup::tag("header", _("Groups of units within this race")) << "\n";
1175  }
1176  for(const auto& sg : subgroups) {
1177  text << font::unicode_bullet << " " << markup::make_link(sg.second, "..race_" + sg.first) << "\n";
1178  }
1179  text << "\n";
1180  }
1181 
1182  if(!race_help_taxonomy.empty()) {
1183  text << markup::tag("header", _("Units of this group")) << "\n";
1184  } else {
1185  text << markup::tag("header", _("Units of this race")) << "\n";
1186  }
1187  for(const auto& u : race_units) {
1188  text << font::unicode_bullet << " " << u << "\n";
1189  }
1190 
1191  topics.emplace_back(race_name, race_id, text.str());
1192 
1193  if(sort_generated)
1194  std::sort(topics.begin(), topics.end(), title_less());
1195 
1196  return topics;
1197 }
1198 
1200 {
1201  // See the docs of HIDDEN_BUT_SHOW_MACROS
1202  if(type.id() == "Fog Clearer") {
1203  return HIDDEN_BUT_SHOW_MACROS;
1204  }
1205 
1206  if(game_config::debug || prefs::get().show_all_units_in_help() ||
1208  return FULL_DESCRIPTION;
1209  }
1210 
1211  const std::set<std::string>& encountered_units = prefs::get().encountered_units();
1212  if(encountered_units.find(type.id()) != encountered_units.end()) {
1213  return FULL_DESCRIPTION;
1214  }
1215 
1216  return NO_DESCRIPTION;
1217 }
1218 
1219 std::string generate_contents_links(const std::string& section_name, const config& help_cfg)
1220 {
1221  auto section_cfg = help_cfg.find_child("section", "id", section_name);
1222  if(!section_cfg) {
1223  return std::string();
1224  }
1225 
1226  std::ostringstream res;
1227 
1228  std::vector<std::string> topics = utils::quoted_split(section_cfg["topics"]);
1229 
1230  // we use an intermediate structure to allow a conditional sorting
1231  typedef std::pair<std::string,std::string> link;
1232  std::vector<link> topics_links;
1233 
1234  // Find all topics in this section.
1235  for(const std::string& topic : topics) {
1236  if(auto topic_cfg = help_cfg.find_child("topic", "id", topic)) {
1237  std::string id = topic_cfg["id"];
1238  if(is_visible_id(id))
1239  topics_links.emplace_back(topic_cfg["title"], id);
1240  }
1241  }
1242 
1243  if(section_cfg["sort_topics"] == "yes") {
1244  std::sort(topics_links.begin(),topics_links.end());
1245  }
1246 
1247  for(const auto& [text, target] : topics_links) {
1248  std::string link = markup::make_link(text, target);
1249  res << font::unicode_bullet << " " << link << "\n";
1250  }
1251 
1252  return res.str();
1253 }
1254 
1255 std::string generate_contents_links(const section& sec)
1256 {
1257  std::stringstream res;
1258 
1259  for(auto& s : sec.sections) {
1260  if(is_visible_id(s.id)) {
1261  std::string link = markup::make_link(s.title, ".."+s.id);
1262  res << font::unicode_bullet << " " << link << "\n";
1263  }
1264  }
1265 
1266  for(const topic& t : sec.topics) {
1267  if(is_visible_id(t.id)) {
1268  std::string link = markup::make_link(t.title, t.id);
1269  res << font::unicode_bullet << " " << link << "\n";
1270  }
1271  }
1272  return res.str();
1273 }
1274 
1275 bool topic::operator==(const topic& t) const
1276 {
1277  return t.id == id;
1278 }
1279 
1280 bool topic::operator<(const topic& t) const
1281 {
1282  return id < t.id;
1283 }
1284 
1285 bool section::operator==(const section& sec) const
1286 {
1287  return sec.id == id;
1288 }
1289 
1290 bool section::operator<(const section& sec) const
1291 {
1292  return id < sec.id;
1293 }
1294 
1296 {
1297  sections.emplace_back(s);
1298 }
1299 
1301 {
1302  sections.emplace_back(std::move(s));
1303 }
1304 
1306 {
1307  topics.clear();
1308  sections.clear();
1309 }
1310 
1311 const topic* find_topic(const section& sec, const std::string& id)
1312 {
1313  topic_list::const_iterator tit =
1314  std::find_if(sec.topics.begin(), sec.topics.end(), has_id(id));
1315  if(tit != sec.topics.end()) {
1316  return &(*tit);
1317  }
1318  for(const auto& s : sec.sections) {
1319  const auto t = find_topic(s, id);
1320  if(t != nullptr) {
1321  return t;
1322  }
1323  }
1324  return nullptr;
1325 }
1326 
1327 const section* find_section(const section& sec, const std::string& id)
1328 {
1329  const auto sit =
1330  std::find_if(sec.sections.begin(), sec.sections.end(), has_id(id));
1331  if(sit != sec.sections.end()) {
1332  return &*sit;
1333  }
1334  for(const auto& subsection : sec.sections) {
1335  const auto s = find_section(subsection, id);
1336  if(s != nullptr) {
1337  return s;
1338  }
1339  }
1340  return nullptr;
1341 }
1342 
1343 section* find_section(section& sec, const std::string& id)
1344 {
1345  return const_cast<section*>(find_section(const_cast<const section&>(sec), id));
1346 }
1347 
1348 std::pair<section, section> generate_contents()
1349 try {
1350  const config& help_config = game_config_manager::get()->game_config().child_or_empty("help");
1351 
1352  std::vector<std::string> hidden_sections;
1353  std::vector<std::string> hidden_topics;
1354 
1355  section toplevel_section = parse_config(help_config);
1356 
1357  for(const config& section : help_config.child_range("section")) {
1358  const std::string id = section["id"];
1359 
1360  // This section is not referenced from the toplevel...
1361  if(find_section(toplevel_section, id) == nullptr) {
1362  // ...nor is it referenced from any other section.
1363  if(!section_is_referenced(id, help_config)) {
1364  hidden_sections.push_back(id);
1365  }
1366  }
1367  }
1368 
1369  for(const config& topic : help_config.child_range("topic")) {
1370  const std::string id = topic["id"];
1371 
1372  if(find_topic(toplevel_section, id) == nullptr) {
1373  if(!topic_is_referenced(id, help_config)) {
1374  hidden_topics.push_back(id);
1375  }
1376  }
1377  }
1378 
1379  // Avoid copying the whole help config if nothing is hidden
1380  if(hidden_sections.empty() && hidden_topics.empty()) {
1381  return {std::move(toplevel_section), section{}};
1382  }
1383 
1384  config hidden_config = help_config;
1385  hidden_config.clear_children("toplevel");
1386 
1387  // Replace the toplevel tag with a new one containing everything not referenced
1388  // by the original. Save these sections and topics so that they can be displayed
1389  // later, but hidden when opening the help browser in the usual manner.
1390  hidden_config.add_child("toplevel", config{
1391  "sections", utils::join(hidden_sections),
1392  "topics", utils::join(hidden_topics)
1393  });
1394 
1395  return {std::move(toplevel_section), parse_config(hidden_config)};
1396 
1397 } catch(const parse_error& e) {
1398  PLAIN_LOG << "Parse error when parsing help text: '" << e.message << "'";
1399  return {};
1400 }
1401 
1402 // id starting with '.' are hidden
1403 std::string hidden_symbol(bool hidden) {
1404  return (hidden ? "." : "");
1405 }
1406 
1407 bool is_visible_id(const std::string& id) {
1408  return (id.empty() || id[0] != '.');
1409 }
1410 
1411 /**
1412  * Return true if the id is valid for user defined topics and
1413  * sections. Some IDs are special, such as toplevel and may not be
1414  * be defined in the config.
1415  */
1416 bool is_valid_id(const std::string& id) {
1417  if(id == "toplevel") {
1418  return false;
1419  }
1420  if(id.compare(0, unit_prefix.length(), unit_prefix) == 0 || id.compare(hidden_symbol().length(), unit_prefix.length(), unit_prefix) == 0) {
1421  return false;
1422  }
1423  if(id.compare(0, ability_prefix.length(), ability_prefix) == 0) {
1424  return false;
1425  }
1426  if(id.compare(0, weaponspecial_prefix.length(), weaponspecial_prefix) == 0) {
1427  return false;
1428  }
1429  if(id == "hidden") {
1430  return false;
1431  }
1432  return true;
1433 }
1434 
1435 } // 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:1513
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:61
const bool & debug
Definition: game_config.cpp:95
std::string hidden_symbol(bool hidden)
Definition: help_impl.cpp:1403
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:973
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:1407
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:1199
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:838
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:1327
void generate_races_sections(const config &help_cfg, section &sec, int level)
Definition: help_impl.cpp:853
void generate_unit_sections(const config &, section &sec, int, const bool, const std::string &race)
Definition: help_impl.cpp:1041
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:1219
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:1416
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:1311
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:949
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:1348
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:1072
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:1290
void add_section(const section &s)
Allocate memory for and add the section.
Definition: help_impl.cpp:1295
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:1285
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:1275
std::string id
Definition: help_impl.hpp:101
bool operator<(const topic &) const
Comparison on the ID.
Definition: help_impl.cpp:1280
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