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