Changeset - r27371:b9ad4c1bff08
[Not reviewed]
master
0 49 5
Patric Stout - 17 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
 
@@ -34,16 +34,18 @@ jobs:
 

	
 
        echo "::group::Install dependencies"
 
        sudo apt-get install -y --no-install-recommends \
 
          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::"
 
      env:
 
        DEBIAN_FRONTEND: noninteractive
 

	
.github/workflows/release-linux.yml
Show inline comments
 
name: Release (Linux)
 

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

	
 
jobs:
 
  linux:
 
    name: Linux (Generic)
 

	
 
    runs-on: ubuntu-20.04
 
@@ -116,12 +121,13 @@ jobs:
 
        cd build
 

	
 
        echo "::group::CMake"
 
        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::"
 

	
 
        echo "::group::Build"
 
        echo "Running on $(nproc) cores"
.github/workflows/release-macos.yml
Show inline comments
 
name: Release (MacOS)
 

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

	
 
jobs:
 
  macos:
 
    name: MacOS
 

	
 
    runs-on: macos-11
 
@@ -101,12 +106,13 @@ jobs:
 
        cmake ${GITHUB_WORKSPACE} \
 
          -DCMAKE_OSX_ARCHITECTURES=arm64 \
 
          -DVCPKG_TARGET_TRIPLET=arm64-osx \
 
          -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::"
 

	
 
        echo "::group::Build"
 
        echo "Running on $(sysctl -n hw.logicalcpu) cores"
 
        cmake --build . -j $(sysctl -n hw.logicalcpu) --target openttd
 
@@ -121,12 +127,13 @@ jobs:
 
        cmake ${GITHUB_WORKSPACE} \
 
          -DCMAKE_OSX_ARCHITECTURES=x86_64 \
 
          -DVCPKG_TARGET_TRIPLET=x64-osx \
 
          -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 \
 
          # EOF
 
        echo "::endgroup::"
 

	
.github/workflows/release-source.yml
Show inline comments
 
@@ -8,24 +8,27 @@ on:
 
      is_tag:
 
        value: ${{ jobs.source.outputs.is_tag }}
 
      trigger_type:
 
        value: ${{ jobs.source.outputs.trigger_type }}
 
      folder:
 
        value: ${{ jobs.source.outputs.folder }}
 
      survey_key:
 
        value: ${{ jobs.source.outputs.survey_key }}
 

	
 
jobs:
 
  source:
 
    name: Source
 

	
 
    runs-on: ubuntu-20.04
 

	
 
    outputs:
 
      version: ${{ steps.metadata.outputs.version }}
 
      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)
 
      if: github.event_name == 'release'
 
      uses: actions/checkout@v3
 
      with:
 
@@ -143,12 +146,25 @@ jobs:
 
      env:
 
        NIGHTLIES_BRANCH: master
 
        FOLDER_RELEASES: openttd-releases
 
        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
 

	
 
    - name: Create bundles
 
      run: |
.github/workflows/release-windows.yml
Show inline comments
 
name: Release (Windows)
 

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

	
 
jobs:
 
  windows:
 
@@ -126,12 +130,13 @@ jobs:
 
          -GNinja \
 
          -DVCPKG_TARGET_TRIPLET=${{ matrix.arch }}-windows-static \
 
          -DCMAKE_TOOLCHAIN_FILE="c:\vcpkg\scripts\buildsystems\vcpkg.cmake" \
 
          -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::"
 

	
 
        echo "::group::Build"
 
        cmake --build . --target openttd
 
@@ -150,12 +155,13 @@ jobs:
 
        cmake ${GITHUB_WORKSPACE} \
 
          -GNinja \
 
          -DVCPKG_TARGET_TRIPLET=${{ matrix.arch }}-windows-static \
 
          -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::"
 

	
 
        echo "::group::Build"
 
        cmake --build . --target openttd
.github/workflows/release.yml
Show inline comments
 
@@ -35,28 +35,35 @@ jobs:
 
    name: Linux (Generic)
 
    needs: source
 

	
 
    uses: ./.github/workflows/release-linux.yml
 
    secrets: inherit
 

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

	
 
  macos:
 
    name: MacOS
 
    needs: source
 

	
 
    uses: ./.github/workflows/release-macos.yml
 
    secrets: inherit
 

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

	
 
  windows:
 
    name: Windows
 
    needs: source
 

	
 
    uses: ./.github/workflows/release-windows.yml
 
    secrets: inherit
 

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

	
 
  windows-store:
 
    name: Windows Store
 
    needs:
 
    - source
 
    - windows
cmake/Options.cmake
Show inline comments
 
@@ -68,12 +68,14 @@ function(set_options)
 
    option(OPTION_TOOLS_ONLY "Build only tools target" OFF)
 
    option(OPTION_DOCS_ONLY "Build only docs target" OFF)
 

	
 
    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.
 
