1 /*
2  Copyright (c) 2013 yvt
3 
4  Portion of the code is based on Serverbrowser.cpp (Copyright (c) 2013 learn_more).
5 
6  This file is part of OpenSpades.
7 
8  OpenSpades is free software: you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation, either version 3 of the License, or
11  (at your option) any later version.
12 
13  OpenSpades is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  GNU General Public License for more details.
17 
18  You should have received a copy of the GNU General Public License
19  along with OpenSpades.  If not, see <http://www.gnu.org/licenses/>.
20 
21  */
22 
23 #include <algorithm>
24 #include <cctype>
25 #include <memory>
26 
27 #include <curl/curl.h>
28 #include <json/json.h>
29 
30 #include "MainScreen.h"
31 #include "MainScreenHelper.h"
32 #include <Core/AutoLocker.h>
33 #include <Core/FileManager.h>
34 #include <Core/IStream.h>
35 #include <Core/Settings.h>
36 #include <Core/Thread.h>
37 #include <Gui/PackageUpdateManager.h>
38 #include <OpenSpades.h>
39 
40 DEFINE_SPADES_SETTING(cl_serverListUrl, "http://services.buildandshoot.com/serverlist.json");
41 
42 namespace spades {
43 	namespace {
44 		struct CURLEasyDeleter {
operator ()spades::__anonb050e4e20111::CURLEasyDeleter45 			void operator()(CURL *ptr) const { curl_easy_cleanup(ptr); }
46 		};
47 	}
48 
49 	class ServerItem {
50 		// NetClient::Connect
51 		std::string mName, mIp, mMap, mGameMode;
52 		std::string mCountry, mVersion;
53 		int mPing, mPlayers, mMaxPlayers;
54 
55 		ServerItem(const std::string &name, const std::string &ip, const std::string &map,
56 		           const std::string &gameMode, const std::string &country,
57 		           const std::string &version, int ping, int players, int maxPlayers);
58 
59 	public:
60 		static ServerItem *Create(Json::Value &val);
61 
GetName() const62 		inline const std::string &GetName() const { return mName; }
GetAddress() const63 		inline const std::string &GetAddress() const { return mIp; }
GetMapName() const64 		inline const std::string &GetMapName() const { return mMap; }
GetGameMode() const65 		inline const std::string &GetGameMode() const { return mGameMode; }
GetCountryCode() const66 		inline const std::string &GetCountryCode() const { return mCountry; }
GetVersion() const67 		inline const std::string &GetVersion() const { return mVersion; }
GetPing() const68 		inline int GetPing() const { return mPing; }
GetNumPlayers() const69 		inline int GetNumPlayers() const { return mPlayers; }
GetMaxNumPlayers() const70 		inline int GetMaxNumPlayers() const { return mMaxPlayers; }
71 	};
72 
ServerItem(const std::string & name,const std::string & ip,const std::string & map,const std::string & gameMode,const std::string & country,const std::string & version,int ping,int players,int maxPlayers)73 	ServerItem::ServerItem(const std::string &name, const std::string &ip, const std::string &map,
74 	                       const std::string &gameMode, const std::string &country,
75 	                       const std::string &version, int ping, int players, int maxPlayers)
76 	    : mName(name),
77 	      mIp(ip),
78 	      mMap(map),
79 	      mGameMode(gameMode),
80 	      mCountry(country),
81 	      mVersion(version),
82 	      mPing(ping),
83 	      mPlayers(players),
84 	      mMaxPlayers(maxPlayers) {}
85 
Create(Json::Value & val)86 	ServerItem *ServerItem::Create(Json::Value &val) {
87 		ServerItem *item = NULL;
88 		if (val.type() == Json::objectValue) {
89 			std::string name, ip, map, gameMode, country, version;
90 			int ping = 0, players = 0, maxPlayers = 0;
91 
92 			name = val["name"].asString();
93 			ip = val["identifier"].asString();
94 			map = val["map"].asString();
95 			gameMode = val["game_mode"].asString();
96 			country = val["country"].asString();
97 			version = val["game_version"].asString();
98 
99 			ping = val["latency"].asInt();
100 			players = val["players_current"].asInt();
101 			maxPlayers = val["players_max"].asInt();
102 			item =
103 			  new ServerItem(name, ip, map, gameMode, country, version, ping, players, maxPlayers);
104 		}
105 		return item;
106 	}
107 
108 	namespace gui {
109 		constexpr auto FAVORITE_PATH = "/favorite_servers.json";
110 
111 		class MainScreenHelper::ServerListQuery final : public Thread {
112 			Handle<MainScreenHelper> owner;
113 			std::string buffer;
114 
ReturnResult(std::unique_ptr<MainScreenServerList> && list)115 			void ReturnResult(std::unique_ptr<MainScreenServerList> &&list) {
116 				owner->resultCell.store(std::move(list));
117 				owner = NULL; // release owner
118 			}
119 
ProcessResponse()120 			void ProcessResponse() {
121 				Json::Reader reader;
122 				Json::Value root;
123 				std::unique_ptr<MainScreenServerList> resp{new MainScreenServerList()};
124 
125 				if (reader.parse(buffer, root, false)) {
126 					for (Json::Value::iterator it = root.begin(); it != root.end(); ++it) {
127 						Json::Value &obj = *it;
128 						std::unique_ptr<ServerItem> srv{ServerItem::Create(obj)};
129 						if (srv) {
130 							resp->list.emplace_back(
131 							  new MainScreenServerItem(
132 							    srv.get(), owner->favorites.count(srv->GetAddress()) >= 1),
133 							  false);
134 						}
135 					}
136 				}
137 
138 				ReturnResult(std::move(resp));
139 			}
140 
141 		public:
ServerListQuery(MainScreenHelper * owner)142 			ServerListQuery(MainScreenHelper *owner) : owner{owner} {}
143 
Run()144 			void Run() override {
145 				try {
146 					std::unique_ptr<CURL, CURLEasyDeleter> cHandle{curl_easy_init()};
147 					if (cHandle) {
148 						size_t (*curlWriteCallback)(void *, size_t, size_t, ServerListQuery *) = [](
149 						  void *ptr, size_t size, size_t nmemb, ServerListQuery *self) -> size_t {
150 							size_t numBytes = size * nmemb;
151 							self->buffer.append(reinterpret_cast<char *>(ptr), numBytes);
152 							return numBytes;
153 						};
154 						curl_easy_setopt(cHandle.get(), CURLOPT_USERAGENT, OpenSpades_VER_STR);
155 						curl_easy_setopt(cHandle.get(), CURLOPT_URL, cl_serverListUrl.CString());
156 						curl_easy_setopt(cHandle.get(), CURLOPT_WRITEFUNCTION, curlWriteCallback);
157 						curl_easy_setopt(cHandle.get(), CURLOPT_WRITEDATA, this);
158 						if (0 == curl_easy_perform(cHandle.get())) {
159 							ProcessResponse();
160 						} else {
161 							SPRaise("HTTP request error.");
162 						}
163 					} else {
164 						SPRaise("Failed to create cURL object.");
165 					}
166 				} catch (std::exception &ex) {
167 					std::unique_ptr<MainScreenServerList> lst{new MainScreenServerList()};
168 					lst->message = ex.what();
169 					ReturnResult(std::move(lst));
170 				} catch (...) {
171 					std::unique_ptr<MainScreenServerList> lst{new MainScreenServerList()};
172 					lst->message = "Unknown error.";
173 					ReturnResult(std::move(lst));
174 				}
175 			}
176 		};
177 
MainScreenHelper(MainScreen * scr)178 		MainScreenHelper::MainScreenHelper(MainScreen *scr) : mainScreen(scr), query(NULL) {
179 			SPADES_MARK_FUNCTION();
180 			LoadFavorites();
181 		}
182 
~MainScreenHelper()183 		MainScreenHelper::~MainScreenHelper() {
184 			SPADES_MARK_FUNCTION();
185 			if (query) {
186 				query->MarkForAutoDeletion();
187 			}
188 		}
189 
MainScreenDestroyed()190 		void MainScreenHelper::MainScreenDestroyed() {
191 			SPADES_MARK_FUNCTION();
192 			SaveFavorites();
193 			mainScreen = NULL;
194 		}
195 
LoadFavorites()196 		void MainScreenHelper::LoadFavorites() {
197 			SPADES_MARK_FUNCTION();
198 			Json::Reader reader;
199 
200 			if (spades::FileManager::FileExists(FAVORITE_PATH)) {
201 				std::string favs = spades::FileManager::ReadAllBytes(FAVORITE_PATH);
202 				Json::Value favorite_root;
203 				if (reader.parse(favs, favorite_root, false)) {
204 					for (const auto &fav : favorite_root) {
205 						if (fav.isString())
206 							favorites.insert(fav.asString());
207 					}
208 				}
209 			}
210 		}
211 
SaveFavorites()212 		void MainScreenHelper::SaveFavorites() {
213 			SPADES_MARK_FUNCTION();
214 			Json::StyledWriter writer;
215 			Json::Value v(Json::ValueType::arrayValue);
216 
217 			IStream *fobj = spades::FileManager::OpenForWriting(FAVORITE_PATH);
218 			for (const auto &favorite : favorites) {
219 				v.append(Json::Value(favorite));
220 			}
221 
222 			fobj->Write(writer.write(v));
223 		}
224 
SetServerFavorite(std::string ip,bool favorite)225 		void MainScreenHelper::SetServerFavorite(std::string ip, bool favorite) {
226 			SPADES_MARK_FUNCTION();
227 			if (favorite) {
228 				favorites.insert(ip);
229 			} else {
230 				favorites.erase(ip);
231 			}
232 
233 			if (result && !result->list.empty()) {
234 				auto entry = std::find_if(
235 				  result->list.begin(), result->list.end(),
236 				  [&](MainScreenServerItem *entry) { return entry->GetAddress() == ip; });
237 				if (entry != result->list.end()) {
238 					(*entry)->SetFavorite(favorite);
239 				}
240 			}
241 		}
242 
PollServerListState()243 		bool MainScreenHelper::PollServerListState() {
244 			SPADES_MARK_FUNCTION();
245 
246 			// Do we have a new result?
247 			auto newResult = resultCell.take();
248 			if (newResult) {
249 				result = std::move(newResult);
250 				query->MarkForAutoDeletion();
251 				query = NULL;
252 				return true;
253 			}
254 
255 			return false;
256 		}
257 
StartQuery()258 		void MainScreenHelper::StartQuery() {
259 			if (query) {
260 				// There already is an ongoing query
261 				return;
262 			}
263 
264 			query = new ServerListQuery(this);
265 			query->Start();
266 		}
267 
268 #include "Credits.inc" // C++11 raw string literal makes some tools (ex. xgettext, Xcode) misbehave
269 
GetCredits()270 		std::string MainScreenHelper::GetCredits() {
271 			std::string html = credits;
272 			html = Replace(html, "${PACKAGE_STRING}", PACKAGE_STRING);
273 			return html;
274 		}
275 
GetServerList(std::string sortKey,bool descending)276 		CScriptArray *MainScreenHelper::GetServerList(std::string sortKey, bool descending) {
277 			if (result == NULL) {
278 				return NULL;
279 			}
280 
281 			using Item = const Handle<MainScreenServerItem> &;
282 			std::vector<Handle<MainScreenServerItem>> &lst = result->list;
283 			if (lst.empty())
284 				return NULL;
285 
286 			auto compareFavorite = [&](Item x, Item y) -> stmp::optional<bool> {
287 				if (x->IsFavorite() && !y->IsFavorite()) {
288 					return true;
289 				} else if (!x->IsFavorite() && y->IsFavorite()) {
290 					return false;
291 				} else {
292 					return {};
293 				}
294 			};
295 
296 			auto compareInts = [&](int x, int y) -> bool {
297 				if (descending) {
298 					return y < x;
299 				} else {
300 					return x < y;
301 				}
302 			};
303 
304 			auto compareStrings = [&](const std::string &x0, const std::string &y0) -> bool {
305 				const auto &x = descending ? y0 : x0;
306 				const auto &y = descending ? x0 : y0;
307 				std::string::size_type t = 0;
308 				for (t = 0; t < x.length() && t < y.length(); ++t) {
309 					int xx = std::tolower(x[t]);
310 					int yy = std::tolower(y[t]);
311 					if (xx != yy) {
312 						return xx < yy;
313 					}
314 				}
315 				if (x.length() == y.length()) {
316 					return false;
317 				}
318 				return x.length() < y.length();
319 			};
320 
321 			if (!sortKey.empty()) {
322 				if (sortKey == "Ping") {
323 					std::stable_sort(lst.begin(), lst.end(), [&](Item x, Item y) {
324 						return compareFavorite(x, y).value_or(
325 						  compareInts(x->GetPing(), y->GetPing()));
326 					});
327 				} else if (sortKey == "NumPlayers") {
328 					std::stable_sort(lst.begin(), lst.end(), [&](Item x, Item y) {
329 						return compareFavorite(x, y).value_or(
330 						  compareInts(x->GetNumPlayers(), y->GetNumPlayers()));
331 					});
332 				} else if (sortKey == "Name") {
333 					std::stable_sort(lst.begin(), lst.end(), [&](Item x, Item y) {
334 						return compareFavorite(x, y).value_or(
335 						  compareStrings(x->GetName(), y->GetName()));
336 					});
337 				} else if (sortKey == "MapName") {
338 					std::stable_sort(lst.begin(), lst.end(), [&](Item x, Item y) {
339 						return compareFavorite(x, y).value_or(
340 						  compareStrings(x->GetMapName(), y->GetMapName()));
341 					});
342 				} else if (sortKey == "GameMode") {
343 					std::stable_sort(lst.begin(), lst.end(), [&](Item x, Item y) {
344 						return compareFavorite(x, y).value_or(
345 						  compareStrings(x->GetGameMode(), y->GetGameMode()));
346 					});
347 				} else if (sortKey == "Protocol") {
348 					std::stable_sort(lst.begin(), lst.end(), [&](Item x, Item y) {
349 						return compareFavorite(x, y).value_or(
350 						  compareStrings(x->GetProtocol(), y->GetProtocol()));
351 					});
352 				} else if (sortKey == "Country") {
353 					std::stable_sort(lst.begin(), lst.end(), [&](Item x, Item y) {
354 						return compareFavorite(x, y).value_or(
355 						  compareStrings(x->GetCountry(), y->GetCountry()));
356 					});
357 				} else {
358 					SPRaise("Invalid sort key: %s", sortKey.c_str());
359 				}
360 			}
361 
362 			asIScriptEngine *eng = ScriptManager::GetInstance()->GetEngine();
363 			asITypeInfo *t = eng->GetTypeInfoByDecl("array<spades::MainScreenServerItem@>");
364 			SPAssert(t != NULL);
365 			CScriptArray *arr = CScriptArray::Create(t, static_cast<asUINT>(lst.size()));
366 			for (size_t i = 0; i < lst.size(); i++) {
367 				arr->SetValue((asUINT)i, &(lst[i]));
368 			}
369 			return arr;
370 		}
371 
ConnectServer(std::string hostname,int protocolVersion)372 		std::string MainScreenHelper::ConnectServer(std::string hostname, int protocolVersion) {
373 			if (mainScreen == NULL) {
374 				return "mainScreen == NULL";
375 			}
376 			return mainScreen->Connect(ServerAddress(
377 			  hostname, protocolVersion == 3 ? ProtocolVersion::v075 : ProtocolVersion::v076));
378 		}
379 
GetServerListQueryMessage()380 		std::string MainScreenHelper::GetServerListQueryMessage() {
381 			if (result == NULL)
382 				return "";
383 			return result->message;
384 		}
385 
GetPendingErrorMessage()386 		std::string MainScreenHelper::GetPendingErrorMessage() {
387 			std::string s = errorMessage;
388 			errorMessage.clear();
389 			return s;
390 		}
391 
GetPackageUpdateManager()392 		PackageUpdateManager &MainScreenHelper::GetPackageUpdateManager() {
393 			return PackageUpdateManager::GetInstance();
394 		}
395 
~MainScreenServerList()396 		MainScreenServerList::~MainScreenServerList() {}
397 
MainScreenServerItem(ServerItem * item,bool favorite)398 		MainScreenServerItem::MainScreenServerItem(ServerItem *item, bool favorite) {
399 			SPADES_MARK_FUNCTION();
400 			name = item->GetName();
401 			address = item->GetAddress();
402 			mapName = item->GetMapName();
403 			gameMode = item->GetGameMode();
404 			country = item->GetCountryCode();
405 			protocol = item->GetVersion();
406 			ping = item->GetPing();
407 			numPlayers = item->GetNumPlayers();
408 			maxPlayers = item->GetMaxNumPlayers();
409 			this->favorite = favorite;
410 		}
411 
~MainScreenServerItem()412 		MainScreenServerItem::~MainScreenServerItem() { SPADES_MARK_FUNCTION(); }
413 	}
414 }
415