Feature: opt-in survey when exiting a game

On first start-up, the game will ask if you want to participate
in our automated survey. You have to opt-in, and can easily opt-out
(via the Options) at any time.

When opt-in, whenever you exit a game, a JSON blob will be send
to the survey server hosted by OpenTTD. This JSON blob contains
information that gives a global picture of the game just played:
- What settings were used
- How many humans vs AIs
- How long the game has been played
- Basic information about the OS / CPU

All this information is kept very generic, so there is no
chance we send private information to our survey server.
Nothing in the JSON blob could identify you as a person; it
mostly tells about the game played. At any time you can see
what the JSON blob includes, by pressing the "Preview Survey
Results" button in-game.
This commit is contained in:
Patric Stout
2023-04-25 19:43:45 +02:00
committed by Patric Stout
parent 021c45c4f6
commit 7634553d22
54 changed files with 1018 additions and 53 deletions

View File

@@ -28,6 +28,8 @@ add_files(
network_server.h
network_stun.cpp
network_stun.h
network_survey.cpp
network_survey.h
network_turn.cpp
network_turn.h
network_type.h

View File

@@ -66,3 +66,13 @@ const char *NetworkContentMirrorUriString()
{
return GetEnv("OTTD_CONTENT_MIRROR_URI", "https://binaries.openttd.org/bananas");
}
/**
* Get the URI string for the survey from the environment variable OTTD_SURVEY_URI,
* or when it has not been set a hard coded URI of the production server.
* @return The survey's URI string.
*/
const char *NetworkSurveyUriString()
{
return GetEnv("OTTD_SURVEY_URI", "https://survey-participate.openttd.org/");
}

View File

@@ -16,6 +16,7 @@ const char *NetworkCoordinatorConnectionString();
const char *NetworkStunConnectionString();
const char *NetworkContentServerConnectionString();
const char *NetworkContentMirrorUriString();
const char *NetworkSurveyUriString();
static const uint16 NETWORK_COORDINATOR_SERVER_PORT = 3976; ///< The default port of the Game Coordinator server (TCP)
static const uint16 NETWORK_STUN_SERVER_PORT = 3975; ///< The default port of the STUN server (TCP)
@@ -26,6 +27,8 @@ static const uint16 NETWORK_ADMIN_PORT = 3977; ///< The d
static const uint16 NETWORK_DEFAULT_DEBUGLOG_PORT = 3982; ///< The default port debug-log is sent to (TCP)
static const uint16 UDP_MTU = 1460; ///< Number of bytes we can pack in a single UDP packet
static const std::string NETWORK_SURVEY_DETAILS_LINK = "https://survey.openttd.org/participate"; ///< Link with more details & privacy statement of the survey.
/*
* Technically a TCP packet could become 64kiB, however the high bit is kept so it becomes possible in the future
* to go to (significantly) larger packets if needed. This would entail a strategy such as employed for UTF-8.
@@ -46,6 +49,7 @@ static const uint16 COMPAT_MTU = 1460; ///< Numbe
static const byte NETWORK_GAME_ADMIN_VERSION = 3; ///< What version of the admin network do we use?
static const byte NETWORK_GAME_INFO_VERSION = 6; ///< What version of game-info do we use?
static const byte NETWORK_COORDINATOR_VERSION = 6; ///< What version of game-coordinator-protocol do we use?
static const byte NETWORK_SURVEY_VERSION = 1; ///< What version of the survey do we use?
static const uint NETWORK_NAME_LENGTH = 80; ///< The maximum length of the server name and map name, in bytes including '\0'
static const uint NETWORK_COMPANY_NAME_LENGTH = 128; ///< The maximum length of the company name, in bytes including '\0'

View File

@@ -14,6 +14,8 @@
#include "tcp.h"
constexpr int HTTP_429_TOO_MANY_REQUESTS = 429;
/** Callback for when the HTTP handler has something to tell us. */
struct HTTPCallback {
/**

View File

@@ -116,6 +116,7 @@ void HttpThread()
/* Reset to default settings. */
curl_easy_reset(curl);
curl_slist *headers = nullptr;
if (_debug_net_level >= 5) {
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
@@ -146,8 +147,16 @@ void HttpThread()
/* Prepare POST body and URI. */
if (!request->data.empty()) {
/* When the payload starts with a '{', it is a JSON payload. */
if (StrStartsWith(request->data, "{")) {
headers = curl_slist_append(headers, "Content-Type: application/json");
} else {
headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded");
}
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->data.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
curl_easy_setopt(curl, CURLOPT_URL, request->uri.c_str());
@@ -174,11 +183,17 @@ void HttpThread()
/* Perform the request. */
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
if (res == CURLE_OK) {
Debug(net, 1, "HTTP request succeeded");
request->callback->OnReceiveData(nullptr, 0);
} else {
Debug(net, (request->callback->IsCancelled() || _http_thread_exit) ? 1 : 0, "HTTP request failed: {}", curl_easy_strerror(res));
long status_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code);
/* No need to be verbose about rate limiting. */
Debug(net, (request->callback->IsCancelled() || _http_thread_exit || status_code == HTTP_429_TOO_MANY_REQUESTS) ? 1 : 0, "HTTP request failed: status_code: {}, error: {}", status_code, curl_easy_strerror(res));
request->callback->OnFailure();
}
}

View File

@@ -131,7 +131,8 @@ void NetworkHTTPRequest::WinHttpCallback(DWORD code, void *info, DWORD length)
/* If there is any error, we simply abort the request. */
if (status_code >= 400) {
Debug(net, 0, "HTTP request failed: status-code {}", status_code);
/* No need to be verbose about rate limiting. */
Debug(net, status_code == HTTP_429_TOO_MANY_REQUESTS ? 1 : 0, "HTTP request failed: status-code {}", status_code);
this->finished = true;
this->callback->OnFailure();
return;
@@ -242,7 +243,9 @@ void NetworkHTTPRequest::Connect()
if (data.empty()) {
WinHttpSendRequest(this->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, reinterpret_cast<DWORD_PTR>(this));
} else {
WinHttpSendRequest(this->request, L"Content-Type: application/x-www-form-urlencoded\r\n", -1, const_cast<char *>(data.c_str()), static_cast<DWORD>(data.size()), static_cast<DWORD>(data.size()), reinterpret_cast<DWORD_PTR>(this));
/* When the payload starts with a '{', it is a JSON payload. */
LPCWSTR content_type = StrStartsWith(data, "{") ? L"Content-Type: application/json\r\n" : L"Content-Type: application/x-www-form-urlencoded\r\n";
WinHttpSendRequest(this->request, content_type, -1, const_cast<char *>(data.c_str()), static_cast<DWORD>(data.size()), static_cast<DWORD>(data.size()), reinterpret_cast<DWORD_PTR>(this));
}
}

View File

@@ -85,6 +85,8 @@ static_assert((int)NETWORK_COMPANY_NAME_LENGTH == MAX_LENGTH_COMPANY_NAME_CHARS
/** The amount of clients connected */
byte _network_clients_connected = 0;
extern std::string GenerateUid(std::string_view subject);
/**
* Return whether there is any client connected or trying to connect at all.
* @return whether we have any client activity
@@ -1204,24 +1206,7 @@ void NetworkGameLoop()
static void NetworkGenerateServerId()
{
Md5 checksum;
uint8 digest[16];
char hex_output[16 * 2 + 1];
char coding_string[NETWORK_NAME_LENGTH];
int di;
seprintf(coding_string, lastof(coding_string), "%d%s", (uint)Random(), "OpenTTD Server ID");
/* Generate the MD5 hash */
checksum.Append((const uint8*)coding_string, strlen(coding_string));
checksum.Finish(digest);
for (di = 0; di < 16; ++di) {
seprintf(hex_output + di * 2, lastof(hex_output), "%02x", digest[di]);
}
/* _settings_client.network.network_id is our id */
_settings_client.network.network_id = hex_output;
_settings_client.network.network_id = GenerateUid("OpenTTD Server ID");
}
class TCPNetworkDebugConnecter : TCPConnecter {

View File

@@ -790,7 +790,7 @@ public:
void OnClick(Point pt, int widget, int click_count) override
{
if (widget >= WID_NCL_TEXTFILE && widget < WID_NCL_TEXTFILE + TFT_END) {
if (widget >= WID_NCL_TEXTFILE && widget < WID_NCL_TEXTFILE + TFT_CONTENT_END) {
if (this->selected == nullptr || this->selected->state != ContentInfo::ALREADY_HERE) return;
ShowContentTextfileWindow((TextfileType)(widget - WID_NCL_TEXTFILE), this->selected);
@@ -997,7 +997,7 @@ public:
this->SetWidgetDisabledState(WID_NCL_SELECT_ALL, !show_select_all);
this->SetWidgetDisabledState(WID_NCL_SELECT_UPDATE, !show_select_upgrade);
this->SetWidgetDisabledState(WID_NCL_OPEN_URL, this->selected == nullptr || this->selected->url.empty());
for (TextfileType tft = TFT_BEGIN; tft < TFT_END; tft++) {
for (TextfileType tft = TFT_CONTENT_BEGIN; tft < TFT_CONTENT_END; tft++) {
this->SetWidgetDisabledState(WID_NCL_TEXTFILE + tft, this->selected == nullptr || this->selected->state != ContentInfo::ALREADY_HERE || !this->selected->GetTextfile(tft).has_value());
}

View File

@@ -18,6 +18,7 @@
#include "network_content.h"
#include "network_server.h"
#include "network_coordinator.h"
#include "network_survey.h"
#include "../gui.h"
#include "network_udp.h"
#include "../window_func.h"
@@ -38,6 +39,7 @@
#include "../timer/timer.h"
#include "../timer/timer_window.h"
#include "../timer/timer_game_calendar.h"
#include "../textfile_gui.h"
#include "../widgets/network_widget.h"
@@ -2515,3 +2517,119 @@ void ShowNetworkAskRelay(const std::string &server_connection_string, const std:
Window *parent = GetMainWindow();
new NetworkAskRelayWindow(&_network_ask_relay_desc, parent, server_connection_string, relay_connection_string, token);
}
/**
* Window used for asking if the user wants to participate in the automated survey.
*/
struct NetworkAskSurveyWindow : public Window {
NetworkAskSurveyWindow(WindowDesc *desc, Window *parent) :
Window(desc)
{
this->parent = parent;
this->InitNested(0);
}
void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
{
if (widget == WID_NAS_TEXT) {
*size = GetStringBoundingBox(STR_NETWORK_ASK_SURVEY_TEXT);
size->width += WidgetDimensions::scaled.frametext.Horizontal();
size->height += WidgetDimensions::scaled.frametext.Vertical();
}
}
void DrawWidget(const Rect &r, int widget) const override
{
if (widget == WID_NAS_TEXT) {
DrawStringMultiLine(r.Shrink(WidgetDimensions::scaled.frametext), STR_NETWORK_ASK_SURVEY_TEXT, TC_BLACK, SA_CENTER);
}
}
void FindWindowPlacementAndResize(int def_width, int def_height) override
{
/* Position query window over the calling window, ensuring it's within screen bounds. */
this->left = Clamp(parent->left + (parent->width / 2) - (this->width / 2), 0, _screen.width - this->width);
this->top = Clamp(parent->top + (parent->height / 2) - (this->height / 2), 0, _screen.height - this->height);
this->SetDirty();
}
void OnClick(Point pt, int widget, int click_count) override
{
switch (widget) {
case WID_NAS_PREVIEW:
ShowSurveyResultTextfileWindow();
break;
case WID_NAS_LINK:
OpenBrowser(NETWORK_SURVEY_DETAILS_LINK.c_str());
break;
case WID_NAS_NO:
_settings_client.network.participate_survey = PS_NO;
this->Close();
break;
case WID_NAS_YES:
_settings_client.network.participate_survey = PS_YES;
this->Close();
break;
}
}
};
static const NWidgetPart _nested_network_ask_survey_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_GREY),
NWidget(WWT_CAPTION, COLOUR_GREY, WID_NAS_CAPTION), SetDataTip(STR_NETWORK_ASK_SURVEY_CAPTION, STR_NULL),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_GREY), SetPIP(0, 4, 8),
NWidget(WWT_TEXT, COLOUR_GREY, WID_NAS_TEXT), SetAlignment(SA_HOR_CENTER), SetFill(1, 1),
NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), SetPIP(10, 15, 10),
NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, WID_NAS_PREVIEW), SetMinimalSize(71, 12), SetFill(1, 1), SetDataTip(STR_NETWORK_ASK_SURVEY_PREVIEW, STR_NULL),
NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, WID_NAS_LINK), SetMinimalSize(71, 12), SetFill(1, 1), SetDataTip(STR_NETWORK_ASK_SURVEY_LINK, STR_NULL),
EndContainer(),
NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), SetPIP(10, 15, 10),
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_NAS_NO), SetMinimalSize(71, 12), SetFill(1, 1), SetDataTip(STR_NETWORK_ASK_SURVEY_NO, STR_NULL),
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_NAS_YES), SetMinimalSize(71, 12), SetFill(1, 1), SetDataTip(STR_NETWORK_ASK_SURVEY_YES, STR_NULL),
EndContainer(),
EndContainer(),
};
static WindowDesc _network_ask_survey_desc(
WDP_CENTER, nullptr, 0, 0,
WC_NETWORK_ASK_SURVEY, WC_NONE,
WDF_MODAL,
_nested_network_ask_survey_widgets, lengthof(_nested_network_ask_survey_widgets)
);
/**
* Show a modal confirmation window with "no" / "preview" / "yes" buttons.
*/
void ShowNetworkAskSurvey()
{
/* If we can't send a survey, don't ask the question. */
if constexpr (!NetworkSurveyHandler::IsSurveyPossible()) return;
CloseWindowByClass(WC_NETWORK_ASK_SURVEY);
Window *parent = GetMainWindow();
new NetworkAskSurveyWindow(&_network_ask_survey_desc, parent);
}
/** Window for displaying the textfile of a survey result. */
struct SurveyResultTextfileWindow : public TextfileWindow {
const GRFConfig *grf_config; ///< View the textfile of this GRFConfig.
SurveyResultTextfileWindow(TextfileType file_type) : TextfileWindow(file_type)
{
auto result = _survey.CreatePayload(NetworkSurveyHandler::Reason::PREVIEW, true);
this->LoadText(result);
this->InvalidateData();
}
};
void ShowSurveyResultTextfileWindow()
{
CloseWindowById(WC_TEXTFILE, TFT_SURVEY_RESULT);
new SurveyResultTextfileWindow(TFT_SURVEY_RESULT);
}

