1 /*
2 Copyright (C) 2013 - 2018 by Andrius Silinskas <silinskas.andrius@gmail.com>
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13 */
14 #include "game_config_manager.hpp"
15
16 #include "about.hpp"
17 #include "addon/manager.hpp"
18 #include "ai/configuration.hpp"
19 #include "cursor.hpp"
20 #include "events.hpp"
21 #include "formatter.hpp"
22 #include "game_config.hpp"
23 #include "gettext.hpp"
24 #include "game_classification.hpp"
25 #include "gui/dialogs/loading_screen.hpp"
26 #include "gui/dialogs/wml_error.hpp"
27 #include "hotkey/hotkey_item.hpp"
28 #include "hotkey/hotkey_command.hpp"
29 #include "language.hpp"
30 #include "log.hpp"
31 #include "preferences/general.hpp"
32 #include "scripting/game_lua_kernel.hpp"
33 #include "terrain/builder.hpp"
34 #include "terrain/type_data.hpp"
35 #include "units/types.hpp"
36 #include "game_version.hpp"
37 #include "theme.hpp"
38 #include "picture.hpp"
39
40 static lg::log_domain log_config("config");
41 #define ERR_CONFIG LOG_STREAM(err, log_config)
42 #define WRN_CONFIG LOG_STREAM(warn, log_config)
43 #define LOG_CONFIG LOG_STREAM(info, log_config)
44
45 static game_config_manager * singleton;
46
game_config_manager(const commandline_options & cmdline_opts,const bool jump_to_editor)47 game_config_manager::game_config_manager(
48 const commandline_options& cmdline_opts,
49 const bool jump_to_editor) :
50 cmdline_opts_(cmdline_opts),
51 jump_to_editor_(jump_to_editor),
52 game_config_(),
53 old_defines_map_(),
54 paths_manager_(),
55 cache_(game_config::config_cache::instance())
56 {
57 assert(!singleton);
58 singleton = this;
59
60 if(cmdline_opts_.nocache) {
61 cache_.set_use_cache(false);
62 }
63 if(cmdline_opts_.validcache) {
64 cache_.set_force_valid_cache(true);
65 }
66 }
67
~game_config_manager()68 game_config_manager::~game_config_manager()
69 {
70 assert(singleton);
71 singleton = nullptr;
72 }
73
get()74 game_config_manager * game_config_manager::get() {
75 return singleton;
76 }
77
init_game_config(FORCE_RELOAD_CONFIG force_reload)78 bool game_config_manager::init_game_config(FORCE_RELOAD_CONFIG force_reload)
79 {
80 // Add preproc defines according to the command line arguments.
81 game_config::scoped_preproc_define multiplayer("MULTIPLAYER",
82 cmdline_opts_.multiplayer);
83 game_config::scoped_preproc_define test("TEST", bool(cmdline_opts_.test));
84 game_config::scoped_preproc_define mptest("MP_TEST", cmdline_opts_.mptest);
85 game_config::scoped_preproc_define editor("EDITOR", jump_to_editor_);
86 game_config::scoped_preproc_define title_screen("TITLE_SCREEN",
87 !cmdline_opts_.multiplayer && !cmdline_opts_.test && !jump_to_editor_);
88
89 game_config::reset_color_info();
90 load_game_config_with_loadscreen(force_reload);
91
92 game_config::load_config(game_config_.child("game_config"));
93
94 hotkey::deactivate_all_scopes();
95 hotkey::set_scope_active(hotkey::SCOPE_MAIN_MENU);
96
97 // Load the standard hotkeys, then apply any player customizations.
98 hotkey::load_hotkeys(game_config(), true);
99 preferences::load_hotkeys();
100
101 ::init_textdomains(game_config());
102 about::set_about(game_config());
103 ai::configuration::init(game_config());
104
105 return true;
106 }
107
108 namespace {
109 /// returns true if every define in special is also defined in general
map_includes(const preproc_map & general,const preproc_map & special)110 bool map_includes(const preproc_map& general, const preproc_map& special)
111 {
112 for (const preproc_map::value_type& pair : special)
113 {
114 preproc_map::const_iterator it = general.find(pair.first);
115 if (it == general.end() || it->second != pair.second) {
116 return false;
117 }
118 }
119 return true;
120 }
121 } // end anonymous namespace
122
load_game_config_with_loadscreen(FORCE_RELOAD_CONFIG force_reload,game_classification const * classification)123 void game_config_manager::load_game_config_with_loadscreen(FORCE_RELOAD_CONFIG force_reload,
124 game_classification const* classification)
125 {
126 game_config::scoped_preproc_define debug_mode("DEBUG_MODE",
127 game_config::debug || game_config::mp_debug);
128
129 // Game_config already holds requested config in memory.
130 if (!game_config_.empty()) {
131 if ((force_reload == NO_FORCE_RELOAD) && old_defines_map_ == cache_.get_preproc_map()) {
132 return;
133 }
134 if ((force_reload == NO_INCLUDE_RELOAD) && map_includes(old_defines_map_, cache_.get_preproc_map())) {
135 return;
136 }
137 }
138
139 gui2::dialogs::loading_screen::display([this, force_reload, classification]() {
140 load_game_config(force_reload, classification);
141 });
142 }
143
load_game_config(FORCE_RELOAD_CONFIG force_reload,game_classification const * classification)144 void game_config_manager::load_game_config(FORCE_RELOAD_CONFIG force_reload,
145 game_classification const* classification)
146 {
147 // Make sure that 'debug mode' symbol is set
148 // if command line parameter is selected
149 // also if we're in multiplayer and actual debug mode is disabled.
150
151 // The loadscreen will erase the titlescreen.
152 // NOTE: even without loadscreen, needed after MP lobby.
153 try {
154 // Read all game configs.
155 // First we load all core configs, the mainline one and the ones from the addons.
156 // Validate the cores and discard the invalid.
157 // Then find the path to the selected core.
158 // Load the selected core.
159 // Handle terrains so that they are last loaded from the core.
160 // Load every compatible addon.
161 gui2::dialogs::loading_screen::progress(loading_stage::verify_cache);
162 filesystem::data_tree_checksum();
163 gui2::dialogs::loading_screen::progress(loading_stage::create_cache);
164
165 // Start transaction so macros are shared.
166 game_config::config_cache_transaction main_transaction;
167
168 config cores_cfg;
169 // Load mainline cores definition file.
170 cache_.get_config(game_config::path + "/data/cores.cfg", cores_cfg);
171
172 // Append the $user_campaign_dir/*/cores.cfg files to the cores.
173 std::vector<std::string> user_dirs;
174 {
175 const std::string user_campaign_dir = filesystem::get_addons_dir();
176 std::vector<std::string> user_files;
177 filesystem::get_files_in_dir(user_campaign_dir, &user_files, &user_dirs,
178 filesystem::ENTIRE_FILE_PATH);
179 }
180 for (const std::string& umc : user_dirs) {
181 const std::string cores_file = umc + "/cores.cfg";
182 if (filesystem::file_exists(cores_file)) {
183 config cores;
184 cache_.get_config(cores_file, cores);
185 cores_cfg.append(cores);
186 }
187 }
188
189 // Validate every core
190 config valid_cores;
191 bool current_core_valid = false;
192 std::string wml_tree_root;
193 for (const config& core : cores_cfg.child_range("core")) {
194
195 const std::string& id = core["id"];
196 if (id.empty()) {
197 events::call_in_main_thread([&]() {
198 gui2::dialogs::wml_error::display(
199 _("Error validating data core."),
200 _("Found a core without id attribute.")
201 + '\n' + _("Skipping the core."));
202 });
203 continue;
204 }
205 if (*&valid_cores.find_child("core", "id", id)) {
206 events::call_in_main_thread([&]() {
207 gui2::dialogs::wml_error::display(
208 _("Error validating data core."),
209 _("Core ID: ") + id
210 + '\n' + _("The ID is already in use.")
211 + '\n' + _("Skipping the core."));
212 });
213 continue;
214 }
215
216 const std::string& path = core["path"];
217 if (!filesystem::file_exists(filesystem::get_wml_location(path))) {
218 events::call_in_main_thread([&]() {
219 gui2::dialogs::wml_error::display(
220 _("Error validating data core."),
221 _("Core ID: ") + id
222 + '\n' + _("Core Path: ") + path
223 + '\n' + _("File not found.")
224 + '\n' + _("Skipping the core."));
225 });
226 continue;
227 }
228
229 if (id == "default" && !current_core_valid) {
230 wml_tree_root = path;
231 }
232 if (id == preferences::core_id()) {
233 current_core_valid = true;
234 wml_tree_root = path;
235 }
236
237 valid_cores.add_child("core", core); // append(core);
238 }
239
240 if (!current_core_valid) {
241 events::call_in_main_thread([&]() {
242 gui2::dialogs::wml_error::display(
243 _("Error loading core data."),
244 _("Core ID: ") + preferences::core_id()
245 + '\n' + _("Error loading the core with named id.")
246 + '\n' + _("Falling back to the default core."));
247 });
248 preferences::set_core_id("default");
249 }
250
251 // check if we have a valid default core which should always be the case.
252 if (wml_tree_root.empty()) {
253 events::call_in_main_thread([&]() {
254 gui2::dialogs::wml_error::display(
255 _("Error loading core data."),
256 _("Can't locate the default core.")
257 + '\n' + _("The game will now exit."));
258 });
259 throw;
260 }
261
262 // Load the selected core
263 cache_.get_config(filesystem::get_wml_location(wml_tree_root), game_config_);
264 game_config_.append(valid_cores);
265
266 main_transaction.lock();
267
268 // Put the gfx rules aside so that we can prepend the add-on
269 // rules to them.
270 config core_terrain_rules;
271 core_terrain_rules.splice_children(game_config_, "terrain_graphics");
272
273 if (!game_config::no_addons && !cmdline_opts_.noaddons)
274 load_addons_cfg();
275
276 // If multiplayer campaign is being loaded, [scenario] tags should
277 // become [multiplayer] tags and campaign's id should be added to them
278 // to allow to recognize which scenarios belongs to a loaded campaign.
279 if (classification != nullptr) {
280 if (const config& campaign = game_config().find_child("campaign", "id", classification->campaign))
281 {
282 const bool require_campaign = campaign["require_campaign"].to_bool(true);
283 for (config& scenario : game_config_.child_range("scenario"))
284 {
285 scenario["require_scenario"] = require_campaign;
286 }
287 }
288 }
289
290 // Extract the Lua scripts at toplevel.
291 game_lua_kernel::extract_preload_scripts(game_config_);
292 game_config_.clear_children("lua");
293
294 // Put the gfx rules back to game config.
295 game_config_.splice_children(core_terrain_rules, "terrain_graphics");
296
297 set_multiplayer_hashes();
298 set_unit_data();
299 game_config::add_color_info(game_config_);
300
301 terrain_builder::set_terrain_rules_cfg(game_config());
302 tdata_ = std::make_shared<terrain_type_data>(game_config_);
303 ::init_strings(game_config());
304 theme::set_known_themes(&game_config());
305 } catch(const game::error& e) {
306 ERR_CONFIG << "Error loading game configuration files\n" << e.message << '\n';
307
308 // Try reloading without add-ons
309 if (!game_config::no_addons) {
310 game_config::no_addons = true;
311 events::call_in_main_thread([&]() {
312 gui2::dialogs::wml_error::display(
313 _("Error loading custom game configuration files. The game will try without loading add-ons."),
314 e.message);
315 });
316 load_game_config(force_reload, classification);
317 } else if (preferences::core_id() != "default") {
318 events::call_in_main_thread([&]() {
319 gui2::dialogs::wml_error::display(
320 _("Error loading custom game configuration files. The game will fallback to the default core files."),
321 e.message);
322 });
323 preferences::set_core_id("default");
324 game_config::no_addons = false;
325 load_game_config(force_reload, classification);
326 } else {
327 events::call_in_main_thread([&]() {
328 gui2::dialogs::wml_error::display(
329 _("Error loading default core game configuration files. The game will now exit."),
330 e.message);
331 });
332 throw;
333 }
334 }
335
336 old_defines_map_ = cache_.get_preproc_map();
337
338 // Set new binary paths.
339 paths_manager_.set_paths(game_config());
340 }
341
load_addons_cfg()342 void game_config_manager::load_addons_cfg()
343 {
344 const std::string user_campaign_dir = filesystem::get_addons_dir();
345
346 std::vector<std::string> error_log;
347 std::vector<std::string> error_addons;
348 std::vector<std::string> user_dirs;
349 std::vector<std::string> user_files;
350
351 filesystem::get_files_in_dir(user_campaign_dir, &user_files, &user_dirs,
352 filesystem::ENTIRE_FILE_PATH);
353
354 // Warn player about addons using the no-longer-supported single-file format.
355 for(const std::string& file : user_files) {
356 const int size_minus_extension = file.size() - 4;
357
358 if(file.substr(size_minus_extension, file.size()) == ".cfg") {
359 ERR_CONFIG << "error reading usermade add-on '" << file << "'\n";
360
361 error_addons.push_back(file);
362
363 const int userdata_loc = file.find("data/add-ons") + 5;
364 const std::string log_msg = formatter()
365 << "The format '~"
366 << file.substr(userdata_loc)
367 << "' (for single-file add-ons) is not supported anymore, use '~"
368 << file.substr(userdata_loc, size_minus_extension - userdata_loc)
369 << "/_main.cfg' instead.";
370
371 error_log.push_back(log_msg);
372 }
373 }
374
375 // Rerun the directory scan using filename only, to get the addon_ids more easily.
376 user_files.clear();
377 user_dirs.clear();
378
379 filesystem::get_files_in_dir(user_campaign_dir, nullptr, &user_dirs,
380 filesystem::FILE_NAME_ONLY);
381
382 // Load the addons.
383 for(const std::string& addon_id : user_dirs) {
384 log_scope2(log_config, "Loading add-on '" + addon_id + "'");
385 const std::string addon_dir = user_campaign_dir + "/" + addon_id;
386
387 const std::string main_cfg = addon_dir + "/_main.cfg";
388 const std::string info_cfg = addon_dir + "/_info.cfg";
389
390 if(!filesystem::file_exists(main_cfg)) {
391 continue;
392 }
393
394 // Try to find this addon's metadata. Author publishing info (_server.pbl) is given
395 // precedence over addon sever-generated info (_info.cfg). If neither are found, it
396 // probably means the addon was installed manually and certain defaults will be used.
397 config metadata;
398
399 if(have_addon_pbl_info(addon_id)) {
400 // Publishing info needs to be read from disk.
401 try {
402 metadata = get_addon_pbl_info(addon_id);
403 } catch(const invalid_pbl_exception& e) {
404 const std::string log_msg = formatter()
405 << "The provided addon has an invalid pbl file"
406 << " for addon "
407 << addon_id;
408
409 error_addons.push_back(e.message);
410 error_log.push_back(log_msg);
411 }
412 } else if(filesystem::file_exists(info_cfg)) {
413 // Addon server-generated info can be fetched from cache.
414 config temp;
415 cache_.get_config(info_cfg, temp);
416
417 metadata = temp.child_or_empty("info");
418 }
419
420 std::string using_core = metadata["core"];
421 if(using_core.empty()) {
422 using_core = "default";
423 }
424
425 // Skip add-ons not matching our current core. Cores themselves should be selectable
426 // at all times, so they aren't considered here.
427 if(!metadata.empty() && metadata["type"] != "core" && using_core != preferences::core_id()) {
428 continue;
429 }
430
431 std::string addon_title = metadata["title"].str();
432 if(addon_title.empty()) {
433 addon_title = addon_id;
434 }
435
436 version_info addon_version(metadata["version"]);
437
438 try {
439 // Load this addon from the cache to a config.
440 config umc_cfg;
441 cache_.get_config(main_cfg, umc_cfg);
442
443 static const std::set<std::string> tags_with_addon_id {
444 "era",
445 "modification",
446 "resource",
447 "multiplayer",
448 "scenario",
449 "campaign"
450 };
451
452 // Annotate appropriate addon types with addon_id info.
453 for(auto child : umc_cfg.all_children_range()) {
454 if(tags_with_addon_id.count(child.key) > 0) {
455 auto& cfg = child.cfg;
456 cfg["addon_id"] = addon_id;
457 cfg["addon_title"] = addon_title;
458 // Note that this may reformat the string in a canonical form.
459 cfg["addon_version"] = addon_version.str();
460 }
461 }
462
463 game_config_.append(std::move(umc_cfg));
464 } catch(const config::error& err) {
465 ERR_CONFIG << "error reading usermade add-on '" << main_cfg << "'" << std::endl;
466 ERR_CONFIG << err.message << '\n';
467 error_addons.push_back(main_cfg);
468 error_log.push_back(err.message);
469 } catch(const preproc_config::error& err) {
470 ERR_CONFIG << "error reading usermade add-on '" << main_cfg << "'" << std::endl;
471 ERR_CONFIG << err.message << '\n';
472 error_addons.push_back(main_cfg);
473 error_log.push_back(err.message);
474 } catch(const filesystem::io_exception&) {
475 ERR_CONFIG << "error reading usermade add-on '" << main_cfg << "'" << std::endl;
476 error_addons.push_back(main_cfg);
477 }
478 }
479
480 if(!error_addons.empty()) {
481 const size_t n = error_addons.size();
482 const std::string& msg1 =
483 _n("The following add-on had errors and could not be loaded:",
484 "The following add-ons had errors and could not be loaded:",
485 n);
486 const std::string& msg2 =
487 _n("Please report this to the author or maintainer of this add-on.",
488 "Please report this to the respective authors or maintainers of these add-ons.",
489 n);
490
491 const std::string& report = utils::join(error_log, "\n\n");
492 events::call_in_main_thread([&]() {
493 gui2::dialogs::wml_error::display(msg1, msg2, error_addons, report);
494 });
495 }
496 }
497
set_multiplayer_hashes()498 void game_config_manager::set_multiplayer_hashes()
499 {
500 config& hashes = game_config_.add_child("multiplayer_hashes");
501 for (const config &ch : game_config_.child_range("multiplayer")) {
502 hashes[ch["id"].str()] = ch.hash();
503 }
504 }
505
set_unit_data()506 void game_config_manager::set_unit_data()
507 {
508 game_config_.merge_children("units");
509 gui2::dialogs::loading_screen::progress(loading_stage::load_unit_types);
510 if(config &units = game_config_.child("units")) {
511 unit_types.set_config(units);
512 }
513 }
514
reload_changed_game_config()515 void game_config_manager::reload_changed_game_config()
516 {
517 // Rebuild addon version info cache.
518 refresh_addon_version_info_cache();
519
520 // Force a reload of configuration information.
521 cache_.recheck_filetree_checksum();
522 old_defines_map_.clear();
523 filesystem::clear_binary_paths_cache();
524 init_game_config(FORCE_RELOAD);
525
526 image::flush_cache();
527 }
528
load_game_config_for_editor()529 void game_config_manager::load_game_config_for_editor()
530 {
531 game_config::scoped_preproc_define editor("EDITOR");
532 load_game_config_with_loadscreen(NO_FORCE_RELOAD);
533 }
534
load_game_config_for_game(const game_classification & classification)535 void game_config_manager::load_game_config_for_game(
536 const game_classification& classification)
537 {
538 game_config::scoped_preproc_define difficulty(classification.difficulty,
539 !classification.difficulty.empty());
540 game_config::scoped_preproc_define campaign(classification.campaign_define,
541 !classification.campaign_define.empty());
542 game_config::scoped_preproc_define scenario(classification.scenario_define,
543 !classification.scenario_define.empty());
544 game_config::scoped_preproc_define era(classification.era_define,
545 !classification.era_define.empty());
546 game_config::scoped_preproc_define multiplayer("MULTIPLAYER",
547 classification.campaign_type == game_classification::CAMPAIGN_TYPE::MULTIPLAYER);
548 game_config::scoped_preproc_define mptest("MP_TEST", cmdline_opts_.mptest &&
549 classification.campaign_type == game_classification::CAMPAIGN_TYPE::MULTIPLAYER);
550
551 //
552 // NOTE: these deques aren't used here, but the objects within are utilized as RAII helpers.
553 //
554
555 typedef std::unique_ptr<game_config::scoped_preproc_define> define;
556
557 std::deque<define> extra_defines;
558 for(const std::string& extra_define : classification.campaign_xtra_defines) {
559 extra_defines.emplace_back(new game_config::scoped_preproc_define(extra_define));
560 }
561
562 std::deque<define> modification_defines;
563 for(const std::string& mod_define : classification.mod_defines) {
564 modification_defines.emplace_back(new game_config::scoped_preproc_define(mod_define, !mod_define.empty()));
565 }
566
567 try {
568 load_game_config_with_loadscreen(NO_FORCE_RELOAD, &classification);
569 } catch(const game::error&) {
570 cache_.clear_defines();
571
572 std::deque<define> previous_defines;
573 for(const preproc_map::value_type& preproc : old_defines_map_) {
574 previous_defines.emplace_back(new game_config::scoped_preproc_define(preproc.first));
575 }
576
577 load_game_config_with_loadscreen(NO_FORCE_RELOAD);
578
579 throw;
580 }
581
582 // This needs to be done in the main thread since this function (load_game_config_for_game)
583 // might be called from a loading screen worker thread (and currently is, in fact). If the
584 // image cache is purged from the worker thread, there's a possibility for a data race where
585 // the main thread accesses the image cache and the worker thread simultaneously clears it.
586 events::call_in_main_thread([]() { image::flush_cache(); });
587 }
588
load_game_config_for_create(bool is_mp,bool is_test)589 void game_config_manager::load_game_config_for_create(bool is_mp, bool is_test)
590 {
591 game_config::scoped_preproc_define multiplayer("MULTIPLAYER", is_mp);
592 game_config::scoped_preproc_define test("TEST", is_test);
593 game_config::scoped_preproc_define mptest("MP_TEST", cmdline_opts_.mptest && is_mp);
594 ///During an mp game the default difficuly define is also defined so better already load it now if we alreeady must reload config cache.
595 game_config::scoped_preproc_define normal(DEFAULT_DIFFICULTY, !map_includes(old_defines_map_, cache_.get_preproc_map()));
596
597 typedef std::unique_ptr<game_config::scoped_preproc_define> define;
598 try{
599 load_game_config_with_loadscreen(NO_INCLUDE_RELOAD);
600 }
601 catch(const game::error&) {
602 cache_.clear_defines();
603
604 std::deque<define> previous_defines;
605 for (const preproc_map::value_type& preproc : old_defines_map_) {
606 previous_defines.emplace_back(new game_config::scoped_preproc_define(preproc.first));
607 }
608
609 load_game_config_with_loadscreen(NO_FORCE_RELOAD);
610
611 throw;
612 }
613 }
614