The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
connect_engine.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2013 - 2025
3 by Andrius Silinskas <silinskas.andrius@gmail.com>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
17
18#include "ai/configuration.hpp"
24#include "gettext.hpp"
25#include "log.hpp"
26#include "map/map.hpp"
27#include "mt_rng.hpp"
28#include "side_controller.hpp"
29#include "team.hpp"
30
31#include <array>
32
33static lg::log_domain log_config("config");
34#define LOG_CF LOG_STREAM(info, log_config)
35#define ERR_CF LOG_STREAM(err, log_config)
36
37static lg::log_domain log_mp_connect_engine("mp/connect/engine");
38#define DBG_MP LOG_STREAM(debug, log_mp_connect_engine)
39#define LOG_MP LOG_STREAM(info, log_mp_connect_engine)
40#define WRN_MP LOG_STREAM(warn, log_mp_connect_engine)
41#define ERR_MP LOG_STREAM(err, log_mp_connect_engine)
42
43static lg::log_domain log_network("network");
44#define LOG_NW LOG_STREAM(info, log_network)
45
46namespace
47{
48const std::array controller_names {
49 side_controller::human,
50 side_controller::human,
51 side_controller::ai,
52 side_controller::none,
53 side_controller::reserved
54};
55
56const std::set<std::string> children_to_swap {
57 "village",
58 "unit",
59 "ai"
60};
61
62} // end anon namespace
63
64namespace ng {
65
66connect_engine::connect_engine(saved_game& state, const bool first_scenario, mp_game_metadata* metadata)
67 : level_(mp::initial_level_config(state))
68 , state_(state)
69 , params_(state.mp_settings())
70 , default_controller_(metadata ? CNTR_NETWORK : CNTR_LOCAL)
71 , mp_metadata_(metadata)
72 , first_scenario_(first_scenario)
73 , force_lock_settings_()
74 , side_engines_()
75 , era_factions_()
76 , team_data_()
77 , era_info_(level_.mandatory_child("era"))
78{
79 const config& era_config = level_.mandatory_child("era");
80
81 const bool is_mp = state_.classification().is_normal_mp_game();
82 force_lock_settings_ = (state.mp_settings().saved_game != saved_game_mode::type::midgame) && scenario()["force_lock_settings"].to_bool(!is_mp);
83
84 // Original level sides.
86
87 // AI algorithms.
90
91 // Set the team name lists and modify the original level sides if necessary.
92 std::vector<std::string> original_team_names;
93 std::string team_prefix(_("Team") + " ");
94
95 int side_count = 1;
96 for(config& side : sides) {
97 const std::string side_str = std::to_string(side_count);
98
99 config::attribute_value& team_name = side["team_name"];
100 config::attribute_value& user_team_name = side["user_team_name"];
101
102 // Revert to default values if appropriate.
103 if(team_name.empty()) {
104 team_name = side_str;
105 }
106
107 if(params_.use_map_settings && user_team_name.empty()) {
108 user_team_name = team_name;
109 }
110
111 bool add_team = true;
113 // Only add a team if it is not found.
114 if(std::any_of(team_data_.begin(), team_data_.end(), [&team_name](const team_data_pod& data){
115 return data.team_name == team_name.str();
116 })) {
117 add_team = false;
118 }
119 } else {
120 // Always add a new team for every side, but leave the specified team assigned to a side if there is one.
121 auto name_itor = std::find(original_team_names.begin(), original_team_names.end(), team_name.str());
122
123 // Note that the prefix "Team " is untranslatable, as team_name is not meant to be translated. This is needed
124 // so that the attribute is not interpretted as an int when reading from config, which causes bugs later.
125 if(name_itor == original_team_names.end()) {
126 original_team_names.push_back(team_name);
127
128 team_name = "Team " + std::to_string(original_team_names.size());
129 } else {
130 team_name = "Team " + std::to_string(std::distance(original_team_names.begin(), name_itor) + 1);
131 }
132
133 user_team_name = team_prefix + side_str;
134 }
135
136 // Write the serialized translatable team name back to the config. Without this,
137 // the string can appear all messed up after leaving and rejoining a game (see
138 // issue #2040. This affected the mp_join_game dialog). I don't know why that issue
139 // didn't appear the first time you join a game, but whatever.
140 //
141 // The difference between that dialog and mp_staging is that the latter has access
142 // to connect_engine object, meaning it has access to serialized versions of the
143 // user_team_name string stored in the team_data_ vector. mp_join_game handled the
144 // raw side config instead. Again, I don't know why issues only cropped up on a
145 // subsequent join and not the first, but it doesn't really matter.
146 //
147 // This ensures both dialogs have access to the serialized form of the utn string.
148 // As for why this needs to be done in the first place, apparently the simple_wml
149 // format the server (wesnothd) uses doesn't preserve translatable strings (see
150 // issue #342).
151 //
152 // --vultraz, 2018-02-06
153 user_team_name = user_team_name.t_str().to_serialized();
154
155 if(add_team) {
157 data.team_name = params_.use_map_settings ? team_name : "Team " + side_str;
158 data.user_team_name = user_team_name.str();
159 data.is_player_team = side["allow_player"].to_bool(true);
160
161 team_data_.push_back(data);
162 }
163
164 ++side_count;
165 }
166
167 // Selected era's factions.
168 for(const config& ms : era_config.child_range("multiplayer_side")) {
169 era_factions_.push_back(&ms);
170 }
171
173
174 // Create side engines.
175 int index = 0;
176 for(const config& s : sides) {
177 side_engines_.emplace_back(new side_engine(s, *this, index++));
178 }
179
180 if(first_scenario_) {
181 // Add host to the connected users list.
182 import_user(prefs::get().login(), false);
183 } else {
184 // Add host but don't assign a side to him.
185 import_user(prefs::get().login(), true);
186
187 // Load reserved players information into the sides.
189 }
190
191 // Only updates the sides in the level.
192 update_level();
193
194 // If we are connected, send data to the connected host.
196}
197
198void connect_engine::import_user(const std::string& name, const bool observer, int side_taken)
199{
201 user_data["name"] = name;
202 import_user(user_data, observer, side_taken);
203}
204
205void connect_engine::import_user(const config& data, const bool observer, int side_taken)
206{
207 const std::string& username = data["name"];
208 assert(!username.empty());
209 if(mp_metadata_) {
210 connected_users_rw().insert(username);
211 }
212
214
215 if(observer) {
216 return;
217 }
218
219 bool side_assigned = false;
220 if(side_taken >= 0) {
221 side_engines_[side_taken]->place_user(data, true);
222 side_assigned = true;
223 }
224
225 // Check if user has a side(s) reserved for him.
226 for(const side_engine_ptr& side : side_engines_) {
227 if(side->reserved_for() == username && side->player_id().empty() && side->controller() != CNTR_COMPUTER) {
228 side->place_user(data);
229
230 side_assigned = true;
231 }
232 }
233
234 // If no sides were assigned for a user,
235 // take a first available side.
236 if(side_taken < 0 && !side_assigned) {
237 for(const side_engine_ptr& side : side_engines_) {
238 if(side->available_for_user(username) ||
239 side->controller() == CNTR_LOCAL) {
240 side->place_user(data);
241
242 side_assigned = true;
243 break;
244 }
245 }
246 }
247
248 // Check if user has taken any sides, which should get control
249 // over any other sides.
250 for(const side_engine_ptr& user_side : side_engines_) {
251 if(user_side->player_id() == username && !user_side->previous_save_id().empty()) {
252 for(const side_engine_ptr& side : side_engines_){
253 if(side->player_id().empty() && side->previous_save_id() == user_side->previous_save_id()) {
254 side->place_user(data);
255 }
256 }
257 }
258 }
259}
260
262{
263 for(const side_engine_ptr& side : side_engines_) {
264 if(side->available_for_user()) {
265 return true;
266 }
267 }
268
269 return false;
270}
271
273{
274 DBG_MP << "updating level";
275
276 scenario().clear_children("side");
277
278 for(const side_engine_ptr& side : side_engines_) {
279 scenario().add_child("side", side->new_config());
280 }
281}
282
284{
285 config old_level = level_;
286 update_level();
287
288 if(config diff = level_.get_diff(old_level); !diff.empty()) {
289 mp::send_to_server(config{"scenario_diff", std::move(diff)});
290 }
291}
292
294{
295 if(side_engines_.empty()) {
296 return true;
297 }
298
299 // First check if all sides are ready to start the game.
300 for(const side_engine_ptr& side : side_engines_) {
301 if(!side->ready_for_start()) {
302 const int side_num = side->index() + 1;
303 DBG_MP << "not all sides are ready, side " <<
304 side_num << " not ready";
305
306 return false;
307 }
308 }
309
310 DBG_MP << "all sides are ready";
311
312 /*
313 * If at least one human player is slotted with a player/ai we're allowed
314 * to start. Before used a more advanced test but it seems people are
315 * creative in what is used in multiplayer [1] so use a simpler test now.
316 * [1] http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=568029
317 */
318 for(const side_engine_ptr& side : side_engines_) {
319 if(side->controller() != CNTR_EMPTY && side->allow_player()) {
320 return true;
321 }
322 }
323
324 return false;
325}
326
327std::multimap<std::string, config> side_engine::get_side_children()
328{
329 std::multimap<std::string, config> children;
330
331 for(const std::string& to_swap : children_to_swap) {
332 for(const config& child : cfg_.child_range(to_swap)) {
333 children.emplace(to_swap, child);
334 }
335 }
336
337 return children;
338}
339
340void side_engine::set_side_children(const std::multimap<std::string, config>& children)
341{
342 for(const std::string& children_to_remove : children_to_swap) {
343 cfg_.clear_children(children_to_remove);
344 }
345
346 for(std::pair<std::string, config> child_map : children) {
347 cfg_.add_child(child_map.first, child_map.second);
348 }
349}
350
352{
353 DBG_MP << "starting a new game";
354
355 // Resolves the "random faction", "random gender" and "random message"
356 // Must be done before shuffle sides, or some cases will cause errors
357 randomness::mt_rng rng; // Make an RNG for all the shuffling and random faction operations
358 for(const side_engine_ptr& side : side_engines_) {
359 std::vector<std::string> avoid_faction_ids;
360
361 // If we aren't resolving random factions independently at random, calculate which factions should not appear for this side.
362 if(params_.mode != random_faction_mode::type::independent) {
363 for(const side_engine_ptr& side2 : side_engines_) {
364 if(!side2->flg().is_random_faction()) {
365 switch(params_.mode) {
366 case random_faction_mode::type::no_mirror:
367 avoid_faction_ids.push_back(side2->flg().current_faction()["id"].str());
368 break;
369 case random_faction_mode::type::no_ally_mirror:
370 if(side2->team() == side->team()) {// TODO: When the connect engines are fixed to allow multiple teams, this should be changed to "if side1 and side2 are allied, i.e. their list of teams has nonempty intersection"
371 avoid_faction_ids.push_back(side2->flg().current_faction()["id"].str());
372 }
373 break;
374 default:
375 break; // assert(false);
376 }
377 }
378 }
379 }
380 side->resolve_random(rng, avoid_faction_ids);
381 }
382
383 // Shuffle sides (check settings and if it is a re-loaded game).
384 // Must be done after resolve_random() or shuffle sides, or they won't work.
385 if(state_.mp_settings().shuffle_sides && !force_lock_settings_ && !(level_.has_child("snapshot") && level_.mandatory_child("snapshot").has_child("side"))) {
386
387 // Only playable sides should be shuffled.
388 std::vector<int> playable_sides;
389 for(const side_engine_ptr& side : side_engines_) {
390 if(side->allow_player() && side->allow_shuffle()) {
391 playable_sides.push_back(side->index());
392 }
393 }
394
395 // Fisher-Yates shuffle.
396 for(int i = playable_sides.size(); i > 1; i--) {
397 const int j_side = playable_sides[rng.get_next_random() % i];
398 const int i_side = playable_sides[i - 1];
399
400 if(i_side == j_side) continue; //nothing to swap
401
402 // First we swap everything about a side with another
403 std::swap(side_engines_[j_side], side_engines_[i_side]);
404
405 // Some 'child' variables such as village ownership and initial side units need to be swapped over as well
406 std::multimap<std::string, config> tmp_side_children = side_engines_[j_side]->get_side_children();
407 side_engines_[j_side]->set_side_children(side_engines_[i_side]->get_side_children());
408 side_engines_[i_side]->set_side_children(tmp_side_children);
409
410 // Then we revert the swap for fields that are unique to player control and the team they selected
411 std::swap(side_engines_[j_side]->index_, side_engines_[i_side]->index_);
412 std::swap(side_engines_[j_side]->team_, side_engines_[i_side]->team_);
413 }
414 }
415
416 // Make other clients not show the results of resolve_random().
417 mp::send_to_server(config{"stop_updates"});
418
420
422
423 // Build the gamestate object after updating the level.
425
426 mp::send_to_server(config{"start_game"});
427}
428
430{
431 DBG_MP << "starting a new game in commandline mode";
432
434
435 unsigned num = 0;
436 for(const side_engine_ptr& side : side_engines_) {
437 num++;
438
439 // Set the faction, if commandline option is given.
440 if(cmdline_opts.multiplayer_side) {
441 for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_side) {
442 if(side_num == num) {
443 if(std::find_if(era_factions_.begin(), era_factions_.end(),
444 [fid = faction_id](const config* faction) { return (*faction)["id"] == fid; })
445 != era_factions_.end()
446 ) {
447 DBG_MP << "\tsetting side " << side_num << "\tfaction: " << faction_id;
448 side->set_faction_commandline(faction_id);
449 } else {
450 ERR_MP << "failed to set side " << side_num << " to faction " << faction_id;
451 }
452 }
453 }
454 }
455
456 // Set the controller, if commandline option is given.
457 if(cmdline_opts.multiplayer_controller) {
458 for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_controller) {
459 if(side_num == num) {
460 DBG_MP << "\tsetting side " << side_num << "\tfaction: " << faction_id;
461 side->set_controller_commandline(faction_id);
462 }
463 }
464 }
465
466 // Set AI algorithm to default for all sides,
467 // then override if commandline option was given.
468 std::string ai_algorithm = game_config.mandatory_child("ais")["default_ai_algorithm"].str();
469 side->set_ai_algorithm(ai_algorithm);
470
471 if(cmdline_opts.multiplayer_algorithm) {
472 for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_algorithm) {
473 if(side_num == num) {
474 DBG_MP << "\tsetting side " << side_num << "\tfaction: " << faction_id;
475 side->set_ai_algorithm(faction_id);
476 }
477 }
478 }
479
480 // Finally, resolve "random faction",
481 // "random gender" and "random message", if any remains unresolved.
482 side->resolve_random(rng);
483 } // end top-level loop
484
486
487 // Update sides with commandline parameters.
488 if(cmdline_opts.multiplayer_turns) {
489 DBG_MP << "\tsetting turns: " << *cmdline_opts.multiplayer_turns;
490 scenario()["turns"] = *cmdline_opts.multiplayer_turns;
491 }
492
493 for(config& side : scenario().child_range("side")) {
494 if(cmdline_opts.multiplayer_ai_config) {
495 for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_ai_config) {
496 if(side_num == side["side"].to_unsigned()) {
497 DBG_MP << "\tsetting side " << side["side"] << "\tai_config: " << faction_id;
498 side["ai_config"] = faction_id;
499 }
500 }
501 }
502
503 // Having hard-coded values here is undesirable,
504 // but that's how it is done in the MP lobby
505 // part of the code also.
506 // Should be replaced by settings/constants in both places
507 if(cmdline_opts.multiplayer_ignore_map_settings) {
508 side["gold"] = 100;
509 side["income"] = 1;
510 }
511
512 if(cmdline_opts.multiplayer_parm) {
513 for(const auto& [side_num, pname, pvalue] : *cmdline_opts.multiplayer_parm) {
514 if(side_num == side["side"].to_unsigned()) {
515 DBG_MP << "\tsetting side " << side["side"] << " " << pname << ": " << pvalue;
516 side[pname] = pvalue;
517 }
518 }
519 }
520 }
521
523
524 // Build the gamestate object after updating the level
526 mp::send_to_server(config{"start_game"});
527}
528
530{
531 DBG_MP << "leaving the game";
532 mp::send_to_server(config{"leave_game"});
533}
534
536{
537 if(data.has_child("leave_game")) {
538 return true;
539 }
540
541 // A side has been dropped.
542 if(auto side_drop = data.optional_child("side_drop")) {
543 unsigned side_index = side_drop["side_num"].to_int() - 1;
544
545 if(side_index < side_engines_.size()) {
546 const side_engine_ptr& side_to_drop = side_engines_[side_index];
547
548 // Remove user, whose side was dropped.
549 connected_users_rw().erase(side_to_drop->player_id());
551
552 side_to_drop->reset();
553
555 return false;
556 }
557 }
558
559 // A player is connecting to the game.
560 if(!data["side"].empty()) {
561 unsigned side_taken = data["side"].to_int() - 1;
562
563 // Checks if the connecting user has a valid and unique name.
564 const std::string name = data["name"];
565 if(name.empty()) {
566 ERR_CF << "ERROR: No username provided with the side.";
567 return false;
568 }
569
570 if(connected_users().find(name) != connected_users().end()) {
571 // TODO: Seems like a needless limitation
572 // to only allow one side per player.
573 if(find_user_side_index_by_id(name) != -1) {
574 return false;
575 } else {
576 connected_users_rw().erase(name);
578 }
579 }
580
581 // Assigns this user to a side.
582 if(side_taken < side_engines_.size()) {
583 if(!side_engines_[side_taken]->available_for_user(name)) {
584 // This side is already taken.
585 // Try to reassing the player to a different position.
586 side_taken = 0;
587 for(const side_engine_ptr& s : side_engines_) {
588 if(s->available_for_user()) {
589 break;
590 }
591
592 side_taken++;
593 }
594
595 if(side_taken >= side_engines_.size()) {
596 mp::send_to_server(config{"kick", config{"username", data["name"]}});
597
599
600 ERR_CF << "ERROR: Couldn't assign a side to '" << name << "'";
601 return false;
602 }
603 }
604
605 LOG_CF << "client has taken a valid position";
606
607 import_user(data, false, side_taken);
609
610 // Wait for them to choose faction if allowed.
611 side_engines_[side_taken]->set_waiting_to_choose_status(side_engines_[side_taken]->allow_changes());
612 LOG_MP << "waiting to choose status = " << side_engines_[side_taken]->allow_changes();
613 LOG_NW << "sent player data";
614 } else {
615 ERR_CF << "tried to take illegal side: " << side_taken;
616 }
617 }
618
619 if(auto change_faction = data.optional_child("change_faction")) {
620 int side_taken = find_user_side_index_by_id(change_faction["name"]);
621 if(side_taken != -1 || !first_scenario_) {
622 import_user(*change_faction, false, side_taken);
624 }
625 }
626
627 if(auto observer = data.optional_child("observer")) {
628 import_user(*observer, true);
630 }
631
632 if(auto observer = data.optional_child("observer_quit")) {
633 const std::string& observer_name = observer["name"];
634
635 if(connected_users().find(observer_name) != connected_users().end()) {
636 connected_users_rw().erase(observer_name);
638
639 // If the observer was assigned a side, we need to send an update to other
640 // players so they no longer see the observer assigned to that side.
641 if(find_user_side_index_by_id(observer_name) != -1) {
643 }
644 }
645 }
646
647 return false;
648}
649
650int connect_engine::find_user_side_index_by_id(const std::string& id) const
651{
652 std::size_t i = 0;
653 for(const side_engine_ptr& side : side_engines_) {
654 if(side->player_id() == id) {
655 break;
656 }
657
658 i++;
659 }
660
661 if(i >= side_engines_.size()) {
662 return -1;
663 }
664
665 return i;
666}
667
669{
670 // Send initial information.
671 if(first_scenario_) {
673 "create_game", config {
674 "name", params_.name,
675 "password", params_.password,
676 "ignored", prefs::get().get_ignored_delim(),
677 // all queue games count as auto hosted, but not all auto hosted games are queue games
678 "auto_hosted", mp_metadata_ ? mp_metadata_->queue_type != queue_type::normal : false,
679 "queue_type", mp_metadata_ ? mp_metadata_->queue_type : queue_type::normal,
680 "queue_id", mp_metadata_ ? mp_metadata_->queue_id : 0,
681 },
682 });
684 } else {
685 mp::send_to_server(config{"store_next_scenario", level_});
686 }
687}
688
690{
691 // Add information about reserved sides to the level config.
692 // N.B. This information is needed only for a host player.
693 std::map<std::string, std::string> side_users = utils::map_split(level_.child_or_empty("multiplayer")["side_users"]);
694 for(const side_engine_ptr& side : side_engines_) {
695 const std::string& save_id = side->save_id();
696 const std::string& player_id = side->player_id();
697 if(!save_id.empty() && !player_id.empty()) {
698 side_users[save_id] = player_id;
699 }
700 }
701
702 level_.mandatory_child("multiplayer")["side_users"] = utils::join_map(side_users);
703}
704
706{
707 std::map<std::string, std::string> side_users = utils::map_split(level_.mandatory_child("multiplayer")["side_users"]);
708 std::set<std::string> names;
709 for(const side_engine_ptr& side : side_engines_) {
710 const std::string& save_id = side->previous_save_id();
711 if(side_users.find(save_id) != side_users.end()) {
712 side->set_reserved_for(side_users[save_id]);
713
714 if(side->controller() != CNTR_COMPUTER) {
715 side->set_controller(CNTR_RESERVED);
716 names.insert(side_users[save_id]);
717 }
718
719 side->update_controller_options();
720 }
721 }
722
723 //Do this in an extra loop to make sure we import each user only once.
724 for(const std::string& name : names)
725 {
726 if(connected_users().find(name) != connected_users().end() || !mp_metadata_) {
727 import_user(name, false);
728 }
729 }
730}
731
733{
734 for(const side_engine_ptr& side : side_engines_) {
735 side->update_controller_options();
736 }
737}
738
739const std::set<std::string>& connect_engine::connected_users() const
740{
741 if(mp_metadata_) {
743 }
744
745 static std::set<std::string> empty;
746 return empty;
747}
748
750{
751 assert(mp_metadata_);
753}
754
755side_engine::side_engine(const config& cfg, connect_engine& parent_engine, const int index)
756 : cfg_(cfg)
757 , parent_(parent_engine)
758 , controller_(CNTR_NETWORK)
759 , current_controller_index_(0)
760 , controller_options_()
761 , allow_player_(cfg["allow_player"].to_bool(true))
762 , controller_lock_(cfg["controller_lock"].to_bool(parent_.force_lock_settings_) && parent_.params_.use_map_settings)
763 , index_(index)
764 , team_(0)
765 , color_(std::min(index, gamemap::MAX_PLAYERS - 1))
766 , gold_(cfg["gold"].to_int(100))
767 , income_(cfg["income"].to_int())
768 , reserved_for_(cfg["current_player"])
769 , player_id_()
770 , ai_algorithm_()
771 , chose_random_(cfg["chose_random"].to_bool(false))
772 , disallow_shuffle_(cfg["disallow_shuffle"].to_bool(false))
773 , flg_(parent_.era_info_, parent_.era_factions_, cfg_, parent_.force_lock_settings_, parent_.params_.use_map_settings, parent_.params_.saved_game == saved_game_mode::type::midgame)
774 , allow_changes_(parent_.params_.saved_game != saved_game_mode::type::midgame && !(flg_.choosable_factions().size() == 1 && flg_.choosable_leaders().size() == 1 && flg_.choosable_genders().size() == 1))
775 , waiting_to_choose_faction_(allow_changes_)
776 , color_options_(game_config::default_colors)
777 //TODO: what should we do if color_ is out of range?
778 , color_id_(color_options_.at(color_))
779{
780 // Save default attributes that could be overwritten by the faction, so that correct faction lists would be
781 // initialized by flg_manager when the new side config is sent over network.
782 cfg_.clear_children("default_faction");
783 cfg_.add_child("default_faction", config {
784 "faction", cfg_["faction"],
785 "recruit", cfg_["recruit"],
786 });
787 if(auto p_cfg = cfg_.optional_child("leader")) {
788 cfg_.mandatory_child("default_faction").add_child("leader", config {
789 "type", (p_cfg)["type"],
790 "gender", (p_cfg)["gender"],
791 });
792 }
793
794 if(cfg_["side"].to_int(index_ + 1) != index_ + 1) {
795 ERR_CF << "found invalid side=" << cfg_["side"].to_int(index_ + 1) << " in definition of side number " << index_ + 1;
796 }
797
798 cfg_["side"] = index_ + 1;
799
800 if(cfg_["controller"] != side_controller::human && cfg_["controller"] != side_controller::ai && cfg_["controller"] != side_controller::none) {
801 //an invalid controller type was specified. Remove it to prevent asertion failures later.
802 cfg_.remove_attribute("controller");
803 }
804
806
807 // Tweak the controllers.
808 if(parent_.state_.classification().is_scenario() && cfg_["controller"].blank()) {
809 cfg_["controller"] = side_controller::ai;
810 }
811
812 if(cfg_["controller"] == side_controller::none) {
814 } else if(cfg_["controller"] == side_controller::ai) {
816 } else if(parent_.default_controller_ == CNTR_NETWORK && !reserved_for_.empty()) {
817 // Reserve a side for "current_player", unless the side
818 // is played by an AI.
820 } else if(allow_player_) {
822 } else {
823 // AI is the default.
825 }
826
827 // Initialize team and color.
828 unsigned team_name_index = 0;
830 if(cfg["team_name"] == data.team_name) {
831 break;
832 }
833
834 ++team_name_index;
835 }
836
837 if(team_name_index >= parent_.team_data_.size()) {
838 assert(!parent_.team_data_.empty());
839 team_ = 0;
840 WRN_MP << "In side_engine constructor: Could not find my team_name " << cfg["team_name"] << " among the mp connect engine's list of team names. I am being assigned to the first team. This may indicate a bug!";
841 } else {
842 team_ = team_name_index;
843 }
844
845 // Check the value of the config's color= key.
846 const std::string given_color = team::get_side_color_id_from_config(cfg_);
847
848 if(!given_color.empty()) {
849 // If it's valid, save the color...
850 color_id_ = given_color;
851
852 // ... and find the appropriate index for it.
853 const auto iter = std::find(color_options_.begin(), color_options_.end(), color_id_);
854
855 if(iter != color_options_.end()) {
856 color_ = std::distance(color_options_.begin(), iter);
857 } else {
858 color_options_.push_back(color_id_);
859 color_ = color_options_.size() - 1;
860 }
861 }
862
863 // Initialize ai algorithm.
864 if(auto ai = cfg.optional_child("ai")) {
865 ai_algorithm_ = ai["ai_algorithm"].str();
866 }
867}
868
870{
871 switch(controller_) {
872 case CNTR_LOCAL:
873 return N_("Anonymous player");
874 case CNTR_COMPUTER:
875 if(allow_player_) {
877 } else {
878 return N_("Computer Player");
879 }
880 default:
881 return "";
882 }
883}
884
886{
887 config res = cfg_;
888
889 // In case of 'shuffle sides' the side index in cfg_ might be wrong which will confuse the team constructor later.
890 res["side"] = index_ + 1;
891
892 // If the user is allowed to change type, faction, leader etc, then import their new values in the config.
893 if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
894 // Merge the faction data to res.
895 config faction = flg_.current_faction();
896 LOG_MP << "side_engine::new_config: side=" << index_ + 1 << " faction=" << faction["id"] << " recruit=" << faction["recruit"];
897 res["faction_name"] = faction["name"];
898 res["faction"] = faction["id"];
899 faction.remove_attributes("id", "name", "image", "gender", "type", "description");
900 res.append(faction);
901 }
902
903 res["controller"] = controller_names[controller_];
904
905 // The hosts receives the serversided controller tweaks after the start event, but
906 // for mp sync it's very important that the controller types are correct
907 // during the start/prestart event (otherwise random unit creation during prestart fails).
908 res["is_local"] = player_id_ == prefs::get().login() || controller_ == CNTR_COMPUTER || controller_ == CNTR_LOCAL;
909
910 // This function (new_config) is only meant to be called by the host's machine, which is why this check
911 // works. It essentially certifies that whatever side has the player_id that matches the host's login
912 // will be flagged. The reason we cannot check mp_game_metadata::is_host is because that flag is *always*
913 // true on the host's machine, meaning this flag would be set to true for every side.
914 res["is_host"] = player_id_ == prefs::get().login();
915
916 std::string desc = user_description();
917 if(!desc.empty()) {
918 res["user_description"] = t_string(desc, "wesnoth");
919
920 desc = VGETTEXT("$playername $side", {
921 {"playername", _(desc.c_str())},
922 {"side", res["side"].str()}
923 });
924 } else if(!player_id_.empty()) {
925 desc = player_id_;
926 }
927
928 if(res["name"].str().empty() && !desc.empty()) {
929 //TODO: maybe we should add this in to the leaders config instead of the side config?
930 res["name"] = desc;
931 }
932
934 // If this is a saved game and "use_saved" (the default) was chosen for the
935 // AI algorithm, we do nothing. Otherwise we add the chosen AI and if this
936 // is a saved game, we also remove the old stages from the AI config.
937 if(ai_algorithm_ != "use_saved") {
938 if(parent_.params_.saved_game == saved_game_mode::type::midgame) {
939 for (config &ai_config : res.child_range("ai")) {
940 ai_config.clear_children("stage");
941 }
942 }
943 res.add_child_at("ai", config {"ai_algorithm", ai_algorithm_}, 0);
944 }
945 }
946
947 // A side's "current_player" is the player which has currently taken that side or the one for which it is reserved.
948 // The "player_id" is the id of the client who controls that side. It's always the host for Local and AI players and
949 // always empty for free/reserved sides or null controlled sides. You can use !res["player_id"].empty() to check
950 // whether a side is already taken.
951 assert(!prefs::get().login().empty());
952 if(controller_ == CNTR_LOCAL) {
953 res["player_id"] = prefs::get().login();
954 res["current_player"] = prefs::get().login();
955 } else if(controller_ == CNTR_RESERVED) {
956 res.remove_attribute("player_id");
957 res["current_player"] = reserved_for_;
958 } else if(controller_ == CNTR_COMPUTER) {
959 // TODO: what is the content of player_id_ here ?
960 res["current_player"] = desc;
961 res["player_id"] = prefs::get().login();
962 } else if(!player_id_.empty()) {
963 res["player_id"] = player_id_;
964 res["current_player"] = player_id_;
965 }
966
967 res["allow_changes"] = allow_changes_;
968 res["chose_random"] = chose_random_;
969
970 if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
971
972 if(controller_ != CNTR_EMPTY) {
973 if(!flg_.leader_lock()) {
974 auto& leader = res.child_or_add("leader");
975 leader["type"] = flg_.current_leader();
976 leader["gender"] = flg_.current_gender();
977 LOG_MP << "side_engine::new_config: side=" << index_ + 1 << " type=" << leader["type"]
978 << " gender=" << leader["gender"];
979 }
980 } else if(!controller_lock_) {
981 // if controller_lock_ == false and controller_ == CNTR_EMPTY, this means the user disalbles this side,
982 // so remove it's leader.
983 res.remove_children("leader");
984 }
985
986 const std::string& new_team_name = parent_.team_data_[team_].team_name;
987
988 if(res["user_team_name"].empty() || !parent_.params_.use_map_settings || res["team_name"] != new_team_name) {
989 res["team_name"] = new_team_name;
990 res["user_team_name"] = parent_.team_data_[team_].user_team_name;
991 }
992
993 res["allow_player"] = allow_player_;
994 res["color"] = color_id_;
995 res["gold"] = gold_;
996 res["income"] = income_;
997 }
998
999
1000 if(parent_.params_.use_map_settings && parent_.params_.saved_game != saved_game_mode::type::midgame) {
1001 if(cfg_.has_attribute("name")){
1002 res["name"] = cfg_["name"];
1003 }
1004 if(cfg_.has_attribute("user_description") && controller_ == CNTR_COMPUTER){
1005 res["user_description"] = cfg_["user_description"];
1006 }
1007 }
1008
1009 return res;
1010}
1011
1013{
1014 if(!allow_player_) {
1015 // Sides without players are always ready.
1016 return true;
1017 }
1018
1019 if((controller_ == CNTR_COMPUTER) ||
1020 (controller_ == CNTR_EMPTY) ||
1021 (controller_ == CNTR_LOCAL)) {
1022
1023 return true;
1024 }
1025
1026 if(available_for_user()) {
1027 // If controller_ == CNTR_NETWORK and player_id_.empty().
1028 return false;
1029 }
1030
1031 if(controller_ == CNTR_NETWORK) {
1033 // The host is ready. A network player, who got a chance
1034 // to choose faction if allowed, is also ready.
1035 return true;
1036 }
1037 }
1038
1039 return false;
1040}
1041
1042bool side_engine::available_for_user(const std::string& name) const
1043{
1044 if(controller_ == CNTR_NETWORK && player_id_.empty()) {
1045 // Side is free and waiting for user.
1046 return true;
1047 }
1048
1049 if(controller_ == CNTR_RESERVED && name.empty()) {
1050 // Side is still available to someone.
1051 return true;
1052 }
1053
1054 if(controller_ == CNTR_RESERVED && reserved_for_ == name) {
1055 // Side is available only for the player with specific name.
1056 return true;
1057 }
1058
1059 return false;
1060}
1061
1062void side_engine::resolve_random(randomness::mt_rng & rng, const std::vector<std::string> & avoid_faction_ids)
1063{
1064 if(parent_.params_.saved_game == saved_game_mode::type::midgame) {
1065 return;
1066 }
1067
1069
1070 flg_.resolve_random(rng, avoid_faction_ids);
1071
1072 LOG_MP << "side " << (index_ + 1) << ": faction=" <<
1073 (flg_.current_faction())["name"] << ", leader=" <<
1074 flg_.current_leader() << ", gender=" << flg_.current_gender();
1075}
1076
1078{
1079 player_id_.clear();
1082
1083 if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
1085 }
1086}
1087
1088void side_engine::place_user(const std::string& name)
1089{
1090 config data;
1091 data["name"] = name;
1092
1094}
1095
1096void side_engine::place_user(const config& data, bool contains_selection)
1097{
1098 player_id_ = data["name"].str();
1100
1101 if(data["change_faction"].to_bool() && contains_selection) {
1102 // Network user's data carry information about chosen
1103 // faction, leader and genders.
1104 flg_.set_current_faction(data["faction"].str());
1105 flg_.set_current_leader(data["leader"].str());
1106 flg_.set_current_gender(data["gender"].str());
1107 }
1108
1110}
1111
1113{
1114 controller_options_.clear();
1115
1116 // Default options.
1117 if(parent_.mp_metadata_) {
1118 add_controller_option(CNTR_NETWORK, _("Network Player"), side_controller::human);
1119 }
1120
1121 add_controller_option(CNTR_LOCAL, _("Local Player"), side_controller::human);
1122 add_controller_option(CNTR_COMPUTER, _("Computer Player"), side_controller::ai);
1123 add_controller_option(CNTR_EMPTY, _("Nobody"), side_controller::none);
1124
1125 if(!reserved_for_.empty()) {
1126 add_controller_option(CNTR_RESERVED, _("Reserved"), side_controller::human);
1127 }
1128
1129 // Connected users.
1130 for(const std::string& user : parent_.connected_users()) {
1131 add_controller_option(parent_.default_controller_, user, side_controller::human);
1132 }
1133
1135}
1136
1138{
1139 int i = 0;
1140 for(const controller_option& option : controller_options_) {
1141 if(option.first == controller_) {
1143
1144 if(player_id_.empty() || player_id_ == option.second) {
1145 // Stop searching if no user is assigned to a side
1146 // or the selected user is found.
1147 break;
1148 }
1149 }
1150
1151 i++;
1152 }
1153
1155}
1156
1157bool side_engine::controller_changed(const int selection)
1158{
1159 const ng::controller selected_cntr = controller_options_[selection].first;
1160
1161 // Check if user was selected. If so assign a side to him/her.
1162 // If not, make sure that no user is assigned to this side.
1163 if(selected_cntr == parent_.default_controller_ && selection != 0) {
1164 player_id_ = controller_options_[selection].second;
1166 } else {
1167 player_id_.clear();
1168 }
1169
1170 set_controller(selected_cntr);
1171
1172 return true;
1173}
1174
1181
1182void side_engine::set_faction_commandline(const std::string& faction_name)
1183{
1184 flg_.set_current_faction(faction_name);
1185}
1186
1187void side_engine::set_controller_commandline(const std::string& controller_name)
1188{
1190
1191 if(controller_name == side_controller::ai) {
1193 }
1194
1195 if(controller_name == side_controller::none) {
1197 }
1198
1199 player_id_.clear();
1200}
1201
1203 const std::string& name, const std::string& controller_value)
1204{
1205 if(controller_lock_ && !cfg_["controller"].empty() && cfg_["controller"] != controller_value) {
1206 return;
1207 }
1208
1209 controller_options_.emplace_back(controller, name);
1210}
1211
1212} // end namespace ng
#define LOG_CF
std::vector< std::string > names
static const config & get_ai_config_for(const std::string &id)
Return the config for a specified ai.
static void add_era_ai_from_config(const config &game_config)
static void add_mod_ai_from_config(const config::const_child_itors &configs)
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_side
Non-empty if –side was given on the command line.
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_ai_config
Non-empty if –ai-config was given on the command line.
utils::optional< std::string > multiplayer_turns
Non-empty if –turns was given on the command line.
bool multiplayer_ignore_map_settings
True if –ignore-map-settings was given at the command line.
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_controller
Non-empty if –controller was given on the command line.
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_algorithm
Non-empty if –algorithm was given on the command line.
utils::optional< std::vector< std::tuple< unsigned int, std::string, std::string > > > multiplayer_parm
Non-empty if –parm was given on the command line.
Variant for storing WML attributes.
std::string str(const std::string &fallback="") const
bool empty() const
Tests for an attribute that either was never set or was set to "".
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
void remove_attribute(std::string_view key)
Definition config.cpp:162
config & add_child(std::string_view key)
Definition config.cpp:436
void append(const config &cfg)
Append data from another config object to this one.
Definition config.cpp:188
void remove_attributes(T... keys)
Definition config.hpp:536
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition config.cpp:380
config & child_or_add(std::string_view key)
Returns a reference to the first child with the given key.
Definition config.cpp:401
boost::iterator_range< child_iterator > child_itors
Definition config.hpp:280
child_itors child_range(std::string_view key)
Definition config.cpp:268
void clear_children(T... keys)
Definition config.hpp:601
bool has_attribute(std::string_view key) const
Definition config.cpp:157
config get_diff(const config &c) const
A function to get the differences between this object, and 'c', as another config object.
Definition config.cpp:884
bool has_child(std::string_view key) const
Determine whether a config has a child or not.
Definition config.cpp:312
const config & child_or_empty(std::string_view key) const
Returns the first child with the given key, or an empty config if there is none.
Definition config.cpp:390
config & add_child_at(std::string_view key, const config &val, std::size_t index)
Definition config.cpp:465
bool empty() const
Definition config.cpp:823
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition config.cpp:362
void remove_children(std::string_view key, const std::function< bool(const config &)> &p={})
Removes all children with tag key for which p returns true.
Definition config.cpp:634
A class grating read only view to a vector of config objects, viewed as one config with all children ...
static game_config_view wrap(const config &cfg)
Encapsulates the map of the game.
Definition map.hpp:176
std::vector< const config * > era_factions_
std::vector< team_data_pod > team_data_
const ng::controller default_controller_
std::set< std::string > & connected_users_rw()
void import_user(const std::string &name, const bool observer, int side_taken=-1)
bool sides_available() const
std::vector< side_engine_ptr > side_engines_
void save_reserved_sides_information()
const std::set< std::string > & connected_users() const
const mp_game_settings & params_
void send_level_data() const
mp_game_metadata * mp_metadata_
void update_side_controller_options()
connect_engine(saved_game &state, const bool first_scenario, mp_game_metadata *metadata)
void start_game_commandline(const commandline_options &cmdline_opts, const game_config_view &game_config)
int find_user_side_index_by_id(const std::string &id) const
bool process_network_data(const config &data)
bool can_start_game() const
void set_current_faction(const unsigned index)
const std::string & current_gender() const
const std::string & current_leader() const
void resolve_random(randomness::mt_rng &rng, const std::vector< std::string > &avoid)
const config & current_faction() const
void set_current_leader(const unsigned index)
bool leader_lock() const
void set_current_gender(const unsigned index)
unsigned current_controller_index_
std::string player_id_
const bool controller_lock_
const bool allow_changes_
void set_controller_commandline(const std::string &controller_name)
void set_faction_commandline(const std::string &faction_name)
void place_user(const std::string &name)
void set_side_children(const std::multimap< std::string, config > &children)
void add_controller_option(ng::controller controller, const std::string &name, const std::string &controller_value)
void resolve_random(randomness::mt_rng &rng, const std::vector< std::string > &avoid_faction_ids=std::vector< std::string >())
void update_current_controller_index()
side_engine(const config &cfg, connect_engine &parent_engine, const int index)
std::vector< controller_option > controller_options_
connect_engine & parent_
std::string ai_algorithm_
std::string reserved_for_
std::vector< std::string > color_options_
void set_controller(ng::controller controller)
void set_waiting_to_choose_status(bool status)
bool controller_changed(const int selection)
std::string color_id_
std::string user_description() const
ng::controller controller_
std::multimap< std::string, config > get_side_children()
bool ready_for_start() const
const config & cfg() const
config new_config() const
ng::controller controller() const
bool available_for_user(const std::string &name="") const
const bool allow_player_
static prefs & get()
const std::string get_ignored_delim()
std::string login()
uint32_t get_next_random()
Get a new random number.
Definition mt_rng.cpp:62
game_classification & classification()
mp_game_settings & mp_settings()
Multiplayer parameters for this game.
std::string to_serialized() const
Definition tstring.hpp:166
static std::string get_side_color_id_from_config(const config &cfg)
Definition team.cpp:999
#define ERR_CF
Definition config.cpp:37
Managing the AIs configuration - headers.
#define WRN_MP
#define ERR_MP
#define DBG_MP
#define LOG_NW
static lg::log_domain log_network("network")
static lg::log_domain log_mp_connect_engine("mp/connect/engine")
#define LOG_MP
static lg::log_domain log_config("config")
const config * cfg
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
#define N_(String)
Definition gettext.hpp:108
static std::string _(const char *str)
Definition gettext.hpp:100
std::string id
Text to match against addon_info.tags()
Definition manager.cpp:199
std::vector< const mp::user_info * > user_data
The associated user data for each node, index-to-index.
Standard logging facilities (interface).
A small explanation about what's going on here: Each action has access to two game_info objects First...
Definition actions.cpp:59
Game configuration data as global variables.
static void add_color_info(const game_config_view &v, bool build_defaults)
Main entry points of multiplayer mode.
void level_to_gamestate(const config &level, saved_game &state)
void send_to_server(const config &data)
Attempts to send given data to server if a connection is open.
@ CNTR_RESERVED
@ CNTR_NETWORK
@ CNTR_COMPUTER
std::pair< ng::controller, std::string > controller_option
std::shared_ptr< side_engine > side_engine_ptr
STL namespace.
std::map< std::string, std::string > map_split(const std::string &val, char major, char minor, int flags, const std::string &default_value)
Splits a string based on two separators into a map.
std::string join_map(const T &v, const std::string &major=",", const std::string &minor=":")
std::string_view data
Definition picture.cpp:188
std::string queue_type
std::set< std::string > connected_players
players and observers
random_faction_mode::type mode
saved_game_mode::type saved_game
The base template for associating string values with enum values.
Definition enum_base.hpp:33
static map_location::direction s
#define i