wesnoth/src/help/help_impl.cpp
2018-01-19 00:02:20 -06:00

1558 lines
48 KiB
C++

/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/help_impl.hpp"
#include "about.hpp" // for get_text
#include "display.hpp" // for display
#include "display_context.hpp" // for display_context
#include "game_config.hpp" // for debug, menu_contract, etc
#include "game_config_manager.hpp" // for game_config_manager
#include "preferences/game.hpp" // for encountered_terrains, etc
#include "gettext.hpp" // for _, gettext, N_
#include "help/help_topic_generators.hpp"
#include "hotkey/hotkey_command.hpp" // for is_scope_active, etc
#include "image.hpp" // for get_image, locator
#include "log.hpp" // for LOG_STREAM, logger, etc
#include "utils/make_enum.hpp" // for operator<<
#include "map/map.hpp" // for gamemap
#include "font/marked-up_text.hpp" // for is_cjk_char, word_wrap_text
#include "font/standard_colors.hpp" // for NORMAL_COLOR
#include "units/race.hpp" // for unit_race, etc
#include "resources.hpp" // for tod_manager, config_manager
#include "sdl/surface.hpp" // for surface
#include "serialization/string_utils.hpp" // for split, quoted_split, etc
#include "serialization/unicode_cast.hpp" // for unicode_cast
#include "serialization/unicode_types.hpp" // for char_t, etc
#include "terrain/terrain.hpp" // for terrain_type
#include "terrain/translation.hpp" // for operator==, ter_list, etc
#include "terrain/type_data.hpp" // for terrain_type_data, etc
#include "time_of_day.hpp" // for time_of_day
#include "tod_manager.hpp" // for tod_manager
#include "tstring.hpp" // for t_string, operator<<
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "serialization/unicode.hpp" // for iterator
#include "color.hpp"
#include <cassert> // for assert
#include <algorithm> // for sort, find, transform, etc
#include <iostream> // for operator<<, basic_ostream, etc
#include <iterator> // for back_insert_iterator, etc
#include <map> // for map, etc
#include <set>
#include <SDL.h>
static lg::log_domain log_display("display");
#define WRN_DP LOG_STREAM(warn, log_display)
static lg::log_domain log_help("help");
#define WRN_HP LOG_STREAM(warn, log_help)
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help {
const config *game_cfg = nullptr;
// The default toplevel.
help::section default_toplevel;
// All sections and topics not referenced from the default toplevel.
help::section hidden_sections;
int last_num_encountered_units = -1;
int last_num_encountered_terrains = -1;
bool last_debug_state = game_config::debug;
config dummy_cfg;
std::vector<std::string> empty_string_vector;
const int max_section_level = 15;
const int title_size = font::SIZE_LARGE;
const int title2_size = font::SIZE_15;
const int box_width = 2;
const int normal_font_size = font::SIZE_NORMAL;
const unsigned max_history = 100;
const std::string topic_img = "help/topic.png";
const std::string closed_section_img = "help/closed_section.png";
const std::string open_section_img = "help/open_section.png";
// The topic to open by default when opening the help dialog.
const std::string default_show_topic = "..introduction";
const std::string unknown_unit_topic = ".unknown_unit";
const std::string unit_prefix = "unit_";
const std::string terrain_prefix = "terrain_";
const std::string race_prefix = "race_";
const std::string faction_prefix = "faction_";
const std::string era_prefix = "era_";
const std::string variation_prefix = "variation_";
bool section_is_referenced(const std::string &section_id, const config &cfg)
{
if (const config &toplevel = cfg.child("toplevel"))
{
const std::vector<std::string> toplevel_refs
= utils::quoted_split(toplevel["sections"]);
if (std::find(toplevel_refs.begin(), toplevel_refs.end(), section_id)
!= toplevel_refs.end()) {
return true;
}
}
for (const config &section : cfg.child_range("section"))
{
const std::vector<std::string> sections_refd
= utils::quoted_split(section["sections"]);
if (std::find(sections_refd.begin(), sections_refd.end(), section_id)
!= sections_refd.end()) {
return true;
}
}
return false;
}
bool topic_is_referenced(const std::string &topic_id, const config &cfg)
{
if (const config &toplevel = cfg.child("toplevel"))
{
const std::vector<std::string> toplevel_refs
= utils::quoted_split(toplevel["topics"]);
if (std::find(toplevel_refs.begin(), toplevel_refs.end(), topic_id)
!= toplevel_refs.end()) {
return true;
}
}
for (const config &section : cfg.child_range("section"))
{
const std::vector<std::string> topics_refd
= utils::quoted_split(section["topics"]);
if (std::find(topics_refd.begin(), topics_refd.end(), topic_id)
!= topics_refd.end()) {
return true;
}
}
return false;
}
void parse_config_internal(const config *help_cfg, const config *section_cfg,
section &sec, int level)
{
if (level > max_section_level) {
std::cerr << "Maximum section depth has been reached. Maybe circular dependency?"
<< std::endl;
}
else if (section_cfg != nullptr) {
const std::vector<std::string> sections = utils::quoted_split((*section_cfg)["sections"]);
sec.level = level;
std::string id = level == 0 ? "toplevel" : (*section_cfg)["id"].str();
if (level != 0) {
if (!is_valid_id(id)) {
std::stringstream ss;
ss << "Invalid ID, used for internal purpose: '" << id << "'";
throw parse_error(ss.str());
}
}
std::string title = level == 0 ? "" : (*section_cfg)["title"].str();
sec.id = id;
sec.title = title;
std::vector<std::string>::const_iterator it;
// Find all child sections.
for (it = sections.begin(); it != sections.end(); ++it) {
if (const config &child_cfg = help_cfg->find_child("section", "id", *it))
{
section child_section;
parse_config_internal(help_cfg, &child_cfg, child_section, level + 1);
sec.add_section(child_section);
}
else {
std::stringstream ss;
ss << "Help-section '" << *it << "' referenced from '"
<< id << "' but could not be found.";
throw parse_error(ss.str());
}
}
generate_sections(help_cfg, (*section_cfg)["sections_generator"], sec, level);
//TODO: harmonize topics/sections sorting
if ((*section_cfg)["sort_sections"] == "yes") {
std::sort(sec.sections.begin(),sec.sections.end(), section_less());
}
bool sort_topics = false;
bool sort_generated = true;
if ((*section_cfg)["sort_topics"] == "yes") {
sort_topics = true;
sort_generated = false;
} else if ((*section_cfg)["sort_topics"] == "no") {
sort_topics = false;
sort_generated = false;
} else if ((*section_cfg)["sort_topics"] == "generated") {
sort_topics = false;
sort_generated = true;
} else if (!(*section_cfg)["sort_topics"].empty()) {
std::stringstream ss;
ss << "Invalid sort option: '" << (*section_cfg)["sort_topics"] << "'";
throw parse_error(ss.str());
}
std::vector<topic> generated_topics =
generate_topics(sort_generated,(*section_cfg)["generator"]);
const std::vector<std::string> topics_id = utils::quoted_split((*section_cfg)["topics"]);
std::vector<topic> topics;
// Find all topics in this section.
for (it = topics_id.begin(); it != topics_id.end(); ++it) {
if (const config &topic_cfg = help_cfg->find_child("topic", "id", *it))
{
std::string text = topic_cfg["text"];
text += generate_topic_text(topic_cfg["generator"], help_cfg, sec, generated_topics);
topic child_topic(topic_cfg["title"], topic_cfg["id"], text);
if (!is_valid_id(child_topic.id)) {
std::stringstream ss;
ss << "Invalid ID, used for internal purpose: '" << id << "'";
throw parse_error(ss.str());
}
topics.push_back(child_topic);
}
else {
std::stringstream ss;
ss << "Help-topic '" << *it << "' referenced from '" << id
<< "' but could not be found." << std::endl;
throw parse_error(ss.str());
}
}
if (sort_topics) {
std::sort(topics.begin(),topics.end(), title_less());
std::sort(generated_topics.begin(),
generated_topics.end(), title_less());
std::merge(generated_topics.begin(),
generated_topics.end(),topics.begin(),topics.end()
,std::back_inserter(sec.topics),title_less());
}
else {
sec.topics.insert(sec.topics.end(),
topics.begin(), topics.end());
sec.topics.insert(sec.topics.end(),
generated_topics.begin(), generated_topics.end());
}
}
}
section parse_config(const config *cfg)
{
section sec;
if (cfg != nullptr) {
config const &toplevel_cfg = cfg->child("toplevel");
parse_config_internal(cfg, toplevel_cfg ? &toplevel_cfg : nullptr, sec);
}
return sec;
}
std::vector<topic> generate_topics(const bool sort_generated,const std::string &generator)
{
std::vector<topic> res;
if (generator.empty()) {
return res;
}
if (generator == "abilities") {
res = generate_ability_topics(sort_generated);
} else if (generator == "weapon_specials") {
res = generate_weapon_special_topics(sort_generated);
} else if (generator == "time_of_days") {
res = generate_time_of_day_topics(sort_generated);
} else if (generator == "traits") {
res = generate_trait_topics(sort_generated);
} else {
std::vector<std::string> parts = utils::split(generator, ':', utils::STRIP_SPACES);
if (parts.size() > 1 && parts[0] == "units") {
res = generate_unit_topics(sort_generated, parts[1]);
} else if (parts[0] == "era" && parts.size()>1) {
res = generate_era_topics(sort_generated, parts[1]);
} else {
WRN_HP << "Found a topic generator that I didn't recognize: " << generator << "\n";
}
}
return res;
}
void generate_sections(const config *help_cfg, const std::string &generator, section &sec, int level)
{
if (generator == "races") {
generate_races_sections(help_cfg, sec, level);
} else if (generator == "terrains") {
generate_terrain_sections(help_cfg, sec, level);
} else if (generator == "eras") {
DBG_HP << "Generating eras...\n";
generate_era_sections(help_cfg, sec, level);
} else {
std::vector<std::string> parts = utils::split(generator, ':', utils::STRIP_SPACES);
if (parts.size() > 1 && parts[0] == "units") {
generate_unit_sections(help_cfg, sec, level, true, parts[1]);
} else if (generator.size() > 0) {
WRN_HP << "Found a section generator that I didn't recognize: " << generator << "\n";
}
}
}
std::string generate_topic_text(const std::string &generator, const config *help_cfg, const section &sec, const std::vector<topic>& generated_topics)
{
std::string empty_string = "";
if (generator.empty()) {
return empty_string;
} else if (generator == "about") {
return generate_about_text();
} else {
std::vector<std::string> parts = utils::split(generator, ':');
if (parts.size() > 1 && parts[0] == "contents") {
if (parts[1] == "generated") {
return generate_contents_links(sec, generated_topics);
} else {
return generate_contents_links(parts[1], help_cfg);
}
}
}
return empty_string;
}
topic_text::~topic_text()
{
if (generator_ && --generator_->count == 0)
delete generator_;
}
topic_text::topic_text(topic_text const &t): parsed_text_(t.parsed_text_), generator_(t.generator_)
{
if (generator_)
++generator_->count;
}
topic_text &topic_text::operator=(topic_generator *g)
{
if (generator_ && --generator_->count == 0)
delete generator_;
generator_ = g;
return *this;
}
const std::vector<std::string>& topic_text::parsed_text() const
{
if (generator_) {
parsed_text_ = parse_text((*generator_)());
if (--generator_->count == 0)
delete generator_;
generator_ = nullptr;
}
return parsed_text_;
}
std::vector<topic> generate_time_of_day_topics(const bool /*sort_generated*/)
{
std::vector<topic> topics;
std::stringstream toplevel;
if (! resources::tod_manager) {
toplevel << N_("Only available during a scenario.");
topics.emplace_back("Time of Day Schedule", "..schedule", toplevel.str());
return topics;
}
const std::vector<time_of_day>& times = resources::tod_manager->times();
for (const time_of_day& time : times)
{
const std::string id = "time_of_day_" + time.id;
const std::string image = "<img>src='" + time.image + "'</img>";
std::stringstream text;
toplevel << make_link(time.name.str(), id) << jump_to(160) <<
image << jump(30) << time.lawful_bonus << '\n';
text << image << '\n' <<
time.description.str() << '\n' <<
"Lawful Bonus: " << time.lawful_bonus << '\n' <<
'\n' << make_link(N_("Schedule"), "..schedule");
topics.emplace_back(time.name.str(), id, text.str());
}
topics.emplace_back("Time of Day Schedule", "..schedule", toplevel.str());
return topics;
}
std::vector<topic> generate_weapon_special_topics(const bool sort_generated)
{
std::vector<topic> topics;
std::map<t_string, std::string> special_description;
std::map<t_string, std::set<std::string, string_less> > special_units;
for (const unit_type_data::unit_type_map::value_type &type_mapping : unit_types.types())
{
const unit_type &type = type_mapping.second;
// Only show the weapon special if we find it on a unit that
// detailed description should be shown about.
if (description_type(type) != FULL_DESCRIPTION)
continue;
for (const attack_type& atk : type.attacks()) {
std::vector<std::pair<t_string, t_string> > specials = atk.special_tooltips();
for ( size_t i = 0; i != specials.size(); ++i )
{
special_description.emplace(specials[i].first, specials[i].second);
if (!type.hide_help()) {
//add a link in the list of units having this special
std::string type_name = type.type_name();
//check for variations (walking corpse/soulless etc)
const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
std::string ref_id = section_prefix + unit_prefix + type.id();
//we put the translated name at the beginning of the hyperlink,
//so the automatic alphabetic sorting of std::set can use it
std::string link = make_link(type_name, ref_id);
special_units[specials[i].first].insert(link);
}
}
}
for(config adv : type.modification_advancements()) {
for(config effect : adv.child_range("effect")) {
if(effect["apply_to"] == "new_attack" && effect.has_child("specials")) {
for(config::any_child spec : effect.child("specials").all_children_range()) {
if(!spec.cfg["name"].empty()) {
special_description.emplace(spec.cfg["name"].t_str(), spec.cfg["description"].t_str());
if(!type.hide_help()) {
//add a link in the list of units having this special
std::string type_name = type.type_name();
//check for variations (walking corpse/soulless etc)
const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
std::string ref_id = section_prefix + unit_prefix + type.id();
//we put the translated name at the beginning of the hyperlink,
//so the automatic alphabetic sorting of std::set can use it
std::string link = make_link(type_name, ref_id);
special_units[spec.cfg["name"]].insert(link);
}
}
}
} else if(effect["apply_to"] == "attack" && effect.has_child("set_specials")) {
for(config::any_child spec : effect.child("set_specials").all_children_range()) {
if(!spec.cfg["name"].empty()) {
special_description.emplace(spec.cfg["name"].t_str(), spec.cfg["description"].t_str());
if(!type.hide_help()) {
//add a link in the list of units having this special
std::string type_name = type.type_name();
//check for variations (walking corpse/soulless etc)
const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
std::string ref_id = section_prefix + unit_prefix + type.id();
//we put the translated name at the beginning of the hyperlink,
//so the automatic alphabetic sorting of std::set can use it
std::string link = make_link(type_name, ref_id);
special_units[spec.cfg["name"]].insert(link);
}
}
}
}
}
}
}
for (std::map<t_string, std::string>::iterator s = special_description.begin();
s != special_description.end(); ++s) {
// use untranslated name to have universal topic id
std::string id = "weaponspecial_" + s->first.base_str();
std::stringstream text;
text << s->second;
text << "\n\n" << _("<header>text='Units with this special attack'</header>") << "\n";
std::set<std::string, string_less> &units = special_units[s->first];
for (std::set<std::string, string_less>::iterator u = units.begin(); u != units.end(); ++u) {
text << font::unicode_bullet << " " << (*u) << "\n";
}
topics.emplace_back(s->first, id, text.str());
}
if (sort_generated)
std::sort(topics.begin(), topics.end(), title_less());
return topics;
}
std::vector<topic> generate_ability_topics(const bool sort_generated)
{
std::vector<topic> topics;
std::map<t_string, std::string> ability_description;
std::map<t_string, std::set<std::string, string_less> > ability_units;
// Look through all the unit types, check if a unit of this type
// should have a full description, if so, add this units abilities
// for display. We do not want to show abilities that the user has
// not encountered yet.
for (const unit_type_data::unit_type_map::value_type &type_mapping : unit_types.types())
{
const unit_type &type = type_mapping.second;
if (description_type(type) == FULL_DESCRIPTION) {
std::vector<t_string> const* abil_vecs[2];
abil_vecs[0] = &type.abilities();
abil_vecs[1] = &type.adv_abilities();
std::vector<t_string> const* desc_vecs[2];
desc_vecs[0] = &type.ability_tooltips();
desc_vecs[1] = &type.adv_ability_tooltips();
for(int i=0; i<2; ++i) {
std::vector<t_string> const& abil_vec = *abil_vecs[i];
std::vector<t_string> const& desc_vec = *desc_vecs[i];
for(size_t j=0; j < abil_vec.size(); ++j) {
t_string const& abil_name = abil_vec[j];
const std::string abil_desc =
j >= desc_vec.size() ? "" : desc_vec[j].str();
ability_description.emplace(abil_name, abil_desc);
if (!type.hide_help()) {
//add a link in the list of units with this ability
std::string type_name = type.type_name();
std::string ref_id = unit_prefix + type.id();
//we put the translated name at the beginning of the hyperlink,
//so the automatic alphabetic sorting of std::set can use it
std::string link = make_link(type_name, ref_id);
ability_units[abil_name].insert(link);
}
}
}
}
}
for (std::map<t_string, std::string>::iterator a = ability_description.begin(); a != ability_description.end(); ++a) {
// we generate topic's id using the untranslated version of the ability's name
std::string id = "ability_" + a->first.base_str();
std::stringstream text;
text << a->second; //description
text << "\n\n" << _("<header>text='Units with this ability'</header>") << "\n";
std::set<std::string, string_less> &units = ability_units[a->first];
for (std::set<std::string, string_less>::iterator u = units.begin(); u != units.end(); ++u) {
text << font::unicode_bullet << " " << (*u) << "\n";
}
topics.emplace_back(a->first, id, text.str());
}
if (sort_generated)
std::sort(topics.begin(), topics.end(), title_less());
return topics;
}
std::vector<topic> generate_era_topics(const bool sort_generated, const std::string & era_id)
{
std::vector<topic> topics;
const config & era = game_cfg->find_child("era","id", era_id);
if(era && !era["hide_help"].to_bool()) {
topics = generate_faction_topics(era, sort_generated);
std::vector<std::string> faction_links;
for (const topic & t : topics) {
faction_links.push_back(make_link(t.title, t.id));
}
std::stringstream text;
text << "<header>text='" << _("Era:") << " " << era["name"] << "'</header>" << "\n";
text << "\n";
const config::attribute_value& description = era["description"];
if (!description.empty()) {
text << description.t_str() << "\n";
text << "\n";
}
text << "<header>text='" << _("Factions") << "'</header>" << "\n";
std::sort(faction_links.begin(), faction_links.end());
for (const std::string &link : faction_links) {
text << font::unicode_bullet << " " << link << "\n";
}
topic era_topic(era["name"], ".." + era_prefix + era["id"].str(), text.str());
topics.push_back( era_topic );
}
return topics;
}
std::vector<topic> generate_faction_topics(const config & era, const bool sort_generated)
{
std::vector<topic> topics;
for (const config &f : era.child_range("multiplayer_side")) {
const std::string& id = f["id"];
if (id == "Random")
continue;
std::stringstream text;
const config::attribute_value& description = f["description"];
if (!description.empty()) {
text << description.t_str() << "\n";
text << "\n";
}
const std::vector<std::string> recruit_ids = utils::split(f["recruit"]);
std::set<std::string> races;
std::set<std::string> alignments;
for (const std::string & u_id : recruit_ids) {
if (const unit_type * t = unit_types.find(u_id, unit_type::HELP_INDEXED)) {
assert(t);
const unit_type & type = *t;
if (const unit_race *r = unit_types.find_race(type.race_id())) {
races.insert(make_link(r->plural_name(), std::string("..") + race_prefix + r->id()));
}
DBG_HP << type.alignment() << " -> " << type.alignment_description(type.alignment(), type.genders().front()) << "\n";
alignments.insert(make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
}
}
if (!races.empty()) {
std::set<std::string>::iterator it = races.begin();
text << _("Races: ") << *(it++);
while(it != races.end()) {
text << ", " << *(it++);
}
text << "\n\n";
}
if (!alignments.empty()) {
std::set<std::string>::iterator it = alignments.begin();
text << _("Alignments: ") << *(it++);
while(it != alignments.end()) {
text << ", " << *(it++);
}
text << "\n\n";
}
text << "<header>text='" << _("Leaders") << "'</header>" << "\n";
const std::vector<std::string> leaders =
make_unit_links_list( utils::split(f["leader"]), true );
for (const std::string &link : leaders) {
text << font::unicode_bullet << " " << link << "\n";
}
text << "\n";
text << "<header>text='" << _("Recruits") << "'</header>" << "\n";
const std::vector<std::string> recruit_links =
make_unit_links_list( recruit_ids, true );
for (const std::string &link : recruit_links) {
text << font::unicode_bullet << " " << link << "\n";
}
const std::string name = f["name"];
const std::string ref_id = faction_prefix + era["id"] + "_" + id;
topics.emplace_back(name, ref_id, text.str());
}
if (sort_generated)
std::sort(topics.begin(), topics.end(), title_less());
return topics;
}
std::vector<topic> generate_trait_topics(const bool sort_generated)
{
std::vector<topic> topics;
std::map<t_string, const config> trait_list;
for (const config & trait : unit_types.traits()) {
const std::string trait_id = trait["id"];
trait_list.emplace(trait_id, trait);
}
for (const unit_type_data::unit_type_map::value_type &i : unit_types.types())
{
const unit_type &type = i.second;
if (description_type(type) == FULL_DESCRIPTION) {
if (config::const_child_itors traits = type.possible_traits()) {
for (const config & trait : traits) {
const std::string trait_id = trait["id"];
trait_list.emplace(trait_id, trait);
}
}
if (const unit_race *r = unit_types.find_race(type.race_id())) {
for (const config & trait : r->additional_traits()) {
const std::string trait_id = trait["id"];
trait_list.emplace(trait_id, trait);
}
}
}
}
for (std::map<t_string, const config>::iterator a = trait_list.begin(); a != trait_list.end(); ++a) {
std::string id = "traits_" + a->first;
const config trait = a->second;
std::string name = trait["male_name"].str();
if (name.empty()) name = trait["female_name"].str();
if (name.empty()) name = trait["name"].str();
if (name.empty()) continue; // Hidden trait
std::stringstream text;
if (!trait["help_text"].empty()) {
text << trait["help_text"];
} else if (!trait["description"].empty()) {
text << trait["description"];
} else {
text << _("No description available.");
}
text << "\n\n";
if (trait["availability"] == "musthave") {
text << _("Availability: ") << _("Must-have") << "\n";
} else if (trait["availability"] == "none") {
text << _("Availability: ") << _("Unavailable") << "\n";
}
topics.emplace_back(name, id, text.str());
}
if (sort_generated)
std::sort(topics.begin(), topics.end(), title_less());
return topics;
}
std::string make_unit_link(const std::string& type_id)
{
std::string link;
const unit_type *type = unit_types.find(type_id, unit_type::HELP_INDEXED);
if (!type) {
std::cerr << "Unknown unit type : " << type_id << "\n";
// don't return an hyperlink (no page)
// instead show the id (as hint)
link = type_id;
} else if (!type->hide_help()) {
std::string name = type->type_name();
std::string ref_id;
if (description_type(*type) == FULL_DESCRIPTION) {
const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
ref_id = section_prefix + unit_prefix + type->id();
} else {
ref_id = unknown_unit_topic;
name += " (?)";
}
link = make_link(name, ref_id);
} // if hide_help then link is an empty string
return link;
}
std::vector<std::string> make_unit_links_list(const std::vector<std::string>& type_id_list, bool ordered)
{
std::vector<std::string> links_list;
for (const std::string &type_id : type_id_list) {
std::string unit_link = make_unit_link(type_id);
if (!unit_link.empty())
links_list.push_back(unit_link);
}
if (ordered)
std::sort(links_list.begin(), links_list.end());
return links_list;
}
void generate_races_sections(const config *help_cfg, section &sec, int level)
{
std::set<std::string, string_less> races;
std::set<std::string, string_less> visible_races;
for (const unit_type_data::unit_type_map::value_type &i : unit_types.types())
{
const unit_type &type = i.second;
UNIT_DESCRIPTION_TYPE desc_type = description_type(type);
if (desc_type == FULL_DESCRIPTION) {
races.insert(type.race_id());
if (!type.hide_help())
visible_races.insert(type.race_id());
}
}
for(std::set<std::string, string_less>::iterator it = races.begin(); it != races.end(); ++it) {
section race_section;
config section_cfg;
bool hidden = (visible_races.count(*it) == 0);
section_cfg["id"] = hidden_symbol(hidden) + race_prefix + *it;
std::string title;
if (const unit_race *r = unit_types.find_race(*it)) {
title = r->plural_name();
} else {
title = _ ("race^Miscellaneous");
}
section_cfg["title"] = title;
section_cfg["sections_generator"] = "units:" + *it;
section_cfg["generator"] = "units:" + *it;
parse_config_internal(help_cfg, &section_cfg, race_section, level+1);
sec.add_section(race_section);
}
}
void generate_era_sections(const config* help_cfg, section & sec, int level)
{
for (const config & era : game_cfg->child_range("era")) {
if (era["hide_help"].to_bool()) {
continue;
}
DBG_HP << "Adding help section: " << era["id"].str() << "\n";
section era_section;
config section_cfg;
section_cfg["id"] = era_prefix + era["id"].str();
section_cfg["title"] = era["name"];
section_cfg["generator"] = "era:" + era["id"].str();
DBG_HP << section_cfg.debug() << "\n";
parse_config_internal(help_cfg, &section_cfg, era_section, level+1);
sec.add_section(era_section);
}
}
void generate_terrain_sections(const config* /*help_cfg*/, section& sec, int /*level*/)
{
ter_data_cache tdata = load_terrain_types_data();
if (!tdata) {
WRN_HP << "When building terrain help sections, couldn't acquire terrain types data, aborting.\n";
return;
}
std::map<std::string, section> base_map;
const t_translation::ter_list& t_listi = tdata->list();
for (const t_translation::terrain_code& t : t_listi) {
const terrain_type& info = tdata->get_terrain_info(t);
bool hidden = info.is_combined() || info.hide_help();
if (preferences::encountered_terrains().find(t)
== preferences::encountered_terrains().end() && !info.is_overlay())
hidden = true;
topic terrain_topic;
terrain_topic.title = info.editor_name();
terrain_topic.id = hidden_symbol(hidden) + terrain_prefix + info.id();
terrain_topic.text = new terrain_topic_generator(info);
t_translation::ter_list base_terrains = tdata->underlying_union_terrain(t);
for (const t_translation::terrain_code& base : base_terrains) {
const terrain_type& base_info = tdata->get_terrain_info(base);
if (!base_info.is_nonnull() || base_info.hide_help())
continue;
section& base_section = base_map[base_info.id()];
base_section.id = terrain_prefix + base_info.id();
base_section.title = base_info.editor_name();
if (base_info.id() == info.id())
terrain_topic.id = ".." + terrain_prefix + info.id();
base_section.topics.push_back(terrain_topic);
}
}
for (std::map<std::string, section>::const_iterator it = base_map.begin(); it != base_map.end(); ++it) {
sec.add_section(it->second);
}
}
void generate_unit_sections(const config* /*help_cfg*/, section& sec, int level, const bool /*sort_generated*/, const std::string& race)
{
for (const unit_type_data::unit_type_map::value_type &i : unit_types.types()) {
const unit_type &type = i.second;
if (type.race_id() != race)
continue;
if (!type.show_variations_in_help())
continue;
section base_unit;
for (const std::string &variation_id : type.variations()) {
// TODO: Do we apply encountered stuff to variations?
const unit_type &var_type = type.get_variation(variation_id);
const std::string topic_name = var_type.type_name() + "\n" + var_type.variation_name();
const std::string var_ref = hidden_symbol(var_type.hide_help()) + variation_prefix + var_type.id() + "_" + variation_id;
topic var_topic(topic_name, var_ref, "");
var_topic.text = new unit_topic_generator(var_type, variation_id);
base_unit.topics.push_back(var_topic);
}
const std::string type_name = type.type_name();
const std::string ref_id = hidden_symbol(type.hide_help()) + unit_prefix + type.id();
base_unit.id = ref_id;
base_unit.title = type_name;
base_unit.level = level +1;
sec.add_section(base_unit);
}
}
std::vector<topic> generate_unit_topics(const bool sort_generated, const std::string& race)
{
std::vector<topic> topics;
std::set<std::string, string_less> race_units;
std::set<std::string, string_less> race_topics;
std::set<std::string> alignments;
for (const unit_type_data::unit_type_map::value_type &i : unit_types.types())
{
const unit_type &type = i.second;
if (type.race_id() != race)
continue;
UNIT_DESCRIPTION_TYPE desc_type = description_type(type);
if (desc_type != FULL_DESCRIPTION)
continue;
const std::string type_name = type.type_name();
const std::string real_prefix = type.show_variations_in_help() ? ".." : "";
const std::string ref_id = hidden_symbol(type.hide_help()) + real_prefix + unit_prefix + type.id();
topic unit_topic(type_name, ref_id, "");
unit_topic.text = new unit_topic_generator(type);
topics.push_back(unit_topic);
if (!type.hide_help()) {
// we also record an hyperlink of this unit
// in the list used for the race topic
std::string link = make_link(type_name, ref_id);
race_units.insert(link);
alignments.insert(make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
}
}
//generate the hidden race description topic
std::string race_id = "..race_"+race;
std::string race_name;
std::string race_description;
if (const unit_race *r = unit_types.find_race(race)) {
race_name = r->plural_name();
race_description = r->description();
// if (description.empty()) description = _("No description Available");
for (const config &additional_topic : r->additional_topics())
{
std::string id = additional_topic["id"];
std::string title = additional_topic["title"];
std::string text = additional_topic["text"];
//topic additional_topic(title, id, text);
topics.emplace_back(title,id,text);
std::string link = make_link(title, id);
race_topics.insert(link);
}
} else {
race_name = _ ("race^Miscellaneous");
// description = _("Here put the description of the Miscellaneous race");
}
std::stringstream text;
if (!race_description.empty()) {
text << race_description << "\n\n";
}
if (!alignments.empty()) {
std::set<std::string>::iterator it = alignments.begin();
text << (alignments.size() > 1 ? _("Alignments: ") : _("Alignment: ")) << *(it++);
while(it != alignments.end()) {
text << ", " << *(it++);
}
text << "\n\n";
}
text << _("<header>text='Units of this race'</header>") << "\n";
for (std::set<std::string, string_less>::iterator u = race_units.begin(); u != race_units.end(); ++u) {
text << font::unicode_bullet << " " << (*u) << "\n";
}
topics.emplace_back(race_name, race_id, text.str());
if (sort_generated)
std::sort(topics.begin(), topics.end(), title_less());
return topics;
}
UNIT_DESCRIPTION_TYPE description_type(const unit_type &type)
{
if (game_config::debug || preferences::show_all_units_in_help() ||
hotkey::is_scope_active(hotkey::SCOPE_EDITOR) ) {
return FULL_DESCRIPTION;
}
const std::set<std::string> &encountered_units = preferences::encountered_units();
if (encountered_units.find(type.id()) != encountered_units.end()) {
return FULL_DESCRIPTION;
}
return NO_DESCRIPTION;
}
std::string generate_about_text()
{
/*std::vector<std::string> about_lines = about::get_text();
std::vector<std::string> res_lines;
std::transform(about_lines.begin(), about_lines.end(), std::back_inserter(res_lines),
about_text_formatter());
res_lines.erase(std::remove(res_lines.begin(), res_lines.end(), ""), res_lines.end());
std::string text = utils::join(res_lines, "\n");
return text;*/
return "";
}
std::string generate_contents_links(const std::string& section_name, config const *help_cfg)
{
config const &section_cfg = help_cfg->find_child("section", "id", section_name);
if (!section_cfg) {
return std::string();
}
std::ostringstream res;
std::vector<std::string> topics = utils::quoted_split(section_cfg["topics"]);
// we use an intermediate structure to allow a conditional sorting
typedef std::pair<std::string,std::string> link;
std::vector<link> topics_links;
std::vector<std::string>::iterator t;
// Find all topics in this section.
for (t = topics.begin(); t != topics.end(); ++t) {
if (config const &topic_cfg = help_cfg->find_child("topic", "id", *t)) {
std::string id = topic_cfg["id"];
if (is_visible_id(id))
topics_links.emplace_back(topic_cfg["title"], id);
}
}
if (section_cfg["sort_topics"] == "yes") {
std::sort(topics_links.begin(),topics_links.end());
}
std::vector<link>::iterator l;
for (l = topics_links.begin(); l != topics_links.end(); ++l) {
std::string link = make_link(l->first, l->second);
res << font::unicode_bullet << " " << link << "\n";
}
return res.str();
}
std::string generate_contents_links(const section &sec, const std::vector<topic>& topics)
{
std::stringstream res;
section_list::const_iterator s;
for (s = sec.sections.begin(); s != sec.sections.end(); ++s) {
if (is_visible_id((*s)->id)) {
std::string link = make_link((*s)->title, ".."+(*s)->id);
res << font::unicode_bullet << " " << link << "\n";
}
}
std::vector<topic>::const_iterator t;
for (t = topics.begin(); t != topics.end(); ++t) {
if (is_visible_id(t->id)) {
std::string link = make_link(t->title, t->id);
res << font::unicode_bullet << " " << link << "\n";
}
}
return res.str();
}
bool topic::operator==(const topic &t) const
{
return t.id == id;
}
bool topic::operator<(const topic &t) const
{
return id < t.id;
}
section::~section()
{
std::for_each(sections.begin(), sections.end(), delete_section());
}
section::section(const section &sec) :
title(sec.title),
id(sec.id),
topics(sec.topics),
sections(),
level(sec.level)
{
std::transform(sec.sections.begin(), sec.sections.end(),
std::back_inserter(sections), create_section());
}
section& section::operator=(const section &sec)
{
title = sec.title;
id = sec.id;
level = sec.level;
topics.insert(topics.end(), sec.topics.begin(), sec.topics.end());
std::transform(sec.sections.begin(), sec.sections.end(),
std::back_inserter(sections), create_section());
return *this;
}
bool section::operator==(const section &sec) const
{
return sec.id == id;
}
bool section::operator<(const section &sec) const
{
return id < sec.id;
}
void section::add_section(const section &s)
{
sections.push_back(new section(s));
}
void section::clear()
{
topics.clear();
std::for_each(sections.begin(), sections.end(), delete_section());
sections.clear();
}
const topic *find_topic(const section &sec, const std::string &id)
{
topic_list::const_iterator tit =
std::find_if(sec.topics.begin(), sec.topics.end(), has_id(id));
if (tit != sec.topics.end()) {
return &(*tit);
}
section_list::const_iterator sit;
for (sit = sec.sections.begin(); sit != sec.sections.end(); ++sit) {
const topic *t = find_topic(*(*sit), id);
if (t != nullptr) {
return t;
}
}
return nullptr;
}
const section *find_section(const section &sec, const std::string &id)
{
section_list::const_iterator sit =
std::find_if(sec.sections.begin(), sec.sections.end(), has_id(id));
if (sit != sec.sections.end()) {
return *sit;
}
for (sit = sec.sections.begin(); sit != sec.sections.end(); ++sit) {
const section *s = find_section(*(*sit), id);
if (s != nullptr) {
return s;
}
}
return nullptr;
}
std::vector<std::string> parse_text(const std::string &text)
{
std::vector<std::string> res;
bool last_char_escape = false;
const char escape_char = '\\';
std::stringstream ss;
size_t pos;
enum { ELEMENT_NAME, OTHER } state = OTHER;
for (pos = 0; pos < text.size(); ++pos) {
const char c = text[pos];
if (c == escape_char && !last_char_escape) {
last_char_escape = true;
}
else {
if (state == OTHER) {
if (c == '<') {
if (last_char_escape) {
ss << c;
}
else {
res.push_back(ss.str());
ss.str("");
state = ELEMENT_NAME;
}
}
else {
ss << c;
}
}
else if (state == ELEMENT_NAME) {
if (c == '/') {
std::string msg = "Erroneous / in element name.";
throw parse_error(msg);
}
else if (c == '>') {
// End of this name.
std::stringstream s;
const std::string element_name = ss.str();
ss.str("");
s << "</" << element_name << ">";
const std::string end_element_name = s.str();
size_t end_pos = text.find(end_element_name, pos);
if (end_pos == std::string::npos) {
std::stringstream msg;
msg << "Unterminated element: " << element_name;
throw parse_error(msg.str());
}
s.str("");
const std::string contents = text.substr(pos + 1, end_pos - pos - 1);
const std::string element = convert_to_wml(element_name, contents);
res.push_back(element);
pos = end_pos + end_element_name.size() - 1;
state = OTHER;
}
else {
ss << c;
}
}
last_char_escape = false;
}
}
if (state == ELEMENT_NAME) {
std::stringstream msg;
msg << "Element '" << ss.str() << "' continues through end of string.";
throw parse_error(msg.str());
}
if (!ss.str().empty()) {
// Add the last string.
res.push_back(ss.str());
}
return res;
}
std::string convert_to_wml(const std::string &element_name, const std::string &contents)
{
std::stringstream ss;
bool in_quotes = false;
bool last_char_escape = false;
const char escape_char = '\\';
std::vector<std::string> attributes;
// Find the different attributes.
// No checks are made for the equal sign or something like that.
// Attributes are just separated by spaces or newlines.
// Attributes that contain spaces must be in single quotes.
for (size_t pos = 0; pos < contents.size(); ++pos) {
const char c = contents[pos];
if (c == escape_char && !last_char_escape) {
last_char_escape = true;
}
else {
if (c == '\'' && !last_char_escape) {
ss << '"';
in_quotes = !in_quotes;
}
else if ((c == ' ' || c == '\n') && !last_char_escape && !in_quotes) {
// Space or newline, end of attribute.
attributes.push_back(ss.str());
ss.str("");
}
else {
ss << c;
}
last_char_escape = false;
}
}
if (in_quotes) {
std::stringstream msg;
msg << "Unterminated single quote after: '" << ss.str() << "'";
throw parse_error(msg.str());
}
if (!ss.str().empty()) {
attributes.push_back(ss.str());
}
ss.str("");
// Create the WML.
ss << "[" << element_name << "]\n";
for (std::vector<std::string>::const_iterator it = attributes.begin();
it != attributes.end(); ++it) {
ss << *it << "\n";
}
ss << "[/" << element_name << "]\n";
return ss.str();
}
color_t string_to_color(const std::string &cmp_str)
{
if (cmp_str == "green") {
return font::GOOD_COLOR;
}
if (cmp_str == "red") {
return font::BAD_COLOR;
}
if (cmp_str == "black") {
return font::BLACK_COLOR;
}
if (cmp_str == "yellow") {
return font::YELLOW_COLOR;
}
if (cmp_str == "white") {
return font::BIGMAP_COLOR;
}
// a #rrggbb color in pango format.
if (*cmp_str.c_str() == '#' && cmp_str.size() == 7) {
return color_t::from_argb_bytes(strtoul(cmp_str.c_str() + 1, nullptr, 16));
}
return font::NORMAL_COLOR;
}
std::vector<std::string> split_in_width(const std::string &s, const int font_size,
const unsigned width)
{
std::vector<std::string> res;
try {
const std::string& first_line = font::word_wrap_text(s, font_size, width, -1, 1, true);
res.push_back(first_line);
if(s.size() > first_line.size()) {
res.push_back(s.substr(first_line.size()));
}
}
catch (utf8::invalid_utf8_exception&)
{
throw parse_error (_("corrupted original file"));
}
return res;
}
std::string remove_first_space(const std::string& text)
{
if (text.length() > 0 && text[0] == ' ') {
return text.substr(1);
}
return text;
}
std::string get_first_word(const std::string &s)
{
size_t first_word_start = s.find_first_not_of(' ');
if (first_word_start == std::string::npos) {
return s;
}
size_t first_word_end = s.find_first_of(" \n", first_word_start);
if( first_word_end == first_word_start ) {
// This word is '\n'.
first_word_end = first_word_start+1;
}
//if no gap(' ' or '\n') found, test if it is CJK character
std::string re = s.substr(0, first_word_end);
utf8::iterator ch(re);
if (ch == utf8::iterator::end(re))
return re;
ucs4::char_t firstchar = *ch;
if (font::is_cjk_char(firstchar)) {
re = unicode_cast<utf8::string>(firstchar);
}
return re;
}
void generate_contents()
{
default_toplevel.clear();
hidden_sections.clear();
if (game_cfg != nullptr) {
const config *help_config = &game_cfg->child("help");
if (!*help_config) {
help_config = &dummy_cfg;
}
try {
default_toplevel = parse_config(help_config);
// Create a config object that contains everything that is
// not referenced from the toplevel element. Read this
// config and save these sections and topics so that they
// can be referenced later on when showing help about
// specified things, but that should not be shown when
// opening the help browser in the default manner.
config hidden_toplevel;
std::stringstream ss;
for (const config &section : help_config->child_range("section"))
{
const std::string id = section["id"];
if (find_section(default_toplevel, id) == nullptr) {
// This section does not exist referenced from the
// toplevel. Hence, add it to the hidden ones if it
// is not referenced from another section.
if (!section_is_referenced(id, *help_config)) {
if (!ss.str().empty()) {
ss << ",";
}
ss << id;
}
}
}
hidden_toplevel["sections"] = ss.str();
ss.str("");
for (const config &topic : help_config->child_range("topic"))
{
const std::string id = topic["id"];
if (find_topic(default_toplevel, id) == nullptr) {
if (!topic_is_referenced(id, *help_config)) {
if (!ss.str().empty()) {
ss << ",";
}
ss << id;
}
}
}
hidden_toplevel["topics"] = ss.str();
config hidden_cfg = *help_config;
// Change the toplevel to our new, custom built one.
hidden_cfg.clear_children("toplevel");
hidden_cfg.add_child("toplevel", std::move(hidden_toplevel));
hidden_sections = parse_config(&hidden_cfg);
}
catch (parse_error& e) {
std::stringstream msg;
msg << "Parse error when parsing help text: '" << e.message << "'";
std::cerr << msg.str() << std::endl;
}
}
}
// id starting with '.' are hidden
std::string hidden_symbol(bool hidden) {
return (hidden ? "." : "");
}
bool is_visible_id(const std::string &id) {
return (id.empty() || id[0] != '.');
}
/// Return true if the id is valid for user defined topics and
/// sections. Some IDs are special, such as toplevel and may not be
/// be defined in the config.
bool is_valid_id(const std::string &id) {
if (id == "toplevel") {
return false;
}
if (id.compare(0, unit_prefix.length(), unit_prefix) == 0 || id.compare(hidden_symbol().length(), unit_prefix.length(), unit_prefix) == 0) {
return false;
}
if (id.compare(0, 8, "ability_") == 0) {
return false;
}
if (id.compare(0, 14, "weaponspecial_") == 0) {
return false;
}
if (id == "hidden") {
return false;
}
return true;
}
// Return the width for the image with filename.
unsigned image_width(const std::string &filename)
{
image::locator loc(filename);
surface surf(image::get_image(loc));
if (surf != nullptr) {
return surf->w;
}
return 0;
}
void push_tab_pair(std::vector<std::pair<std::string, unsigned int> > &v, const std::string &s)
{
v.emplace_back(s, font::line_width(s, normal_font_size));
}
std::string generate_table(const table_spec &tab, const unsigned int spacing)
{
table_spec::const_iterator row_it;
std::vector<std::pair<std::string, unsigned> >::const_iterator col_it;
unsigned int num_cols = 0;
for (row_it = tab.begin(); row_it != tab.end(); ++row_it) {
if (row_it->size() > num_cols) {
num_cols = row_it->size();
}
}
std::vector<unsigned int> col_widths(num_cols, 0);
// Calculate the width of all columns, including spacing.
for (row_it = tab.begin(); row_it != tab.end(); ++row_it) {
unsigned int col = 0;
for (col_it = row_it->begin(); col_it != row_it->end(); ++col_it) {
if (col_widths[col] < col_it->second + spacing) {
col_widths[col] = col_it->second + spacing;
}
++col;
}
}
std::vector<unsigned int> col_starts(num_cols);
// Calculate the starting positions of all columns
for (unsigned int i = 0; i < num_cols; ++i) {
unsigned int this_col_start = 0;
for (unsigned int j = 0; j < i; ++j) {
this_col_start += col_widths[j];
}
col_starts[i] = this_col_start;
}
std::stringstream ss;
for (row_it = tab.begin(); row_it != tab.end(); ++row_it) {
unsigned int col = 0;
for (col_it = row_it->begin(); col_it != row_it->end(); ++col_it) {
ss << jump_to(col_starts[col]) << col_it->first;
++col;
}
ss << "\n";
}
return ss.str();
}
/// Prepend all chars with meaning inside attributes with a backslash.
std::string escape(const std::string &s)
{
return utils::escape(s, "'\\");
}
/// Load the appropriate terrain types data to use
ter_data_cache load_terrain_types_data() {
if (display::get_singleton()) {
return display::get_singleton()->get_disp_context().map().tdata();
} else if (game_config_manager::get()){
return game_config_manager::get()->terrain_types();
} else {
return ter_data_cache();
}
}
} // end namespace help