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