#
 
# show_options()
 
#
 
@@ -81,12 +83,18 @@ function(show_options)
 
    message(STATUS "Option Package Dependencies - ${OPTION_PACKAGE_DEPENDENCIES}")
 
    message(STATUS "Option Dedicated - ${OPTION_DEDICATED}")
 
    message(STATUS "Option Install FHS - ${OPTION_INSTALL_FHS}")
 
    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.
 
#
 
# add_definitions_based_on_options()
 
#
 
@@ -101,7 +109,11 @@ function(add_definitions_based_on_option
 

	
 
    if(OPTION_USE_ASSERTS)
 
        add_definitions(-DWITH_ASSERT)
 
    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
 
@@ -180,13 +180,13 @@ 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);
 
			return;
 
		}
 

	
 
@@ -281,13 +281,13 @@ struct AIConfigWindow : public Window {
 
		this->SetWidgetDisabledState(WID_AIC_INCREASE_INTERVAL, GetGameSettings().difficulty.competitors_interval == MAX_COMPETITORS_INTERVAL);
 
		this->SetWidgetDisabledState(WID_AIC_CHANGE, this->selected_slot == INVALID_COMPANY);
 
		this->SetWidgetDisabledState(WID_AIC_CONFIGURE, this->selected_slot == INVALID_COMPANY || AIConfig::GetConfig(this->selected_slot)->GetConfigList()->size() == 0);
 
		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());
 
		}
 
	}
 
};
 

	
 
/** Open the AI config window. */
src/crashlog.cpp
Show inline comments
 
@@ -20,12 +20,13 @@
 
#include "sound/sound_driver.hpp"
 
#include "video/video_driver.hpp"
 
#include "saveload/saveload.h"
 
#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"
 

	
 
#include "ai/ai_info.hpp"
 
#include "game/game.hpp"
 
@@ -508,12 +509,16 @@ bool CrashLog::MakeCrashLog() const
 
		printf("Crash screenshot written to %s. Please add this file to any bug reports.\n\n", filename);
 
	} else {
 
		ret = false;
 
		printf("Writing crash screenshot failed.\n\n");
 
	}
 

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

	
 
	return ret;
 
}
 

	
 
/**
 
 * Sets a message for the error message handler.
 
 * @param message The error message of the error.
src/game/game_gui.cpp
Show inline comments
 
@@ -237,13 +237,13 @@ struct GSConfigWindow : public Window {
 
		}
 
		this->DrawWidgets();
 
	}
 

	
 
	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);
 
			return;
 
		}
 

	
 
@@ -401,13 +401,13 @@ struct GSConfigWindow : public Window {
 
	void OnInvalidateData(int data = 0, bool gui_scope = true) override
 
	{
 
		if (!gui_scope) return;
 

	
 
		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();
 
		HideDropDownMenu(this);
 
		this->CloseChildWindows(WC_QUERY_STRING);
 
	}
src/gfx.cpp
Show inline comments
 
@@ -44,12 +44,13 @@ bool _right_button_down;    ///< Is righ
 
bool _right_button_clicked; ///< Is right mouse button clicked?
 
DrawPixelInfo _screen;
 
bool _screen_disable_anim = false;   ///< Disable palette animation (important for 32bpp-anim blitter during giant screenshot)
 
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;
 

	
 
static byte _stringwidth_table[FS_END][224]; ///< Cache containing width of often used characters. @see GetCharacterWidth()
 
DrawPixelInfo *_cur_dpi;
 
byte _colour_gradient[COLOUR_END][8];
src/intro_gui.cpp
Show inline comments
 
@@ -14,12 +14,13 @@
 
#include "window_func.h"
 
#include "textbuf_gui.h"
 
#include "network/network.h"
 
#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"
 
#include "fios.h"
 
#include "ai/ai_gui.hpp"
 
#include "game/game_gui.hpp"
 
@@ -501,13 +502,16 @@ void ShowSelectGameWindow()
 
{
 
	new SelectGameWindow(&_select_game_desc);
 
}
 

	
 
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()
 
{
 
	ShowQuery(
 
		STR_QUIT_CAPTION,
src/lang/english.txt
Show inline comments
 
@@ -1038,12 +1038,20 @@ STR_GAME_OPTIONS_GUI_SCALE_BEVELS_TOOLTI
 
STR_GAME_OPTIONS_GUI_SCALE_1X                                   :1x
 
STR_GAME_OPTIONS_GUI_SCALE_2X                                   :2x
 
STR_GAME_OPTIONS_GUI_SCALE_3X                                   :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
 
STR_GAME_OPTIONS_REFRESH_RATE_TOOLTIP                           :{BLACK}Select the screen refresh rate to use
 
STR_GAME_OPTIONS_REFRESH_RATE_ITEM                              :{NUM}Hz
 
STR_GAME_OPTIONS_REFRESH_RATE_WARNING                           :{WHITE}Refresh rates higher than 60Hz might impact performance.
 
@@ -2399,12 +2407,19 @@ STR_NETWORK_CLIENT_LIST_ASK_COMPANY_UNLO
 
STR_NETWORK_ASK_RELAY_CAPTION                                   :{WHITE}Use relay?
 
STR_NETWORK_ASK_RELAY_TEXT                                      :{YELLOW}Failed to establish a connection between you and server '{RAW_STRING}'.{}Would you like to relay this session via '{RAW_STRING}'?
 
STR_NETWORK_ASK_RELAY_NO                                        :{BLACK}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
 
STR_COMPANY_PASSWORD_CANCEL                                     :{BLACK}Do not save the entered password
 
STR_COMPANY_PASSWORD_OK                                         :{BLACK}Give the company the new password
 
STR_COMPANY_PASSWORD_CAPTION                                    :{WHITE}Company password
 
@@ -4651,16 +4666,17 @@ STR_AI_SETTINGS_SETTING                 
 
# Textfile window
 
STR_TEXTFILE_WRAP_TEXT                                          :{WHITE}Wrap text
 
STR_TEXTFILE_WRAP_TEXT_TOOLTIP                                  :{BLACK}Wrap the text of the window so it all fits without having to scroll
 
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
 
STR_PERCENT_UP_SMALL                                            :{TINY_FONT}{WHITE}{NUM}%{UP_ARROW}
 
STR_PERCENT_UP                                                  :{WHITE}{NUM}%{UP_ARROW}
 
STR_PERCENT_DOWN_SMALL                                          :{TINY_FONT}{WHITE}{NUM}%{DOWN_ARROW}
src/misc.cpp
Show inline comments
 
@@ -28,15 +28,17 @@
 
#include "game/game.hpp"
 
#include "linkgraph/linkgraphschedule.h"
 
#include "station_kdtree.h"
 
#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();
 

	
 
void InitializeSound();
 
void InitializeMusic();
 
@@ -53,12 +55,46 @@ void InitializeObjects();
 
void InitializeTrees();
 
void InitializeCompanies();
 
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
 
	 * related to the new game we're about to start/load. */
 
	UnInitWindowSystem();
 

	
