Help: massive refactor and cleanup of the help backend

This basically splits all the stuff in help/help_impl.*pp into multiple files by their
function, and refactors the entire workflow into a proper object-oriented interface in
modern C++.

There are still a features missing (such as hidden section parsing in the manager) that
I'll get back to.

Few incidental changes and fixes:
* Terrain topics now sorted alphabetically.
* Help text now small
* Fixed wrong toggle button id in browser. This is what was making it impossible to expand
  any sections.

The GUI2 help browser is now back in working order, inasmuch as you can view all sections'
and topics' text (save units').
This commit is contained in:
Charles Dang 2018-04-03 11:51:57 +11:00
parent 2caed06cdb
commit 23e78abb20
27 changed files with 2661 additions and 404 deletions

View file

@ -67,7 +67,7 @@
{_GUI_NODE "section" ( {_GUI_NODE "section" (
[toggle_button] [toggle_button]
id = "tree_view_node_icon" id = "tree_view_node_toggle"
definition = "help_section_toggle" definition = "help_section_toggle"
linked_group = "images" linked_group = "images"
[/toggle_button] [/toggle_button]
@ -188,7 +188,7 @@
vertical_grow = true vertical_grow = true
[scroll_label] [scroll_label]
definition = "default" definition = "default_small"
id = "topic_text" id = "topic_text"
use_markup = true use_markup = true
[/scroll_label] [/scroll_label]

View file

@ -2399,6 +2399,9 @@
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\halo.cpp" /> <ClCompile Include="..\..\src\halo.cpp" />
<ClCompile Include="..\..\src\hash.cpp" /> <ClCompile Include="..\..\src\hash.cpp" />
<ClCompile Include="..\..\src\help\constants.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile>
<ClCompile Include="..\..\src\help\help.cpp"> <ClCompile Include="..\..\src\help\help.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)Help\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='ReleaseDEBUG|Win32'">$(IntDir)Help\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='ReleaseDEBUG|Win32'">$(IntDir)Help\</ObjectFileName>
@ -2406,19 +2409,26 @@
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Debug|Win32'">$(IntDir)Help\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Debug|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Release|Win32'">$(IntDir)Help\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\help\help_impl.cpp"> <ClCompile Include="..\..\src\help\manager.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='ReleaseDEBUG|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Debug|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\help\help_topic_generators.cpp"> <ClCompile Include="..\..\src\help\section.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)Help\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='ReleaseDEBUG|Win32'">$(IntDir)Help\</ObjectFileName> </ClCompile>
<ClCompile Include="..\..\src\help\section_generators.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile>
<ClCompile Include="..\..\src\help\topic.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile>
<ClCompile Include="..\..\src\help\topic_generators.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile>
<ClCompile Include="..\..\src\help\topic_text_generators.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile>
<ClCompile Include="..\..\src\help\utils.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Debug|Win32'">$(IntDir)Help\</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Test_Release|Win32'">$(IntDir)Help\</ObjectFileName>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\hotkey\command_executor.cpp"> <ClCompile Include="..\..\src\hotkey\command_executor.cpp">
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)Hotkeys\</ObjectFileName> <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)Hotkeys\</ObjectFileName>
@ -3825,9 +3835,15 @@
<ClInclude Include="..\..\src\gui\widgets\window_private.hpp" /> <ClInclude Include="..\..\src\gui\widgets\window_private.hpp" />
<ClInclude Include="..\..\src\halo.hpp" /> <ClInclude Include="..\..\src\halo.hpp" />
<ClInclude Include="..\..\src\hash.hpp" /> <ClInclude Include="..\..\src\hash.hpp" />
<ClInclude Include="..\..\src\help\constants.hpp" />
<ClInclude Include="..\..\src\help\help.hpp" /> <ClInclude Include="..\..\src\help\help.hpp" />
<ClInclude Include="..\..\src\help\help_impl.hpp" /> <ClInclude Include="..\..\src\help\manager.hpp" />
<ClInclude Include="..\..\src\help\help_topic_generators.hpp" /> <ClInclude Include="..\..\src\help\section.hpp" />
<ClInclude Include="..\..\src\help\section_generators.hpp" />
<ClInclude Include="..\..\src\help\topic.hpp" />
<ClInclude Include="..\..\src\help\topic_generators.hpp" />
<ClInclude Include="..\..\src\help\topic_text_generators.hpp" />
<ClInclude Include="..\..\src\help\utils.hpp" />
<ClInclude Include="..\..\src\hotkey\command_executor.hpp" /> <ClInclude Include="..\..\src\hotkey\command_executor.hpp" />
<ClInclude Include="..\..\src\hotkey\hotkey_command.hpp" /> <ClInclude Include="..\..\src\hotkey\hotkey_command.hpp" />
<ClInclude Include="..\..\src\hotkey\hotkey_handler.hpp" /> <ClInclude Include="..\..\src\hotkey\hotkey_handler.hpp" />

View file

@ -1295,12 +1295,6 @@
<ClCompile Include="..\..\src\help\help.cpp"> <ClCompile Include="..\..\src\help\help.cpp">
<Filter>Help</Filter> <Filter>Help</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\help\help_impl.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\help_topic_generators.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\formula\callable_objects.cpp"> <ClCompile Include="..\..\src\formula\callable_objects.cpp">
<Filter>Formula</Filter> <Filter>Formula</Filter>
</ClCompile> </ClCompile>
@ -1536,6 +1530,30 @@
<ClCompile Include="..\..\src\preferences\lobby.cpp"> <ClCompile Include="..\..\src\preferences\lobby.cpp">
<Filter>Preferences</Filter> <Filter>Preferences</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\help\constants.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\section.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\section_generators.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\topic.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\topic_generators.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\topic_text_generators.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\utils.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\help\manager.cpp">
<Filter>Help</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\..\src\addon\client.hpp"> <ClInclude Include="..\..\src\addon\client.hpp">
@ -2684,12 +2702,6 @@
<ClInclude Include="..\..\src\help\help.hpp"> <ClInclude Include="..\..\src\help\help.hpp">
<Filter>Help</Filter> <Filter>Help</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\..\src\help\help_impl.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\help_topic_generators.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\formula\callable.hpp"> <ClInclude Include="..\..\src\formula\callable.hpp">
<Filter>Formula</Filter> <Filter>Formula</Filter>
</ClInclude> </ClInclude>
@ -2979,6 +2991,30 @@
<ClInclude Include="..\..\src\map\hex_rect.hpp"> <ClInclude Include="..\..\src\map\hex_rect.hpp">
<Filter>Map</Filter> <Filter>Map</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\..\src\help\constants.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\section.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\section_generators.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\topic.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\topic_generators.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\topic_text_generators.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\utils.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\help\manager.hpp">
<Filter>Help</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<CustomBuild Include="..\..\src\tests\test_sdl_utils.hpp"> <CustomBuild Include="..\..\src\tests\test_sdl_utils.hpp">

View file

@ -266,9 +266,15 @@ gui/widgets/viewport.cpp
gui/widgets/widget.cpp gui/widgets/widget.cpp
gui/widgets/widget_helpers.cpp gui/widgets/widget_helpers.cpp
halo.cpp halo.cpp
help/constants.cpp
help/help.cpp help/help.cpp
help/help_impl.cpp help/manager.cpp
help/help_topic_generators.cpp help/section.cpp
help/section_generators.cpp
help/topic.cpp
help/topic_generators.cpp
help/topic_text_generators.cpp
help/utils.cpp
hotkey/hotkey_handler.cpp hotkey/hotkey_handler.cpp
hotkey/hotkey_handler_mp.cpp hotkey/hotkey_handler_mp.cpp
hotkey/hotkey_handler_sp.cpp hotkey/hotkey_handler_sp.cpp

View file

@ -159,6 +159,8 @@ void editor_controller::init_music(const config& game_config)
editor_controller::~editor_controller() editor_controller::~editor_controller()
{ {
help::reset();
resources::tod_manager = nullptr; resources::tod_manager = nullptr;
resources::filter_con = nullptr; resources::filter_con = nullptr;

View file

@ -16,47 +16,29 @@
#include "gui/dialogs/help_browser.hpp" #include "gui/dialogs/help_browser.hpp"
#include "game_config_manager.hpp" #include "font/pango/escape.hpp"
#include "font/pango/hyperlink.hpp"
#include "gui/auxiliary/find_widget.hpp" #include "gui/auxiliary/find_widget.hpp"
#include "gui/widgets/button.hpp" #include "gui/widgets/button.hpp"
#include "gui/widgets/image.hpp"
#include "gui/widgets/multi_page.hpp" #include "gui/widgets/multi_page.hpp"
#include "gui/widgets/scroll_label.hpp"
#include "gui/widgets/settings.hpp" #include "gui/widgets/settings.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/text_box.hpp"
#include "gui/widgets/tree_view.hpp" #include "gui/widgets/tree_view.hpp"
#include "gui/widgets/tree_view_node.hpp" #include "gui/widgets/tree_view_node.hpp"
#include "gui/widgets/window.hpp" #include "gui/widgets/window.hpp"
#include "help/section.hpp"
#ifdef GUI2_EXPERIMENTAL_LISTBOX #include "help/topic.hpp"
#include "gui/widgets/list.hpp" #include "help/utils.hpp"
#else
#include "gui/widgets/listbox.hpp"
#endif
#include "help/help.hpp"
#include "help/help_impl.hpp"
#include "font/pango/escape.hpp"
#include "font/pango/hyperlink.hpp"
namespace gui2 namespace gui2
{ {
namespace dialogs namespace dialogs
{ {
REGISTER_DIALOG(help_browser) REGISTER_DIALOG(help_browser)
help_browser::help_browser(const help::section& toplevel, const std::string& initial) help_browser::help_browser(const help::section& toplevel, const std::string& initial)
: initial_topic_(initial.empty() ? help::default_show_topic : initial) : initial_topic_(initial)
, toplevel_(toplevel) , toplevel_(toplevel)
{ {
if(initial_topic_.compare(0, 2, "..") == 0) {
initial_topic_.replace(0, 2, "+");
} else {
initial_topic_.insert(0, "-");
}
help::init_help();
} }
void help_browser::pre_show(window& window) void help_browser::pre_show(window& window)
@ -69,8 +51,11 @@ void help_browser::pre_show(window& window)
next_button.set_visible(widget::visibility::hidden); next_button.set_visible(widget::visibility::hidden);
back_button.set_visible(widget::visibility::hidden); back_button.set_visible(widget::visibility::hidden);
connect_signal_mouse_left_click(back_button, std::bind(&help_browser::on_history_navigate, this, std::ref(window), true)); connect_signal_mouse_left_click(back_button,
connect_signal_mouse_left_click(next_button, std::bind(&help_browser::on_history_navigate, this, std::ref(window), false)); std::bind(&help_browser::on_history_navigate, this, std::ref(window), true));
connect_signal_mouse_left_click(next_button,
std::bind(&help_browser::on_history_navigate, this, std::ref(window), false));
topic_tree.set_selection_change_callback(std::bind(&help_browser::on_topic_select, this, std::ref(window))); topic_tree.set_selection_change_callback(std::bind(&help_browser::on_topic_select, this, std::ref(window)));
@ -86,21 +71,21 @@ void help_browser::pre_show(window& window)
void help_browser::add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node) void help_browser::add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node)
{ {
for(const help::section* section : parent_section.sections) { for(const auto& section : parent_section.sections()) {
tree_view_node& section_node = add_topic(section->id, section->title, true, parent_node); tree_view_node& section_node = add_topic(section->id, section->title, true, parent_node);
add_topics_for_section(*section, section_node); add_topics_for_section(*section, section_node);
} }
for(const help::topic& topic : parent_section.topics) { for(const help::topic& topic : parent_section.topics()) {
if(topic.id.compare(0,2,"..") != 0) { if(topic.id.compare(0, 2, "..") != 0) {
add_topic(topic.id, topic.title, false, parent_node); add_topic(topic.id, topic.title, false, parent_node);
} }
} }
} }
tree_view_node& help_browser::add_topic(const std::string& topic_id, const std::string& topic_title, tree_view_node& help_browser::add_topic(
bool expands, tree_view_node& parent) const std::string& topic_id, const std::string& topic_title, bool expands, tree_view_node& parent)
{ {
std::map<std::string, string_map> data; std::map<std::string, string_map> data;
string_map item; string_map item;
@ -119,12 +104,14 @@ static std::string format_help_text(const config& cfg)
std::stringstream ss; std::stringstream ss;
for(auto& item : cfg.all_children_range()) { for(auto& item : cfg.all_children_range()) {
if(item.key == "text") { if(item.key == "text") {
ss << font::escape_text(item.cfg["text"]); //ss << font::escape_text(item.cfg["text"]);
ss << item.cfg["text"];
} else if(item.key == "ref") { } else if(item.key == "ref") {
if(item.cfg["dst"].empty()) { if(item.cfg["dst"].empty()) {
std::stringstream msg; std::stringstream msg;
msg << "Ref markup must have dst attribute. Please submit a bug" msg << "Ref markup must have dst attribute. Please submit a bug"
" report if you have not modified the game files yourself. Erroneous config: " << cfg; " report if you have not modified the game files yourself. Erroneous config: "
<< cfg;
throw help::parse_error(msg.str()); throw help::parse_error(msg.str());
}; };
// TODO: Get the proper link shade from somewhere // TODO: Get the proper link shade from somewhere
@ -141,31 +128,9 @@ static std::string format_help_text(const config& cfg)
throw help::parse_error("Jump markup must have either a to or an amount attribute."); throw help::parse_error("Jump markup must have either a to or an amount attribute.");
} }
ss << '\t'; ss << '\t';
} else if(item.key == "format") {
if(item.cfg["text"].empty()) {
throw help::parse_error("Header markup must have text attribute.");
}
ss << "<span";
if(item.cfg.has_attribute("font_size")) {
ss << " size='" << item.cfg["font_size"].to_int(font::SIZE_NORMAL) << "'";
}
if(item.cfg.has_attribute("color")) {
ss << " color='" << help::string_to_color(item.cfg["color"]).to_hex_string() << "'";
}
if(item.cfg["bold"]) {
ss << " font_weight='bold'";
}
if(item.cfg["italic"]) {
ss << " font_style='italic'";
}
ss << '>' << font::escape_text(item.cfg["text"]) << "</span>";
} }
} }
return ss.str(); return ss.str();
} }
@ -197,7 +162,7 @@ void help_browser::on_topic_select(window& window)
auto iter = parsed_pages_.find(topic_id); auto iter = parsed_pages_.find(topic_id);
if(iter == parsed_pages_.end()) { if(iter == parsed_pages_.end()) {
const help::topic* topic = help::find_topic(sec, topic_id); const help::topic* topic = sec.find_topic(topic_id);
if(topic == nullptr) { if(topic == nullptr) {
return; return;
} }
@ -205,7 +170,7 @@ void help_browser::on_topic_select(window& window)
std::map<std::string, string_map> data; std::map<std::string, string_map> data;
string_map item; string_map item;
item["label"] = format_help_text(topic->text.parsed_text()); item["label"] = format_help_text(topic->parsed_text());
data.emplace("topic_text", item); data.emplace("topic_text", item);
item.clear(); item.clear();
@ -215,6 +180,7 @@ void help_browser::on_topic_select(window& window)
parsed_pages_.emplace(topic_id, topic_pages.get_page_count()); parsed_pages_.emplace(topic_id, topic_pages.get_page_count());
topic_pages.add_page(data); topic_pages.add_page(data);
// TODO: refactor out
window.invalidate_layout(); window.invalidate_layout();
} }
@ -226,8 +192,9 @@ void help_browser::on_topic_select(window& window)
history_pos_ = std::prev(history_.end()); history_pos_ = std::prev(history_.end());
if(history_pos_ != history_.begin()) { if(history_pos_ != history_.begin()) {
find_widget<button>(&window, "back", false).set_visible(widget::visibility::visible); find_widget<button>(&window, "back", false).set_visible(widget::visibility::visible);
} }
find_widget<button>(&window, "next", false).set_visible(widget::visibility::hidden); find_widget<button>(&window, "next", false).set_visible(widget::visibility::hidden);
const unsigned topic_i = parsed_pages_.at(topic_id); const unsigned topic_i = parsed_pages_.at(topic_id);
@ -237,15 +204,18 @@ void help_browser::on_topic_select(window& window)
void help_browser::on_history_navigate(window& window, bool backwards) void help_browser::on_history_navigate(window& window, bool backwards)
{ {
if(backwards) { if(backwards) {
history_pos_--; --history_pos_;
} else { } else {
history_pos_++; ++history_pos_;
} }
find_widget<button>(&window, "back", false).set_visible( find_widget<button>(&window, "back", false).set_visible(history_pos_ == history_.begin()
history_pos_ == history_.begin() ? widget::visibility::hidden : widget::visibility::visible); ? widget::visibility::hidden
find_widget<button>(&window, "next", false).set_visible( : widget::visibility::visible);
history_pos_ == std::prev(history_.end()) ? widget::visibility::hidden : widget::visibility::visible);
find_widget<button>(&window, "next", false).set_visible(history_pos_ == std::prev(history_.end())
? widget::visibility::hidden
: widget::visibility::visible);
const unsigned topic_i = parsed_pages_.at(*history_pos_); const unsigned topic_i = parsed_pages_.at(*history_pos_);
find_widget<multi_page>(&window, "topic_text_pages", false).select_page(topic_i); find_widget<multi_page>(&window, "topic_text_pages", false).select_page(topic_i);

View file

@ -22,7 +22,7 @@
class config; class config;
namespace help { namespace help {
struct section; class section;
} }
namespace gui2 namespace gui2

View file

@ -33,7 +33,7 @@
#include "preferences/game.hpp" #include "preferences/game.hpp"
#include "gettext.hpp" #include "gettext.hpp"
#include "help/help.hpp" #include "help/help.hpp"
#include "help/help_impl.hpp" #include "help/utils.hpp"
#include "play_controller.hpp" #include "play_controller.hpp"
#include "resources.hpp" #include "resources.hpp"
#include "team.hpp" #include "team.hpp"

36
src/help/constants.cpp Normal file
View file

@ -0,0 +1,36 @@
/*
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/constants.hpp"
namespace help
{
const unsigned max_section_recursion_level = 15;
const unsigned max_history = 100;
const std::string default_topic = "..introduction";
const std::string unknown_unit_topic = ".unknown_unit";
const std::string ability_prefix = "ability_";
const std::string era_prefix = "era_";
const std::string faction_prefix = "faction_";
const std::string race_prefix = "race_";
const std::string terrain_prefix = "terrain_";
const std::string tod_prefix = "time_of_day_";
const std::string trait_prefix = "traits_";
const std::string unit_prefix = "unit_";
const std::string variation_prefix = "variation_";
const std::string weapon_special_prefix = "weaponspecial_";
} // namespace help

49
src/help/constants.hpp Normal file
View file

@ -0,0 +1,49 @@
/*
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.
*/
#pragma once
#include <string>
namespace help
{
extern const unsigned max_section_recursion_level;
extern const unsigned max_history;
//
// Constant topics IDs
//
/** Topic to open by default when opening the help browser. */
extern const std::string default_topic;
/** Topic to show when a unit hasn't been encountered in-game yet */
extern const std::string unknown_unit_topic;
//
// Standard topic ID prefixes
//
extern const std::string ability_prefix;
extern const std::string era_prefix;
extern const std::string faction_prefix;
extern const std::string race_prefix;
extern const std::string terrain_prefix;
extern const std::string tod_prefix;
extern const std::string trait_prefix;
extern const std::string unit_prefix;
extern const std::string variation_prefix;
extern const std::string weapon_special_prefix;
} // namespace help

View file

@ -17,33 +17,35 @@
* Routines for showing the help-dialog. * Routines for showing the help-dialog.
*/ */
#define GETTEXT_DOMAIN "wesnoth-help"
#include "help/help.hpp" #include "help/help.hpp"
#include "config.hpp" // for config, etc #include "help/constants.hpp"
#include "gettext.hpp" // for _ #include "help/manager.hpp"
#include "gui/dialogs/help_browser.hpp" #include "help/utils.hpp"
#include "help/help_impl.hpp" #include "terrain/terrain.hpp"
#include "preferences/game.hpp" #include "units/types.hpp"
#include "terrain/terrain.hpp" // for terrain_type #include "units/unit.hpp"
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "units/unit.hpp" // for unit
#include <cassert> // for assert #include <cassert>
#include <ostream> // for basic_ostream, operator<<, etc
#include <vector> // for vector, vector<>::iterator
namespace help namespace help
{ {
void show_unit_description(const unit& u) /** The help manager. What else would it be? */
static help_manager manager;
void show_help(const std::string& show_topic)
{ {
help::show_unit_description(u.type()); manager.open_help_browser_to(show_topic);
} }
void show_terrain_description(const terrain_type& t) void reset()
{ {
help::show_terrain_help(t.id(), t.hide_in_editor() || t.is_combined()); manager.reset_contents();
}
void show_unit_description(const unit& u)
{
show_unit_description(u.type());
} }
void show_unit_description(const unit_type& t) void show_unit_description(const unit_type& t)
@ -68,54 +70,30 @@ void show_unit_description(const unit_type& t)
} }
if(use_variation) { if(use_variation) {
help::show_variation_help(t.id(), var_id, hide_help); show_variation_help(t.id(), var_id, hide_help);
} else { } else {
help::show_unit_help(t.id(), t.show_variations_in_help(), hide_help); show_unit_help(t.id(), t.show_variations_in_help(), hide_help);
} }
} }
void show_help(const std::string& show_topic)
{
show_help(default_toplevel, show_topic);
}
void show_unit_help(const std::string& show_topic, bool has_variations, bool hidden) void show_unit_help(const std::string& show_topic, bool has_variations, bool hidden)
{ {
show_help(default_toplevel, hidden_symbol(hidden) + (has_variations ? ".." : "") + unit_prefix + show_topic); show_help(hidden_symbol(hidden) + (has_variations ? ".." : "") + unit_prefix + show_topic);
}
void show_terrain_help(const std::string& show_topic, bool hidden)
{
show_help(default_toplevel, hidden_symbol(hidden) + terrain_prefix + show_topic);
} }
void show_variation_help(const std::string& unit, const std::string& variation, bool hidden) void show_variation_help(const std::string& unit, const std::string& variation, bool hidden)
{ {
show_help(default_toplevel, hidden_symbol(hidden) + variation_prefix + unit + "_" + variation); show_help(hidden_symbol(hidden) + variation_prefix + unit + "_" + variation);
} }
void init_help() void show_terrain_description(const terrain_type& t)
{ {
// Find all unit_types that have not been constructed yet and fill in the information show_terrain_help(t.id(), t.hide_in_editor() || t.is_combined());
// needed to create the help topics
unit_types.build_all(unit_type::HELP_INDEXED);
if(preferences::encountered_units().size() != std::size_t(last_num_encountered_units) ||
preferences::encountered_terrains().size() != std::size_t(last_num_encountered_terrains) ||
last_debug_state != game_config::debug || last_num_encountered_units < 0)
{
// More units or terrains encountered, update the contents.
last_num_encountered_units = preferences::encountered_units().size();
last_num_encountered_terrains = preferences::encountered_terrains().size();
last_debug_state = game_config::debug;
generate_contents();
}
} }
void show_help(const section& toplevel_sec, const std::string& show_topic) void show_terrain_help(const std::string& show_topic, bool hidden)
{ {
gui2::dialogs::help_browser::display(toplevel_sec, show_topic); show_help(hidden_symbol(hidden) + terrain_prefix + show_topic);
} }
} // End namespace help. } // End namespace help.

View file

@ -16,24 +16,14 @@
#include <string> #include <string>
class config;
class terrain_type; class terrain_type;
class unit; class unit;
class unit_type; class unit_type;
namespace help namespace help
{ {
struct section; /** Flags the help manager's contents for regeneration. */
void reset();
void init_help();
/**
* Open a help dialog using a toplevel other than the default.
*
* This allows for complete customization of the contents, although not in a
* very easy way.
*/
void show_help(const section& toplevel, const std::string& show_topic = "");
/** /**
* Open the help browser, show topic with id show_topic. * Open the help browser, show topic with id show_topic.

194
src/help/manager.cpp Normal file
View file

@ -0,0 +1,194 @@
/*
Copyright (C) 2018 by 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/manager.hpp"
#include "config.hpp"
#include "game_config.hpp"
#include "game_config_manager.hpp"
#include "gui/dialogs/help_browser.hpp"
#include "help/constants.hpp"
#include "help/utils.hpp"
#include "preferences/game.hpp"
#include "units/types.hpp"
namespace help
{
help_manager::help_manager()
: game_cfg_(nullptr)
, help_cfg_(nullptr)
, toplevel_section_(nullptr)
, hidden_sections_(nullptr)
, num_last_encountered_units_(-1)
, num_last_encountered_terrains_(-1)
, last_debug_state_(boost::indeterminate)
{
}
#if 0
void init_help()
{
// Find all unit_types that have not been constructed yet and fill in the information
// needed to create the help topics
//unit_types.build_all(unit_type::HELP_INDEXED);
if(preferences::encountered_units().size() != std::size_t(last_num_encountered_units) ||
preferences::encountered_terrains().size() != std::size_t(last_num_encountered_terrains) ||
last_debug_state != game_config::debug || last_num_encountered_units < 0)
{
// More units or terrains encountered, update the contents.
last_num_encountered_units = preferences::encountered_units().size();
last_num_encountered_terrains = preferences::encountered_terrains().size();
last_debug_state = game_config::debug;
generate_contents();
}
}
#endif
void help_manager::open_help_browser_to(std::string show_topic)
{
// Find all unit_types that have not been constructed yet and fill in the information
// needed to create the help topics
unit_types.build_all(unit_type::HELP_INDEXED);
// Build the actual stuff to display, if we need to.
if(content_update_needed()) {
num_last_encountered_units_ = preferences::encountered_units().size();
num_last_encountered_terrains_ = preferences::encountered_terrains().size();
last_debug_state_ = game_config::debug;
build_topic_tree();
}
if(show_topic.empty()) {
show_topic = help::default_topic;
}
// TODO: what's this for?
if(show_topic.compare(0, 2, "..") == 0) {
show_topic.replace(0, 2, "+");
} else {
show_topic.insert(0, "-");
}
assert(toplevel_section_);
gui2::dialogs::help_browser::display(*toplevel_section_, show_topic);
}
bool help_manager::content_update_needed() const
{
return
toplevel_section_ == nullptr ||
preferences::encountered_units().size() != static_cast<std::size_t>(num_last_encountered_units_) ||
preferences::encountered_terrains().size() != static_cast<std::size_t>(num_last_encountered_terrains_) ||
last_debug_state_ != game_config::debug ||
num_last_encountered_units_ < 0;
}
void help_manager::reset_contents()
{
toplevel_section_.reset(nullptr);
hidden_sections_.reset(nullptr);
num_last_encountered_units_ = -1;
num_last_encountered_terrains_ = -1;
}
void help_manager::update_config_pointers()
{
game_cfg_ = &game_config_manager::get()->game_config();
assert(game_cfg_);
help_cfg_ = &game_cfg_->child_or_empty("help");
assert(help_cfg_);
}
void help_manager::build_topic_tree()
{
// We probaby don't need to update the pointers every time, but it's the
// simplest way to ensure these are always valid.
update_config_pointers();
try {
// Start by parsing [toplevel]. It cascades down and parses all referenced sections and topics.
toplevel_section_ = std::make_unique<section>(help_cfg_->child_or_empty("toplevel"), this);
#if 0
//
// TODO: REIMPLEMENT
//
// 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::ostringstream 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 is not 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);
#endif
} catch(const parse_error& e) {
std::cerr << "Error while parsing help text: '" << e.message << "'" << std::endl;
}
}
const config& help_manager::get_section_config(const std::string& id) const
{
return help_cfg_->find_child("section", "id", id);
}
const config& help_manager::get_topic_config(const std::string& id) const
{
return help_cfg_->find_child("topic", "id", id);
}
} // namespace help

83
src/help/manager.hpp Normal file
View file

@ -0,0 +1,83 @@
/*
Copyright (C) 2018 by 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.
*/
#pragma once
#include "config.hpp"
#include "help/section.hpp"
#include "help/topic.hpp"
#include <boost/logic/tribool.hpp>
#include <memory>
#include <string>
class config;
namespace help
{
class help_manager
{
public:
help_manager();
/**
* Displays the help browser.
*
* @param show_topic The initial topic to open to.
*
* The topic tree will be regenerated as needed beforehand.
*
* @todo should we allow passing an arbitrary toplevel section here?
* The old code did expose a public interface to open to any topic, but it wasn't
* used in any user-facing code, only as an implementation helper for the other
* help display functions.
*/
void open_help_browser_to(std::string show_topic);
/** Clears the generated section data. */
void reset_contents();
/***** ***** ***** ***** Data getters ***** ***** ****** *****/
/** Returns the [section] child with the given id. */
const config& get_section_config(const std::string& id) const;
/** Returns the [topic] child with the given id. */
const config& get_topic_config(const std::string& id) const;
private:
/** Returns true if we need to regenerate the toplevel and hidden sections. */
bool content_update_needed() const;
void update_config_pointers();
/** Fills in both toplevel_section_ and hidden_sections_. */
void build_topic_tree();
const config* game_cfg_;
const config* help_cfg_;
/** The default toplevel section node. */
section::section_ptr toplevel_section_;
/** All sections and topics not referenced from the default toplevel node. */
section::section_ptr hidden_sections_;
int num_last_encountered_units_;
int num_last_encountered_terrains_;
boost::tribool last_debug_state_;
};
} // namespace help

322
src/help/section.cpp Normal file
View file

@ -0,0 +1,322 @@
/*
Copyright (C) 2018 by 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/section.hpp"
#include "config.hpp"
#include "formatter.hpp"
#include "gettext.hpp"
#include "help/constants.hpp"
#include "help/manager.hpp"
#include "help/section_generators.hpp"
#include "help/topic_generators.hpp"
#include "help/utils.hpp"
#include "log.hpp"
#include "serialization/string_utils.hpp"
#include <iterator>
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
{
section::section(const config& section_cfg, const help_manager* manager)
: id("toplevel")
, title()
, topics_()
, sections_()
, recursion_level_(0)
, manager_(manager)
{
assert(manager_ && "Y U NO provide help manager!");
initialize(section_cfg);
}
section::section(const config& section_cfg, const section& parent)
: id(section_cfg["id"])
, title(section_cfg["title"])
, topics_()
, sections_()
, recursion_level_(parent.recursion_level_ + 1)
, manager_(parent.manager_)
{
assert(manager_);
initialize(section_cfg);
}
void section::initialize(const config& section_cfg)
{
// If we're in too deep, exit.
if(recursion_level_ > max_section_recursion_level) {
throw max_recursion_reached("Maximum section depth has been reached. Possible circular dependency?");
}
if(recursion_level_ > 0 && !is_valid_id(id)) {
throw parse_error(formatter() << "Invalid ID, used for internal purpose: '" << id << "'");
}
if(section_cfg.empty()) {
return; // TODO: throw something?
}
//
// Parse static sub-sections.
//
for(const std::string& child_section : utils::quoted_split(section_cfg["sections"])) {
if(const config& child_cfg = manager_->get_section_config(child_section)) {
add_section(child_cfg);
} else {
throw parse_error(formatter()
<< "Help section '" << child_section << "' referenced from '" << id << "' but could not be found.");
}
}
//
// Generate dynamic sub-sections.
//
generate_sections(section_cfg["sections_generator"].str());
if(section_cfg["sort_sections"] == "yes") {
std::sort(sections_.begin(), sections_.end(), [](const section_ptr& lhs, const section_ptr& rhs) {
return translation::compare(lhs->title, rhs->title) < 0; }
);
}
const config::attribute_value& st = section_cfg["sort_topics"];
bool sort_topics = false;
bool sort_generated = false;
if(st == "yes") {
sort_topics = true;
sort_generated = false;
} else if(st == "no") {
sort_topics = false;
sort_generated = false;
} else if(st == "generated") {
sort_topics = false;
sort_generated = true;
} else if(!st.empty()) {
throw parse_error(formatter() << "Invalid sort option: '" << st << "'");
}
//
// Parse static section topics
//
for(const std::string& topic_id : utils::quoted_split(section_cfg["topics"])) {
if(const config& topic_cfg = manager_->get_topic_config(topic_id)) {
std::ostringstream text;
text << topic_cfg["text"];
text << generate_table_of_contents(topic_cfg["generator"]);
if(!is_valid_id(topic_id)) {
throw parse_error(formatter() << "Invalid ID, used for internal purpose: '" << id << "'");
}
// We don't need to use add_topic here since we don't need a special text generator.
topics_.emplace_back(topic_id, topic_cfg["title"], text.str());
} else {
throw parse_error(formatter()
<< "Help-topic '" << topic_id << "' referenced from '" << id << "' but could not be found.");
}
}
//
// Generate dynamic topics.
//
generate_topics(section_cfg["generator"], sort_generated);
if(sort_topics) {
this->sort_topics();
}
}
void section::generate_sections(const std::string& generator_type)
{
if(generator_type == "races") {
generate_races_sections(*this);
} else if(generator_type == "terrains") {
generate_terrain_sections(*this);
} else if(generator_type == "eras") {
DBG_HP << "Generating eras...\n";
generate_era_sections(*this);
} else {
std::vector<std::string> parts = utils::split(generator_type, ':', utils::STRIP_SPACES);
if(parts.size() > 1 && parts[0] == "units") {
generate_unit_sections(*this, parts[1]);
} else if(!generator_type.empty()) {
WRN_HP << "Unknown section generator: " << generator_type << "\n";
}
}
}
void section::generate_topics(const std::string& generator_type, const bool sort_generated)
{
if(generator_type.empty()) {
return;
}
topic_list res;
if(generator_type == "abilities") {
res = generate_ability_topics(sort_generated);
} else if(generator_type == "weapon_specials") {
res = generate_weapon_special_topics(sort_generated);
} else if(generator_type == "time_of_days") {
res = generate_time_of_day_topics(sort_generated);
} else if(generator_type == "traits") {
res = generate_trait_topics(sort_generated);
} else {
std::vector<std::string> parts = utils::split(generator_type, ':', 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 << "Unknown topic generator: " << generator_type << "\n";
}
}
// Add the generated topics to this section.
topics_.splice(topics_.end(), res);
}
std::string section::generate_table_of_contents(const std::string& generator)
{
if(generator.empty()) {
return "";
}
std::vector<std::string> parts = utils::split(generator, ':');
if(parts.size() > 1 && parts[0] == "contents") {
if(parts[1] == "generated") {
return print_table_of_contents();
} else {
return print_table_of_contents_for(parts[1]);
}
}
return "";
}
std::string section::print_table_of_contents_for(const std::string& section_id) const
{
const config& section_cfg = manager_->get_section_config(section_id);
if(!section_cfg) {
return "";
}
// We use an intermediate structure to allow a conditional sorting
std::vector<std::pair<std::string, std::string>> topics_links;
// Find all topics in this section.
for(const std::string& topic_id : utils::quoted_split(section_cfg["topics"])) {
if(const config& topic_cfg = manager_->get_topic_config(topic_id)) {
if(is_visible_id(topic_id)) {
topics_links.emplace_back(topic_cfg["title"], topic_id);
}
}
}
if(section_cfg["sort_topics"].to_bool()) {
std::sort(topics_links.begin(), topics_links.end());
}
std::ostringstream res;
for(const auto& link : topics_links) {
res << font::unicode_bullet << ' ' << make_link(link.first, link.second) << '\n';
}
return res.str();
}
std::string section::print_table_of_contents() const
{
std::ostringstream res;
for(const section_ptr& s : sections_) {
if(is_visible_id(s->id)) {
res << font::unicode_bullet << ' ' << make_link(s->title, ".." + s->id) << '\n';
}
}
for(const topic& t : topics_) {
if(is_visible_id(t.id)) {
res << font::unicode_bullet << ' ' << make_link(t.title, t.id) << '\n';
}
}
return res.str();
}
section* section::add_section(const config& new_section_cfg)
{
try {
sections_.emplace_back(std::make_unique<section>(new_section_cfg, *this));
return sections_.back().get();
} catch(const parse_error& e) {
std::cerr << "Error while parsing help text: " << e.message << std::endl;
} catch(const max_recursion_reached& e) {
std::cerr << e.message << std::endl;
}
return nullptr;
}
const section* section::find_section(const std::string& s_id) const
{
// First, check this section's sub-sections...
auto sec_iter = std::find_if(sections_.begin(), sections_.end(),
[&s_id](const section_ptr& s) { return s && s_id == s->id; });
if(sec_iter != sections_.end()) {
return sec_iter->get();
}
// ...then recursively check this section's sub-section's sub-sections.
for(const section_ptr& s : sections_) {
if(const section* ss = s->find_section(s_id)) {
return ss;
}
}
return nullptr;
}
const topic* section::find_topic(const std::string& t_id) const
{
// First, check this section's topics...
auto topic_iter = std::find_if(topics_.begin(), topics_.end(),
[&t_id](const topic& t) { return t_id == t.id; });
if(topic_iter != topics_.end()) {
return &(*topic_iter);
}
// ...then recursively check this section's sub-section's topics.
for(const section::section_ptr& s : sections_) {
if(const topic* t = s->find_topic(t_id)) {
return t;
}
}
return nullptr;
}
} // namespace help

151
src/help/section.hpp Normal file
View file

@ -0,0 +1,151 @@
/*
Copyright (C) 2018 by 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.
*/
#pragma once
#include "help/topic.hpp"
#include <memory>
#include <string>
#include <vector>
namespace help
{
class help_manager;
/**
* A help section.
*
* Sections serve as the branches of the topic tree. Each section has a set of topics,
* and can have additional sub-sections.
*
* A section on its own doesn't have any text. That's the exclusive domain of topics.
*/
class section
{
public:
using section_ptr = std::unique_ptr<section>;
using section_list = std::vector<section_ptr>;
/**
* Creates a section owned by the given manager. This is considered the toplevel node.
*
* The manager argument is mandatory; it's only a pointer since this class keeps a
* pointer to the manager and there's no reason to go ptr->ref->ptr.
*/
section(const config& section_cfg, const help_manager* manager);
/**
* Creates a new sub-section owned by the given parent section.
*/
section(const config& section_cfg, const section& parent);
section(const section&) = delete;
section& operator=(const section&) = delete;
bool operator==(const section& s) const
{
return id == s.id;
}
/**
* Adds a new sub-section to this section.
*
* @param new_section_cfg The config data for the new section.
*
* @returns A pointer to the newly-added section.
*/
section* add_section(const config& new_section_cfg);
/**
* Adds a new topic to this section.
*
* @tparam T The text generator type.
* @tparam Args The values to forward to the text generator's constructor.
*
* @param id The id of the new section.
* @param title The title of the new section.
*
* @todo Might make sense for this to return a pointer-to-topic like @ref add_section
* does, but we don't need that right now.
*/
template<typename T, typename... Args>
void add_topic(const std::string& id, const std::string& title, Args&&... generator_args)
{
topics_.emplace_back(id, title, std::make_unique<T>(std::forward<Args>(generator_args)...));
}
/**
* Returns a pointer to the section with the given id, or nullptr if none is found.
* Sub-sections' sub-sections are also searched.
*/
const section* find_section(const std::string& s_id) const;
/**
* Returns a pointer to the topic with the given id, or nullptr if none is found.
* Sub-sections' topics are also searched.
*/
const topic* find_topic(const std::string& t_id) const;
/** Deletes this section's sub-sections and topics. */
void clear()
{
topics_.clear();
sections_.clear();
}
void sort_topics()
{
topics_.sort();
}
const topic_list& topics() const
{
return topics_;
}
const section_list& sections() const
{
return sections_;
}
std::string id;
std::string title;
private:
/** Constructor implementation detail. */
void initialize(const config& section_cfg);
void generate_sections(const std::string& generator_type);
void generate_topics(const std::string& generator_type, const bool sort_generated);
std::string generate_table_of_contents(const std::string& generator);
std::string print_table_of_contents() const;
std::string section::print_table_of_contents_for(const std::string& section_id) const;
/** All topics this section owns. */
topic_list topics_;
/** All sub-sections. */
section_list sections_;
/** Tracks how many levels deep this section is in a topic tree. */
unsigned recursion_level_;
/** The manager that owns the toplevel node. This the same for all sections in a tree */
const help_manager* manager_;
};
} // namespace help

View file

@ -0,0 +1,196 @@
/*
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.
*/
#define GETTEXT_DOMAIN "wesnoth-help"
#include "help/section_generators.hpp"
#include "formatter.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "help/manager.hpp"
#include "help/constants.hpp"
#include "help/section.hpp"
#include "help/utils.hpp"
#include "log.hpp"
#include "preferences/game.hpp"
#include "terrain/terrain.hpp"
#include "terrain/translation.hpp"
#include "terrain/type_data.hpp"
#include "units/race.hpp"
#include "units/types.hpp"
#include <set>
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
{
void generate_races_sections(section& sec)
{
std::set<std::string, string_less> races;
std::set<std::string, string_less> visible_races;
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(description_type(type) == FULL_DESCRIPTION) {
races.insert(type.race_id());
if(!type.hide_help()) {
visible_races.insert(type.race_id());
}
}
}
for(const auto& race : races) {
config section_cfg;
const bool hidden = (visible_races.count(race) == 0);
std::string title;
if(const unit_race* r = unit_types.find_race(race)) {
title = r->plural_name();
} else {
title = _("race^Miscellaneous");
}
section_cfg["id"] = hidden_symbol(hidden) + race_prefix + race;
section_cfg["title"] = title;
section_cfg["sections_generator"] = "units:" + race;
section_cfg["generator"] = "units:" + race;
sec.add_section(section_cfg);
}
}
void generate_terrain_sections(section& sec)
{
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;
for(const t_translation::terrain_code& t : tdata->list()) {
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;
}
std::string topic_id = hidden_symbol(hidden) + terrain_prefix + info.id();
for(const t_translation::terrain_code& base : tdata->underlying_union_terrain(t)) {
const terrain_type& base_info = tdata->get_terrain_info(base);
const std::string& base_id = base_info.id();
if(!base_info.is_nonnull() || base_info.hide_help()) {
continue;
}
if(base_id == info.id()) {
topic_id = ".." + terrain_prefix + info.id();
}
section* base_section = nullptr;
try {
base_section = base_map.at(base_id);
} catch(const std::out_of_range&) {
config base_section_config;
base_section_config["id"] = terrain_prefix + base_id;
base_section_config["title"] = base_info.editor_name();
base_section = base_map[base_id] = sec.add_section(base_section_config);
}
// May be null if a parse/recursion depth error ocurred during construction.
if(base_section) {
base_section->add_topic<terrain_topic_generator>(topic_id, info.editor_name(), info);
}
}
}
for(auto& t_sec : base_map) {
if(section* s = t_sec.second) {
s->sort_topics();
}
}
}
void generate_era_sections(section& sec)
{
// TODO: should we be using the gcm here?
for(const config& era : game_config_manager::get()->game_config().child_range("era")) {
if(era["hide_help"].to_bool()) {
continue;
}
DBG_HP << "Adding help section: " << era["id"].str() << "\n";
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";
sec.add_section(section_cfg);
}
}
void generate_unit_sections(section& sec, const std::string& race)
{
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(type.race_id() != race || !type.show_variations_in_help()) {
continue;
}
config base_unit_config;
base_unit_config["id"] = hidden_symbol(type.hide_help()) + unit_prefix + type.id();
base_unit_config["title"] = type.type_name();
section* base_unit = sec.add_section(base_unit_config);
// May be null if a parse/recursion depth error ocurred during construction.
// In that case, no need to parse variations since we have no section to which to
// append them.
if(!base_unit) {
continue;
}
// Add topics for each of the unit's variations.
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 topic_id = formatter()
<< hidden_symbol(var_type.hide_help()) << variation_prefix << var_type.id() << "_" << variation_id;
base_unit->add_topic<unit_topic_generator>(topic_id, topic_name, var_type, variation_id);
}
}
}
} // namespace help

View file

@ -0,0 +1,28 @@
/*
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.
*/
#pragma once
#include <string>
namespace help
{
class section;
void generate_races_sections(section& sec);
void generate_terrain_sections(section& sec);
void generate_era_sections(section& sec);
void generate_unit_sections(section& sec, const std::string& race);
} // namespace help

204
src/help/topic.cpp Normal file
View file

@ -0,0 +1,204 @@
/*
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/topic.hpp"
#include "config.hpp"
#include "formatter.hpp"
#include "gettext.hpp"
#include "help/utils.hpp"
#include "serialization/parser.hpp"
#include <iostream>
namespace help
{
namespace
{
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(std::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) {
throw parse_error(formatter() << "Unterminated single quote after: '" << ss.str() << "'");
}
if(!ss.str().empty()) {
attributes.push_back(ss.str());
}
ss.str("");
// Create the WML.
ss << "[" << element_name << "]\n";
for(const std::string& attr : attributes) {
ss << attr << "\n";
}
ss << "[/" << element_name << "]\n";
return ss.str();
}
config parse_text(const std::string& text)
{
config res;
const auto add_text_child =
[&res](const std::string& text) { res.add_child("text", config {"text", text}); };
bool last_char_escape = false;
const char escape_char = '\\';
std::stringstream ss;
std::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 {
add_text_child(ss.str());
ss.str("");
state = ELEMENT_NAME;
}
} else {
ss << c;
}
} else if(state == ELEMENT_NAME) {
if(c == '/') {
throw parse_error("Erroneous / in element name.");
} else if(c == '>') {
const std::string element_name = ss.str();
ss.str("");
// End of this name.
// For markup such as "<span color=''>", we only want the first word. If the name
// has no spaces, the entirety will be used.
std::stringstream s;
s << "</" << element_name.substr(0, element_name.find_first_of(' ')) << ">";
const std::string end_element_name = s.str();
std::size_t end_pos = text.find(end_element_name, pos);
if(end_pos == std::string::npos) {
throw parse_error(formatter() << "Unterminated element: " << element_name);
}
const std::string contents = text.substr(pos + 1, end_pos - pos - 1);
// If we find no '=' character, we assume we're dealing with Pango markup.
if(contents.find('=') != std::string::npos) {
s.str(convert_to_wml(element_name, contents));
s.seekg(0);
try {
config cfg;
read(cfg, s);
res.append_children(cfg);
} catch(const config::error& e) {
throw parse_error(formatter() << "Error when parsing help markup as WML: " << e.message);
}
} else {
add_text_child(formatter() << "<" << element_name << ">" << contents << end_element_name);
}
pos = end_pos + end_element_name.size() - 1;
state = OTHER;
} else {
ss << c;
}
}
last_char_escape = false;
}
}
if(state == ELEMENT_NAME) {
throw parse_error(formatter() << "Element '" << ss.str() << "' continues through end of string.");
}
// Add the last string.
if(!ss.str().empty()) {
res.add_child("text", config{"text", ss.str()});
}
return res;
}
} // end anon namespace
const config& topic::parsed_text() const
{
if(text_generator_) {
try {
parsed_text_ = parse_text(text_generator_->generate());
} catch(const parse_error& e) {
std::cerr << e.message << std::endl;
}
text_generator_.reset(nullptr);
}
return parsed_text_;
}
bool topic::operator<(const topic& t) const
{
return translation::compare(title, t.title) < 0;
}
} // namespace help

80
src/help/topic.hpp Normal file
View file

@ -0,0 +1,80 @@
/*
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.
*/
#pragma once
#include "help/topic_text_generators.hpp"
#include <list>
#include <memory>
#include <string>
namespace help
{
/** A topic contains a title, an id and some text. */
struct topic
{
using text_gen_ptr_t = std::unique_ptr<topic_text_generator>;
topic(const std::string& t_id, const std::string& t_title)
: id(t_id)
, title(t_title)
, text_generator_(nullptr)
, parsed_text_()
{
}
topic(const std::string& t_id, const std::string& t_title, const std::string& t_text)
: id(t_id)
, title(t_title)
, text_generator_(std::make_unique<plain_text_topic_generator>(t_text))
, parsed_text_()
{
}
topic(const std::string& t_id, const std::string& t_title, text_gen_ptr_t g)
: id(t_id)
, title(t_title)
, text_generator_(std::move(g))
, parsed_text_()
{
}
bool operator==(const topic& t) const
{
return id == t.id;
}
bool operator!=(const topic& t) const
{
return !operator==(t);
}
/** Case-sensitive, locale-dependent sort by title. */
bool operator<(const topic& t) const;
std::string id;
std::string title;
const config& parsed_text() const;
private:
mutable std::unique_ptr<topic_text_generator> text_generator_;
mutable config parsed_text_;
};
using topic_list = std::list<topic>;
} // namespace help

View file

@ -0,0 +1,481 @@
/*
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.
*/
#define GETTEXT_DOMAIN "wesnoth-help"
#include "help/topic_generators.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "help/constants.hpp"
#include "help/topic.hpp"
#include "help/topic_text_generators.hpp"
#include "help/utils.hpp"
#include "log.hpp"
#include "resources.hpp"
#include "serialization/string_utils.hpp"
#include "tod_manager.hpp"
#include "units/types.hpp"
#include <map>
#include <set>
#include <string>
static lg::log_domain log_help("help");
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help
{
topic_list generate_ability_topics(const bool sort_generated)
{
topic_list topics;
std::map<std::string, const unit_type::ability_metadata*> ability_topic_data;
std::map<std::string, std::set<std::string, string_less>> ability_units;
const auto parse = [&](const unit_type& type, const unit_type::ability_metadata& ability) {
// NOTE: neither ability names nor ability ids are necessarily unique. Creating
// topics for either each unique name or each unique id means certain abilities
// will be excluded from help. So... the ability topic ref id is a combination
// of id and (untranslated) name. It's rather ugly, but it works.
const std::string topic_ref = ability.id + ability.name.base_str();
ability_topic_data.emplace(topic_ref, &ability);
if(!type.hide_help()) {
// Add a link in the list of units with this ability
// We put the translated name at the beginning of the hyperlink,
// so the automatic alphabetic sorting of std::set can use it
const std::string link = make_link(type.type_name(), unit_prefix + type.id());
ability_units[topic_ref].insert(link);
}
};
// Look through all the unit types. If a unit of that type would have a full
// description, add its abilities to the potential topic list. We don't want
// to show abilities that the user has not encountered yet.
for(const auto& type_mapping : unit_types.types()) {
const unit_type& type = type_mapping.second;
if(description_type(type) != FULL_DESCRIPTION) {
continue;
}
for(const unit_type::ability_metadata& ability : type.abilities_metadata()) {
parse(type, ability);
}
for(const unit_type::ability_metadata& ability : type.adv_abilities_metadata()) {
parse(type, ability);
}
}
for(const auto& a : ability_topic_data) {
std::ostringstream text;
text << a.second->description;
text << "\n\n" << "<big>" << _("Units with this ability") << "</big>"<< "\n";
for(const auto& u : ability_units[a.first]) { // first is the topic ref id
text << font::unicode_bullet << " " << u << "\n";
}
topics.emplace_back(ability_prefix + a.first, a.second->name, text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_weapon_special_topics(const bool sort_generated)
{
topic_list topics;
std::map<t_string, std::string> special_description;
std::map<t_string, std::set<std::string, string_less>> special_units;
// FIXME: make this use the same id + name ref method as the ability topics.
const auto parse = [&](const unit_type& type, const std::pair<t_string, t_string>& data) {
special_description.emplace(data.first, data.second);
if(!type.hide_help()) {
// Check for variations (walking corpse/soulless etc)
const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
const 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
special_units[data.first].insert(make_link(type.type_name(), ref_id));
}
};
const auto parse_config = [&parse](const unit_type& type, const config& specials) {
for(const config::any_child& spec : specials.all_children_range()) {
if(!spec.cfg["name"].empty()) {
parse(type, std::make_pair(spec.cfg["name"].t_str(), spec.cfg["description"].t_str()));
}
}
};
for(const auto& 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()) {
for(const auto& s_tooltip : atk.special_tooltips()) {
parse(type, s_tooltip);
}
}
for(const config& adv : type.modification_advancements()) {
for(const config& effect : adv.child_range("effect")) {
if(effect["apply_to"] == "new_attack" && effect.has_child("specials")) {
parse_config(type, effect.child("specials"));
} else if(effect["apply_to"] == "attack" && effect.has_child("set_specials")) {
parse_config(type, effect.child("set_specials"));
}
}
}
}
for(auto& s : special_description) {
// use untranslated name to have universal topic id
std::string id = weapon_special_prefix + s.first.base_str();
std::ostringstream text;
text << s.second;
text << "\n\n" << "<big>" << _("Units with this special attack") << "</big>" << "\n";
for(const std::string& u : special_units[s.first]) {
text << font::unicode_bullet << " " << u << "\n";
}
topics.emplace_back(id, s.first, text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_time_of_day_topics(const bool /*sort_generated*/)
{
topic_list topics;
std::ostringstream toplevel;
if(!resources::tod_manager) {
toplevel << _("Only available during a scenario.");
topics.emplace_back("..schedule", _("Time of Day Schedule"), toplevel.str());
return topics;
}
for(const time_of_day& time : resources::tod_manager->times()) {
const std::string id = tod_prefix + time.id;
const std::string image = "<img>src='" + time.image + "'</img>";
toplevel << make_link(time.name.str(), id) << jump_to(160) << image << jump(30) << time.lawful_bonus << '\n';
std::ostringstream text;
text << image << '\n'
<< time.description.str() << '\n'
<< _("Lawful Bonus:") << ' ' << time.lawful_bonus << '\n'
<< '\n'
<< make_link(_("Schedule"), "..schedule");
topics.emplace_back(id, time.name.str(), text.str());
}
topics.emplace_back("..schedule", _("Time of Day Schedule"), toplevel.str());
return topics;
}
topic_list generate_trait_topics(const bool sort_generated)
{
topic_list topics;
std::map<t_string, const config> trait_list;
for(const config& trait : unit_types.traits()) {
trait_list.emplace(trait["id"], trait);
}
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(description_type(type) == FULL_DESCRIPTION) {
if(auto traits = type.possible_traits()) {
for(const config& trait : traits) {
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()) {
trait_list.emplace(trait["id"], trait);
}
}
}
}
for(auto& a : trait_list) {
const std::string id = trait_prefix + 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::ostringstream 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(id, name, text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_unit_topics(const bool sort_generated, const std::string& race)
{
topic_list topics;
std::set<std::string, string_less> race_units;
std::set<std::string, string_less> race_topics;
std::set<std::string> alignments;
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(type.race_id() != race) {
continue;
}
if(description_type(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();
topics.emplace_back(ref_id, type_name, std::make_unique<unit_topic_generator>(type));
if(!type.hide_help()) {
// we also record an hyperlink of this unit in the list used for the race topic/
race_units.insert(make_link(type_name, ref_id));
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_prefix + 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();
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"];
topics.emplace_back(id, title, text);
race_topics.insert(make_link(title, id));
}
} else {
race_name = _("race^Miscellaneous");
}
std::ostringstream text;
if(!race_description.empty()) {
text << race_description << "\n\n";
}
if(!alignments.empty()) {
text << (alignments.size() > 1 ? _("Alignments:") : _("Alignment:")) << ' ';
text << utils::join(alignments, ", ") << "\n\n";
}
text << "<big>" << _("Units of this race") << "</big>\n";
for(const std::string& u : race_units) {
text << font::unicode_bullet << " " << u << "\n";
}
topics.emplace_back(race_id, race_name, text.str());
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_faction_topics(const bool sort_generated, const config& era)
{
topic_list topics;
for(const config& f : era.child_range("multiplayer_side")) {
const std::string& id = f["id"];
if(id == "Random") {
continue;
}
std::ostringstream 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()) {
text << _("Races:") << ' ' << utils::join(races, ", ");
text << "\n\n";
}
if(!alignments.empty()) {
text << _("Alignments:") << ' ' << utils::join(alignments, ", ");
text << "\n\n";
}
text << "<big>" << _("Leaders") << "</big>";
text << "\n";
for(const std::string& link : make_unit_links_list(utils::split(f["leader"]), true)) {
text << font::unicode_bullet << " " << link << "\n";
}
text << "\n";
text << "<big>" << _("Recruits") << "</big>";
text << "\n";
for(const std::string& link : make_unit_links_list(recruit_ids, true)) {
text << font::unicode_bullet << " " << link << "\n";
}
const std::string ref_id = faction_prefix + era["id"] + "_" + id;
topics.emplace_back(ref_id, f["name"], text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_era_topics(const bool sort_generated, const std::string& era_id)
{
topic_list topics;
// TODO: should we be using the gcm here?
const config& era = game_config_manager::get()->game_config().find_child("era", "id", era_id);
if(era && !era["hide_help"].to_bool()) {
topics = generate_faction_topics(sort_generated, era);
std::vector<std::string> faction_links;
for(const topic& t : topics) {
faction_links.push_back(make_link(t.title, t.id));
}
std::ostringstream text;
text << "<big>" << _("Era:") << " " << era["name"] << "</big>";
text << "\n\n";
const config::attribute_value& description = era["description"];
if(!description.empty()) {
text << description.t_str() << "\n\n";
}
text << "<big>" << _("Factions") << "</big>";
text << "\n";
std::sort(faction_links.begin(), faction_links.end());
for(const std::string& link : faction_links) {
text << font::unicode_bullet << " " << link << "\n";
}
const std::string ref_id = ".." + era_prefix + era["id"].str();
topics.emplace_back(ref_id, era["name"], text.str());
}
return topics;
}
} // namespace help

View file

@ -0,0 +1,31 @@
/*
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.
*/
#pragma once
#include "help/topic.hpp"
class config;
namespace help
{
topic_list generate_ability_topics(bool sort_generated);
topic_list generate_weapon_special_topics(bool sort_generated);
topic_list generate_time_of_day_topics(bool sort_generated);
topic_list generate_trait_topics(bool sort_generated);
topic_list generate_unit_topics(bool sort_generated, const std::string& race);
topic_list generate_faction_topics(bool sort_generated, const config& era);
topic_list generate_era_topics(bool sort_generated, const std::string& era_id);
} // namespace help

View file

@ -12,141 +12,205 @@
See the COPYING file for more details. See the COPYING file for more details.
*/ */
#include "help/help_topic_generators.hpp" #define GETTEXT_DOMAIN "wesnoth-help"
#include "game_config.hpp" // for debug, menu_contract, etc #include "help/topic_text_generators.hpp"
#include "preferences/game.hpp" // for encountered_terrains, etc
#include "gettext.hpp" // for _, gettext, N_
#include "language.hpp" // for string_table, symbol_table
#include "log.hpp" // for LOG_STREAM, logger, etc
#include "movetype.hpp" // for movetype, movetype::effects, etc
#include "units/race.hpp" // for unit_race, 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 "tstring.hpp" // for t_string, operator<<
#include "units/helper.hpp" // for resistance_color
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "video.hpp" // fore current_resolution
#include <boost/optional.hpp> // for optional #include "font/text_formatting.hpp"
#include <iostream> // for operator<<, basic_ostream, etc #include "formatter.hpp"
#include <map> // for map, etc #include "game_config.hpp" // for debug, menu_contract, etc
#include "gettext.hpp" // for _, gettext, N_
#include "language.hpp" // for string_table, symbol_table
#include "log.hpp" // for LOG_STREAM, logger, etc
#include "movetype.hpp" // for movetype, movetype::effects, etc
#include "preferences/game.hpp" // for encountered_terrains, 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 "tstring.hpp" // for t_string, operator<<
#include "units/helper.hpp" // for resistance_color
#include "units/race.hpp" // for unit_race, etc
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "video.hpp" // fore current_resolution
#include "help/constants.hpp"
#include "help/utils.hpp"
#include <boost/optional.hpp> // for optional
#include <iostream> // for operator<<, basic_ostream, etc
#include <map> // for map, etc
#include <set> #include <set>
#include <SDL.h>
static lg::log_domain log_help("help"); static lg::log_domain log_help("help");
#define WRN_HP LOG_STREAM(warn, log_help) #define WRN_HP LOG_STREAM(warn, log_help)
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help { namespace help
{
static std::string best_str(bool best) { namespace
std::string lang_policy = (best ? _("Best of") : _("Worst of")); {
std::string color_policy = (best ? "green": "red"); std::string yes_no_str(bool value)
{
return "<format>color='" + color_policy + "' text='" + lang_policy + "'</format>"; // TODO: do we want these translated?
#if 0
return value
? translation::dgettext("wesnoth", "Yes")
: translation::dgettext("wesnoth", "No");
#endif
return value ? "Yes" : "No";
} }
typedef t_translation::ter_list::const_iterator ter_iter; std::string best_str(bool best)
// Gets an english description of a terrain ter_list alias behavior: "Best of cave, hills", "Worst of Swamp, Forest" etc.
static std::string print_behavior_description(ter_iter start, ter_iter end, const ter_data_cache & tdata, bool first_level = true, bool begin_best = true)
{ {
if(best) {
return formatter() << font::span_color(font::GOOD_COLOR) << _("Best of") << "</span>";
} else {
return formatter() << font::span_color(font::BAD_COLOR) << _("Worst of") << "</span>";
}
}
if (start == end) return ""; using ter_iter = t_translation::ter_list::const_iterator;
if (*start == t_translation::MINUS || *start == t_translation::PLUS) return print_behavior_description(start+1, end, tdata, first_level, *start == t_translation::PLUS); //absorb any leading mode changes by calling again, with a new default value begin_best.
// Gets an english description of a terrain ter_list alias behavior:
// "Best of cave, hills", "Worst of Swamp, Forest" etc.
std::string print_behavior_description(
ter_iter start, ter_iter end, const ter_data_cache& tdata, bool first_level = true, bool begin_best = true)
{
if(start == end) {
return "";
}
// absorb any leading mode changes by calling again, with a new default value begin_best.
if(*start == t_translation::MINUS || *start == t_translation::PLUS) {
return print_behavior_description(start + 1, end, tdata, first_level, *start == t_translation::PLUS);
}
boost::optional<ter_iter> last_change_pos; boost::optional<ter_iter> last_change_pos;
bool best = begin_best; bool best = begin_best;
for (ter_iter i = start; i != end; ++i) { for(ter_iter i = start; i != end; ++i) {
if ((best && *i == t_translation::MINUS) || (!best && *i == t_translation::PLUS)) { if((best && *i == t_translation::MINUS) || (!best && *i == t_translation::PLUS)) {
best = !best; best = !best;
last_change_pos = i; last_change_pos = i;
} }
} }
std::stringstream ss; std::ostringstream ss;
if (!last_change_pos) { if(!last_change_pos) {
std::vector<std::string> names; std::vector<std::string> names;
for (ter_iter i = start; i != end; ++i) {
for(ter_iter i = start; i != end; ++i) {
const terrain_type tt = tdata->get_terrain_info(*i); const terrain_type tt = tdata->get_terrain_info(*i);
if (!tt.editor_name().empty())
if(!tt.editor_name().empty()) {
names.push_back(tt.editor_name()); names.push_back(tt.editor_name());
}
} }
if (names.empty()) return ""; if(names.empty()) {
if (names.size() == 1) return names.at(0); return "";
}
if(names.size() == 1) {
return names.at(0);
}
ss << best_str(best) << " "; ss << best_str(best) << " ";
if (!first_level) ss << "( ";
ss << names.at(0);
for (std::size_t i = 1; i < names.size(); i++) { if(!first_level) {
ss << ", " << names.at(i); ss << "( ";
} }
if (!first_level) ss << " )"; ss << utils::join(names, ", ");
if(!first_level) {
ss << " )";
}
} else { } else {
std::vector<std::string> names; std::vector<std::string> names;
for (ter_iter i = *last_change_pos+1; i != end; ++i) {
for(ter_iter i = *last_change_pos + 1; i != end; ++i) {
const terrain_type tt = tdata->get_terrain_info(*i); const terrain_type tt = tdata->get_terrain_info(*i);
if (!tt.editor_name().empty())
if(!tt.editor_name().empty()) {
names.push_back(tt.editor_name()); names.push_back(tt.editor_name());
}
} }
if (names.empty()) { //This alias list is apparently padded with junk at the end, so truncate it without adding more parens // This alias list is apparently padded with junk at the end, so truncate it without adding more parens.
if(names.empty()) {
return print_behavior_description(start, *last_change_pos, tdata, first_level, begin_best); return print_behavior_description(start, *last_change_pos, tdata, first_level, begin_best);
} }
ss << best_str(best) << " "; ss << best_str(best) << " ";
if (!first_level) ss << "( ";
ss << print_behavior_description(start, *last_change_pos-1, tdata, false, begin_best); if(!first_level) {
ss << "( ";
}
ss << print_behavior_description(start, *last_change_pos - 1, tdata, false, begin_best);
// Printed the (parenthesized) leading part from before the change, now print the remaining names in this group. // Printed the (parenthesized) leading part from before the change, now print the remaining names in this group.
for (const std::string & s : names) { for(const std::string& s : names) {
ss << ", " << s; ss << ", " << s;
} }
if (!first_level) ss << " )";
if(!first_level) {
ss << " )";
}
} }
return ss.str(); return ss.str();
} }
std::string terrain_topic_generator::operator()() const { } // end anon namespace
std::stringstream ss;
if (!type_.icon_image().empty()) //
ss << "<img>src='images/buttons/icon-base-32.png~RC(magenta>" << type_.id() // TERRAIN TOPIC GENERATOR =========================================================================
<< ")~BLIT("<< "terrain/" << type_.icon_image() << "_30.png)" << "'</img> "; //
if (!type_.editor_image().empty()) std::string terrain_topic_generator::generate() const
{
std::ostringstream ss;
if(!type_.icon_image().empty()) {
ss << "<img>src='images/buttons/icon-base-32.png~RC(magenta>" << type_.id() << ")~BLIT("
<< "terrain/" << type_.icon_image() << "_30.png)"
<< "'</img> ";
}
if(!type_.editor_image().empty()) {
ss << "<img>src='" << type_.editor_image() << "'</img> "; ss << "<img>src='" << type_.editor_image() << "'</img> ";
}
if (!type_.help_topic_text().empty()) if(!type_.help_topic_text().empty()) {
ss << "\n\n" << type_.help_topic_text().str() << "\n"; ss << "\n\n" << type_.help_topic_text().str() << "\n";
else } else {
ss << "\n"; ss << "\n";
}
ter_data_cache tdata = load_terrain_types_data(); ter_data_cache tdata = load_terrain_types_data();
if (!tdata) { if(!tdata) {
WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data\n"; WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data\n";
return ss.str(); return ss.str();
} }
if (!(type_.union_type().size() == 1 && type_.union_type()[0] == type_.number() && type_.is_nonnull())) { if(!(type_.union_type().size() == 1 && type_.union_type()[0] == type_.number() && type_.is_nonnull())) {
const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number()); const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
ss << "\n" << _("Base Terrain: "); ss << "\n" << _("Base Terrain: ");
bool first = true; bool first = true;
for (const t_translation::terrain_code& underlying_terrain : underlying_mvt_terrains) { for(const t_translation::terrain_code& underlying_terrain : underlying_mvt_terrains) {
const terrain_type& mvt_base = tdata->get_terrain_info(underlying_terrain); const terrain_type& mvt_base = tdata->get_terrain_info(underlying_terrain);
if (mvt_base.editor_name().empty()) continue; if(mvt_base.editor_name().empty()) {
continue;
}
if (!first) { if(!first) {
ss << ", "; ss << ", ";
} else { } else {
first = false; first = false;
@ -161,65 +225,65 @@ std::string terrain_topic_generator::operator()() const {
ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), tdata) << "\n"; ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), tdata) << "\n";
const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number()); const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
ss << "\n" << _("Defense properties: "); ss << "\n" << _("Defense properties: ");
ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), tdata) << "\n"; ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), tdata) << "\n";
} }
if (game_config::debug) { if(game_config::debug) {
ss << "\n"; ss << "\n";
ss << "ID: " << type_.id() << "\n"; ss << "ID: " << type_.id() << "\n";
ss << "Village: " << (type_.is_village() ? "Yes" : "No") << "\n"; ss << "Village: " << yes_no_str(type_.is_village()) << "\n";
ss << "Gives Healing: " << type_.gives_healing() << "\n"; ss << "Gives Healing: " << type_.gives_healing() << "\n";
ss << "Keep: " << (type_.is_keep() ? "Yes" : "No") << "\n"; ss << "Keep: " << yes_no_str(type_.is_keep()) << "\n";
ss << "Castle: " << (type_.is_castle() ? "Yes" : "No") << "\n"; ss << "Castle: " << yes_no_str(type_.is_castle()) << "\n";
ss << "Overlay: " << (type_.is_overlay() ? "Yes" : "No") << "\n"; ss << "Overlay: " << yes_no_str(type_.is_overlay()) << "\n";
ss << "Combined: " << (type_.is_combined() ? "Yes" : "No") << "\n"; ss << "Combined: " << yes_no_str(type_.is_combined()) << "\n";
ss << "Nonnull: " << (type_.is_nonnull() ? "Yes" : "No") << "\n"; ss << "Nonnull: " << yes_no_str(type_.is_nonnull()) << "\n";
ss << "Terrain string:" << type_.number() << "\n"; ss << "Terrain string: " << type_.number() << "\n";
ss << "Hide in Editor: " << (type_.hide_in_editor() ? "Yes" : "No") << "\n"; ss << "Hide in Editor: " << yes_no_str(type_.hide_in_editor()) << "\n";
ss << "Hide Help: " << (type_.hide_help() ? "Yes" : "No") << "\n"; ss << "Hide Help: " << yes_no_str(type_.hide_help()) << "\n";
ss << "Editor Group: " << type_.editor_group() << "\n"; ss << "Editor Group: " << type_.editor_group() << "\n";
ss << "Light Bonus: " << type_.light_bonus(0) << "\n"; ss << "Light Bonus: " << type_.light_bonus(0) << "\n";
ss << type_.income_description(); ss << type_.income_description();
if (type_.editor_image().empty()) { // Note: this is purely temporary to help make a different help entry if(type_.editor_image().empty()) { // Note: this is purely temporary to help make a different help entry
ss << "\nEditor Image: Empty\n"; ss << "\nEditor Image: Empty\n";
} else { } else {
ss << "\nEditor Image: " << type_.editor_image() << "\n"; ss << "\nEditor Image: " << type_.editor_image() << "\n";
} }
const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
ss << "\nDebug Mvt Description String:"; ss << "\nDebug Mvt Description String:";
for (const t_translation::terrain_code & t : underlying_mvt_terrains) { for(const t_translation::terrain_code& t : tdata->underlying_mvt_terrain(type_.number())) {
ss << " " << t; ss << " " << t;
} }
const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
ss << "\nDebug Def Description String:"; ss << "\nDebug Def Description String:";
for (const t_translation::terrain_code & t : underlying_def_terrains) { for(const t_translation::terrain_code& t : tdata->underlying_def_terrain(type_.number())) {
ss << " " << t; ss << " " << t;
} }
} }
return ss.str(); return ss.str();
} }
//
// UNIT TOPIC GENERATOR =========================================================================
//
//Typedef to help with formatting list of traits // Typedef to help with formatting list of traits
typedef std::pair<std::string, std::string> trait_data; using trait_data = std::pair<std::string, std::string>;
#if 0 #if 0
//Helper function for printing a list of trait data //Helper function for printing a list of trait data
static void print_trait_list(std::stringstream & ss, const std::vector<trait_data> & l) static void print_trait_list(std::ostringstream & ss, const std::vector<trait_data> & l)
{ {
std::size_t i = 0; std::size_t i = 0;
ss << make_link(l[i].first, l[i].second); ss << make_link(l[i].first, l[i].second);
@ -230,12 +294,13 @@ static void print_trait_list(std::stringstream & ss, const std::vector<trait_dat
} }
} }
#endif #endif
std::string unit_topic_generator::operator()() const { std::string unit_topic_generator::generate() const
{
#if 0 #if 0
// Force the lazy loading to build this unit. // Force the lazy loading to build this unit.
unit_types.build_unit_type(type_, unit_type::FULL); unit_types.build_unit_type(type_, unit_type::FULL);
std::stringstream ss; std::ostringstream ss;
std::string clear_stringstream; std::string clear_stringstream;
const std::string detailed_description = type_.unit_description(); const std::string detailed_description = type_.unit_description();
const unit_type& female_type = type_.get_gender_unit_type(unit_race::FEMALE); const unit_type& female_type = type_.get_gender_unit_type(unit_race::FEMALE);
@ -248,40 +313,44 @@ std::string unit_topic_generator::operator()() const {
ss << "<img>src='" << male_type.image(); ss << "<img>src='" << male_type.image();
ss << "~RC(" << male_type.flag_rgb() << ">red)"; ss << "~RC(" << male_type.flag_rgb() << ">red)";
if (screen_width >= 1200) ss << "~XBRZ(2)"; if(screen_width >= 1200)
ss << "~XBRZ(2)";
ss << "' box='no'</img> "; ss << "' box='no'</img> ";
if(&female_type != &male_type) {
if (&female_type != &male_type) {
ss << "<img>src='" << female_type.image(); ss << "<img>src='" << female_type.image();
ss << "~RC(" << female_type.flag_rgb() << ">red)"; ss << "~RC(" << female_type.flag_rgb() << ">red)";
if (screen_width >= 1200) ss << "~XBRZ(2)"; if(screen_width >= 1200)
ss << "~XBRZ(2)";
ss << "' box='no'</img> "; ss << "' box='no'</img> ";
} }
const std::string &male_portrait = male_type.small_profile().empty() ? const std::string& male_portrait
male_type.big_profile() : male_type.small_profile(); = male_type.small_profile().empty() ? male_type.big_profile() : male_type.small_profile();
const std::string &female_portrait = female_type.small_profile().empty() ? const std::string& female_portrait
female_type.big_profile() : female_type.small_profile(); = female_type.small_profile().empty() ? female_type.big_profile() : female_type.small_profile();
const bool has_male_portrait = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image"; const bool has_male_portrait
const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait && female_portrait != female_type.image() && female_portrait != "unit_image"; = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait
&& female_portrait != female_type.image() && female_portrait != "unit_image";
int sz = (has_male_portrait && has_female_portrait ? 300 : 400); int sz = (has_male_portrait && has_female_portrait ? 300 : 400);
if (screen_width <= 1366) { if(screen_width <= 1366) {
sz = (has_male_portrait && has_female_portrait ? 200 : 300); sz = (has_male_portrait && has_female_portrait ? 200 : 300);
} else if (screen_width >= 1920) { } else if(screen_width >= 1920) {
sz = 400; sz = 400;
} }
// TODO: figure out why the second checks don't match but the last does // TODO: figure out why the second checks don't match but the last does
if (has_male_portrait) { if(has_male_portrait) {
ss << "<img>src='" << male_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> "; ss << "<img>src='" << male_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz
<< ")' box='no' align='right' float='yes'</img> ";
} }
if(has_female_portrait) {
if (has_female_portrait) { ss << "<img>src='" << female_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz
ss << "<img>src='" << female_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> "; << ")' box='no' align='right' float='yes'</img> ";
} }
ss << "\n\n\n"; ss << "\n\n\n";
@ -290,20 +359,19 @@ std::string unit_topic_generator::operator()() const {
// Cross reference to the topics containing information about those units. // Cross reference to the topics containing information about those units.
const bool first_reverse_value = true; const bool first_reverse_value = true;
bool reverse = first_reverse_value; bool reverse = first_reverse_value;
if (variation_.empty()) { if(variation_.empty()) {
do { do {
std::vector<std::string> adv_units = std::vector<std::string> adv_units = reverse ? type_.advances_from() : type_.advances_to();
reverse ? type_.advances_from() : type_.advances_to();
bool first = true; bool first = true;
for (const std::string &adv : adv_units) { for(const std::string& adv : adv_units) {
const unit_type *type = unit_types.find(adv, unit_type::HELP_INDEXED); const unit_type* type = unit_types.find(adv, unit_type::HELP_INDEXED);
if (!type || type->hide_help()) { if(!type || type->hide_help()) {
continue; continue;
} }
if (first) { if(first) {
if (reverse) { if(reverse) {
ss << _("Advances from: "); ss << _("Advances from: ");
} else { } else {
ss << _("Advances to: "); ss << _("Advances to: ");
@ -315,7 +383,7 @@ std::string unit_topic_generator::operator()() const {
std::string lang_unit = type->type_name(); std::string lang_unit = type->type_name();
std::string ref_id; std::string ref_id;
if (description_type(*type) == FULL_DESCRIPTION) { if(description_type(*type) == FULL_DESCRIPTION) {
const std::string section_prefix = type->show_variations_in_help() ? ".." : ""; const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
ref_id = section_prefix + unit_prefix + type->id(); ref_id = section_prefix + unit_prefix + type->id();
} else { } else {
@ -324,22 +392,21 @@ std::string unit_topic_generator::operator()() const {
} }
ss << make_link(lang_unit, ref_id); ss << make_link(lang_unit, ref_id);
} }
if (!first) { if(!first) {
ss << "\n"; ss << "\n";
} }
reverse = !reverse; //switch direction reverse = !reverse; // switch direction
} while(reverse != first_reverse_value); // don't restart } while(reverse != first_reverse_value); // don't restart
} }
const unit_type* parent = variation_.empty() ? &type_ : const unit_type* parent = variation_.empty() ? &type_ : unit_types.find(type_.id(), unit_type::HELP_INDEXED);
unit_types.find(type_.id(), unit_type::HELP_INDEXED); if(!variation_.empty()) {
if (!variation_.empty()) {
ss << _("Base unit: ") << make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n"; ss << _("Base unit: ") << make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
} else { } else {
bool first = true; bool first = true;
for (const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) { for(const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
if (first) { if(first) {
ss << _("Base units: "); ss << _("Base units: ");
first = false; first = false;
} }
@ -350,14 +417,14 @@ std::string unit_topic_generator::operator()() const {
} }
bool first = true; bool first = true;
for (const std::string &var_id : parent->variations()) { for(const std::string& var_id : parent->variations()) {
const unit_type &type = parent->get_variation(var_id); const unit_type& type = parent->get_variation(var_id);
if(type.hide_help()) { if(type.hide_help()) {
continue; continue;
} }
if (first) { if(first) {
ss << _("Variations: "); ss << _("Variations: ");
first = false; first = false;
} else { } else {
@ -367,7 +434,7 @@ std::string unit_topic_generator::operator()() const {
std::string ref_id; std::string ref_id;
std::string var_name = type.variation_name(); std::string var_name = type.variation_name();
if (description_type(type) == FULL_DESCRIPTION) { if(description_type(type) == FULL_DESCRIPTION) {
ref_id = variation_prefix + type.id() + "_" + var_id; ref_id = variation_prefix + type.id() + "_" + var_id;
} else { } else {
ref_id = unknown_unit_topic; ref_id = unknown_unit_topic;
@ -376,13 +443,13 @@ std::string unit_topic_generator::operator()() const {
ss << make_link(var_name, ref_id); ss << make_link(var_name, ref_id);
} }
ss << "\n"; //added even if empty, to avoid shifting ss << "\n"; // added even if empty, to avoid shifting
// Print the race of the unit, cross-reference it to the respective topic. // Print the race of the unit, cross-reference it to the respective topic.
const std::string race_id = type_.race_id(); const std::string race_id = type_.race_id();
std::string race_name = type_.race()->plural_name(); std::string race_name = type_.race()->plural_name();
if (race_name.empty()) { if(race_name.empty()) {
race_name = _ ("race^Miscellaneous"); race_name = _("race^Miscellaneous");
} }
ss << _("Race: "); ss << _("Race: ");
ss << make_link(race_name, "..race_" + race_id); ss << make_link(race_name, "..race_" + race_id);
@ -390,36 +457,38 @@ std::string unit_topic_generator::operator()() const {
// Print the possible traits of the unit, cross-reference them // Print the possible traits of the unit, cross-reference them
// to their respective topics. // to their respective topics.
if (config::const_child_itors traits = type_.possible_traits()) { if(config::const_child_itors traits = type_.possible_traits()) {
std::vector<trait_data> must_have_traits; std::vector<trait_data> must_have_traits;
std::vector<trait_data> random_traits; std::vector<trait_data> random_traits;
int must_have_nameless_traits = 0; int must_have_nameless_traits = 0;
for (const config & trait : traits) { for(const config& trait : traits) {
const std::string trait_name = trait["male_name"]; const std::string trait_name = trait["male_name"];
std::string lang_trait_name = translation::gettext(trait_name.c_str()); std::string lang_trait_name = translation::gettext(trait_name.c_str());
if (lang_trait_name.empty() && trait["availability"].str() == "musthave") { if(lang_trait_name.empty() && trait["availability"].str() == "musthave") {
++must_have_nameless_traits; ++must_have_nameless_traits;
continue; continue;
} }
const std::string ref_id = "traits_"+trait["id"].str(); const std::string ref_id = "traits_" + trait["id"].str();
((trait["availability"].str() == "musthave") ? must_have_traits : random_traits).emplace_back(lang_trait_name, ref_id); ((trait["availability"].str() == "musthave") ? must_have_traits : random_traits)
.emplace_back(lang_trait_name, ref_id);
} }
bool line1 = !must_have_traits.empty(); bool line1 = !must_have_traits.empty();
bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size(); bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size();
if (line1) { if(line1) {
std::string traits_label = _("Traits"); std::string traits_label = _("Traits");
ss << traits_label; ss << traits_label;
if (line2) { if(line2) {
std::stringstream must_have_count; std::ostringstream must_have_count;
must_have_count << " (" << must_have_traits.size() << ") : "; must_have_count << " (" << must_have_traits.size() << ") : ";
std::stringstream random_count; std::ostringstream random_count;
random_count << " (" << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits) << ") : "; random_count << " (" << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits)
<< ") : ";
int second_line_whitespace = font::line_width(traits_label+must_have_count.str(), normal_font_size) int second_line_whitespace = font::line_width(traits_label + must_have_count.str(), normal_font_size)
- font::line_width(random_count.str(), normal_font_size); - font::line_width(random_count.str(), normal_font_size);
// This ensures that the second line is justified so that the ':' characters are aligned. // This ensures that the second line is justified so that the ':' characters are aligned.
ss << must_have_count.str(); ss << must_have_count.str();
@ -432,7 +501,7 @@ std::string unit_topic_generator::operator()() const {
} }
ss << "\n\n"; ss << "\n\n";
} else { } else {
if (line2) { if(line2) {
ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << ") : "; ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << ") : ";
print_trait_list(ss, random_traits); print_trait_list(ss, random_traits);
ss << "\n\n"; ss << "\n\n";
@ -479,30 +548,26 @@ std::string unit_topic_generator::operator()() const {
// Print some basic information such as HP and movement points. // Print some basic information such as HP and movement points.
// TODO: Make this info update according to musthave traits, similar to movetype below. // TODO: Make this info update according to musthave traits, similar to movetype below.
ss << _("HP: ") << type_.hitpoints() << jump(30) ss << _("HP: ") << type_.hitpoints() << jump(30) << _("Moves: ") << type_.movement() << jump(30);
<< _("Moves: ") << type_.movement() << jump(30); if(type_.vision() != type_.movement()) {
if (type_.vision() != type_.movement()) {
ss << _("Vision: ") << type_.vision() << jump(30); ss << _("Vision: ") << type_.vision() << jump(30);
} }
if (type_.jamming() > 0) { if(type_.jamming() > 0) {
ss << _("Jamming: ") << type_.jamming() << jump(30); ss << _("Jamming: ") << type_.jamming() << jump(30);
} }
ss << _("Cost: ") << type_.cost() << jump(30) ss << _("Cost: ") << type_.cost() << jump(30) << _("Alignment: ")
<< _("Alignment: ") << make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day") << jump(30);
<< make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day") if(type_.can_advance()) {
<< jump(30);
if (type_.can_advance()) {
ss << _("Required XP: ") << type_.experience_needed(); ss << _("Required XP: ") << type_.experience_needed();
} }
// Print the detailed description about the unit. // Print the detailed description about the unit.
ss << "\n\n" << detailed_description; ss << "\n\n" << detailed_description;
// Print the different attacks a unit has, if it has any. // Print the different attacks a unit has, if it has any.
if (!type_.attacks().empty()) { if(!type_.attacks().empty()) {
// Print headers for the table. // Print headers for the table.
ss << "\n\n<header>text='" << escape(_("unit help^Attacks")) ss << "\n\n<header>text='" << escape(_("unit help^Attacks")) << "'</header>\n\n";
<< "'</header>\n\n";
table_spec table; table_spec table;
std::vector<item> first_row; std::vector<item> first_row;
@ -519,17 +584,17 @@ std::string unit_topic_generator::operator()() const {
std::string lang_weapon = attack.name(); std::string lang_weapon = attack.name();
std::string lang_type = string_table["type_" + attack.type()]; std::string lang_type = string_table["type_" + attack.type()];
std::vector<item> row; std::vector<item> row;
std::stringstream attack_ss; std::ostringstream attack_ss;
attack_ss << "<img>src='" << attack.icon() << "'</img>"; attack_ss << "<img>src='" << attack.icon() << "'</img>";
row.emplace_back(attack_ss.str(),image_width(attack.icon())); row.emplace_back(attack_ss.str(), image_width(attack.icon()));
push_tab_pair(row, lang_weapon); push_tab_pair(row, lang_weapon);
push_tab_pair(row, lang_type); push_tab_pair(row, lang_type);
attack_ss.str(clear_stringstream); attack_ss.str(clear_stringstream);
attack_ss << attack.damage() << font::unicode_en_dash << attack.num_attacks() attack_ss << attack.damage() << font::unicode_en_dash << attack.num_attacks() << " "
<< " " << attack.accuracy_parry_description(); << attack.accuracy_parry_description();
push_tab_pair(row, attack_ss.str()); push_tab_pair(row, attack_ss.str());
attack_ss.str(clear_stringstream); attack_ss.str(clear_stringstream);
if (attack.min_range() > 1 || attack.max_range() > 1) { if(attack.min_range() > 1 || attack.max_range() > 1) {
attack_ss << attack.min_range() << "-" << attack.max_range() << ' '; attack_ss << attack.min_range() << "-" << attack.max_range() << ' ';
} }
attack_ss << string_table["range_" + attack.range()]; attack_ss << string_table["range_" + attack.range()];
@ -538,16 +603,15 @@ std::string unit_topic_generator::operator()() const {
// Show this attack's special, if it has any. Cross // Show this attack's special, if it has any. Cross
// reference it to the section describing the special. // reference it to the section describing the special.
std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips(); std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
if (!specials.empty()) { if(!specials.empty()) {
std::string lang_special = ""; std::string lang_special = "";
const std::size_t specials_size = specials.size(); const std::size_t specials_size = specials.size();
for (std::size_t i = 0; i != specials_size; ++i) { for(std::size_t i = 0; i != specials_size; ++i) {
const std::string ref_id = std::string("weaponspecial_") const std::string ref_id = std::string("weaponspecial_") + specials[i].first.base_str();
+ specials[i].first.base_str();
lang_special = (specials[i].first); lang_special = (specials[i].first);
attack_ss << make_link(lang_special, ref_id); attack_ss << make_link(lang_special, ref_id);
if (i+1 != specials_size) { if(i + 1 != specials_size) {
attack_ss << ", "; //comma placed before next special attack_ss << ", "; // comma placed before next special
} }
} }
row.emplace_back(attack_ss.str(), font::line_width(lang_special, normal_font_size)); row.emplace_back(attack_ss.str(), font::line_width(lang_special, normal_font_size));
@ -557,14 +621,16 @@ std::string unit_topic_generator::operator()() const {
ss << generate_table(table); ss << generate_table(table);
} }
// Generate the movement type of the unit, with resistance, defense, movement, jamming and vision data updated according to any 'musthave' traits which always apply // Generate the movement type of the unit, with resistance, defense, movement, jamming and vision data updated
// according to any 'musthave' traits which always apply
movetype movement_type = type_.movement_type(); movetype movement_type = type_.movement_type();
config::const_child_itors traits = type_.possible_traits(); config::const_child_itors traits = type_.possible_traits();
if (!traits.empty() && type_.num_traits() > 0) { if(!traits.empty() && type_.num_traits() > 0) {
for (const config & t : traits) { for(const config& t : traits) {
if (t["availability"].str() == "musthave") { if(t["availability"].str() == "musthave") {
for (const config & effect : t.child_range("effect")) { for(const config& effect : t.child_range("effect")) {
if (!effect.child("filter") // If this is musthave but has a unit filter, it might not always apply, so don't apply it in the help. if(!effect.child("filter") // If this is musthave but has a unit filter, it might not always apply,
// so don't apply it in the help.
&& movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) { && movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
movement_type.merge(effect, effect["replace"].to_bool()); movement_type.merge(effect, effect["replace"].to_bool());
} }
@ -574,8 +640,7 @@ std::string unit_topic_generator::operator()() const {
} }
// Print the resistance table of the unit. // Print the resistance table of the unit.
ss << "\n\n<header>text='" << escape(_("Resistances")) ss << "\n\n<header>text='" << escape(_("Resistances")) << "'</header>\n\n";
<< "'</header>\n\n";
table_spec resistance_table; table_spec resistance_table;
std::vector<item> first_res_row; std::vector<item> first_res_row;
push_header(first_res_row, _("Attack Type")); push_header(first_res_row, _("Attack Type"));
@ -587,17 +652,18 @@ std::string unit_topic_generator::operator()() const {
int resistance = 100; int resistance = 100;
try { try {
resistance -= std::stoi(dam_it.second); resistance -= std::stoi(dam_it.second);
} catch(std::invalid_argument&) {} } catch(std::invalid_argument&) {
}
std::string resist = std::to_string(resistance) + '%'; std::string resist = std::to_string(resistance) + '%';
const std::size_t pos = resist.find('-'); const std::size_t pos = resist.find('-');
if (pos != std::string::npos) { if(pos != std::string::npos) {
resist.replace(pos, 1, font::unicode_minus); resist.replace(pos, 1, font::unicode_minus);
} }
std::string color = unit_helper::resistance_color(resistance); std::string color = unit_helper::resistance_color(resistance);
std::string lang_weapon = string_table["type_" + dam_it.first]; std::string lang_weapon = string_table["type_" + dam_it.first];
push_tab_pair(row, lang_weapon); push_tab_pair(row, lang_weapon);
std::stringstream str; std::ostringstream str;
str << "<format>color=\"" << color << "\" text='"<< resist << "'</format>"; str << "<format>color=\"" << color << "\" text='" << resist << "'</format>";
const std::string markup = str.str(); const std::string markup = str.str();
str.str(clear_stringstream); str.str(clear_stringstream);
str << resist; str << resist;
@ -606,89 +672,88 @@ std::string unit_topic_generator::operator()() const {
} }
ss << generate_table(resistance_table); ss << generate_table(resistance_table);
if (ter_data_cache tdata = load_terrain_types_data()) { if(ter_data_cache tdata = load_terrain_types_data()) {
// Print the terrain modifier table of the unit. // Print the terrain modifier table of the unit.
ss << "\n\n<header>text='" << escape(_("Terrain Modifiers")) ss << "\n\n<header>text='" << escape(_("Terrain Modifiers")) << "'</header>\n\n";
<< "'</header>\n\n";
std::vector<item> first_row; std::vector<item> first_row;
table_spec table; table_spec table;
push_header(first_row, _("Terrain")); push_header(first_row, _("Terrain"));
push_header(first_row, _("Defense")); push_header(first_row, _("Defense"));
push_header(first_row, _("Movement Cost")); push_header(first_row, _("Movement Cost"));
const bool has_terrain_defense_caps = movement_type.has_terrain_defense_caps(preferences::encountered_terrains()); const bool has_terrain_defense_caps
if (has_terrain_defense_caps) { = movement_type.has_terrain_defense_caps(preferences::encountered_terrains());
if(has_terrain_defense_caps) {
push_header(first_row, _("Defense Cap")); push_header(first_row, _("Defense Cap"));
} }
const bool has_vision = type_.movement_type().has_vision_data(); const bool has_vision = type_.movement_type().has_vision_data();
if (has_vision) { if(has_vision) {
push_header(first_row, _("Vision Cost")); push_header(first_row, _("Vision Cost"));
} }
const bool has_jamming = type_.movement_type().has_jamming_data(); const bool has_jamming = type_.movement_type().has_jamming_data();
if (has_jamming) { if(has_jamming) {
push_header(first_row, _("Jamming Cost")); push_header(first_row, _("Jamming Cost"));
} }
table.push_back(first_row); table.push_back(first_row);
std::set<t_translation::terrain_code>::const_iterator terrain_it = std::set<t_translation::terrain_code>::const_iterator terrain_it = preferences::encountered_terrains().begin();
preferences::encountered_terrains().begin();
for (; terrain_it != preferences::encountered_terrains().end(); for(; terrain_it != preferences::encountered_terrains().end(); ++terrain_it) {
++terrain_it) {
const t_translation::terrain_code terrain = *terrain_it; const t_translation::terrain_code terrain = *terrain_it;
if (terrain == t_translation::FOGGED || terrain == t_translation::VOID_TERRAIN || t_translation::terrain_matches(terrain, t_translation::ALL_OFF_MAP)) { if(terrain == t_translation::FOGGED || terrain == t_translation::VOID_TERRAIN
|| t_translation::terrain_matches(terrain, t_translation::ALL_OFF_MAP)) {
continue; continue;
} }
const terrain_type& info = tdata->get_terrain_info(terrain); const terrain_type& info = tdata->get_terrain_info(terrain);
const int moves = movement_type.movement_cost(terrain); const int moves = movement_type.movement_cost(terrain);
const bool cannot_move = moves > type_.movement(); const bool cannot_move = moves > type_.movement();
if (cannot_move && info.hide_if_impassable()) { if(cannot_move && info.hide_if_impassable()) {
continue; continue;
} }
if (info.union_type().size() == 1 && info.union_type()[0] == info.number() && info.is_nonnull()) { if(info.union_type().size() == 1 && info.union_type()[0] == info.number() && info.is_nonnull()) {
std::vector<item> row; std::vector<item> row;
const std::string& name = info.name(); const std::string& name = info.name();
const std::string& id = info.id(); const std::string& id = info.id();
const int views = movement_type.vision_cost(terrain); const int views = movement_type.vision_cost(terrain);
const int jams = movement_type.jamming_cost(terrain); const int jams = movement_type.jamming_cost(terrain);
bool high_res = false; bool high_res = false;
const std::string tc_base = high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png"; const std::string tc_base
= high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
const std::string terrain_image = "icons/terrain/terrain_type_" + id + (high_res ? "_30.png" : ".png"); const std::string terrain_image = "icons/terrain/terrain_type_" + id + (high_res ? "_30.png" : ".png");
const std::string final_image = tc_base + "~RC(magenta>" + id + ")~BLIT(" + terrain_image + ")"; const std::string final_image = tc_base + "~RC(magenta>" + id + ")~BLIT(" + terrain_image + ")";
row.emplace_back("<img>src='" + final_image + "'</img> " + row.emplace_back("<img>src='" + final_image + "'</img> " + make_link(name, "..terrain_" + id),
make_link(name, "..terrain_" + id), font::line_width(name, normal_font_size) + (high_res ? 32 : 16));
font::line_width(name, normal_font_size) + (high_res ? 32 : 16) );
//defense - range: +10 % .. +70 % // defense - range: +10 % .. +70 %
const int defense = 100 - movement_type.defense_modifier(terrain); const int defense = 100 - movement_type.defense_modifier(terrain);
std::string color; std::string color;
if (defense <= 10) { if(defense <= 10) {
color = "red"; color = "red";
} else if (defense <= 30) { } else if(defense <= 30) {
color = "yellow"; color = "yellow";
} else if (defense <= 50) { } else if(defense <= 50) {
color = "white"; color = "white";
} else { } else {
color = "green"; color = "green";
} }
std::stringstream str; std::ostringstream str;
str << "<format>color=" << color << " text='"<< defense << "%'</format>"; str << "<format>color=" << color << " text='" << defense << "%'</format>";
std::string markup = str.str(); std::string markup = str.str();
str.str(clear_stringstream); str.str(clear_stringstream);
str << defense << "%"; str << defense << "%";
row.emplace_back(markup, font::line_width(str.str(), normal_font_size)); row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
//movement - range: 1 .. 5, movetype::UNREACHABLE=impassable // movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
str.str(clear_stringstream); str.str(clear_stringstream);
if (cannot_move) { // cannot move in this terrain if(cannot_move) { // cannot move in this terrain
color = "red"; color = "red";
} else if (moves > 1) { } else if(moves > 1) {
color = "yellow"; color = "yellow";
} else { } else {
color = "white"; color = "white";
@ -707,18 +772,18 @@ std::string unit_topic_generator::operator()() const {
str << moves; str << moves;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size)); row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
//defense cap // defense cap
if (has_terrain_defense_caps) { if(has_terrain_defense_caps) {
str.str(clear_stringstream); str.str(clear_stringstream);
const bool has_cap = movement_type.get_defense().capped(terrain); const bool has_cap = movement_type.get_defense().capped(terrain);
if (has_cap) { if(has_cap) {
str << "<format>color='"<< color <<"' text='" << defense << "%'</format>"; str << "<format>color='" << color << "' text='" << defense << "%'</format>";
} else { } else {
str << "<format>color=white text='" << font::unicode_figure_dash << "'</format>"; str << "<format>color=white text='" << font::unicode_figure_dash << "'</format>";
} }
markup = str.str(); markup = str.str();
str.str(clear_stringstream); str.str(clear_stringstream);
if (has_cap) { if(has_cap) {
str << defense << '%'; str << defense << '%';
} else { } else {
str << font::unicode_figure_dash; str << font::unicode_figure_dash;
@ -726,43 +791,43 @@ std::string unit_topic_generator::operator()() const {
row.emplace_back(markup, font::line_width(str.str(), normal_font_size)); row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
} }
//vision // vision
if (has_vision) { if(has_vision) {
str.str(clear_stringstream); str.str(clear_stringstream);
const bool cannot_view = views > type_.vision(); const bool cannot_view = views > type_.vision();
if (cannot_view) { // cannot view in this terrain if(cannot_view) { // cannot view in this terrain
color = "red"; color = "red";
} else if (views > moves) { } else if(views > moves) {
color = "yellow"; color = "yellow";
} else if (views == moves) { } else if(views == moves) {
color = "white"; color = "white";
} else { } else {
color = "green"; color = "green";
}
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the vision costs go above
// the unit's vision + 5, we replace it with dashes.
if(cannot_view && (views > type_.vision() + 5)) {
str << font::unicode_figure_dash;
} else {
str << views;
}
str << "'</format>";
markup = str.str();
str.str(clear_stringstream);
str << views;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
} }
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the vision costs go above
// the unit's vision + 5, we replace it with dashes.
if(cannot_view && (views > type_.vision() + 5)) {
str << font::unicode_figure_dash;
} else {
str << views;
}
str << "'</format>";
markup = str.str();
str.str(clear_stringstream);
str << views;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
}
//jamming // jamming
if (has_jamming) { if(has_jamming) {
str.str(clear_stringstream); str.str(clear_stringstream);
const bool cannot_jam = jams > type_.jamming(); const bool cannot_jam = jams > type_.jamming();
if (cannot_jam) { // cannot jamm in this terrain if(cannot_jam) { // cannot jamm in this terrain
color = "red"; color = "red";
} else if (jams > views) { } else if(jams > views) {
color = "yellow"; color = "yellow";
} else if (jams == views) { } else if(jams == views) {
color = "white"; color = "white";
} else { } else {
color = "green"; color = "green";
@ -770,7 +835,7 @@ std::string unit_topic_generator::operator()() const {
str << "<format>color=" << color << " text='"; str << "<format>color=" << color << " text='";
// A 5 MP margin; if the jamming costs go above // A 5 MP margin; if the jamming costs go above
// the unit's jamming + 5, we replace it with dashes. // the unit's jamming + 5, we replace it with dashes.
if ( cannot_jam && jams > type_.jamming() + 5 ) { if(cannot_jam && jams > type_.jamming() + 5) {
str << font::unicode_figure_dash; str << font::unicode_figure_dash;
} else { } else {
str << jams; str << jams;
@ -786,15 +851,17 @@ std::string unit_topic_generator::operator()() const {
ss << generate_table(table); ss << generate_table(table);
} else { } else {
WRN_HP << "When building unit help topics, the display object was null and we couldn't get the terrain info we need.\n"; WRN_HP << "When building unit help topics, the display object was null and we couldn't get the terrain info we "
"need.\n";
} }
return ss.str(); return ss.str();
#endif #endif
return ""; return "";
} }
void unit_topic_generator::push_header(std::vector< item > &/*row*/, const std::string& /*name*/) const { void unit_topic_generator::push_header(std::vector<item>& /*row*/, const std::string& /*name*/) const
//row.emplace_back(bold(name), font::line_width(name, normal_font_size, TTF_STYLE_BOLD)); {
// row.emplace_back(bold(name), font::line_width(name, normal_font_size, TTF_STYLE_BOLD));
} }
} // end namespace help } // end namespace help

View file

@ -0,0 +1,92 @@
/*
Copyright (C) 2018 by 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.
*/
#pragma once
#include "config.hpp"
#include <memory>
#include <string>
#include <utility>
#include <vector>
class terrain_type;
class unit_type;
namespace help
{
/**
* Abstract topic generator base class.
*/
class topic_text_generator
{
public:
virtual ~topic_text_generator() {}
virtual std::string generate() const = 0;
};
class plain_text_topic_generator : public topic_text_generator
{
public:
explicit plain_text_topic_generator(const std::string& t)
: text_(t)
{
}
/** Inherited from @ref topic_text_generator. */
virtual std::string generate() const override
{
return text_;
}
private:
std::string text_;
};
class terrain_topic_generator : public topic_text_generator
{
public:
explicit terrain_topic_generator(const terrain_type& type)
: type_(type)
{
}
/** Inherited from @ref topic_text_generator. */
virtual std::string generate() const override;
private:
const terrain_type& type_;
};
class unit_topic_generator : public topic_text_generator
{
public:
unit_topic_generator(const unit_type& t, std::string variation = "")
: type_(t)
, variation_(variation)
{
}
/** Inherited from @ref topic_text_generator. */
virtual std::string generate() const override;
private:
const unit_type& type_;
const std::string variation_;
using item = std::pair<std::string, unsigned>;
void push_header(std::vector<item>& row, const std::string& name) const;
};
} // namespace help

157
src/help/utils.cpp Normal file
View file

@ -0,0 +1,157 @@
/*
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/utils.hpp"
#include "display.hpp"
#include "game_config.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "help/constants.hpp"
#include "help/section.hpp"
#include "help/topic.hpp"
#include "hotkey/hotkey_command.hpp"
#include "map/map.hpp"
#include "preferences/game.hpp"
#include "terrain/type_data.hpp"
#include "units/types.hpp"
#include <iostream>
namespace help
{
/** Implementation helpers. */
namespace
{
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 a 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;
}
} // end anon namespace
bool string_less::operator()(const std::string& s1, const std::string& s2) const
{
return translation::compare(s1, s2) < 0;
}
std::string hidden_symbol(bool hidden)
{
return hidden ? "." : "";
}
bool is_visible_id(const std::string& id)
{
return id.empty() || id[0] != '.';
}
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, ability_prefix.length(), ability_prefix) == 0) {
return false;
}
if(id.compare(0, weapon_special_prefix.length(), weapon_special_prefix) == 0) {
return false;
}
if(id == "hidden") {
return false;
}
return true;
}
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 escape(const std::string& s)
{
return utils::escape(s, "'\\");
}
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(std::move(unit_link));
}
}
if(ordered) {
std::sort(links_list.begin(), links_list.end());
}
return links_list;
}
ter_data_cache load_terrain_types_data()
{
if(display* d = display::get_singleton()) {
return d->get_disp_context().map().tdata();
} else if(game_config_manager* g = game_config_manager::get()) {
return g->terrain_types();
} else {
return ter_data_cache();
}
}
} // namespace help

86
src/help/utils.hpp Normal file
View file

@ -0,0 +1,86 @@
/*
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.
*/
#pragma once
#include "exceptions.hpp"
#include "formatter.hpp"
#include <memory>
#include <string>
#include <vector>
class terrain_type_data;
class unit_type;
namespace help
{
/** Thrown when the help manager fails to parse something. */
struct parse_error : public game::error
{
parse_error(const std::string& msg) : game::error(msg) {}
};
/** Thrown when trying to create a sub-section deeper than the max allowed depth. */
struct max_recursion_reached : public game::error
{
max_recursion_reached(const std::string& msg) : game::error(msg) {}
};
struct string_less
{
bool operator()(const std::string& s1, const std::string& s2) const;
};
enum UNIT_DESCRIPTION_TYPE {FULL_DESCRIPTION, NO_DESCRIPTION, NON_REVEALING_DESCRIPTION};
UNIT_DESCRIPTION_TYPE description_type(const unit_type& type);
std::string hidden_symbol(bool hidden = true);
/** An ID beginning with '.' denotes a hidden section or topic. */
bool is_visible_id(const std::string& id);
/// 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);
/// Prepend all chars with meaning inside attributes with a backslash.
std::string escape(const std::string &s);
inline std::string make_link(const std::string& text, const std::string& dst)
{
// some sorting done on list of links may rely on the fact that text is first
return formatter() << "<ref>text='" << help::escape(text) << "' dst='" << help::escape(dst) << "'</ref>";
}
inline std::string jump_to(const unsigned pos)
{
return formatter() << "<jump>to=" << pos << "</jump>";
}
inline std::string jump(const unsigned amount)
{
return formatter() << "<jump>amount=" << amount << "</jump>";
}
std::vector<std::string> make_unit_links_list(const std::vector<std::string>& type_id_list, bool ordered);
using ter_data_cache = std::shared_ptr<terrain_type_data>;
/// Load the appropriate terrain types data to use
ter_data_cache load_terrain_types_data();
} // namespace help

View file

@ -35,6 +35,7 @@
#include "gui/dialogs/game_ui.hpp" #include "gui/dialogs/game_ui.hpp"
#include "gui/dialogs/loading_screen.hpp" #include "gui/dialogs/loading_screen.hpp"
#include "gui/dialogs/transient_message.hpp" #include "gui/dialogs/transient_message.hpp"
#include "help/help.hpp"
#include "hotkey/command_executor.hpp" #include "hotkey/command_executor.hpp"
#include "hotkey/hotkey_handler.hpp" #include "hotkey/hotkey_handler.hpp"
#include "hotkey/hotkey_item.hpp" #include "hotkey/hotkey_item.hpp"
@ -190,6 +191,7 @@ play_controller::play_controller(const config& level,
play_controller::~play_controller() play_controller::~play_controller()
{ {
help::reset();
hotkey::delete_all_wml_hotkeys(); hotkey::delete_all_wml_hotkeys();
clear_resources(); clear_resources();
} }