Changeset - r27371:b9ad4c1bff08
[Not reviewed]
master
0 49 5
Patric Stout - 19 months ago 2023-04-25 17:43:45
truebrain@openttd.org
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.
54 files changed with 1018 insertions and 53 deletions:
0 comments (0 inline, 0 general)
.github/workflows/codeql.yml
Show inline comments
 
@@ -37,10 +37,12 @@ jobs:
 
          liballegro4-dev \
 
          libcurl4-openssl-dev \
 
          libfontconfig-dev \
 
          libharfbuzz-dev \
 
          libicu-dev \
 
          liblzma-dev \
 
          liblzo2-dev \
 
          libsdl2-dev \
 
          nlohmann-json3-dev \
 
          zlib1g-dev \
 
          # EOF
 
        echo "::endgroup::"
.github/workflows/release-linux.yml
Show inline comments
 
@@ -2,6 +2,11 @@ name: Release (Linux)
 

	
 
on:
 
  workflow_call:
 
    inputs:
 
      survey_key:
 
        required: false
 
        type: string
 
        default: ""
 

	
 
jobs:
 
  linux:
 
@@ -119,6 +124,7 @@ jobs:
 
        cmake ${GITHUB_WORKSPACE} \
 
          -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake \
 
          -DCMAKE_BUILD_TYPE=RelWithDebInfo \
 
          -DOPTION_SURVEY_KEY=${{ inputs.survey_key }} \
 
          -DOPTION_PACKAGE_DEPENDENCIES=ON \
 
          # EOF
 
        echo "::endgroup::"
.github/workflows/release-macos.yml
Show inline comments
 
@@ -2,6 +2,11 @@ name: Release (MacOS)
 

	
 
on:
 
  workflow_call:
 
    inputs:
 
      survey_key:
 
        required: false
 
        type: string
 
        default: ""
 

	
 
jobs:
 
  macos:
 
@@ -104,6 +109,7 @@ jobs:
 
          -DCMAKE_TOOLCHAIN_FILE=/usr/local/share/vcpkg/scripts/buildsystems/vcpkg.cmake \
 
          -DHOST_BINARY_DIR=${GITHUB_WORKSPACE}/build-host \
 
          -DCMAKE_BUILD_TYPE=RelWithDebInfo \
 
          -DOPTION_SURVEY_KEY=${{ inputs.survey_key }} \
 
          # EOF
 
        echo "::endgroup::"
 

	
 
@@ -124,6 +130,7 @@ jobs:
 
          -DCMAKE_TOOLCHAIN_FILE=/usr/local/share/vcpkg/scripts/buildsystems/vcpkg.cmake \
 
          -DHOST_BINARY_DIR=${GITHUB_WORKSPACE}/build-host \
 
          -DCMAKE_BUILD_TYPE=RelWithDebInfo \
 
          -DOPTION_SURVEY_KEY=${{ inputs.survey_key }} \
 
          -DCPACK_BUNDLE_APPLE_CERT_APP=${{ secrets.APPLE_DEVELOPER_CERTIFICATE_ID }} \
 
          "-DCPACK_BUNDLE_APPLE_CODESIGN_PARAMETER=--deep -f --options runtime" \
 
          -DAPPLE_UNIVERSAL_PACKAGE=1 \
.github/workflows/release-source.yml
Show inline comments
 
@@ -11,6 +11,8 @@ on:
 
        value: ${{ jobs.source.outputs.trigger_type }}
 
      folder:
 
        value: ${{ jobs.source.outputs.folder }}
 
      survey_key:
 
        value: ${{ jobs.source.outputs.survey_key }}
 

	
 
jobs:
 
  source:
 
@@ -23,6 +25,7 @@ jobs:
 
      is_tag: ${{ steps.metadata.outputs.is_tag }}
 
      trigger_type: ${{ steps.metadata.outputs.trigger_type }}
 
      folder: ${{ steps.metadata.outputs.folder }}
 
      survey_key: ${{ steps.survey_key.outputs.survey_key }}
 

	
 
    steps:
 
    - name: Checkout (Release)
 
