The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
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
53static 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
57namespace help {
58
59const int max_section_level = 15;
60// The topic to open by default when opening the help dialog.
61const std::string default_show_topic = "..introduction";
62const std::string unknown_unit_topic = ".unknown_unit";
63const std::string unit_prefix = "unit_";
64const std::string terrain_prefix = "terrain_";
65const std::string race_prefix = "race_";
66const std::string faction_prefix = "faction_";
67const std::string era_prefix = "era_";
68const std::string variation_prefix = "variation_";
69const std::string ability_prefix = "ability_";
70const std::string weaponspecial_prefix = "weaponspecial_";
71
72bool 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
90bool 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
108section 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
210std::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
239void 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") {
244 generate_terrain_sections(sec, level);
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
258std::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
286static 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
291std::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
341std::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
440std::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
498std::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
530std::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) {
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
611namespace {
612const 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.
616void 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
660std::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()) {
686 UNIT_DESCRIPTION_TYPE desc_type = description_type(type);
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()) {
712 UNIT_DESCRIPTION_TYPE desc_type = description_type(type);
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
810std::string make_unit_link(const std::string& type_id)
811{
812 const unit_type* type = unit_types.find(type_id, unit_type::HELP_INDEXED);
813 if(!type) {
814 // don't return an hyperlink (no page)
815 // instead show the id (as hint)
816 return type_id;
817 }
818 return make_unit_link(*type);
819}
820
821std::string make_unit_link(const unit_type& type)
822{
823 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
838std::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
853void 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()) {
860 UNIT_DESCRIPTION_TYPE desc_type = description_type(type);
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
949void 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
973void 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
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
1041void 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
1072std::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
1084 UNIT_DESCRIPTION_TYPE desc_type = description_type(type);
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") {
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
1219std::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
1255std::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
1275bool topic::operator==(const topic& t) const
1276{
1277 return t.id == id;
1278}
1279
1280bool topic::operator<(const topic& t) const
1281{
1282 return id < t.id;
1283}
1284
1285bool section::operator==(const section& sec) const
1286{
1287 return sec.id == id;
1288}
1289
1290bool 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
1311const 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
1327const 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
1343section* find_section(section& sec, const std::string& id)
1344{
1345 return const_cast<section*>(find_section(const_cast<const section&>(sec), id));
1346}
1347
1348std::pair<section, section> generate_contents()
1349try {
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
1403std::string hidden_symbol(bool hidden) {
1404 return (hidden ? "." : "");
1405}
1406
1407bool 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 */
1416bool 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:1538
Various functions that implement attacks and attack calculations.
double t
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
config_array_view child_range(std::string_view key) const
optional_const_config find_child(std::string_view key, const std::string &name, const std::string &value) const
To be used as a function object to locate sections and topics with a specified ID.
To be used as a function object when sorting section lists on the title.
To be used as a function object when sorting topic lists on the title.
std::shared_ptr< topic_generator > generator_
Definition help_impl.hpp:78
const config & parsed_text() const
std::set< t_translation::terrain_code > & encountered_terrains()
static prefs & get()
std::set< std::string > & encountered_units()
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
const std::vector< time_of_day > & times() const
int get_max_liminal_bonus() const
std::string get_help_topic_id() const
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:408
const unit_race * find_race(const std::string &) const
Definition types.cpp:1359
const unit_type_map & types() const
Definition types.hpp:398
config_array_view traits() const
Definition types.hpp:412
A single unit type that the player may recruit.
Definition types.hpp:43
const t_string & variation_name() const
Definition types.hpp:178
@ HELP_INDEXED
Definition types.hpp:77
bool hide_help() const
Definition types.cpp:582
const std::string & id() const
The id for this unit_type.
Definition types.hpp:145
const config * cfg
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
static std::string _(const char *str)
Definition gettext.hpp:100
std::string id
Text to match against addon_info.tags()
Definition manager.cpp:199
#define DBG_HP
#define WRN_HP
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
const bool & debug
std::string hidden_symbol(bool hidden)
UNIT_DESCRIPTION_TYPE
@ FULL_DESCRIPTION
@ HIDDEN_BUT_SHOW_MACROS
Although the unit itself is hidden, traits reachable via this unit are not hidden.
@ NO_DESCRIPTION
Ignore this unit for documentation purposes.
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)
section parse_config_internal(const config &help_cfg, const config &section_cfg, int level)
Recursive function used by parse_config.
void generate_terrain_sections(section &sec, int)
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...
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)
std::vector< topic > generate_faction_topics(const config &era, const bool sort_generated)
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.
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)
std::vector< topic > generate_topics(const bool sort_generated, const std::string &generator)
void generate_sections(const config &help_cfg, const std::string &generator, section &sec, int level)
Dispatch generators to their appropriate functions.
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.
void generate_races_sections(const config &help_cfg, section &sec, int level)
void generate_unit_sections(const config &, section &sec, int, const bool, const std::string &race)
static std::string time_of_day_bonus_colored(const int time_of_day_bonus)
const std::string terrain_prefix
Definition help_impl.cpp:64
std::string generate_contents_links(const std::string &section_name, const config &help_cfg)
std::vector< topic > generate_weapon_special_topics(const bool sort_generated)
std::string generate_topic_text(const std::string &generator, const config &help_cfg, const section &sec)
std::vector< topic > generate_ability_topics(const bool sort_generated)
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.
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.
std::vector< topic > generate_era_topics(const std::string &era_id, const bool sort_generated)
std::vector< topic > generate_trait_topics(const bool sort_generated)
void generate_era_sections(const config &help_cfg, section &sec, int level)
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.
section parse_config(const config &cfg)
Parse a help config, return the top level section.
std::vector< topic > generate_unit_topics(const std::string &race, const bool sort_generated)
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.
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
::tod_manager * tod_manager
Definition resources.cpp:29
std::vector< terrain_code > ter_list
@ 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:88
std::string join(const Range &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)
Thrown when the help system fails to parse something.
A section contains topics and sections along with title and ID.
section_list sections
bool operator<(const section &) const
Comparison on the ID.
void add_section(const section &s)
Allocate memory for and add the section.
std::string id
std::string title
bool operator==(const section &) const
Two sections are equal if their IDs are equal.
topic_list topics
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.
std::string id
bool operator<(const topic &) const
Comparison on the ID.
A terrain string which is converted to a terrain is a string with 1 or 2 layers the layers are separa...
Object which defines a time of day with associated bonuses, image, sounds etc.
static map_location::direction s
unit_type_data unit_types
Definition types.cpp:1499
#define i
#define e
#define f