/* * This file is part of OpenTTD. * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . */ /** @file network_survey.cpp Opt-in survey part of the network protocol. */ #include "../stdafx.h" #include "network_survey.h" #include "settings_table.h" #include "network.h" #include "../debug.h" #include "../rev.h" #include "../settings_type.h" #include "../timer/timer_game_tick.h" #include "../currency.h" #include "../fontcache.h" #include "../language.h" #include "../ai/ai_info.hpp" #include "../game/game.hpp" #include "../game/game_info.hpp" #include "../music/music_driver.hpp" #include "../sound/sound_driver.hpp" #include "../video/video_driver.hpp" #include "../base_media_base.h" #include "../blitter/factory.hpp" #ifdef WITH_NLOHMANN_JSON #include #endif /* WITH_NLOHMANN_JSON */ #include "../safeguards.h" extern std::string _savegame_id; NetworkSurveyHandler _survey = {}; #ifdef WITH_NLOHMANN_JSON NLOHMANN_JSON_SERIALIZE_ENUM(NetworkSurveyHandler::Reason, { {NetworkSurveyHandler::Reason::PREVIEW, "preview"}, {NetworkSurveyHandler::Reason::LEAVE, "leave"}, {NetworkSurveyHandler::Reason::EXIT, "exit"}, {NetworkSurveyHandler::Reason::CRASH, "crash"}, }) NLOHMANN_JSON_SERIALIZE_ENUM(GRFStatus, { {GRFStatus::GCS_UNKNOWN, "unknown"}, {GRFStatus::GCS_DISABLED, "disabled"}, {GRFStatus::GCS_NOT_FOUND, "not found"}, {GRFStatus::GCS_INITIALISED, "initialised"}, {GRFStatus::GCS_ACTIVATED, "activated"}, }) static const std::string _vehicle_type_to_string[] = { "train", "roadveh", "ship", "aircraft", }; /* Defined in one of the os/ survey files. */ extern void SurveyOS(nlohmann::json &json); /** * List of all the generic setting tables. * * There are a few tables that are special and not processed like the rest: * - _currency_settings * - _misc_settings * - _company_settings * - _win32_settings * As such, they are not part of this list. */ static auto &GenericSettingTables() { static const SettingTable _generic_setting_tables[] = { _difficulty_settings, _economy_settings, _game_settings, _gui_settings, _linkgraph_settings, _locale_settings, _multimedia_settings, _network_settings, _news_display_settings, _pathfinding_settings, _script_settings, _world_settings, }; return _generic_setting_tables; } /** * Convert a settings table to JSON. * * @param survey The JSON object. * @param table The settings table to convert. * @param object The object to get the settings from. */ static void SurveySettingsTable(nlohmann::json &survey, const SettingTable &table, void *object) { char buf[512]; for (auto &desc : table) { const SettingDesc *sd = GetSettingDesc(desc); /* Skip any old settings we no longer save/load. */ if (!SlIsObjectCurrentlyValid(sd->save.version_from, sd->save.version_to)) continue; auto name = sd->GetName(); sd->FormatValue(buf, lastof(buf), object); survey[name] = buf; } } /** * Convert settings to JSON. * * @param survey The JSON object. */ static void SurveySettings(nlohmann::json &survey) { SurveySettingsTable(survey, _misc_settings, nullptr); #if defined(_WIN32) && !defined(DEDICATED) SurveySettingsTable(survey, _win32_settings, nullptr); #endif for (auto &table : GenericSettingTables()) { SurveySettingsTable(survey, table, &_settings_game); } SurveySettingsTable(survey, _currency_settings, &_custom_currency); SurveySettingsTable(survey, _company_settings, &_settings_client.company); } /** * Convert generic OpenTTD information to JSON. * * @param survey The JSON object. */ static void SurveyOpenTTD(nlohmann::json &survey) { survey["version"] = std::string(_openttd_revision); survey["newgrf_version"] = _openttd_newgrf_version; survey["build_date"] = std::string(_openttd_build_date); survey["bits"] = #ifdef POINTER_IS_64BIT 64 #else 32 #endif ; survey["endian"] = #if (TTD_ENDIAN == TTD_LITTLE_ENDIAN) "little" #else "big" #endif ; survey["dedicated_build"] = #ifdef DEDICATED "yes" #else "no" #endif ; } /** * Convert generic game information to JSON. * * @param survey The JSON object. */ static void SurveyConfiguration(nlohmann::json &survey) { survey["network"] = _networking ? (_network_server ? "server" : "client") : "no"; if (_current_language != nullptr) { std::string_view language_basename(_current_language->file); auto e = language_basename.rfind(PATHSEPCHAR); if (e != std::string::npos) { language_basename = language_basename.substr(e + 1); } survey["language"]["filename"] = language_basename; survey["language"]["name"] = _current_language->name; survey["language"]["isocode"] = _current_language->isocode; } if (BlitterFactory::GetCurrentBlitter() != nullptr) { survey["blitter"] = BlitterFactory::GetCurrentBlitter()->GetName(); } if (MusicDriver::GetInstance() != nullptr) { survey["music_driver"] = MusicDriver::GetInstance()->GetName(); } if (SoundDriver::GetInstance() != nullptr) { survey["sound_driver"] = SoundDriver::GetInstance()->GetName(); } if (VideoDriver::GetInstance() != nullptr) { survey["video_driver"] = VideoDriver::GetInstance()->GetName(); survey["video_info"] = VideoDriver::GetInstance()->GetInfoString(); } if (BaseGraphics::GetUsedSet() != nullptr) { survey["graphics_set"] = fmt::format("{}.{}", BaseGraphics::GetUsedSet()->name, BaseGraphics::GetUsedSet()->version); } if (BaseMusic::GetUsedSet() != nullptr) { survey["music_set"] = fmt::format("{}.{}", BaseMusic::GetUsedSet()->name, BaseMusic::GetUsedSet()->version); } if (BaseSounds::GetUsedSet() != nullptr) { survey["sound_set"] = fmt::format("{}.{}", BaseSounds::GetUsedSet()->name, BaseSounds::GetUsedSet()->version); } } /** * Convert font information to JSON. * * @param survey The JSON object. */ static void SurveyFont(nlohmann::json &survey) { survey["small"] = FontCache::Get(FS_SMALL)->GetFontName(); survey["medium"] = FontCache::Get(FS_NORMAL)->GetFontName(); survey["large"] = FontCache::Get(FS_LARGE)->GetFontName(); survey["mono"] = FontCache::Get(FS_MONO)->GetFontName(); } /** * Convert company information to JSON. * * @param survey The JSON object. */ static void SurveyCompanies(nlohmann::json &survey) { for (const Company *c : Company::Iterate()) { auto &company = survey[std::to_string(c->index)]; if (c->ai_info == nullptr) { company["type"] = "human"; } else { company["type"] = "ai"; company["script"] = fmt::format("{}.{}", c->ai_info->GetName(), c->ai_info->GetVersion()); } for (VehicleType type = VEH_BEGIN; type < VEH_COMPANY_END; type++) { uint amount = c->group_all[type].num_vehicle; company["vehicles"][_vehicle_type_to_string[type]] = amount; } company["infrastructure"]["road"] = c->infrastructure.GetRoadTotal(); company["infrastructure"]["tram"] = c->infrastructure.GetTramTotal(); company["infrastructure"]["rail"] = c->infrastructure.GetRailTotal(); company["infrastructure"]["signal"] = c->infrastructure.signal; company["infrastructure"]["water"] = c->infrastructure.water; company["infrastructure"]["station"] = c->infrastructure.station; company["infrastructure"]["airport"] = c->infrastructure.airport; } } /** * Convert GRF information to JSON. * * @param survey The JSON object. */ static void SurveyGrfs(nlohmann::json &survey) { for (GRFConfig *c = _grfconfig; c != nullptr; c = c->next) { auto grfid = fmt::format("{:08x}", BSWAP32(c->ident.grfid)); auto &grf = survey[grfid]; grf["md5sum"] = MD5SumToString(c->ident.md5sum); grf["status"] = c->status; if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_UNSET) grf["palette"] = "unset"; if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_DOS) grf["palette"] = "dos"; if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_WINDOWS) grf["palette"] = "windows"; if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_ANY) grf["palette"] = "any"; if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_UNSET) grf["blitter"] = "unset"; if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_32BPP) grf["blitter"] = "32bpp"; grf["is_static"] = HasBit(c->flags, GCF_STATIC); std::vector parameters; for (int i = 0; i < c->num_params; i++) { parameters.push_back(c->param[i]); } grf["parameters"] = parameters; } } /** * Convert game-script information to JSON. * * @param survey The JSON object. */ static void SurveyGameScript(nlohmann::json &survey) { if (Game::GetInfo() == nullptr) return; survey = fmt::format("{}.{}", Game::GetInfo()->GetName(), Game::GetInfo()->GetVersion()); } #endif /* WITH_NLOHMANN_JSON */ /** * Create the payload for the survey. * * @param reason The reason for sending the survey. * @param for_preview Whether the payload is meant for preview. This indents the result, and filters out the id/key. * @return std::string The JSON payload as string for the survey. */ std::string NetworkSurveyHandler::CreatePayload(Reason reason, bool for_preview) { #ifndef WITH_NLOHMANN_JSON return ""; #else nlohmann::json survey; survey["schema"] = NETWORK_SURVEY_VERSION; survey["reason"] = reason; survey["id"] = _savegame_id; #ifdef SURVEY_KEY /* We censor the key to avoid people trying to be "clever" and use it to send their own surveys. */ survey["key"] = for_preview ? "(redacted)" : SURVEY_KEY; #else survey["key"] = ""; #endif { auto &info = survey["info"]; SurveyOS(info["os"]); info["os"]["hardware_concurrency"] = std::thread::hardware_concurrency(); SurveyOpenTTD(info["openttd"]); SurveyConfiguration(info["configuration"]); SurveyFont(info["font"]); } { auto &game = survey["game"]; game["ticks"] = TimerGameTick::counter; game["time"] = std::chrono::duration_cast(std::chrono::steady_clock::now() - _switch_mode_time).count(); SurveyCompanies(game["companies"]); SurveySettings(game["settings"]); SurveyGrfs(game["grfs"]); SurveyGameScript(game["game_script"]); } /* For preview, we indent with 4 whitespaces to make things more readable. */ int indent = for_preview ? 4 : -1; return survey.dump(indent); #endif /* WITH_NLOHMANN_JSON */ } /** * Transmit the survey. * * @param reason The reason for sending the survey. * @param blocking Whether to block until the survey is sent. */ void NetworkSurveyHandler::Transmit(Reason reason, bool blocking) { if constexpr (!NetworkSurveyHandler::IsSurveyPossible()) { Debug(net, 4, "Survey: not possible to send survey; most likely due to missing JSON library at compile-time"); return; } if (_settings_client.network.participate_survey != PS_YES) { Debug(net, 5, "Survey: user is not participating in survey; skipping survey"); return; } Debug(net, 1, "Survey: sending survey results"); NetworkHTTPSocketHandler::Connect(NetworkSurveyUriString(), this, this->CreatePayload(reason)); if (blocking) { std::unique_lock lock(this->mutex); /* Block no longer than 2 seconds. If we failed to send the survey in that time, so be it. */ this->loaded.wait_for(lock, std::chrono::seconds(2)); } } void NetworkSurveyHandler::OnFailure() { Debug(net, 1, "Survey: failed to send survey results"); this->loaded.notify_all(); } void NetworkSurveyHandler::OnReceiveData(const char *data, size_t length) { if (data == nullptr) { Debug(net, 1, "Survey: survey results sent"); this->loaded.notify_all(); } }