@@ -146,6 +149,19 @@ jobs:
 
        FOLDER_NIGHTLIES: openttd-nightlies
 
        FOLDER_BRANCHES: openttd-branches
 

	
 
    - name: Generate survey key
 
      id: survey_key
 
      run: |
 
        PAYLOAD='{"version":"${{ steps.metadata.outputs.version }}","type":"${{ vars.SURVEY_TYPE }}"}'
 

	
 
        echo "${{ secrets.SURVEY_SIGNING_KEY }}" > survey_signing_key.pem
 
        SIGNATURE=$(echo -n "${PAYLOAD}" | openssl dgst -sha256 -sign survey_signing_key.pem | base64 -w0)
 
        rm -f survey_signing_key.pem
 

	
 
        SURVEY_KEY=$(curl -f -s -X POST -d "${PAYLOAD}" -H "Content-Type: application/json" -H "X-Signature: ${SIGNATURE}" https://survey-participate.openttd.org/create-survey-key/${{ vars.SURVEY_TYPE }})
 

	
 
        echo "survey_key=${SURVEY_KEY}" >> $GITHUB_OUTPUT
 

	
 
    - name: Remove VCS information
 
      run: |
 
        rm -rf .git
.github/workflows/release-windows.yml
Show inline comments
 
@@ -3,6 +3,10 @@ name: Release (Windows)
 
on:
 
  workflow_call:
 
    inputs:
 
      survey_key:
 
        required: false
 
        type: string
 
        default: ""
 
      is_tag:
 
        required: true
 
        type: string
 
@@ -129,6 +133,7 @@ jobs:
 
          -DOPTION_USE_NSIS=ON \
 
          -DHOST_BINARY_DIR=${GITHUB_WORKSPACE}/build-host \
 
          -DCMAKE_BUILD_TYPE=RelWithDebInfo \
 
          -DOPTION_SURVEY_KEY=${{ inputs.survey_key }} \
 
          -DWINDOWS_CERTIFICATE_COMMON_NAME="${WINDOWS_CERTIFICATE_COMMON_NAME}" \
 
          # EOF
 
        echo "::endgroup::"
 
@@ -153,6 +158,7 @@ jobs:
 
          -DCMAKE_TOOLCHAIN_FILE="c:\vcpkg\scripts\buildsystems\vcpkg.cmake" \
 
          -DHOST_BINARY_DIR=${GITHUB_WORKSPACE}/build-host \
 
          -DCMAKE_BUILD_TYPE=RelWithDebInfo \
 
          -DOPTION_SURVEY_KEY=${{ inputs.survey_key }} \
 
          -DWINDOWS_CERTIFICATE_COMMON_NAME="${WINDOWS_CERTIFICATE_COMMON_NAME}" \
 
          # EOF
 
        echo "::endgroup::"
.github/workflows/release.yml
Show inline comments
 
@@ -38,6 +38,9 @@ jobs:
 
    uses: ./.github/workflows/release-linux.yml
 
    secrets: inherit
 

	
 
    with:
 
      survey_key: ${{ needs.source.outputs.survey_key }}
 

	
 
  macos:
 
    name: MacOS
 
    needs: source
 
@@ -45,6 +48,9 @@ jobs:
 
    uses: ./.github/workflows/release-macos.yml
 
    secrets: inherit
 

	
 
    with:
 
      survey_key: ${{ needs.source.outputs.survey_key }}
 

	
 
  windows:
 
    name: Windows
 
    needs: source
 
@@ -54,6 +60,7 @@ jobs:
 

	
 
    with:
 
      is_tag: ${{ needs.source.outputs.is_tag }}
 
      survey_key: ${{ needs.source.outputs.survey_key }}
 

	
 
  windows-store:
 
    name: Windows Store
cmake/Options.cmake
Show inline comments
 
@@ -71,6 +71,8 @@ function(set_options)
 
    if (OPTION_DOCS_ONLY)
 
        set(OPTION_TOOLS_ONLY ON PARENT_SCOPE)
 
    endif()
 

	
 
    option(OPTION_SURVEY_KEY "Survey-key to use for the opt-in survey (empty if you have none)" "")
 
endfunction()
 

	
 
# Show the values of the generic options.
 
@@ -84,6 +86,12 @@ function(show_options)
 
    message(STATUS "Option Use assert - ${OPTION_USE_ASSERTS}")
 
    message(STATUS "Option Use threads - ${OPTION_USE_THREADS}")
 
    message(STATUS "Option Use NSIS - ${OPTION_USE_NSIS}")
 

	
 
    if(OPTION_SURVEY_KEY)
 
        message(STATUS "Option Survey Key - USED")
 
    else()
 
        message(STATUS "Option Survey Key - NOT USED")
 
    endif()
 
endfunction()
 

	
 
# Add the definitions for the options that are selected.
 
@@ -104,4 +112,8 @@ function(add_definitions_based_on_option
 
    else()
 
        add_definitions(-DNDEBUG)
 
    endif()
 

	
 
    if(OPTION_SURVEY_KEY)
 
        add_definitions(-DSURVEY_KEY="${OPTION_SURVEY_KEY}")
 
    endif()
 
endfunction()
src/ai/ai_gui.cpp
Show inline comments
 
@@ -183,7 +183,7 @@ struct AIConfigWindow : public Window {
 

	
 
	void OnClick(Point pt, int widget, int click_count) override
 
	{
 
		if (widget >= WID_AIC_TEXTFILE && widget < WID_AIC_TEXTFILE + TFT_END) {
 
		if (widget >= WID_AIC_TEXTFILE && widget < WID_AIC_TEXTFILE + TFT_CONTENT_END) {
 
			if (this->selected_slot == INVALID_COMPANY || AIConfig::GetConfig(this->selected_slot) == nullptr) return;
 

	
 
			ShowScriptTextfileWindow((TextfileType)(widget - WID_AIC_TEXTFILE), this->selected_slot);
 
@@ -284,7 +284,7 @@ struct AIConfigWindow : public Window {
 
		this->SetWidgetDisabledState(WID_AIC_MOVE_UP, this->selected_slot == INVALID_COMPANY || !IsEditable((CompanyID)(this->selected_slot - 1)));
 
		this->SetWidgetDisabledState(WID_AIC_MOVE_DOWN, this->selected_slot == INVALID_COMPANY || !IsEditable((CompanyID)(this->selected_slot + 1)));
 

	
 
		for (TextfileType tft = TFT_BEGIN; tft < TFT_END; tft++) {
 
		for (TextfileType tft = TFT_CONTENT_BEGIN; tft < TFT_CONTENT_END; tft++) {
 
			this->SetWidgetDisabledState(WID_AIC_TEXTFILE + tft, this->selected_slot == INVALID_COMPANY || !AIConfig::GetConfig(this->selected_slot)->GetTextfile(tft, this->selected_slot).has_value());
 
		}
 
	}
src/crashlog.cpp
Show inline comments
 
@@ -23,6 +23,7 @@
 
#include "screenshot.h"
 
#include "gfx_func.h"
 
#include "network/network.h"
 
#include "network/network_survey.h"
 
#include "language.h"
 
#include "fontcache.h"
 
#include "news_gui.h"
 
@@ -511,6 +512,10 @@ bool CrashLog::MakeCrashLog() const
 
		printf("Writing crash screenshot failed.\n\n");
 
	}
 

	
 
	if (_game_mode == GM_NORMAL) {
 
		_survey.Transmit(NetworkSurveyHandler::Reason::CRASH, true);
 
	}
 

	
 
	return ret;
 
}
 

	
src/game/game_gui.cpp
Show inline comments
 
@@ -240,7 +240,7 @@ struct GSConfigWindow : public Window {
 

	
 
	void OnClick(Point pt, int widget, int click_count) override
 
	{
 
		if (widget >= WID_GSC_TEXTFILE && widget < WID_GSC_TEXTFILE + TFT_END) {
 
		if (widget >= WID_GSC_TEXTFILE && widget < WID_GSC_TEXTFILE + TFT_CONTENT_END) {
 
			if (GameConfig::GetConfig() == nullptr) return;
 

	
 
			ShowScriptTextfileWindow((TextfileType)(widget - WID_GSC_TEXTFILE), (CompanyID)OWNER_DEITY);
 
@@ -404,7 +404,7 @@ struct GSConfigWindow : public Window {
 

	
 
		this->SetWidgetDisabledState(WID_GSC_CHANGE, (_game_mode == GM_NORMAL) || !IsEditable());
 

	
 
		for (TextfileType tft = TFT_BEGIN; tft < TFT_END; tft++) {
 
		for (TextfileType tft = TFT_CONTENT_BEGIN; tft < TFT_CONTENT_END; tft++) {
 
			this->SetWidgetDisabledState(WID_GSC_TEXTFILE + tft, !GameConfig::GetConfig()->GetTextfile(tft, (CompanyID)OWNER_DEITY).has_value());
 
		}
 
		this->RebuildVisibleSettings();
src/gfx.cpp
Show inline comments
 
@@ -47,6 +47,7 @@ bool _screen_disable_anim = false;   ///
 
std::atomic<bool> _exit_game;
 
GameMode _game_mode;
 
SwitchMode _switch_mode;  ///< The next mainloop command.
 
std::chrono::steady_clock::time_point _switch_mode_time; ///< The time when the switch mode was requested.
 
PauseMode _pause_mode;
 
Palette _cur_palette;
 

	
src/intro_gui.cpp
Show inline comments
 
@@ -17,6 +17,7 @@
 
#include "genworld.h"
 
#include "network/network_gui.h"
 
#include "network/network_content.h"
 
#include "network/network_survey.h"
 
#include "landscape_type.h"
 
#include "landscape.h"
 
#include "strings_func.h"
 
@@ -504,7 +505,10 @@ void ShowSelectGameWindow()
 

	
 
static void AskExitGameCallback(Window *w, bool confirmed)
 
{
 
	if (confirmed) _exit_game = true;
 
	if (confirmed) {
 
		_survey.Transmit(NetworkSurveyHandler::Reason::EXIT, true);
 
		_exit_game = true;
 
	}
 
}
 

	
 
void AskExitGame()
src/lang/english.txt
Show inline comments
 
@@ -1041,6 +1041,14 @@ STR_GAME_OPTIONS_GUI_SCALE_3X           
 
STR_GAME_OPTIONS_GUI_SCALE_4X                                   :4x
 
STR_GAME_OPTIONS_GUI_SCALE_5X                                   :5x
 

	
 
STR_GAME_OPTIONS_PARTICIPATE_SURVEY_FRAME                       :{BLACK}Automated survey
 
STR_GAME_OPTIONS_PARTICIPATE_SURVEY                             :{BLACK}Participate in automated survey
 
STR_GAME_OPTIONS_PARTICIPATE_SURVEY_TOOLTIP                     :{BLACK}When enabled, OpenTTD will transmit a survey when leaving a game
 
STR_GAME_OPTIONS_PARTICIPATE_SURVEY_LINK                        :{BLACK}About survey and privacy
 
STR_GAME_OPTIONS_PARTICIPATE_SURVEY_LINK_TOOLTIP                :{BLACK}This opens a browser with more information about the automated survey
 
STR_GAME_OPTIONS_PARTICIPATE_SURVEY_PREVIEW                     :{BLACK}Preview survey result
 
STR_GAME_OPTIONS_PARTICIPATE_SURVEY_PREVIEW_TOOLTIP             :{BLACK}Show the survey result of the current running game
 

	
 
STR_GAME_OPTIONS_GRAPHICS                                       :{BLACK}Graphics
 

	
 
STR_GAME_OPTIONS_REFRESH_RATE                                   :{BLACK}Display refresh rate
 
@@ -2402,6 +2410,13 @@ STR_NETWORK_ASK_RELAY_NO                
 
STR_NETWORK_ASK_RELAY_YES_ONCE                                  :{BLACK}Yes, this once
 
STR_NETWORK_ASK_RELAY_YES_ALWAYS                                :{BLACK}Yes, don't ask again
 

	
 
STR_NETWORK_ASK_SURVEY_CAPTION                                  :Participate in automated survey?
 
STR_NETWORK_ASK_SURVEY_TEXT                                     :Would you like to participate in the automated survey?{}OpenTTD will transmit a survey when leaving a game.{}You can change this at any time under "Game Options".
 
STR_NETWORK_ASK_SURVEY_PREVIEW                                  :Preview survey result
 
STR_NETWORK_ASK_SURVEY_LINK                                     :About survey and privacy
 
STR_NETWORK_ASK_SURVEY_NO                                       :No
 
STR_NETWORK_ASK_SURVEY_YES                                      :Yes
 

	
 
STR_NETWORK_SPECTATORS                                          :Spectators
 

	
 
# Network set password
 
@@ -4654,10 +4669,11 @@ STR_TEXTFILE_WRAP_TEXT_TOOLTIP          
 
STR_TEXTFILE_VIEW_README                                        :{BLACK}View readme
 
STR_TEXTFILE_VIEW_CHANGELOG                                     :{BLACK}Changelog
 
STR_TEXTFILE_VIEW_LICENCE                                       :{BLACK}Licence
 
###length 3
 
###length 4
 
STR_TEXTFILE_README_CAPTION                                     :{WHITE}{STRING} readme of {RAW_STRING}
 
STR_TEXTFILE_CHANGELOG_CAPTION                                  :{WHITE}{STRING} changelog of {RAW_STRING}
 
STR_TEXTFILE_LICENCE_CAPTION                                    :{WHITE}{STRING} licence of {RAW_STRING}
 
STR_TEXTFILE_SURVEY_RESULT_CAPTION                              :{WHITE}Preview of survey result
 

	
 

	
 
# Vehicle loading indicators
src/misc.cpp
Show inline comments
 
@@ -31,9 +31,11 @@
 
#include "town_kdtree.h"
 
#include "viewport_kdtree.h"
 
#include "newgrf_profiling.h"
 
#include "3rdparty/md5/md5.h"
 

	
 
#include "safeguards.h"
 

	
 
std::string _savegame_id; ///< Unique ID of the current savegame.
 

	
 
extern TileIndex _cur_tileloop_tile;
 
extern void MakeNewgameSettingsLive();
 
@@ -56,6 +58,40 @@ void InitializeCheats();
 
void InitializeNPF();
 
void InitializeOldNames();
 

	
 
/**
 
 * Generate an unique ID.
 
 *
 
 * It isn't as much of an unique ID as we would like, but our random generator
 
 * can only produce 32bit random numbers.
 
 * That is why we combine InteractiveRandom with the current (steady) clock.
 
 * The first to add a bit of randomness, the second to ensure you can't get
 
 * the same unique ID when you run it twice from the same state at different
 
 * times.
 
 *
 
 * This makes it unlikely that two users generate the same ID for different
 
 * subjects. But as this is not an UUID, so it can't be ruled out either.
 
 */
 
std::string GenerateUid(std::string_view subject)
 
{
 
	auto current_time = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
 
	std::string coding_string = fmt::format("{}{}{}", InteractiveRandom(), current_time, subject);
 

	
 
	Md5 checksum;
 
	uint8 digest[16];
 
	checksum.Append(coding_string.c_str(), coding_string.length());
 
	checksum.Finish(digest);
 

	
 
	return MD5SumToString(digest);
 
}
 

	
 
/**
 
 * Generate an unique savegame ID.
 
 */
 
void GenerateSavegameId()
 
{
 
	_savegame_id = GenerateUid("OpenTTD Savegame ID");
 
}
 

	
 
void InitializeGame(uint size_x, uint size_y, bool reset_date, bool reset_settings)
 
{
 
	/* Make sure there isn't any window that can influence anything
src/network/CMakeLists.txt
Show inline comments
 
@@ -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
src/network/core/config.cpp
Show inline comments
 
@@ -66,3 +66,13 @@ const char *NetworkContentMirrorUriStrin
 
{
 
	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/");
 
}
src/network/core/config.h
Show inline comments
 
@@ -16,6 +16,7 @@ const char *NetworkCoordinatorConnection
 
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  
 
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          
 
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'
src/network/core/http.h
Show inline comments
 
@@ -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 {
 
	/**
src/network/core/http_curl.cpp
Show inline comments
 
@@ -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();
 
		}
 
	}
src/network/core/http_winhttp.cpp
Show inline comments
 
@@ -131,7 +131,8 @@ void NetworkHTTPRequest::WinHttpCallback
 

	
 
			/* 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));
 
	}
 
}
 

	
src/network/network.cpp
Show inline comments
 
@@ -85,6 +85,8 @@ static_assert((int)NETWORK_COMPANY_NAME_
 
/** 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 {
src/network/network_content_gui.cpp
Show inline comments
 
@@ -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());
 
		}
 

	
src/network/network_gui.cpp
Show inline comments
 
@@ -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::stri
 
	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);
 
}
src/network/network_gui.h
Show inline comments
 
@@ -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 {
src/network/network_survey.cpp
Show inline comments
 
new file 100644
 
/*
 
 * 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();
 
	}
 
}
src/network/network_survey.h
Show inline comments
 
new file 100644
 
/*
 
 * 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 */
src/newgrf_gui.cpp
Show inline comments
 
@@ -925,7 +925,7 @@ struct NewGRFWindow : public Window, New
 

	
 
	void OnClick(Point pt, int widget, int click_count) override
 
	{
 
		if (widget >= WID_NS_NEWGRF_TEXTFILE && widget < WID_NS_NEWGRF_TEXTFILE + TFT_END) {
 
		if (widget >= WID_NS_NEWGRF_TEXTFILE && widget < WID_NS_NEWGRF_TEXTFILE + TFT_CONTENT_END) {
 
			if (this->active_sel == nullptr && this->avail_sel == nullptr) return;
 

	
 
			ShowNewGRFTextfileWindow((TextfileType)(widget - WID_NS_NEWGRF_TEXTFILE), this->active_sel != nullptr ? this->active_sel : this->avail_sel);
 
@@ -1286,7 +1286,7 @@ struct NewGRFWindow : public Window, New
 
		);
 

	
 
		const GRFConfig *selected_config = (this->avail_sel == nullptr) ? this->active_sel : this->avail_sel;
 
		for (TextfileType tft = TFT_BEGIN; tft < TFT_END; tft++) {
 
		for (TextfileType tft = TFT_CONTENT_BEGIN; tft < TFT_CONTENT_END; tft++) {
 
			this->SetWidgetDisabledState(WID_NS_NEWGRF_TEXTFILE + tft, selected_config == nullptr || !selected_config->GetTextfile(tft).has_value());
 
		}
 
		this->SetWidgetDisabledState(WID_NS_OPEN_URL, selected_config == nullptr || StrEmpty(selected_config->GetURL()));
src/openttd.cpp
Show inline comments
 
@@ -67,6 +67,7 @@
 
#include "framerate_type.h"
 
#include "industry.h"
 
#include "network/network_gui.h"
 
#include "network/network_survey.h"
 
#include "misc_cmd.h"
 
#include "timer/timer.h"
 
#include "timer/timer_game_calendar.h"
 
@@ -822,6 +823,7 @@ void HandleExitGameRequest()
 
		_exit_game = true;
 
	} else if (_settings_client.gui.autosave_on_exit) {
 
		DoExitSave();
 
		_survey.Transmit(NetworkSurveyHandler::Reason::EXIT, true);
 
		_exit_game = true;
 
	} else {
 
		AskExitGame();
 
@@ -1036,9 +1038,16 @@ void SwitchToMode(SwitchMode new_mode)
 
	/* When we change mode, reset the autosave. */
 
	if (new_mode != SM_SAVE_GAME) ChangeAutosaveFrequency(true);
 

	
 
	/* Transmit the survey if we were in normal-mode and not saving. It always means we leaving the current game. */
 
	if (_game_mode == GM_NORMAL && new_mode != SM_SAVE_GAME) _survey.Transmit(NetworkSurveyHandler::Reason::LEAVE);
 

	
 
	/* Keep track when we last switch mode. Used for survey, to know how long someone was in a game. */
 
	if (new_mode != SM_SAVE_GAME) _switch_mode_time = std::chrono::steady_clock::now();
 

	
 
	switch (new_mode) {
 
		case SM_EDITOR: // Switch to scenario editor
 
			MakeNewEditorWorld();
 
			GenerateSavegameId();
 
			break;
 

	
 
		case SM_RELOADGAME: // Reload with what-ever started the game
 
@@ -1055,11 +1064,13 @@ void SwitchToMode(SwitchMode new_mode)
 
			}
 

	
 
			MakeNewGame(false, new_mode == SM_NEWGAME);
 
			GenerateSavegameId();
 
			break;
 

	
 
		case SM_RESTARTGAME: // Restart --> 'Random game' with current settings
 
		case SM_NEWGAME: // New Game --> 'Random game'
 
			MakeNewGame(false, new_mode == SM_NEWGAME);
 
			GenerateSavegameId();
 
			break;
 

	
 
		case SM_LOAD_GAME: { // Load game, Play Scenario
 
@@ -1083,18 +1094,21 @@ void SwitchToMode(SwitchMode new_mode)
 
		case SM_RESTART_HEIGHTMAP: // Load a heightmap and start a new game from it with current settings
 
		case SM_START_HEIGHTMAP: // Load a heightmap and start a new game from it
 
			MakeNewGame(true, new_mode == SM_START_HEIGHTMAP);
 
			GenerateSavegameId();
 
			break;
 

	
 
		case SM_LOAD_HEIGHTMAP: // Load heightmap from scenario editor
 
			SetLocalCompany(OWNER_NONE);
 

	
 
			GenerateWorld(GWM_HEIGHTMAP, 1 << _settings_game.game_creation.map_x, 1 << _settings_game.game_creation.map_y);
 
			GenerateSavegameId();
 
			MarkWholeScreenDirty();
 
			break;
 

	
 
		case SM_LOAD_SCENARIO: { // Load scenario from scenario editor
 
			if (SafeLoad(_file_to_saveload.name, _file_to_saveload.file_op, _file_to_saveload.detail_ftype, GM_EDITOR, NO_DIRECTORY)) {
 
				SetLocalCompany(OWNER_NONE);
 
				GenerateSavegameId();
 
				_settings_newgame.game_creation.starting_year = TimerGameCalendar::year;
 
				/* Cancel the saveload pausing */
 
				Command<CMD_PAUSE>::Post(PM_PAUSED_SAVELOAD, false);
 
@@ -1116,6 +1130,14 @@ void SwitchToMode(SwitchMode new_mode)
 
				ShowErrorMessage(STR_WARNING_FALLBACK_SOUNDSET, INVALID_STRING_ID, WL_CRITICAL);
 
				BaseSounds::ini_set = BaseSounds::GetUsedSet()->name;
 
			}
 
			if (_settings_client.network.participate_survey == PS_ASK) {
 
				/* No matter how often you go back to the main menu, only ask the first time. */
 
				static bool asked_once = false;
 
				if (!asked_once) {
 
					asked_once = true;
 
					ShowNetworkAskSurvey();
 
				}
 
			}
 
			break;
 

	
 
		case SM_SAVE_GAME: // Save game.
src/openttd.h
Show inline comments
 
@@ -11,6 +11,7 @@
 
#define OPENTTD_H
 

	
 
#include <atomic>
 
#include <chrono>
 
#include "core/enum_type.hpp"
 

	
 
/** Mode which defines the state of the game. */
 
@@ -53,6 +54,7 @@ enum DisplayOptions {
 

	
 
extern GameMode _game_mode;
 
extern SwitchMode _switch_mode;
 
extern std::chrono::steady_clock::time_point _switch_mode_time;
 
extern std::atomic<bool> _exit_game;
 
extern bool _save_config;
 

	
 
@@ -86,6 +88,7 @@ void HandleExitGameRequest();
 
void SwitchToMode(SwitchMode new_mode);
 

	
 
bool RequestNewGRFScan(struct NewGRFScanCallback *callback = nullptr);
 
void GenerateSavegameId();
 

	
 
void OpenBrowser(const char *url);
 
void ChangeAutosaveFrequency(bool reset);
src/os/macosx/CMakeLists.txt
Show inline comments
 
@@ -7,6 +7,7 @@ add_files(
 
    osx_stdafx.h
 
    string_osx.cpp
 
    string_osx.h
 
    survey_osx.cpp
 
    CONDITION APPLE
 
)
 

	
src/os/macosx/macos.h
Show inline comments
 
@@ -38,6 +38,8 @@ bool IsMonospaceFont(CFStringRef name);
 

	
 
void MacOSSetThreadName(const char *name);
 

	
 
uint64 MacOSGetPhysicalMemory();
 

	
 

	
 
/** Deleter that calls CFRelease rather than deleting the pointer. */
 
template <typename T> struct CFDeleter {
src/os/macosx/macos.mm
Show inline comments
 
@@ -272,3 +272,8 @@ void MacOSSetThreadName(const char *name
 
		[ cur performSelector:@selector(setName:) withObject:[ NSString stringWithUTF8String:name ] ];
 
	}
 
}
 

	
 
uint64 MacOSGetPhysicalMemory()
 
{
 
	return [ [ NSProcessInfo processInfo ] physicalMemory ];
 
}
src/os/macosx/survey_osx.cpp
Show inline comments
 
new file 100644
 
/*
 
 * 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 survey_osx.cpp OSX implementation of OS-specific survey information. */
 

	
 
#ifdef WITH_NLOHMANN_JSON
 

	
 
#include "../../stdafx.h"
 

	
 
#include "../../3rdparty/fmt/format.h"
 
#include "macos.h"
 

	
 
#include <mach-o/arch.h>
 
#include <nlohmann/json.hpp>
 

	
 
#include "../../safeguards.h"
 

	
 
void SurveyOS(nlohmann::json &json)
 
{
 
	int ver_maj, ver_min, ver_bug;
 
	GetMacOSVersion(&ver_maj, &ver_min, &ver_bug);
 

	
 
	const NXArchInfo *arch = NXGetLocalArchInfo();
 

	
 
	json["os"] = "MacOS";
 
	json["release"] = fmt::format("{}.{}.{}", ver_maj, ver_min, ver_bug);
 
	json["machine"] = arch != nullptr ? arch->description : "unknown";
 
	json["min_ver"] = MAC_OS_X_VERSION_MIN_REQUIRED;
 
	json["max_ver"] = MAC_OS_X_VERSION_MAX_ALLOWED;
 

	
 
	json["memory"] = MacOSGetPhysicalMemory();
 
}
 

	
 
#endif /* WITH_NLOHMANN_JSON */
src/os/unix/CMakeLists.txt
Show inline comments
 
add_files(
 
    crashlog_unix.cpp
 
    survey_unix.cpp
 
    CONDITION UNIX AND NOT APPLE AND NOT OPTION_OS2
 
)
 

	
src/os/unix/survey_unix.cpp
Show inline comments
 
new file 100644
 
/*
 
 * 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 survey_unix.cpp Unix implementation of OS-specific survey information. */
 

	
 
#ifdef WITH_NLOHMANN_JSON
 

	
 
#include "../../stdafx.h"
 

	
 
#include <nlohmann/json.hpp>
 
#include <sys/utsname.h>
 
#include <unistd.h>
 

	
 
#include "../../safeguards.h"
 

	
 
void SurveyOS(nlohmann::json &json)
 
{
 
	struct utsname name;
 
	if (uname(&name) < 0) {
 
		json["os"] = "Unix";
 
		return;
 
	}
 

	
 
	json["os"] = name.sysname;
 
	json["release"] = name.release;
 
	json["machine"] = name.machine;
 
	json["version"] = name.version;
 

	
 
	long pages = sysconf(_SC_PHYS_PAGES);
 
	long page_size = sysconf(_SC_PAGE_SIZE);
 
	json["memory"] = pages * page_size;
 
}
 

	
 
#endif /* WITH_NLOHMANN_JSON */
src/os/windows/CMakeLists.txt
Show inline comments
 
@@ -4,6 +4,7 @@ add_files(
 
    font_win32.h
 
    string_uniscribe.cpp
 
    string_uniscribe.h
 
    survey_win.cpp
 
    win32.cpp
 
    win32.h
 
    CONDITION WIN32
src/os/windows/survey_win.cpp
Show inline comments
 
new file 100644
 
/*
 
 * 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 survey_win.cpp Windows implementation of OS-specific survey information. */
 

	
 
#ifdef WITH_NLOHMANN_JSON
 

	
 
#include "../../stdafx.h"
 

	
 
#include "../../3rdparty/fmt/format.h"
 

	
 
#include <nlohmann/json.hpp>
 
#include <windows.h>
 

	
 
#include "../../safeguards.h"
 

	
 
void SurveyOS(nlohmann::json &json)
 
{
 
	_OSVERSIONINFOA os;
 
	os.dwOSVersionInfoSize = sizeof(os);
 
	GetVersionExA(&os);
 

	
 
	json["os"] = "Windows";
 
	json["release"] = fmt::format("{}.{}.{} ({})", os.dwMajorVersion, os.dwMinorVersion, os.dwBuildNumber, os.szCSDVersion);
 

	
 
	MEMORYSTATUSEX status;
 
	status.dwLength = sizeof(status);
 
	GlobalMemoryStatusEx(&status);
 

	
 
	json["memory"] = status.ullTotalPhys;
 
}
 

	
 
#endif /* WITH_NLOHMANN_JSON */
src/saveload/afterload.cpp
Show inline comments
 
@@ -3224,6 +3224,10 @@ bool AfterLoadGame()
 
		for (Station *st : Station::Iterate()) UpdateStationAcceptance(st, false);
 
	}
 

	
 
	if (IsSavegameVersionBefore(SLV_SAVEGAME_ID)) {
 
		GenerateSavegameId();
 
	}
 

	
 
	if (IsSavegameVersionBefore(SLV_AI_START_DATE)) {
 
		/* For older savegames, we don't now the actual interval; so set it to the newgame value. */
 
		_settings_game.difficulty.competitors_interval = _settings_newgame.difficulty.competitors_interval;
src/saveload/misc_sl.cpp
Show inline comments
 
@@ -29,6 +29,7 @@
 
extern TileIndex _cur_tileloop_tile;
 
extern uint16 _disaster_delay;
 
extern byte _trees_tick_ctr;
 
extern std::string _savegame_id;
 

	
 
/* Keep track of current game position */
 
int _saved_scrollpos_x;
 
@@ -87,6 +88,7 @@ static const SaveLoad _date_desc[] = {
 
	    SLEG_VAR("company_tick_counter", _cur_company_tick_index, SLE_FILE_U8  | SLE_VAR_U32),
 
	    SLEG_VAR("trees_tick_counter",     _trees_tick_ctr,         SLE_UINT8),
 
	SLEG_CONDVAR("pause_mode",             _pause_mode,             SLE_UINT8,                   SLV_4, SL_MAX_VERSION),
 
	SLEG_CONDSSTR("id",                    _savegame_id,            SLE_STR,                     SLV_SAVEGAME_ID, SL_MAX_VERSION),
 
	/* For older savegames, we load the current value as the "period"; afterload will set the "fired" and "elapsed". */
 
	SLEG_CONDVAR("next_competitor_start",        _new_competitor_timeout.period,          SLE_FILE_U16 | SLE_VAR_U32,  SL_MIN_VERSION, SLV_109),
 
	SLEG_CONDVAR("next_competitor_start",        _new_competitor_timeout.period,          SLE_UINT32,                  SLV_109, SLV_AI_START_DATE),
src/saveload/saveload.h
Show inline comments
 
@@ -353,6 +353,7 @@ enum SaveLoadVersion : uint16 {
 
	SLV_EXTEND_VEHICLE_RANDOM,              ///< 310  PR#10701 Extend vehicle random bits.
 
	SLV_EXTEND_ENTITY_MAPPING,              ///< 311  PR#10672 Extend entity mapping range.
 
	SLV_DISASTER_VEH_STATE,                 ///< 312  PR#10798 Explicit storage of disaster vehicle state.
 
	SLV_SAVEGAME_ID,                        ///< 313  PR#10719 Add an unique ID to every savegame (used to deduplicate surveys).
 

	
 
	SL_MAX_VERSION,                         ///< Highest possible saveload version
 
};
src/settings_gui.cpp
Show inline comments
 
@@ -41,6 +41,9 @@
 
#include "music/music_driver.hpp"
 
#include "gui.h"
 
#include "mixer.h"
 
#include "network/core/config.h"
 
#include "network/network_gui.h"
 
#include "network/network_survey.h"
 

	
 

	
 
#include "safeguards.h"
 
@@ -190,6 +193,8 @@ struct GameOptionsWindow : Window {
 
		this->OnInvalidateData(0);
 

	
 
		this->SetTab(WID_GO_TAB_GENERAL);
 

	
 
		if constexpr (!NetworkSurveyHandler::IsSurveyPossible()) this->GetWidget<NWidgetStacked>(WID_GO_SURVEY_SEL)->SetDisplayedPlane(SZSP_NONE);
 
	}
 

	
 
	void Close() override
 
@@ -464,19 +469,19 @@ struct GameOptionsWindow : Window {
 

	
 
	void OnClick(Point pt, int widget, int click_count) override
 
	{
 
		if (widget >= WID_GO_BASE_GRF_TEXTFILE && widget < WID_GO_BASE_GRF_TEXTFILE + TFT_END) {
 
		if (widget >= WID_GO_BASE_GRF_TEXTFILE && widget < WID_GO_BASE_GRF_TEXTFILE + TFT_CONTENT_END) {
 
			if (BaseGraphics::GetUsedSet() == nullptr) return;
 

	
 
			ShowBaseSetTextfileWindow((TextfileType)(widget - WID_GO_BASE_GRF_TEXTFILE), BaseGraphics::GetUsedSet(), STR_CONTENT_TYPE_BASE_GRAPHICS);
 
			return;
 
		}
 
		if (widget >= WID_GO_BASE_SFX_TEXTFILE && widget < WID_GO_BASE_SFX_TEXTFILE + TFT_END) {
 
		if (widget >= WID_GO_BASE_SFX_TEXTFILE && widget < WID_GO_BASE_SFX_TEXTFILE + TFT_CONTENT_END) {
 
			if (BaseSounds::GetUsedSet() == nullptr) return;
 

	
 
			ShowBaseSetTextfileWindow((TextfileType)(widget - WID_GO_BASE_SFX_TEXTFILE), BaseSounds::GetUsedSet(), STR_CONTENT_TYPE_BASE_SOUNDS);
 
			return;
 
		}
 
		if (widget >= WID_GO_BASE_MUSIC_TEXTFILE && widget < WID_GO_BASE_MUSIC_TEXTFILE + TFT_END) {
 
		if (widget >= WID_GO_BASE_MUSIC_TEXTFILE && widget < WID_GO_BASE_MUSIC_TEXTFILE + TFT_CONTENT_END) {
 
			if (BaseMusic::GetUsedSet() == nullptr) return;
 

	
 
			ShowBaseSetTextfileWindow((TextfileType)(widget - WID_GO_BASE_MUSIC_TEXTFILE), BaseMusic::GetUsedSet(), STR_CONTENT_TYPE_BASE_MUSIC);
 
@@ -489,6 +494,30 @@ struct GameOptionsWindow : Window {
 
				this->SetTab(widget);
 
				break;
 

	
 
			case WID_GO_SURVEY_PARTICIPATE_BUTTON:
 
				switch (_settings_client.network.participate_survey) {
 
					case PS_ASK:
 
					case PS_NO:
 
						_settings_client.network.participate_survey = PS_YES;
 
						break;
 

	
 
					case PS_YES:
 
						_settings_client.network.participate_survey = PS_NO;
 
						break;
 
				}
 

	
 
				this->SetWidgetLoweredState(WID_GO_SURVEY_PARTICIPATE_BUTTON, _settings_client.network.participate_survey == PS_YES);
 
				this->SetWidgetDirty(WID_GO_SURVEY_PARTICIPATE_BUTTON);
 
				break;
 

	
 
			case WID_GO_SURVEY_LINK_BUTTON:
 
				OpenBrowser(NETWORK_SURVEY_DETAILS_LINK.c_str());
 
				break;
 

	
 
			case WID_GO_SURVEY_PREVIEW_BUTTON:
 
				ShowSurveyResultTextfileWindow();
 
				break;
 

	
 
			case WID_GO_FULLSCREEN_BUTTON: // Click fullscreen on/off
 
				/* try to toggle full-screen on/off */
 
				if (!ToggleFullScreen(!_fullscreen)) {
 
@@ -686,6 +715,7 @@ struct GameOptionsWindow : Window {
 
	void OnInvalidateData(int data = 0, bool gui_scope = true) override
 
	{
 
		if (!gui_scope) return;
 
		this->SetWidgetLoweredState(WID_GO_SURVEY_PARTICIPATE_BUTTON, _settings_client.network.participate_survey == PS_YES);
 
		this->SetWidgetLoweredState(WID_GO_FULLSCREEN_BUTTON, _fullscreen);
 
		this->SetWidgetLoweredState(WID_GO_VIDEO_ACCEL_BUTTON, _video_hw_accel);
 
		this->SetWidgetDisabledState(WID_GO_REFRESH_RATE_DROPDOWN, _video_vsync);
 
@@ -701,7 +731,7 @@ struct GameOptionsWindow : Window {
 
		bool missing_files = BaseGraphics::GetUsedSet()->GetNumMissing() == 0;
 
		this->GetWidget<NWidgetCore>(WID_GO_BASE_GRF_STATUS)->SetDataTip(missing_files ? STR_EMPTY : STR_GAME_OPTIONS_BASE_GRF_STATUS, STR_NULL);
 

	
 
		for (TextfileType tft = TFT_BEGIN; tft < TFT_END; tft++) {
 
		for (TextfileType tft = TFT_CONTENT_BEGIN; tft < TFT_CONTENT_END; tft++) {
 
			this->SetWidgetDisabledState(WID_GO_BASE_GRF_TEXTFILE + tft, BaseGraphics::GetUsedSet() == nullptr || !BaseGraphics::GetUsedSet()->GetTextfile(tft).has_value());
 
			this->SetWidgetDisabledState(WID_GO_BASE_SFX_TEXTFILE + tft, BaseSounds::GetUsedSet() == nullptr || !BaseSounds::GetUsedSet()->GetTextfile(tft).has_value());
 
			this->SetWidgetDisabledState(WID_GO_BASE_MUSIC_TEXTFILE + tft, BaseMusic::GetUsedSet() == nullptr || !BaseMusic::GetUsedSet()->GetTextfile(tft).has_value());
 
@@ -739,6 +769,20 @@ static const NWidgetPart _nested_game_op
 
				NWidget(WWT_FRAME, COLOUR_GREY), SetDataTip(STR_GAME_OPTIONS_CURRENCY_UNITS_FRAME, STR_NULL),
 
					NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_GO_CURRENCY_DROPDOWN), SetMinimalSize(100, 12), SetDataTip(STR_JUST_STRING, STR_GAME_OPTIONS_CURRENCY_UNITS_DROPDOWN_TOOLTIP), SetFill(1, 0),
 
				EndContainer(),
 

	
 
				NWidget(NWID_SELECTION, INVALID_COLOUR, WID_GO_SURVEY_SEL),
 
					NWidget(WWT_FRAME, COLOUR_GREY), SetDataTip(STR_GAME_OPTIONS_PARTICIPATE_SURVEY_FRAME, STR_NULL), SetPIP(0, WidgetDimensions::unscaled.vsep_normal, 0),
 
						NWidget(NWID_HORIZONTAL),
 
							NWidget(WWT_TEXT, COLOUR_GREY), SetMinimalSize(0, 12), SetDataTip(STR_GAME_OPTIONS_PARTICIPATE_SURVEY, STR_NULL),
 
							NWidget(NWID_SPACER), SetMinimalSize(1, 0), SetFill(1, 0),
 
							NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_GO_SURVEY_PARTICIPATE_BUTTON), SetMinimalSize(21, 9), SetDataTip(STR_EMPTY, STR_GAME_OPTIONS_PARTICIPATE_SURVEY_TOOLTIP),
 
						EndContainer(),
 
						NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), SetPIP(7, 0, 7),
 
							NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_GO_SURVEY_PREVIEW_BUTTON), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_GAME_OPTIONS_PARTICIPATE_SURVEY_PREVIEW, STR_GAME_OPTIONS_PARTICIPATE_SURVEY_PREVIEW_TOOLTIP),
 
							NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_GO_SURVEY_LINK_BUTTON), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_GAME_OPTIONS_PARTICIPATE_SURVEY_LINK, STR_GAME_OPTIONS_PARTICIPATE_SURVEY_LINK_TOOLTIP),
 
						EndContainer(),
 
					EndContainer(),
 
				EndContainer(),
 
			EndContainer(),
 

	
 
			/* Graphics tab */
src/settings_type.h
Show inline comments
 
@@ -63,13 +63,20 @@ enum IndustryDensity {
 
	ID_END,       ///< Number of industry density settings.
 
};
 

	
 
/** Possible values for "userelayservice" setting. */
 
/** Possible values for "use_relay_service" setting. */
 
enum UseRelayService {
 
	URS_NEVER = 0,
 
	URS_ASK,
 
	URS_ALLOW,
 
};
 

	
 
/** Possible values for "participate_survey" setting. */
 
enum ParticipateSurvey {
 
	PS_ASK = 0,
 
	PS_NO,
 
	PS_YES,
 
};
 

	
 
/** Settings related to the difficulty of the game */
 
struct DifficultySettings {
 
	byte   competitor_start_time;            ///< Unused value, used to load old savegames.
 
@@ -306,7 +313,8 @@ struct NetworkSettings {
 
	bool        reload_cfg;                               ///< reload the config file before restarting
 
	std::string last_joined;                              ///< Last joined server
 
	bool        no_http_content_downloads;                ///< do not do content downloads over HTTP
 
	UseRelayService use_relay_service;                        ///< Use relay service?
 
	UseRelayService use_relay_service;                    ///< Use relay service?
 
	ParticipateSurvey participate_survey;                 ///< Participate in the automated survey
 
};
 

	
 
/** Settings related to the creation of games. */
src/table/settings/network_private_settings.ini
Show inline comments
 
@@ -8,6 +8,7 @@
 

	
 
[pre-amble]
 
static constexpr std::initializer_list<const char*> _use_relay_service{"never", "ask", "allow"};
 
static constexpr std::initializer_list<const char*> _participate_survey{"ask", "no", "yes"};
 

	
 
static const SettingVariant _network_private_settings_table[] = {
 
[post-amble]
 
@@ -16,6 +17,7 @@ static const SettingVariant _network_pri
 
SDTC_BOOL  =  SDTC_BOOL(              $var,        $flags, $def,                              $str, $strhelp, $strval, $pre_cb, $post_cb, $from, $to,        $cat, $extra, $startup),
 
SDTC_OMANY = SDTC_OMANY(              $var, $type, $flags, $def,             $max, $full,     $str, $strhelp, $strval, $pre_cb, $post_cb, $from, $to,        $cat, $extra, $startup),
 
SDTC_SSTR  =  SDTC_SSTR(              $var, $type, $flags, $def,             $length,                                  $pre_cb, $post_cb, $from, $to,        $cat, $extra, $startup),
 
SDTC_OMANY = SDTC_OMANY(              $var, $type, $flags, $def,             $max, $full,     $str, $strhelp, $strval, $pre_cb, $post_cb, $from, $to,        $cat, $extra, $startup),
 

	
 
[validation]
 
SDTC_OMANY = static_assert($max <= MAX_$type, "Maximum value for $var exceeds storage size");
 
@@ -90,3 +92,12 @@ str      = STR_CONFIG_SETTING_USE_RELAY_
 
strhelp  = STR_CONFIG_SETTING_USE_RELAY_SERVICE_HELPTEXT
 
strval   = STR_CONFIG_SETTING_USE_RELAY_SERVICE_NEVER
 
cat      = SC_BASIC
 

	
 
[SDTC_OMANY]
 
var      = network.participate_survey
 
type     = SLE_UINT8
 
flags    = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC
 
def      = PS_ASK
 
min      = PS_ASK
 
max      = PS_YES
 
full     = _participate_survey
src/textfile_gui.cpp
Show inline comments
 
@@ -366,8 +366,21 @@ static void Xunzip(byte **bufp, size_t *
 
	if (StrStartsWith(sv_buf, u8"\ufeff")) sv_buf.remove_prefix(3);
 

	
 
	/* Replace any invalid characters with a question-mark. This copies the buf in the process. */
 
	this->text = StrMakeValid(sv_buf, SVS_REPLACE_WITH_QUESTION_MARK | SVS_ALLOW_NEWLINE | SVS_REPLACE_TAB_CR_NL_WITH_SPACE);
 
	this->LoadText(sv_buf);
 
	free(buf);
 
}
 

	
 
/**
 
 * Load a text into the textfile viewer.
 
 *
 
 * This will split the text into newlines and stores it for fast drawing.
 
 *
 
 * @param buf The text to load.
 
 */
 
void TextfileWindow::LoadText(std::string_view buf)
 
{
 
	this->text = StrMakeValid(buf, SVS_REPLACE_WITH_QUESTION_MARK | SVS_ALLOW_NEWLINE | SVS_REPLACE_TAB_CR_NL_WITH_SPACE);
 
	this->lines.clear();
 

	
 
	/* Split the string on newlines. */
 
	std::string_view p(this->text);
 
@@ -406,7 +419,7 @@ std::optional<std::string> GetTextfile(T
 
		"changelog",
 
		"license",
 
	};
 
	static_assert(lengthof(prefixes) == TFT_END);
 
	static_assert(lengthof(prefixes) == TFT_CONTENT_END);
 

	
 
	std::string_view prefix = prefixes[type];
 

	
src/textfile_gui.h
Show inline comments
 
@@ -42,6 +42,9 @@ struct TextfileWindow : public Window, M
 

	
 
	virtual void LoadTextfile(const std::string &textfile, Subdirectory dir);
 

	
 
protected:
 
	void LoadText(std::string_view buf);
 

	
 
private:
 
	struct Line {
 
		int top;               ///< Top scroll position.
src/textfile_type.h
Show inline comments
 
@@ -12,13 +12,15 @@
 

	
 
/** Additional text files accompanying Tar archives */
 
enum TextfileType {
 
	TFT_BEGIN,
 
	TFT_CONTENT_BEGIN,
 

	
 
	TFT_README = TFT_BEGIN, ///< NewGRF readme
 
	TFT_CHANGELOG,          ///< NewGRF changelog
 
	TFT_LICENSE,            ///< NewGRF license
 
	TFT_README = TFT_CONTENT_BEGIN, ///< Content readme
 
	TFT_CHANGELOG,                  ///< Content changelog
 
	TFT_LICENSE,                    ///< Content license
 

	
 
	TFT_END,
 
	TFT_CONTENT_END, // This marker is used to generate the above three buttons in sequence by various of places in the code.
 

	
 
	TFT_SURVEY_RESULT = TFT_CONTENT_END, ///< Survey result (preview)
 
};
 
DECLARE_POSTFIX_INCREMENT(TextfileType)
 

	
src/widget.cpp
Show inline comments
 
@@ -1172,7 +1172,7 @@ NWidgetCore::NWidgetCore(WidgetType tp, 
 
	this->widget_data = widget_data;
 
	this->tool_tip = tool_tip;
 
	this->scrollbar_index = -1;
 
	this->text_colour = TC_BLACK;
 
	this->text_colour = tp == WWT_CAPTION ? TC_WHITE : TC_BLACK;
 
	this->text_size = FS_NORMAL;
 
	this->align = SA_CENTER;
 
}
src/widgets/ai_widget.h
Show inline comments
 
@@ -29,7 +29,7 @@ enum AIConfigWidgets {
 
	WID_AIC_CONFIGURE,        ///< Change AI settings button.
 
	WID_AIC_CLOSE,            ///< Close window button.
 
	WID_AIC_TEXTFILE,         ///< Open AI readme, changelog (+1) or license (+2).
 
	WID_AIC_CONTENT_DOWNLOAD = WID_AIC_TEXTFILE + TFT_END, ///< Download content button.
 
	WID_AIC_CONTENT_DOWNLOAD = WID_AIC_TEXTFILE + TFT_CONTENT_END, ///< Download content button.
 
};
 

	
 
#endif /* WIDGETS_AI_WIDGET_H */
src/widgets/game_widget.h
Show inline comments
 
@@ -20,7 +20,7 @@ enum GSConfigWidgets {
 
	WID_GSC_SCROLLBAR,        ///< Scrollbar to scroll through the selected AIs.
 
	WID_GSC_CHANGE,           ///< Select another Game Script button.
 
	WID_GSC_TEXTFILE,         ///< Open GS readme, changelog (+1) or license (+2).
 
	WID_GSC_CONTENT_DOWNLOAD = WID_GSC_TEXTFILE + TFT_END, ///< Download content button.
 
	WID_GSC_CONTENT_DOWNLOAD = WID_GSC_TEXTFILE + TFT_CONTENT_END, ///< Download content button.
 
	WID_GSC_ACCEPT,           ///< Accept ("Close") button
 
	WID_GSC_RESET,            ///< Reset button.
 
};
src/widgets/network_content_widget.h
Show inline comments
 
@@ -36,7 +36,7 @@ enum NetworkContentListWidgets {
 
	WID_NCL_DETAILS,        ///< Panel with content details.
 
	WID_NCL_TEXTFILE,       ///< Open readme, changelog (+1) or license (+2) of a file in the content window.
 

	
 
	WID_NCL_SELECT_ALL = WID_NCL_TEXTFILE + TFT_END, ///< 'Select all' button.
 
	WID_NCL_SELECT_ALL = WID_NCL_TEXTFILE + TFT_CONTENT_END, ///< 'Select all' button.
 
	WID_NCL_SELECT_UPDATE,  ///< 'Select updates' button.
 
	WID_NCL_UNSELECT,       ///< 'Unselect all' button.
 
	WID_NCL_OPEN_URL,       ///< 'Open url' button.
src/widgets/network_widget.h
Show inline comments
 
@@ -119,4 +119,14 @@ enum NetworkAskRelayWidgets {
 
	WID_NAR_YES_ALWAYS, ///< "Yes, always" button.
 
};
 

	
 
/** Widgets of the #NetworkAskSurveyWindow class. */
 
enum NetworkAskSurveyWidgets {
 
	WID_NAS_CAPTION,    ///< Caption of the window.
 
	WID_NAS_TEXT,       ///< Text in the window.
 
	WID_NAS_PREVIEW,    ///< "Preview" button.
 
	WID_NAS_LINK,       ///< "Details & Privacy" button.
 
	WID_NAS_NO,         ///< "No" button.
 
	WID_NAS_YES,        ///< "Yes" button.
 
};
 

	
 
#endif /* WIDGETS_NETWORK_WIDGET_H */
src/widgets/newgrf_widget.h
Show inline comments
 
@@ -47,7 +47,7 @@ enum NewGRFStateWidgets {
 
	WID_NS_NEWGRF_INFO,       ///< Panel for Info on selected NewGRF.
 
	WID_NS_OPEN_URL,          ///< Open URL of NewGRF.
 
	WID_NS_NEWGRF_TEXTFILE,   ///< Open NewGRF readme, changelog (+1) or license (+2).
 
	WID_NS_SET_PARAMETERS = WID_NS_NEWGRF_TEXTFILE + TFT_END,   ///< Open Parameters Window for selected NewGRF for editing parameters.
 
	WID_NS_SET_PARAMETERS = WID_NS_NEWGRF_TEXTFILE + TFT_CONTENT_END,   ///< Open Parameters Window for selected NewGRF for editing parameters.
 
	WID_NS_VIEW_PARAMETERS,   ///< Open Parameters Window for selected NewGRF for viewing parameters.
 
	WID_NS_TOGGLE_PALETTE,    ///< Toggle Palette of selected, active NewGRF.
 
	WID_NS_APPLY_CHANGES,     ///< Apply changes to NewGRF config.
src/widgets/settings_widget.h
Show inline comments
 
@@ -28,23 +28,27 @@ enum GameOptionsWidgets {
 
	WID_GO_BASE_GRF_DROPDOWN,      ///< Use to select a base GRF.
 
	WID_GO_BASE_GRF_STATUS,        ///< Info about missing files etc.
 
	WID_GO_BASE_GRF_TEXTFILE,      ///< Open base GRF readme, changelog (+1) or license (+2).
 
	WID_GO_BASE_GRF_DESCRIPTION = WID_GO_BASE_GRF_TEXTFILE + TFT_END,     ///< Description of selected base GRF.
 
	WID_GO_BASE_GRF_DESCRIPTION = WID_GO_BASE_GRF_TEXTFILE + TFT_CONTENT_END,     ///< Description of selected base GRF.
 
	WID_GO_BASE_SFX_DROPDOWN,      ///< Use to select a base SFX.
 
	WID_GO_TEXT_SFX_VOLUME,        ///< Sound effects volume label.
 
	WID_GO_BASE_SFX_VOLUME,        ///< Change sound effects volume.
 
	WID_GO_BASE_SFX_TEXTFILE,      ///< Open base SFX readme, changelog (+1) or license (+2).
 
	WID_GO_BASE_SFX_DESCRIPTION = WID_GO_BASE_SFX_TEXTFILE + TFT_END,     ///< Description of selected base SFX.
 
	WID_GO_BASE_SFX_DESCRIPTION = WID_GO_BASE_SFX_TEXTFILE + TFT_CONTENT_END,     ///< Description of selected base SFX.
 
	WID_GO_BASE_MUSIC_DROPDOWN,    ///< Use to select a base music set.
 
	WID_GO_TEXT_MUSIC_VOLUME,      ///< Music volume label.
 
	WID_GO_BASE_MUSIC_VOLUME,      ///< Change music volume.
 
	WID_GO_BASE_MUSIC_JUKEBOX,     ///< Open the jukebox.
 
	WID_GO_BASE_MUSIC_STATUS,      ///< Info about corrupted files etc.
 
	WID_GO_BASE_MUSIC_TEXTFILE,    ///< Open base music readme, changelog (+1) or license (+2).
 
	WID_GO_BASE_MUSIC_DESCRIPTION = WID_GO_BASE_MUSIC_TEXTFILE + TFT_END, ///< Description of selected base music set.
 
	WID_GO_BASE_MUSIC_DESCRIPTION = WID_GO_BASE_MUSIC_TEXTFILE + TFT_CONTENT_END, ///< Description of selected base music set.
 
	WID_GO_VIDEO_ACCEL_BUTTON,     ///< Toggle for video acceleration.
 
	WID_GO_VIDEO_VSYNC_BUTTON,     ///< Toggle for video vsync.
 
	WID_GO_REFRESH_RATE_DROPDOWN,  ///< Dropdown for all available refresh rates.
 
	WID_GO_VIDEO_DRIVER_INFO,      ///< Label showing details about the current video driver.
 
	WID_GO_SURVEY_SEL,             ///< Selection to hide survey if no JSON library is compiled in.
 
	WID_GO_SURVEY_PARTICIPATE_BUTTON, ///< Toggle for participating in the automated survey.
 
	WID_GO_SURVEY_LINK_BUTTON,     ///< Button to open browser to go to the survey website.
 
	WID_GO_SURVEY_PREVIEW_BUTTON,  ///< Button to open a preview window with the survey results
 
};
 

	
 
/** Widgets of the #GameSettingsWindow class. */
src/window_type.h
Show inline comments
 
@@ -484,6 +484,12 @@ enum WindowClass {
 
	WC_NETWORK_ASK_RELAY,
 

	
 
	/**
 
	 * Network ask survey window; %Window numbers:
 
	 *  - 0 - #NetworkAskSurveyWidgets
 
	 */
 
	WC_NETWORK_ASK_SURVEY,
 

	
 
	/**
 
	 * Chatbox; %Window numbers:
 
	 *   - #DestType = #NetWorkChatWidgets
 
	 */
0 comments (0 inline, 0 general)