View File

@@ -24,7 +24,8 @@ void ShowNetworkGameWindow();
void ShowClientList();
void ShowNetworkCompanyPasswordWindow(Window *parent);
void ShowNetworkAskRelay(const std::string &server_connection_string, const std::string &relay_connection_string, const std::string &token);
void ShowNetworkAskSurvey();
void ShowSurveyResultTextfileWindow();
/** Company information stored at the client side */
struct NetworkCompanyInfo : NetworkCompanyStats {

View File

@@ -0,0 +1,397 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/** @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 <nlohmann/json.hpp>
#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<uint32> 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::seconds>(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<std::mutex> 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();
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file network_survey.h Part of the network protocol handling opt-in survey. */
#ifndef NETWORK_SURVEY_H
#define NETWORK_SURVEY_H
#include <condition_variable>
#include <mutex>
#include "core/http.h"
/**
* Socket handler for the survey connection
*/
class NetworkSurveyHandler : public HTTPCallback {
protected:
void OnFailure() override;
void OnReceiveData(const char *data, size_t length) override;
bool IsCancelled() const override { return false; }
public:
enum class Reason {
PREVIEW, ///< User is previewing the survey result.
LEAVE, ///< User is leaving the game (but not exiting the application).
EXIT, ///< User is exiting the application.
CRASH, ///< Game crashed.
};
void Transmit(Reason reason, bool blocking = false);
std::string CreatePayload(Reason reason, bool for_preview = false);
constexpr static bool IsSurveyPossible()
{
#ifndef WITH_NLOHMANN_JSON
/* Without JSON library, we cannot send a payload; so we disable the survey. */
return false;
#else
return true;
#endif /* WITH_NLOHMANN_JSON */
}
private:
std::mutex mutex; ///< Mutex for the condition variable.
std::condition_variable loaded; ///< Condition variable to wait for the survey to be sent.
};
extern NetworkSurveyHandler _survey;
#endif /* NETWORK_SURVEY_H */