src/network/CMakeLists.txt
Show inline comments
 
@@ -25,12 +25,14 @@ add_files(
 
    network_query.cpp
 
    network_query.h
 
    network_server.cpp
 
    network_server.h
 
    network_stun.cpp
 
    network_stun.h
 
    network_survey.cpp
 
    network_survey.h
 
    network_turn.cpp
 
    network_turn.h
 
    network_type.h
 
    network_udp.cpp
 
    network_udp.h
 
)
src/network/core/config.cpp
Show inline comments
 
@@ -63,6 +63,16 @@ const char *NetworkContentServerConnecti
 
 * @return The content mirror's URI string.
 
 */
 
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/");
 
}
src/network/core/config.h
Show inline comments
 
@@ -13,22 +13,25 @@
 
#define NETWORK_CORE_CONFIG_H
 

	
 
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)
 
static const uint16 NETWORK_TURN_SERVER_PORT        = 3974;           ///< The default port of the TURN server (TCP)
 
static const uint16 NETWORK_CONTENT_SERVER_PORT     = 3978;           ///< The default port of the content server (TCP)
 
static const uint16 NETWORK_DEFAULT_PORT            = 3979;           ///< The default port of the game server (TCP & UDP)
 
static const uint16 NETWORK_ADMIN_PORT              = 3977;           ///< The default port for admin network
 
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.
 
 *
 
 * Packets up to 32 KiB have the high bit not set:
 
 * 00000000 00000000 0bbbbbbb aaaaaaaa -> aaaaaaaa 0bbbbbbb
 
@@ -43,12 +46,13 @@ static const uint16 UDP_MTU             
 
static const uint16 TCP_MTU                         = 32767;          ///< Number of bytes we can pack in a single TCP packet
 
static const uint16 COMPAT_MTU                      = 1460;           ///< Number of bytes we can pack in a single packet for backward compatibility
 

	
 
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'
 
static const uint NETWORK_HOSTNAME_LENGTH           =   80;           ///< The maximum length of the host name, in bytes including '\0'
 
static const uint NETWORK_HOSTNAME_PORT_LENGTH      =   80 + 6;       ///< The maximum length of the host name + port, in bytes including '\0'. The extra six is ":" + port number (with a max of 65536)
 
static const uint NETWORK_SERVER_ID_LENGTH          =   33;           ///< The maximum length of the network id of the servers, in bytes including '\0'
src/network/core/http.h
Show inline comments
 
@@ -11,12 +11,14 @@
 

	
 
