1 /*
2 Copyright (C) 2009 - 2018 by Tomasz Sniatowski <kailoran@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
15 #include "game_initialization/lobby_info.hpp"
16
17 #include "gettext.hpp"
18 #include "log.hpp"
19 #include "map/exception.hpp"
20 #include "map/map.hpp"
21 #include "mp_ui_alerts.hpp"
22 #include "preferences/game.hpp"
23 #include "wesnothd_connection.hpp"
24
25 #include <iterator>
26
27 static lg::log_domain log_engine("engine");
28 #define WRN_NG LOG_STREAM(warn, log_engine)
29
30 static lg::log_domain log_lobby("lobby");
31 #define DBG_LB LOG_STREAM(debug, log_lobby)
32 #define WRN_LB LOG_STREAM(warn, log_lobby)
33 #define ERR_LB LOG_STREAM(err, log_lobby)
34 #define SCOPE_LB log_scope2(log_lobby, __func__)
35
36 namespace mp
37 {
lobby_info(const std::vector<std::string> & installed_addons)38 lobby_info::lobby_info(const std::vector<std::string>& installed_addons)
39 : installed_addons_(installed_addons)
40 , gamelist_()
41 , gamelist_initialized_(false)
42 , rooms_()
43 , games_by_id_()
44 , games_()
45 , users_()
46 , whispers_()
47 , game_filters_()
48 , game_filter_invert_(false)
49 , games_visibility_()
50 {
51 }
52
do_notify(notify_mode mode,const std::string & sender,const std::string & message)53 void do_notify(notify_mode mode, const std::string& sender, const std::string& message)
54 {
55 switch(mode) {
56 case NOTIFY_WHISPER:
57 case NOTIFY_WHISPER_OTHER_WINDOW:
58 case NOTIFY_OWN_NICK:
59 mp_ui_alerts::private_message(true, sender, message);
60 break;
61 case NOTIFY_FRIEND_MESSAGE:
62 mp_ui_alerts::friend_message(true, sender, message);
63 break;
64 case NOTIFY_SERVER_MESSAGE:
65 mp_ui_alerts::server_message(true, sender, message);
66 break;
67 case NOTIFY_LOBBY_QUIT:
68 mp_ui_alerts::player_leaves(true);
69 break;
70 case NOTIFY_LOBBY_JOIN:
71 mp_ui_alerts::player_joins(true);
72 break;
73 case NOTIFY_MESSAGE:
74 mp_ui_alerts::public_message(true, sender, message);
75 break;
76 case NOTIFY_GAME_CREATED:
77 mp_ui_alerts::game_created(sender, message);
78 break;
79 default:
80 break;
81 }
82 }
83
84 namespace
85 {
dump_games_map(const lobby_info::game_info_map & games)86 std::string dump_games_map(const lobby_info::game_info_map& games)
87 {
88 std::stringstream ss;
89 for(const auto& v : games) {
90 const game_info& game = v.second;
91 ss << "G" << game.id << "(" << game.name << ") " << game.display_status_string() << " ";
92 }
93
94 ss << "\n";
95 return ss.str();
96 }
97
dump_games_config(const config & gamelist)98 std::string dump_games_config(const config& gamelist)
99 {
100 std::stringstream ss;
101 for(const auto& c : gamelist.child_range("game")) {
102 ss << "g" << c["id"] << "(" << c["name"] << ") " << c[config::diff_track_attribute] << " ";
103 }
104
105 ss << "\n";
106 return ss.str();
107 }
108
109 } // end anonymous namespace
110
process_gamelist(const config & data)111 void lobby_info::process_gamelist(const config& data)
112 {
113 SCOPE_LB;
114
115 gamelist_ = data;
116 gamelist_initialized_ = true;
117
118 games_by_id_.clear();
119
120 for(const auto& c : gamelist_.child("gamelist").child_range("game")) {
121 game_info game(c, installed_addons_);
122 games_by_id_.emplace(game.id, std::move(game));
123 }
124
125 DBG_LB << dump_games_map(games_by_id_);
126 DBG_LB << dump_games_config(gamelist_.child("gamelist"));
127
128 process_userlist();
129 }
130
process_gamelist_diff(const config & data)131 bool lobby_info::process_gamelist_diff(const config& data)
132 {
133 if(!process_gamelist_diff_impl(data)) {
134 // the gamelist is now corrupted, stop further processing and wait for a fresh list.
135 gamelist_initialized_ = false;
136 return false;
137 }
138 else {
139 return true;
140 }
141 }
process_gamelist_diff_impl(const config & data)142 bool lobby_info::process_gamelist_diff_impl(const config& data)
143 {
144 SCOPE_LB;
145 if(!gamelist_initialized_) {
146 return false;
147 }
148
149 DBG_LB << "prediff " << dump_games_config(gamelist_.child("gamelist"));
150
151 try {
152 gamelist_.apply_diff(data, true);
153 } catch(const config::error& e) {
154 ERR_LB << "Error while applying the gamelist diff: '" << e.message << "' Getting a new gamelist.\n";
155 return false;
156 }
157
158 DBG_LB << "postdiff " << dump_games_config(gamelist_.child("gamelist"));
159 DBG_LB << dump_games_map(games_by_id_);
160
161 for(config& c : gamelist_.child("gamelist").child_range("game")) {
162 DBG_LB << "data process: " << c["id"] << " (" << c[config::diff_track_attribute] << ")\n";
163
164 const int game_id = c["id"];
165 if(game_id == 0) {
166 ERR_LB << "game with id 0 in gamelist config" << std::endl;
167 return false;
168 }
169
170 auto current_i = games_by_id_.find(game_id);
171
172 const std::string& diff_result = c[config::diff_track_attribute];
173
174 if(diff_result == "new" || diff_result == "modified") {
175 // note: at this point (1.14.3) the server never sends a 'modified' and instead
176 // just sends a 'delete' followed by a 'new', it still works becasue the delete doesn't
177 // delete the element and just marks it as game_info::DELETED so that game_info::DELETED
178 // is replaced by game_info::UPDATED below. See also
179 // https://github.com/wesnoth/wesnoth/blob/1.14/src/server/server.cpp#L149
180 if(current_i == games_by_id_.end()) {
181 games_by_id_.emplace(game_id, game_info(c, installed_addons_));
182 continue;
183 }
184
185 // Had a game with that id, so update it and mark it as such
186 current_i->second = game_info(c, installed_addons_);
187 current_i->second.display_status = game_info::UPDATED;
188 } else if(diff_result == "deleted") {
189 if(current_i == games_by_id_.end()) {
190 WRN_LB << "Would have to delete a game that I don't have: " << game_id << std::endl;
191 continue;
192 }
193
194 if(current_i->second.display_status == game_info::NEW) {
195 // This means the game never made it through to the user interface,
196 // so just deleting it is fine.
197 games_by_id_.erase(current_i);
198 } else {
199 current_i->second.display_status = game_info::DELETED;
200 }
201 }
202 }
203
204 DBG_LB << dump_games_map(games_by_id_);
205
206 try {
207 gamelist_.clear_diff_track(data);
208 } catch(const config::error& e) {
209 ERR_LB << "Error while applying the gamelist diff (2): '" << e.message << "' Getting a new gamelist.\n";
210 return false;
211 }
212
213 DBG_LB << "postclean " << dump_games_config(gamelist_.child("gamelist"));
214
215 process_userlist();
216 return true;
217 }
218
process_userlist()219 void lobby_info::process_userlist()
220 {
221 SCOPE_LB;
222
223 users_.clear();
224 for(const auto& c : gamelist_.child_range("user")) {
225 users_.emplace_back(c);
226 }
227
228 std::stable_sort(users_.begin(), users_.end());
229
230 for(auto& ui : users_) {
231 if(ui.game_id == 0) {
232 continue;
233 }
234
235 game_info* g = get_game_by_id(ui.game_id);
236 if(!g) {
237 WRN_NG << "User " << ui.name << " has unknown game_id: " << ui.game_id << std::endl;
238 continue;
239 }
240
241 switch(ui.relation) {
242 case user_info::FRIEND:
243 g->has_friends = true;
244 break;
245 case user_info::IGNORED:
246 g->has_ignored = true;
247 break;
248 default:
249 break;
250 }
251 }
252 }
253
sync_games_display_status()254 void lobby_info::sync_games_display_status()
255 {
256 DBG_LB << "lobby_info::sync_games_display_status";
257 DBG_LB << "games_by_id_ size: " << games_by_id_.size();
258
259 auto i = games_by_id_.begin();
260
261 while(i != games_by_id_.end()) {
262 if(i->second.display_status == game_info::DELETED) {
263 i = games_by_id_.erase(i);
264 } else {
265 i->second.display_status = game_info::CLEAN;
266 ++i;
267 }
268 }
269
270 DBG_LB << " -> " << games_by_id_.size() << std::endl;
271
272 make_games_vector();
273 }
274
get_game_by_id(int id)275 game_info* lobby_info::get_game_by_id(int id)
276 {
277 auto i = games_by_id_.find(id);
278 return i == games_by_id_.end() ? nullptr : &i->second;
279 }
280
get_game_by_id(int id) const281 const game_info* lobby_info::get_game_by_id(int id) const
282 {
283 auto i = games_by_id_.find(id);
284 return i == games_by_id_.end() ? nullptr : &i->second;
285 }
286
get_room(const std::string & name)287 room_info* lobby_info::get_room(const std::string& name)
288 {
289 for(auto& r : rooms_) {
290 if(r.name() == name) {
291 return &r;
292 }
293 }
294
295 return nullptr;
296 }
297
get_room(const std::string & name) const298 const room_info* lobby_info::get_room(const std::string& name) const
299 {
300 for(const auto& r : rooms_) {
301 if(r.name() == name) {
302 return &r;
303 }
304 }
305
306 return nullptr;
307 }
308
has_room(const std::string & name) const309 bool lobby_info::has_room(const std::string& name) const
310 {
311 return get_room(name) != nullptr;
312 }
313
get_user(const std::string & name)314 user_info* lobby_info::get_user(const std::string& name)
315 {
316 for(auto& user : users_) {
317 if(user.name == name) {
318 return &user;
319 }
320 }
321
322 return nullptr;
323 }
324
open_room(const std::string & name)325 void lobby_info::open_room(const std::string& name)
326 {
327 if(!has_room(name)) {
328 rooms_.emplace_back(name);
329 }
330 }
331
close_room(const std::string & name)332 void lobby_info::close_room(const std::string& name)
333 {
334 DBG_LB << "lobby info: closing room " << name << std::endl;
335
336 if(room_info* r = get_room(name)) {
337 rooms_.erase(rooms_.begin() + (r - &rooms_[0]));
338 }
339 }
340
make_games_vector()341 void lobby_info::make_games_vector()
342 {
343 games_.reserve(games_by_id_.size());
344 games_.clear();
345
346 for(auto& v : games_by_id_) {
347 games_.push_back(&v.second);
348 }
349
350 // Reset the visibility mask. Its size should then match games_'s and all its bits be true.
351 games_visibility_.resize(games_.size());
352 games_visibility_.reset();
353 games_visibility_.flip();
354 }
355
apply_game_filter()356 void lobby_info::apply_game_filter()
357 {
358 // Since games_visibility_ is a visibility mask over games_,
359 // they need to be the same size or we'll end up with issues.
360 assert(games_visibility_.size() == games_.size());
361
362 for(unsigned i = 0; i < games_.size(); ++i) {
363 bool show = true;
364
365 for(const auto& filter_func : game_filters_) {
366 show = filter_func(*games_[i]);
367
368 if(!show) {
369 break;
370 }
371 }
372
373 if(game_filter_invert_) {
374 show = !show;
375 }
376
377 games_visibility_[i] = show;
378 }
379 }
380
update_user_statuses(int game_id,const room_info * room)381 void lobby_info::update_user_statuses(int game_id, const room_info* room)
382 {
383 for(auto& user : users_) {
384 user.update_state(game_id, room);
385 }
386 }
387
388 } // end namespace mp
389