The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
lobby_data.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2009 - 2025
3 by Tomasz Sniatowski <kailoran@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 "config.hpp"
19#include "filesystem.hpp"
20#include "font/pango/escape.hpp"
21#include "formatter.hpp"
24#include "game_config_view.hpp"
26#include "game_version.hpp"
27#include "gettext.hpp"
28#include "log.hpp"
29#include "map/exception.hpp"
30#include "map/map.hpp"
31#include "mp_game_settings.hpp"
34#include "utils/general.hpp"
35#include "wml_exception.hpp"
36
37#include <boost/algorithm/string.hpp>
38
39static lg::log_domain log_config("config");
40#define ERR_CF LOG_STREAM(err, log_config)
41static lg::log_domain log_engine("engine");
42#define WRN_NG LOG_STREAM(warn, log_engine)
43
44static lg::log_domain log_lobby("lobby");
45#define DBG_LB LOG_STREAM(info, log_lobby)
46#define LOG_LB LOG_STREAM(info, log_lobby)
47#define ERR_LB LOG_STREAM(err, log_lobby)
48
49namespace mp {
50
52 : name(c["name"])
53 , forum_id(c["forum_id"].to_int())
54 , game_id(c["game_id"].to_int())
55 , registered(c["registered"].to_bool())
56 , observing(c["status"] == "observing")
57 , moderator(c["moderator"].to_bool(false))
58{
59}
60
62{
63 if(game_id == 0) {
64 return user_state::LOBBY;
65 } else if(game_id == selected_game_id) {
67 } else {
68 return user_state::GAME;
69 }
70}
71
73{
74 if(name == prefs::get().login()) {
75 return user_relation::ME;
76 } else if(prefs::get().is_ignored(name)) {
78 } else if(prefs::get().is_friend(name)) {
80 } else {
82 }
83}
84
86{
87 const auto ar = get_relation();
88 const auto br = b.get_relation();
89 return ar < br || (ar == br && translation::icompare(name, b.name) < 0);
90}
91
92namespace
93{
94const std::string& spaced_em_dash()
95{
96 static const std::string res = " " + font::unicode_em_dash + " ";
97 return res;
98}
99
100std::string make_game_type_marker(const std::string& text, bool color_for_missing)
101{
102 if(color_for_missing) {
103 return markup::span_color("#f00", markup::bold("[", text, "] "));
104 } else {
105 return markup::bold("[", text, "] ");
106 }
107}
108
109} // end anon namespace
110
111game_info::game_info(const config& game, const std::vector<std::string>& installed_addons)
112 : id(game["id"].to_int())
113 , map_data(game["map_data"])
114 , name(font::escape_text(game["name"].str()))
115 , scenario()
116 , scenario_id()
117 , type_marker()
118 , remote_scenario(false)
119 , map_info()
120 , map_size_info()
121 , era()
122 , gold(game["mp_village_gold"])
123 , support(game["mp_village_support"])
124 , xp(game["experience_modifier"].str() + "%")
125 , vision()
126 , status()
127 , time_limit()
128 , vacant_slots()
129 , current_turn(0)
130 , reloaded(saved_game_mode::get_enum(game["savegame"].str()).value_or(saved_game_mode::type::no) != saved_game_mode::type::no)
131 , started(false)
132 , fog(game["mp_fog"].to_bool())
133 , shroud(game["mp_shroud"].to_bool())
134 , observers(game["observer"].to_bool(true))
135 , shuffle_sides(game["shuffle_sides"].to_bool(true))
136 , use_map_settings(game["mp_use_map_settings"].to_bool())
137 , private_replay(game["private_replay"].to_bool())
138 , verified(true)
139 , password_required(game["password"].to_bool())
140 , have_era(true)
141 , have_all_mods(true)
142 , has_friends(false)
143 , has_ignored(false)
144 , auto_hosted(game["auto_hosted"].to_bool())
145 , game_preset(game["game_preset"].to_bool())
146 , display_status(disp_status::NEW)
147 , required_addons()
148 , addons_outcome(addon_req::SATISFIED)
149{
151
152 // Parse the list of addons required to join this game.
153 for(const config& addon : game.child_range("addon")) {
154 if(addon.has_attribute("id") && addon["required"].to_bool(false)) {
155 if(!utils::contains(installed_addons, addon["id"].str())) {
157 r.addon_id = addon["id"].str();
159
160 // Use addon name if provided, else fall back on the addon id.
161 if(addon.has_attribute("name")) {
162 r.message = VGETTEXT("Missing addon: $name", {{"name", addon["name"].str()}});
163 } else {
164 r.message = VGETTEXT("Missing addon: $id", {{"id", addon["id"].str()}});
165 }
166
167 required_addons.push_back(std::move(r));
168
171 }
172 }
173 }
174 }
175
176 if(!game["mp_era"].empty()) {
177 auto era_cfg = game_config.find_child("era", "id", game["mp_era"]);
178 const bool require = game["require_era"].to_bool(true);
179 if(era_cfg) {
180 era = era_cfg["name"].str();
181
182 if(require) {
184 addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
185 }
186 } else {
187 have_era = !require;
188 era = game["mp_era_name"].str();
189 verified = false;
190
191 if(!have_era) {
193 }
194 }
195 } else {
196 era = _("Unknown era");
197 verified = false;
198 }
199
200 std::stringstream info_stream;
201 info_stream << era;
202
203 for(const config& cfg : game.child_range("modification")) {
204 mod_info.emplace_back(cfg["name"].str(), true);
205 info_stream << ' ' << mod_info.back().first;
206
207 if(cfg["require_modification"].to_bool(true)) {
208 if(auto mod = game_config.find_child("modification", "id", cfg["id"])) {
210 addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
211 } else {
212 have_all_mods = false;
213 mod_info.back().second = false;
214
216 }
217 }
218 }
219
220 std::sort(mod_info.begin(), mod_info.end(), [](const auto& lhs, const auto& rhs) {
221 return translation::icompare(lhs.first, rhs.first) < 0;
222 });
223
224 info_stream << ' ';
225
226 if(map_data.empty()) {
227 map_data = filesystem::read_map(game["mp_scenario"]);
228 }
229
230 if(map_data.empty()) {
231 info_stream << " — ??×??";
232 } else {
233 try {
234 gamemap map(map_data);
235 std::ostringstream msi;
236 msi << map.w() << font::unicode_multiplication_sign << map.h();
237 map_size_info = msi.str();
238 info_stream << spaced_em_dash() << map_size_info;
239 } catch(const incorrect_map_format_error&) {
240 verified = false;
241 } catch(const wml_exception& e) {
242 ERR_CF << "map could not be loaded: " << e.dev_message;
243 verified = false;
244 }
245 }
246
247 info_stream << " ";
248
249 //
250 // Check scenarios and campaigns
251 //
252 if(!game["mp_scenario"].empty() && game["mp_campaign"].empty()) {
253 // Check if it's a multiplayer scenario
254 const config* level_cfg = game_config.find_child("multiplayer", "id", game["mp_scenario"]).ptr();
255 const bool require = game["require_scenario"].to_bool(false);
256
257 // Check if it's a user map
258 if(!level_cfg) {
259 level_cfg = game_config.find_child("generic_multiplayer", "id", game["mp_scenario"]).ptr();
260 }
261
262 if(level_cfg) {
263 type_marker = make_game_type_marker(_("scenario_abbreviation^S"), false);
264 scenario = (*level_cfg)["name"].str();
265 scenario_id = (*level_cfg)["id"].str();
266 info_stream << scenario;
267
268 // Reloaded games do not match the original scenario hash, so it makes no sense
269 // to test them, since they always would appear as remote scenarios
270 if(!reloaded) {
271 if(auto hashes = game_config.optional_child("multiplayer_hashes")) {
272 std::string scenario = game["mp_scenario"];
273 if(hashes[scenario] != game["hash"]) {
274 remote_scenario = true;
275 info_stream << spaced_em_dash();
276 info_stream << _("Remote scenario");
277 verified = false;
278 }
279 }
280 }
281
282 if(require) {
283 addon_req result = check_addon_version_compatibility((*level_cfg), game);
284 addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
285 }
286 } else {
287 if(require) {
288 addons_outcome = std::max(addons_outcome, addon_req::NEED_DOWNLOAD); // Elevate to most severe error level encountered so far
289 }
290 type_marker = make_game_type_marker(_("scenario_abbreviation^S"), true);
291 scenario = game["mp_scenario_name"].str();
292 info_stream << scenario;
293 verified = false;
294 }
295 } else if(!game["mp_campaign"].empty()) {
296 if(auto campaign_cfg = game_config.find_child("campaign", "id", game["mp_campaign"])) {
297 type_marker = make_game_type_marker(_("campaign_abbreviation^C"), false);
298
299 std::stringstream campaign_text;
300 campaign_text
301 << campaign_cfg["name"] << spaced_em_dash()
302 << game["mp_scenario_name"];
303
304 // Difficulty
305 for(const config& difficulty : campaign_cfg->child_range("difficulty")) {
306 if(difficulty["define"] == game["difficulty_define"]) {
307 campaign_text << spaced_em_dash() << difficulty["description"];
308 break;
309 }
310 }
311
312 scenario = campaign_text.str();
313 info_stream << campaign_text.rdbuf();
314
315 // TODO: should we have this?
316 //if(game["require_scenario"].to_bool(false)) {
317 addon_req result = check_addon_version_compatibility(*campaign_cfg, game);
318 addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
319 //}
320 } else {
321 type_marker = make_game_type_marker(_("campaign_abbreviation^C"), true);
322 scenario = game["mp_campaign_name"].str();
323 info_stream << scenario;
324 verified = false;
325 }
326 } else {
327 scenario = _("Unknown scenario");
328 info_stream << scenario;
329 verified = false;
330 }
331
332 // Remove any newlines that might have been in game names (the player-set ones)
333 // No idea how this could happen, but I've seen it (vultraz, 2020-10-26)
334 boost::erase_all(name, "\n");
335
336 // Remove any newlines that might have been in game titles (scenario/campaign name, etc.)
337 boost::replace_all(scenario, "\n", " " + font::unicode_em_dash + " ");
338
339 if(reloaded) {
340 info_stream << spaced_em_dash();
341 info_stream << _("Reloaded game");
342 verified = false;
343 }
344
345 // These should always be present in the data the server sends, but may or may not be empty.
346 // I'm just using child_or_empty here to preempt any cases where they might not be included.
347 const config& s = game.child_or_empty("slot_data");
348 const config& t = game.child_or_empty("turn_data");
349
350 if(!s.empty()) {
351 started = false;
352
353 vacant_slots = s["vacant"].to_unsigned();
354
355 if(vacant_slots > 0) {
356 status = formatter() << _n("Vacant Slot:", "Vacant Slots:", vacant_slots) << " " << vacant_slots << "/" << s["max"];
357 } else {
358 status = _("mp_game_available_slots^Full");
359 }
360 }
361
362 if(!t.empty()) {
363 started = true;
364
365 current_turn = t["current"].to_unsigned();
366 const int max_turns = t["max"].to_int();
367
368 if(max_turns > -1) {
369 status = formatter() << _("Turn") << " " << t["current"] << "/" << max_turns;
370 } else {
371 status = formatter() << _("Turn") << " " << t["current"];
372 }
373 }
374
375 if(fog) {
376 vision = _("Fog");
377 if(shroud) {
378 vision += "/";
379 vision += _("Shroud");
380 }
381 } else if(shroud) {
382 vision = _("Shroud");
383 } else {
384 vision = _("vision^none");
385 }
386
387 if(game["mp_countdown"].to_bool()) {
389 << game["mp_countdown_init_time"].str() << "+"
390 << game["mp_countdown_turn_bonus"].str() << "/"
391 << game["mp_countdown_action_bonus"].str();
392 } else {
393 time_limit = _("time limit^none");
394 }
395
396 map_info = info_stream.str();
397}
398
400{
401 if(!local_item.has_attribute("addon_id") || !local_item.has_attribute("addon_version")) {
403 }
404
405 if(auto game_req = game.find_child("addon", "id", local_item["addon_id"])) {
406 if(!game_req["required"].to_bool(false)) {
408 }
409
410 required_addon r{local_item["addon_id"].str(), addon_req::SATISFIED, ""};
411
412 // Local version
413 const version_info local_ver(local_item["addon_version"].str());
414 version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
415
416 // If the UMC didn't specify last compatible version, assume no backwards compatibility.
417 // Also apply some sanity checking regarding min version; if the min ver doesn't make sense, ignore it.
418 local_min_ver = std::min(local_min_ver, local_ver);
419
420 // Remote version
421 const version_info remote_ver(game_req["version"].str());
422 version_info remote_min_ver(game_req->has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
423
424 remote_min_ver = std::min(remote_min_ver, remote_ver);
425
426 // Check if the host is too out of date to play.
427 if(local_min_ver > remote_ver) {
428 DBG_LB << "r.outcome = CANNOT_SATISFY for item='" << local_item["id"]
429 << "' addon='" << local_item["addon_id"]
430 << "' addon_min_version='" << local_item["addon_min_version"]
431 << "' addon_min_version_parsed='" << local_min_ver.str()
432 << "' addon_version='" << local_item["addon_version"]
433 << "' remote_ver='" << remote_ver.str()
434 << "'";
435 r.outcome = addon_req::CANNOT_SATISFY;
436
437 r.message = VGETTEXT("The host’s version of <i>$addon</i> is incompatible. They have version <b>$host_ver</b> while you have version <b>$local_ver</b>.", {
438 {"addon", local_item["addon_title"].str()},
439 {"host_ver", remote_ver.str()},
440 {"local_ver", local_ver.str()}
441 });
442
443 required_addons.push_back(r);
444 return r.outcome;
445 }
446
447 // Check if our version is too out of date to play.
448 if(remote_min_ver > local_ver) {
449 r.outcome = addon_req::NEED_DOWNLOAD;
450
451 r.message = VGETTEXT("Your version of <i>$addon</i> is incompatible. You have version <b>$local_ver</b> while the host has version <b>$host_ver</b>.", {
452 {"addon", local_item["addon_title"].str()},
453 {"host_ver", remote_ver.str()},
454 {"local_ver", local_ver.str()}
455 });
456
457 required_addons.push_back(r);
458 return r.outcome;
459 }
460 }
461
463}
464
466{
467 return !started && vacant_slots > 0;
468}
469
471{
473}
474
476{
477 switch(display_status) {
479 return "clean";
481 return "new";
483 return "deleted";
485 return "updated";
486 default:
487 ERR_CF << "BAD display_status " << static_cast<int>(display_status) << " in game " << id;
488 return "?";
489 }
490}
491
492bool game_info::match_string_filter(const std::string& filter) const
493{
494 const std::string& s1 = name;
495 const std::string& s2 = map_info;
496 return translation::ci_search(s1, filter) || translation::ci_search(s2, filter);
497}
498
499}
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition manager.cpp:186
double t
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
bool has_attribute(std::string_view key) const
Definition config.cpp:157
std::ostringstream wrapper.
Definition formatter.hpp:40
static game_config_manager * get()
const game_config_view & game_config() const
A class grating read only view to a vector of config objects, viewed as one config with all children ...
int w() const
Effective map width.
Definition map.hpp:50
int h() const
Effective map height.
Definition map.hpp:53
Encapsulates the map of the game.
Definition map.hpp:176
static prefs & get()
Represents version numbers.
std::string str() const
Serializes the version number into string form.
#define ERR_CF
Definition config.cpp:37
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
Declarations for File-IO.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
Interfaces for manipulating version numbers of engine, add-ons, etc.
static std::string _n(const char *str1, const char *str2, int n)
Definition gettext.hpp:104
static std::string _(const char *str)
Definition gettext.hpp:100
std::string id
Text to match against addon_info.tags()
Definition manager.cpp:199
static lg::log_domain log_engine("engine")
static lg::log_domain log_lobby("lobby")
#define DBG_LB
static lg::log_domain log_config("config")
Standard logging facilities (interface).
std::string read_map(const std::string &name)
Graphical text output.
const std::string unicode_em_dash
Definition constants.cpp:44
const std::string unicode_multiplication_sign
Definition constants.cpp:46
Game configuration data as global variables.
std::string bold(Args &&... data)
Applies bold Pango markup to the input.
Definition markup.hpp:161
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition markup.hpp:110
Main entry points of multiplayer mode.
bool logged_in_as_moderator()
Gets whether the currently logged-in user is a moderator.
int icompare(const std::string &s1, const std::string &s2)
Case-insensitive lexicographical comparison.
Definition gettext.cpp:519
bool ci_search(const std::string &s1, const std::string &s2)
Case-insensitive search.
Definition gettext.cpp:559
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition general.hpp:88
std::string name
std::string scenario_id
std::string type_marker
bool can_join() const
std::vector< required_addon > required_addons
std::string scenario
std::string era
std::string map_size_info
addon_req check_addon_version_compatibility(const config &local_item, const config &game)
unsigned int current_turn
std::string map_info
std::string status
addon_req addons_outcome
std::size_t vacant_slots
game_info(const config &c, const std::vector< std::string > &installed_addons)
disp_status display_status
bool can_observe() const
std::string vision
std::string time_limit
const char * display_status_string() const
std::vector< std::pair< std::string, bool > > mod_info
List of modification names and whether they're installed or not.
bool match_string_filter(const std::string &filter) const
std::string map_data
This class represents the information a client has about another player.
user_state get_state(int selected_game_id) const
user_relation get_relation() const
std::string name
bool operator<(const user_info &b) const
user_info(const config &c)
The base template for associating string values with enum values.
Definition enum_base.hpp:33
Helper class, don't construct this directly.
static map_location::direction s
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define e
#define c
#define b