#ifndef NETWORK_CORE_HTTP_H
 
#define NETWORK_CORE_HTTP_H
 

	
 
#include "tcp.h"
 

	
 
constexpr int HTTP_429_TOO_MANY_REQUESTS = 429;
 

	
 
/** Callback for when the HTTP handler has something to tell us. */
 
struct HTTPCallback {
 
	/**
 
	 * An error has occurred and the connection has been closed.
 
	 * @note HTTP socket handler is closed/freed.
 
	 */
src/network/core/http_curl.cpp
Show inline comments
 
@@ -113,12 +113,13 @@ void HttpThread()
 

	
 
		/* Release the lock, as we will take a while to process the request. */
 
		lock.unlock();
 

	
 
		/* Reset to default settings. */
 
		curl_easy_reset(curl);
 
		curl_slist *headers = nullptr;
 

	
 
		if (_debug_net_level >= 5) {
 
			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
 
		}
 

	
 
		/* Setup some default options. */
 
@@ -143,14 +144,22 @@ void HttpThread()
 

	
 
		/* Fail our call if we don't receive a 2XX return value. */
 
		curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
 

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

	
 
		/* Setup our (C-style) callback function which we pipe back into the callback. */
 
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](char *ptr, size_t size, size_t nmemb, void *userdata) -> size_t {
 
			Debug(net, 4, "HTTP callback: {} bytes", size * nmemb);
 
@@ -171,17 +180,23 @@ void HttpThread()
 
		});
 
		curl_easy_setopt(curl, CURLOPT_XFERINFODATA, request->callback);
 

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

	
 
	curl_easy_cleanup(curl);
 
}
src/network/core/http_winhttp.cpp
Show inline comments
 
@@ -128,13 +128,14 @@ void NetworkHTTPRequest::WinHttpCallback
 
			DWORD status_code_size = sizeof(status_code);
 
			WinHttpQueryHeaders(this->request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX);
 
			Debug(net, 3, "HTTP request status code: {}", status_code);
 

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

	
 
			/* Next step: query for any data. */
 
@@ -239,13 +240,15 @@ void NetworkHTTPRequest::Connect()
 
	}
 

	
 
	/* Send the request (possibly with a payload). */
 
	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));
 
	}
 
}
 

	
 
/**
 
 * Poll and process the HTTP request/response.
 
 *
src/network/network.cpp
Show inline comments
 
@@ -82,12 +82,14 @@ CompanyMask _network_company_passworded;
 

	
 
static_assert((int)NETWORK_COMPANY_NAME_LENGTH == MAX_LENGTH_COMPANY_NAME_CHARS * MAX_CHAR_LENGTH);
 

	
 
/** 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
 
 */
 
bool HasClients()
 
{
 
@@ -1201,30 +1203,13 @@ void NetworkGameLoop()
 

	
 
	NetworkSend();
 
}
 

	
 
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 {
 
private:
 
	std::string connection_string;
 

	
src/network/network_content_gui.cpp
Show inline comments
 
@@ -787,13 +787,13 @@ 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);
 
			return;
 
		}
 

	
 
@@ -994,13 +994,13 @@ public:
 
		/* If data == 2 then the status window caused this OnInvalidate */
 
		this->SetWidgetDisabledState(WID_NCL_DOWNLOAD, this->filesize_sum == 0 || (FindWindowById(WC_NETWORK_STATUS_WINDOW, WN_NETWORK_STATUS_WINDOW_CONTENT_DOWNLOAD) != nullptr && data != 2));
 
		this->SetWidgetDisabledState(WID_NCL_UNSELECT, this->filesize_sum == 0);
 
		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());
 
		}
 

	
 
		this->GetWidget<NWidgetCore>(WID_NCL_CANCEL)->widget_data = this->filesize_sum == 0 ? STR_AI_SETTINGS_CLOSE : STR_AI_LIST_CANCEL;
 
	}
 
};
src/network/network_gui.cpp
Show inline comments
 
@@ -15,12 +15,13 @@
 
#include "network_gamelist.h"
 
#include "network.h"
 
#include "network_base.h"
 
#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"
 
#include "../gfx_func.h"
 
#include "../widgets/dropdown_type.h"
 
#include "../widgets/dropdown_func.h"
 
@@ -35,12 +36,13 @@
 
#include "../sprite.h"
 
#include "../settings_internal.h"
 
#include "../company_cmd.h"
 
#include "../timer/timer.h"
 
#include "../timer/timer_window.h"
 
#include "../timer/timer_game_calendar.h"
 
#include "../textfile_gui.h"
 

	
 
#include "../widgets/network_widget.h"
 

	
 
#include "table/strings.h"
 
#include "../table/sprites.h"
 

	
 
@@ -2512,6 +2514,122 @@ void ShowNetworkAskRelay(const std::stri
 
{
 
	CloseWindowByClass(WC_NETWORK_ASK_RELAY);
 

	
 
	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
 
@@ -21,13 +21,14 @@ void ShowNetworkNeedPassword(NetworkPass
 
void ShowNetworkChatQueryWindow(DestType type, int dest);
 
void ShowJoinStatusWindow();
 
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 {
 
	std::string company_name; ///< Company name
 
	TimerGameCalendar::Year inaugurated_year; ///< What year the company started in
 
	Money company_value;      ///< The company value
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
 
@@ -922,13 +922,13 @@ 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);
 
			return;
 
		}
 

	
 
@@ -1283,13 +1283,13 @@ struct NewGRFWindow : public Window, New
 
			WID_NS_MOVE_UP,
 
			WID_NS_MOVE_DOWN,
 
			WIDGET_LIST_END
 
		);
 

	
 
		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()));
 

	
 
		this->SetWidgetDisabledState(WID_NS_SET_PARAMETERS, !this->show_params || this->active_sel == nullptr || this->active_sel->num_valid_params == 0);
 
		this->SetWidgetDisabledState(WID_NS_VIEW_PARAMETERS, !this->show_params || this->active_sel == nullptr || this->active_sel->num_valid_params == 0);
src/openttd.cpp
Show inline comments
 
@@ -64,12 +64,13 @@
 
#include "gfx_layout.h"
 
#include "viewport_func.h"
 
#include "viewport_sprite_sorter.h"
 
#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"
 
#include "timer/timer_game_realtime.h"
 
#include "timer/timer_game_tick.h"
 

	
 
@@ -819,12 +820,13 @@ int openttd_main(int argc, char *argv[])
 
void HandleExitGameRequest()
 
{
 
	if (_game_mode == GM_MENU || _game_mode == GM_BOOTSTRAP) { // do not ask to quit on the main screen
 
		_exit_game = true;
 
	} else if (_settings_client.gui.autosave_on_exit) {
 
		DoExitSave();
 
		_survey.Transmit(NetworkSurveyHandler::Reason::EXIT, true);
 
		_exit_game = true;
 
	} else {
 
		AskExitGame();
 
	}
 
}
 

	
 
@@ -1033,15 +1035,22 @@ void SwitchToMode(SwitchMode new_mode)
 
	/* Make sure all AI controllers are gone at quitting game */
 
	if (new_mode != SM_SAVE_GAME) AI::KillAll();
 

	
 
	/* 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
 
			if (_file_to_saveload.abstract_ftype == FT_SAVEGAME || _file_to_saveload.abstract_ftype == FT_SCENARIO) {
 
				/* Reload current savegame/scenario */
 
				_switch_mode = _game_mode == GM_EDITOR ? SM_LOAD_SCENARIO : SM_LOAD_GAME;
 
@@ -1052,17 +1061,19 @@ void SwitchToMode(SwitchMode new_mode)
 
				_switch_mode = _game_mode == GM_EDITOR ? SM_LOAD_HEIGHTMAP : SM_RESTART_HEIGHTMAP;
 
				SwitchToMode(_switch_mode);
 
				break;
 
			}
 

	
 
			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
 
			ResetGRFConfig(true);
 
			ResetWindowSystem();
 

	
 
@@ -1080,24 +1091,27 @@ void SwitchToMode(SwitchMode new_mode)
 
			break;
 
		}
 

	
 
		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);
 
			} else {
 
				SetDParamStr(0, GetSaveLoadErrorString());
 
				ShowErrorMessage(STR_JUST_RAW_STRING, INVALID_STRING_ID, WL_CRITICAL);
 
@@ -1113,12 +1127,20 @@ void SwitchToMode(SwitchMode new_mode)
 
		case SM_MENU: // Switch to game intro menu
 
			LoadIntroGame();
 
			if (BaseSounds::ini_set.empty() && BaseSounds::GetUsedSet()->fallback && SoundDriver::GetInstance()->HasOutput()) {
 
				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.
 
			/* Make network saved games on pause compatible to singleplayer mode */
 
			if (SaveOrLoad(_file_to_saveload.name, SLO_SAVE, DFT_GAME_FILE, NO_DIRECTORY) != SL_OK) {
 
				SetDParamStr(0, GetSaveLoadErrorString());
src/openttd.h
Show inline comments
 
@@ -8,12 +8,13 @@
 
/** @file openttd.h Some generic types. */
 

	
 
#ifndef OPENTTD_H
 
#define OPENTTD_H
 

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

	
 
/** Mode which defines the state of the game. */
 
enum GameMode {
 
	GM_MENU,
 
	GM_NORMAL,
 
@@ -50,12 +51,13 @@ enum DisplayOptions {
 
	DO_SHOW_WAYPOINT_NAMES = 6, ///< Display waypoint names.
 
	DO_SHOW_COMPETITOR_SIGNS = 7, ///< Display signs, station names and waypoint names of opponent companies. Buoys and oilrig-stations are always shown, even if this option is turned off.
 
};
 

	
 
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;
 

	
 
/** Modes of pausing we've got */
 
enum PauseMode : byte {
 
	PM_UNPAUSED              = 0,      ///< A normal unpaused game
 
@@ -83,11 +85,12 @@ int openttd_main(int argc, char *argv[])
 
void StateGameLoop();
 
void HandleExitGameRequest();
 

	
 
void SwitchToMode(SwitchMode new_mode);
 

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

	
 
void OpenBrowser(const char *url);
 
void ChangeAutosaveFrequency(bool reset);
 

	
 
#endif /* OPENTTD_H */
src/os/macosx/CMakeLists.txt
Show inline comments
 
@@ -4,12 +4,13 @@ add_files(
 
    font_osx.h
 
    macos.h
 
    macos.mm
 
    osx_stdafx.h
 
    string_osx.cpp
 
    string_osx.h
 
    survey_osx.cpp
 
    CONDITION APPLE
 
)
 

	
 
if(APPLE)
 
    target_sources(openttd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/osx_main.cpp)
 
endif()
src/os/macosx/macos.h
Show inline comments
 
@@ -35,12 +35,14 @@ static inline bool MacOSVersionIsAtLeast
 
}
 

	
 
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 {
 
	void operator()(T *p)
 
	{
 
		if (p) ::CFRelease(p);
src/os/macosx/macos.mm
Show inline comments
 
@@ -269,6 +269,11 @@ void MacOSSetThreadName(const char *name
 

	
 
	NSThread *cur = [ NSThread currentThread ];
 
	if (cur != nil && [ cur respondsToSelector:@selector(setName:) ]) {
 
		[ 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
 
)
 

	
 
add_files(
 
    unix.cpp
 
    CONDITION UNIX 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
 
add_files(
 
    crashlog_win.cpp
 
    font_win32.cpp
 
    font_win32.h
 
    string_uniscribe.cpp
 
    string_uniscribe.h
 
    survey_win.cpp
 
    win32.cpp
 
    win32.h
 
    CONDITION WIN32
 
)
 

	
 
if(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
 
@@ -3221,12 +3221,16 @@ bool AfterLoadGame()
 

	
 
	/* Station acceptance is some kind of cache */
 
	if (IsSavegameVersionBefore(SLV_127)) {
 
		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;
 

	
 
		/* We did load the "period" of the timer, but not the fired/elapsed. We can deduce that here. */
 
		extern TimeoutTimer<TimerGameTick> _new_competitor_timeout;
src/saveload/misc_sl.cpp
Show inline comments
 
@@ -26,12 +26,13 @@
 

	
 
#include "../safeguards.h"
 

	
 
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;
 
int _saved_scrollpos_y;
 
ZoomLevel _saved_scrollpos_zoom;
 

	
 
@@ -84,12 +85,13 @@ static const SaveLoad _date_desc[] = {
 
	    SLEG_VAR("next_disaster_start",         _disaster_delay,         SLE_UINT16),
 
	    SLEG_VAR("random_state[0]",        _random.state[0],        SLE_UINT32),
 
	    SLEG_VAR("random_state[1]",        _random.state[1],        SLE_UINT32),
 
	    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),
 
	SLEG_CONDVAR("competitors_interval",         _new_competitor_timeout.period,          SLE_UINT32,                  SLV_AI_START_DATE, SL_MAX_VERSION),
 
	SLEG_CONDVAR("competitors_interval_elapsed", _new_competitor_timeout.storage.elapsed, SLE_UINT32,                  SLV_AI_START_DATE, SL_MAX_VERSION),
 
	SLEG_CONDVAR("competitors_interval_fired",   _new_competitor_timeout.fired,           SLE_BOOL,                    SLV_AI_START_DATE, SL_MAX_VERSION),
src/saveload/saveload.h
Show inline comments
 
@@ -350,12 +350,13 @@ enum SaveLoadVersion : uint16 {
 
	SLV_LINKGRAPH_SECONDS,                  ///< 308  PR#10610 Store linkgraph update intervals in seconds instead of days.
 
	SLV_AI_START_DATE,                      ///< 309  PR#10653 Removal of individual AI start dates and added a generic one.
 

	
 
	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
 
};
 

	
 
/** Save or load result codes. */
 
enum SaveOrLoadResult {
src/settings_gui.cpp
Show inline comments
 
@@ -38,12 +38,15 @@
 
#include "zoom_func.h"
 
#include "rev.h"
 
#include "video/video_driver.hpp"
 
#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"
 
#include "video/video_driver.hpp"
 

	
 

	
 
@@ -187,12 +190,14 @@ struct GameOptionsWindow : Window {
 
		AddCustomRefreshRates();
 

	
 
		this->InitNested(WN_GAME_OPTIONS_GAME_OPTIONS);
 
		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
 
	{
 
		CloseWindowById(WC_CUSTOM_CURRENCY, 0);
 
		CloseWindowByClass(WC_TEXTFILE);
 
@@ -461,37 +466,61 @@ 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);
 
			return;
 
		}
 
		switch (widget) {
 
			case WID_GO_TAB_GENERAL:
 
			case WID_GO_TAB_GRAPHICS:
 
			case WID_GO_TAB_SOUND:
 
				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)) {
 
					ShowErrorMessage(STR_ERROR_FULLSCREEN_FAILED, INVALID_STRING_ID, WL_ERROR);
 
				}
 
				this->SetWidgetLoweredState(WID_GO_FULLSCREEN_BUTTON, _fullscreen);
 
@@ -683,12 +712,13 @@ struct GameOptionsWindow : Window {
 
	 * @param data Information about the changed data. @see GameOptionsInvalidationData
 
	 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
 
	 */
 
	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);
 

	
 
#ifndef __APPLE__
 
		this->SetWidgetLoweredState(WID_GO_VIDEO_VSYNC_BUTTON, _video_vsync);
 
@@ -698,13 +728,13 @@ struct GameOptionsWindow : Window {
 
		this->SetWidgetLoweredState(WID_GO_GUI_SCALE_AUTO, _gui_scale_cfg == -1);
 
		this->SetWidgetLoweredState(WID_GO_GUI_SCALE_BEVEL_BUTTON, _settings_client.gui.scale_bevels);
 

	
 
		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());
 
		}
 

	
 
		missing_files = BaseMusic::GetUsedSet()->GetNumInvalid() == 0;
 
@@ -736,12 +766,26 @@ static const NWidgetPart _nested_game_op
 
					NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_GO_AUTOSAVE_DROPDOWN), SetMinimalSize(100, 12), SetDataTip(STR_JUST_STRING, STR_GAME_OPTIONS_AUTOSAVE_DROPDOWN_TOOLTIP), SetFill(1, 0),
 
				EndContainer(),
 

	
 
				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 */
 
			NWidget(NWID_VERTICAL), SetPadding(10), SetPIP(0, WidgetDimensions::unscaled.vsep_wide, 0),
 
				NWidget(WWT_FRAME, COLOUR_GREY), SetDataTip(STR_GAME_OPTIONS_GUI_SCALE_FRAME, STR_NULL),
 
					NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_normal, 0),
src/settings_type.h
Show inline comments
 
@@ -60,19 +60,26 @@ enum IndustryDensity {
 

	
 
	ID_CUSTOM,    ///< Custom number of industries.
 

	
 
	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.
 
	byte   competitor_intelligence;          ///< Unused value, used to load old savegames.
 

	
 
	byte   max_no_competitors;               ///< the number of competitors (AIs)
 
@@ -303,13 +310,14 @@ struct NetworkSettings {
 
	uint8       max_clients;                              ///< maximum amount of clients
 
	TimerGameCalendar::Year restart_game_year;            ///< year the server restarts
 
	uint8       min_active_clients;                       ///< minimum amount of active clients to unpause the game
 
	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. */
 
struct GameCreationSettings {
 
	uint32 generation_seed;                  ///< noise seed for world generation
 
	TimerGameCalendar::Year starting_year;   ///< starting date
src/table/settings/network_private_settings.ini
Show inline comments
 
@@ -5,20 +5,22 @@
 
;
 

	
 
; Network settings as stored in the private configuration file ("private.cfg").
 

	
 
[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]
 
};
 
[templates]
 
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");
 

	
 
[defaults]
 
flags    = SF_NONE
 
@@ -87,6 +89,15 @@ min      = URS_NO
 
max      = URS_ALLOW
 
full     = _use_relay_service
 
str      = STR_CONFIG_SETTING_USE_RELAY_SERVICE
 
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
 
@@ -363,14 +363,27 @@ static void Xunzip(byte **bufp, size_t *
 
	std::string_view sv_buf(buf, filesize);
 

	
 
	/* Check for the byte-order-mark, and skip it if needed. */
 
	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);
 
	int row = 0;
 
	auto next = p.find_first_of('\n');
 
	while (next != std::string_view::npos) {
 
@@ -403,13 +416,13 @@ std::optional<std::string> GetTextfile(T
 
{
 
	static const char * const prefixes[] = {
 
		"readme",
 
		"changelog",
 
		"license",
 
	};
 
	static_assert(lengthof(prefixes) == TFT_END);
 
	static_assert(lengthof(prefixes) == TFT_CONTENT_END);
 

	
 
	std::string_view prefix = prefixes[type];
 

	
 
	if (filename.empty()) return std::nullopt;
 

	
 
	auto slash = filename.find_last_of(PATHSEPCHAR);
src/textfile_gui.h
Show inline comments
 
@@ -39,12 +39,15 @@ struct TextfileWindow : public Window, M
 
	std::optional<std::string_view> NextString() override;
 
	bool Monospace() override;
 
	void SetFontNames(FontCacheSettings *settings, const char *font_name, const void *os_data) override;
 

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

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

	
 
private:
 
	struct Line {
 
		int top;               ///< Top scroll position.
 
		int bottom;            ///< Bottom scroll position.
 
		std::string_view text; ///< Pointer to text buffer.
 

	
src/textfile_type.h
Show inline comments
 
@@ -9,17 +9,19 @@
 

	
 
#ifndef TEXTFILE_TYPE_H
 
#define TEXTFILE_TYPE_H
 

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

	
 
#endif /* TEXTFILE_TYPE_H */
src/widget.cpp
Show inline comments
 
@@ -1169,13 +1169,13 @@ NWidgetCore::NWidgetCore(WidgetType tp, 
 
{
 
	this->colour = colour;
 
	this->index = -1;
 
	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;
 
}
 

	
 
/**
 
 * Set index of the nested widget in the widget array.
src/widgets/ai_widget.h
Show inline comments
 
@@ -26,10 +26,10 @@ enum AIConfigWidgets {
 
	WID_AIC_MOVE_UP,          ///< Move up button.
 
	WID_AIC_MOVE_DOWN,        ///< Move down button.
 
	WID_AIC_CHANGE,           ///< Select another AI button.
 
	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
 
@@ -17,12 +17,12 @@ enum GSConfigWidgets {
 
	WID_GSC_BACKGROUND,       ///< Window background.
 
	WID_GSC_GSLIST,           ///< List with current selected Game Script.
 
	WID_GSC_SETTINGS,         ///< Panel to draw the Game Script settings on
 
	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.
 
};
 

	
 
#endif /* WIDGETS_GS_WIDGET_H */
src/widgets/network_content_widget.h
Show inline comments
 
@@ -33,13 +33,13 @@ enum NetworkContentListWidgets {
 
	WID_NCL_MATRIX,         ///< Panel with list of content.
 
	WID_NCL_SCROLLBAR,      ///< Scrollbar of matrix.
 

	
 
	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.
 
	WID_NCL_CANCEL,         ///< 'Cancel' button.
 
	WID_NCL_DOWNLOAD,       ///< 'Download' button.
 

	
src/widgets/network_widget.h
Show inline comments
 
@@ -116,7 +116,17 @@ enum NetworkAskRelayWidgets {
 
	WID_NAR_TEXT,       ///< Text in the window.
 
	WID_NAR_NO,         ///< "No" button.
 
	WID_NAR_YES_ONCE,   ///< "Yes, once" button.
 
	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
 
@@ -44,13 +44,13 @@ enum NewGRFStateWidgets {
 
	WID_NS_AVAIL_LIST,        ///< List window of available NewGRFs.
 
	WID_NS_SCROLL2BAR,        ///< Scrollbar for available NewGRF list.
 
	WID_NS_NEWGRF_INFO_TITLE, ///< Title for Info on selected NewGRF.
 
	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.
 
	WID_NS_RESCAN_FILES,      ///< Rescan files (available NewGRFs).
 
	WID_NS_RESCAN_FILES2,     ///< Rescan files (active NewGRFs).
 
	WID_NS_CONTENT_DOWNLOAD,  ///< Open content download (available NewGRFs).
src/widgets/settings_widget.h
Show inline comments
 
@@ -25,29 +25,33 @@ enum GameOptionsWidgets {
 
	WID_GO_GUI_SCALE,              ///< GUI Scale slider.
 
	WID_GO_GUI_SCALE_AUTO,         ///< Autodetect GUI scale button.
 
	WID_GO_GUI_SCALE_BEVEL_BUTTON, ///< Toggle for chunky bevels.
 
	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. */
 
enum GameSettingsWidgets {
 
	WID_GS_FILTER,             ///< Text filter.
 
	WID_GS_OPTIONSPANEL,       ///< Panel widget containing the option lists.
src/window_type.h
Show inline comments
 
@@ -481,12 +481,18 @@ enum WindowClass {
 
	 * Network ask relay window; %Window numbers:
 
	 *   - 0 - #NetworkAskRelayWidgets
 
	 */
 
	WC_NETWORK_ASK_RELAY,
 

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

	
 
	/**
 
	 * Chatbox; %Window numbers:
 
	 *   - #DestType = #NetWorkChatWidgets
 
	 */
 
	WC_SEND_NETWORK_MSG,
 

	
 
	/**
0 comments (0 inline, 0 general)