/*
 * 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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
 */

/** @file settings_table.cpp The tables of all the settings as well as the implementation of most of their callbacks. */

#include "stdafx.h"
#include "settings_table.h"
#include "currency_type.h"
#include "screenshot.h"
#include "network/network.h"
#include "network/network_func.h"
#include "network/core/config.h"
#include "pathfinder/pathfinder_type.h"
#include "linkgraph/linkgraphschedule.h"
#include "genworld.h"
#include "train.h"
#include "news_func.h"
#include "window_func.h"
#include "company_func.h"
#include "timer/timer_game_calendar.h"
#if defined(WITH_FREETYPE) || defined(_WIN32) || defined(WITH_COCOA)
#define HAS_TRUETYPE_FONT
#include "fontcache.h"
#endif
#include "textbuf_gui.h"
#include "rail_gui.h"
#include "elrail_func.h"
#include "error.h"
#include "town.h"
#include "video/video_driver.hpp"
#include "sound/sound_driver.hpp"
#include "music/music_driver.hpp"
#include "blitter/factory.hpp"
#include "base_media_base.h"
#include "base_media_music.h"
#include "base_media_sounds.h"
#include "ai/ai_config.hpp"
#include "ai/ai.hpp"
#include "game/game_config.hpp"
#include "ship.h"
#include "smallmap_gui.h"
#include "roadveh.h"
#include "roadveh_cmd.h"
#include "vehicle_func.h"
#include "viewport_func.h"
#include "void_map.h"
#include "station_func.h"
#include "station_base.h"
#include "aircraft.h"
#include "aircraft_cmd.h"

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

#include "safeguards.h"

SettingTable _company_settings{ _company_settings_table };
SettingTable _currency_settings{ _currency_settings_table };
SettingTable _difficulty_settings{ _difficulty_settings_table };
SettingTable _multimedia_settings{ _multimedia_settings_table };
SettingTable _economy_settings{ _economy_settings_table };
SettingTable _game_settings{ _game_settings_table };
SettingTable _gui_settings{ _gui_settings_table };
SettingTable _linkgraph_settings{ _linkgraph_settings_table };
SettingTable _locale_settings{ _locale_settings_table };
SettingTable _misc_settings{ _misc_settings_table };
SettingTable _network_private_settings{ _network_private_settings_table };
SettingTable _network_secrets_settings{ _network_secrets_settings_table };
SettingTable _network_settings{ _network_settings_table };
SettingTable _news_display_settings{ _news_display_settings_table };
SettingTable _old_gameopt_settings{ _old_gameopt_settings_table };
SettingTable _pathfinding_settings{ _pathfinding_settings_table };
SettingTable _script_settings{ _script_settings_table };
SettingTable _window_settings{ _window_settings_table };
SettingTable _world_settings{ _world_settings_table };
#if defined(_WIN32) && !defined(DEDICATED)
SettingTable _win32_settings{ _win32_settings_table };
#endif /* _WIN32 */


/* Begin - Callback Functions for the various settings. */

/** Switch setting title depending on wallclock setting. @copydoc IntSettingDesc::GetTitleCallback */
static StringID SettingTitleWallclock(const IntSettingDesc &sd)
{
	return TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu) ? sd.str + 1 : sd.str;
}

/** Switch setting help depending on wallclock setting. @copydoc IntSettingDesc::GetHelpCallback */
static StringID SettingHelpWallclock(const IntSettingDesc &sd)
{
	return TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu) ? sd.str_help + 1 : sd.str_help;
}

/** Setting values for velocity unit localisation. @copydoc IntSettingDesc::GetValueParamsCallback */
static std::pair<StringParameter, StringParameter> SettingsValueVelocityUnit([[maybe_unused]] const IntSettingDesc &sd, int32_t value)
{
	StringID val;
	switch (value) {
		case 0: val = STR_CONFIG_SETTING_LOCALISATION_UNITS_VELOCITY_IMPERIAL; break;
		case 1: val = STR_CONFIG_SETTING_LOCALISATION_UNITS_VELOCITY_METRIC; break;
		case 2: val = STR_CONFIG_SETTING_LOCALISATION_UNITS_VELOCITY_SI; break;
		case 3: val = TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu) ? STR_CONFIG_SETTING_LOCALISATION_UNITS_VELOCITY_GAMEUNITS_SECS : STR_CONFIG_SETTING_LOCALISATION_UNITS_VELOCITY_GAMEUNITS_DAYS; break;
		case 4: val = STR_CONFIG_SETTING_LOCALISATION_UNITS_VELOCITY_KNOTS; break;
		default: NOT_REACHED();
	}
	return {val, {}};
}

/** A negative value has another string (the one after "strval"). @copydoc IntSettingDesc::GetValueParamsCallback */
static std::pair<StringParameter, StringParameter> SettingsValueAbsolute(const IntSettingDesc &sd, int32_t value)
{
	return {sd.str_val + ((value >= 0) ? 1 : 0), abs(value)};
}

/** Service Interval Settings Default Value displays the correct units or as a percentage. @copydoc IntSettingDesc::GetValueParamsCallback */
static std::pair<StringParameter, StringParameter> ServiceIntervalSettingsValueText(const IntSettingDesc &sd, int32_t value)
{
	VehicleDefaultSettings *vds;
	if (_game_mode == GameMode::Menu || !Company::IsValidID(_current_company)) {
		vds = &_settings_client.company.vehicle;
	} else {
		vds = &Company::Get(_current_company)->settings.vehicle;
	}

	StringID str;
	if (value == 0) {
		str = sd.str_val + 3;
	} else if (vds->servint_ispercent) {
		str = sd.str_val + 2;
	} else if (TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu)) {
		str = sd.str_val + 1;
	} else {
		str = sd.str_val;
	}
	return {str, value};
}

/** Reposition the main toolbar as the setting changed. */
static void v_PositionMainToolbar(int32_t)
{
	if (_game_mode != GameMode::Menu) PositionMainToolbar(nullptr);
}

/** Reposition the statusbar as the setting changed. */
static void v_PositionStatusbar(int32_t)
{
	if (_game_mode != GameMode::Menu) {
		PositionStatusbar(nullptr);
		PositionNewsMessage(nullptr);
		PositionNetworkChatWindow(nullptr);
	}
}

/**
 * Redraw the smallmap after a colour scheme change.
 */
static void RedrawSmallmap(int32_t)
{
	BuildLandLegend();
	BuildOwnerLegend();
	SetWindowClassesDirty(WindowClass::SmallMap);
}

/** Redraw linkgraph links after a colour scheme change. */
static void UpdateLinkgraphColours(int32_t)
{
	BuildLinkStatsLegend();
	MarkWholeScreenDirty();
}

static void StationSpreadChanged(int32_t)
{
	InvalidateWindowData(WindowClass::JoinStation, 0);
	InvalidateWindowData(WindowClass::BuildStation, 0);
}

static void UpdateConsists(int32_t)
{
	for (Train *t : Train::Iterate()) {
		/* Update the consist of all trains so the maximum speed is set correctly. */
		if (t->IsFrontEngine() || t->IsFreeWagon()) t->ConsistChanged(CCF_TRACK);
	}
	InvalidateWindowClassesData(WindowClass::BuildVehicle, 0);
}

/**
 * Check and update if needed all vehicle service intervals.
 * @param new_value Contains 0 if service intervals are in time (days or real-world minutes), otherwise intervals use percents.
 */
static void UpdateAllServiceInterval(int32_t new_value)
{
	bool update_vehicles;
	VehicleDefaultSettings *vds;
	if (_game_mode == GameMode::Menu || !Company::IsValidID(_current_company)) {
		vds = &_settings_client.company.vehicle;
		update_vehicles = false;
	} else {
		vds = &Company::Get(_current_company)->settings.vehicle;
		update_vehicles = true;
	}

	if (new_value != 0) {
		/* Service intervals are in percents. */
		vds->servint_trains   = DEF_SERVINT_PERCENT;
		vds->servint_roadveh  = DEF_SERVINT_PERCENT;
		vds->servint_aircraft = DEF_SERVINT_PERCENT;
		vds->servint_ships    = DEF_SERVINT_PERCENT;
	} else if (TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu)) {
		/* Service intervals are in minutes. */
		vds->servint_trains   = DEF_SERVINT_MINUTES_TRAINS;
		vds->servint_roadveh  = DEF_SERVINT_MINUTES_ROADVEH;
		vds->servint_aircraft = DEF_SERVINT_MINUTES_AIRCRAFT;
		vds->servint_ships    = DEF_SERVINT_MINUTES_SHIPS;
	} else {
		/* Service intervals are in days. */
		vds->servint_trains   = DEF_SERVINT_DAYS_TRAINS;
		vds->servint_roadveh  = DEF_SERVINT_DAYS_ROADVEH;
		vds->servint_aircraft = DEF_SERVINT_DAYS_AIRCRAFT;
		vds->servint_ships    = DEF_SERVINT_DAYS_SHIPS;
	}

	if (update_vehicles) {
		const Company *c = Company::Get(_current_company);
		for (Vehicle *v : Vehicle::Iterate()) {
			if (v->owner == _current_company && v->IsPrimaryVehicle() && !v->ServiceIntervalIsCustom()) {
				v->SetServiceInterval(CompanyServiceInterval(c, v->type));
				v->SetServiceIntervalIsPercent(new_value != 0);
			}
		}
	}

	SetWindowClassesDirty(WindowClass::VehicleDetails);
}

static bool CanUpdateServiceInterval(VehicleType, int32_t &new_value)
{
	VehicleDefaultSettings *vds;
	if (_game_mode == GameMode::Menu || !Company::IsValidID(_current_company)) {
		vds = &_settings_client.company.vehicle;
	} else {
		vds = &Company::Get(_current_company)->settings.vehicle;
	}

	/* Test if the interval is valid */
	int32_t interval = GetServiceIntervalClamped(new_value, vds->servint_ispercent);
	return new_value == 0 || interval == new_value;
}

static void UpdateServiceInterval(VehicleType type, int32_t new_value)
{
	if (_game_mode != GameMode::Menu && Company::IsValidID(_current_company)) {
		for (Vehicle *v : Vehicle::Iterate()) {
			if (v->owner == _current_company && v->type == type && v->IsPrimaryVehicle() && !v->ServiceIntervalIsCustom()) {
				v->SetServiceInterval(new_value);
			}
		}
	}

	SetWindowClassesDirty(WindowClass::VehicleDetails);
}

/**
 * Checks if the service intervals in the settings are specified as percentages and corrects the default value accordingly.
 * @param sd The current setting.
 * @param type The vehicle's type.
 * @return The appropriate service interval.
 */
static int32_t GetDefaultServiceInterval(const IntSettingDesc &sd, VehicleType type)
{
	VehicleDefaultSettings *vds;
	if (_game_mode == GameMode::Menu || !Company::IsValidID(_current_company)) {
		vds = &_settings_client.company.vehicle;
	} else {
		vds = &Company::Get(_current_company)->settings.vehicle;
	}

	if (vds->servint_ispercent) return DEF_SERVINT_PERCENT;

	if (TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu)) {
		switch (type) {
			case VehicleType::Train: return DEF_SERVINT_MINUTES_TRAINS;
			case VehicleType::Road: return DEF_SERVINT_MINUTES_ROADVEH;
			case VehicleType::Aircraft: return DEF_SERVINT_MINUTES_AIRCRAFT;
			case VehicleType::Ship: return DEF_SERVINT_MINUTES_SHIPS;
			default: NOT_REACHED();
		}
	}

	return sd.def;
}

static std::tuple<int32_t, uint32_t> GetServiceIntervalRange(const IntSettingDesc &)
{
	VehicleDefaultSettings *vds;
	if (_game_mode == GameMode::Menu || !Company::IsValidID(_current_company)) {
		vds = &_settings_client.company.vehicle;
	} else {
		vds = &Company::Get(_current_company)->settings.vehicle;
	}

	if (vds->servint_ispercent) return { MIN_SERVINT_PERCENT, MAX_SERVINT_PERCENT };

	if (TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu)) {
		return { MIN_SERVINT_MINUTES, MAX_SERVINT_MINUTES };
	}

	return { MIN_SERVINT_DAYS, MAX_SERVINT_DAYS };
}

static void TrainAccelerationModelChanged(int32_t)
{
	for (Train *t : Train::Iterate()) {
		if (t->IsFrontEngine()) {
			t->tcache.cached_max_curve_speed = t->GetCurveSpeedLimit();
			t->UpdateAcceleration();
		}
	}

	/* These windows show acceleration values only when realistic acceleration is on. They must be redrawn after a setting change. */
	SetWindowClassesDirty(WindowClass::EnginePreview);
	InvalidateWindowClassesData(WindowClass::BuildVehicle, 0);
	SetWindowClassesDirty(WindowClass::VehicleDetails);
}

/**
 * This function updates the train acceleration cache after a steepness change.
 */
static void TrainSlopeSteepnessChanged(int32_t)
{
	for (Train *t : Train::Iterate()) {
		if (t->IsFrontEngine()) t->CargoChanged();
	}
}

/**
 * This function updates realistic acceleration caches when the setting "Road vehicle acceleration model" is set.
 */
static void RoadVehAccelerationModelChanged(int32_t)
{
	if (_settings_game.vehicle.roadveh_acceleration_model != AccelerationModel::Original) {
		for (RoadVehicle *rv : RoadVehicle::Iterate()) {
			if (rv->IsFrontEngine()) {
				rv->CargoChanged();
			}
		}
	}

	/* These windows show acceleration values only when realistic acceleration is on. They must be redrawn after a setting change. */
	SetWindowClassesDirty(WindowClass::EnginePreview);
	InvalidateWindowClassesData(WindowClass::BuildVehicle, 0);
	SetWindowClassesDirty(WindowClass::VehicleDetails);
}

/**
 * This function updates the road vehicle acceleration cache after a steepness change.
 */
static void RoadVehSlopeSteepnessChanged(int32_t)
{
	for (RoadVehicle *rv : RoadVehicle::Iterate()) {
		if (rv->IsFrontEngine()) rv->CargoChanged();
	}
}

/**
 * This function updates the aircraft cache when the aircraft range setting is changed.
 */
static void AircraftRangeChanged(int32_t)
{
	for (Aircraft *v : Aircraft::Iterate()) {
		v->acache.cached_max_range = Engine::Get(v->engine_type)->GetRange();
		v->acache.cached_max_range_sqr = v->acache.cached_max_range * v->acache.cached_max_range;

		/* Reset destination is too far state */
		if (v->flags.Test(VehicleAirFlag::DestinationTooFar)) {
			v->flags.Reset(VehicleAirFlag::DestinationTooFar);
			SetWindowWidgetDirty(WindowClass::VehicleView, v->index, WID_VV_START_STOP);
			DeleteVehicleNews(v->index, AdviceType::AircraftDestinationTooFar);
		}
	}
}

static void TownFoundingChanged(int32_t)
{
	if (_game_mode != GameMode::Editor && _settings_game.economy.found_town == TownFounding::Forbidden) {
		CloseWindowById(WindowClass::FoundTown, 0);
	} else {
		InvalidateWindowData(WindowClass::FoundTown, 0);
	}
}

static void ZoomMinMaxChanged(int32_t)
{
	ConstrainAllViewportsZoom();
	GfxClearSpriteCache();
	InvalidateWindowClassesData(WindowClass::SpriteAligner);
	if (AdjustGUIZoom(false)) {
		ReInitAllWindows(true);
	}
}

static void SpriteZoomMinChanged(int32_t)
{
	GfxClearSpriteCache();
	/* Force all sprites to redraw at the new chosen zoom level */
	MarkWholeScreenDirty();
}

/**
 * Update any possible saveload window and delete any newgrf dialogue as
 * its widget parts might change. Reinit all windows as it allows access to the
 * newgrf debug button.
 */
static void InvalidateNewGRFChangeWindows(int32_t)
{
	InvalidateWindowClassesData(WindowClass::SaveLoad);
	CloseWindowByClass(WindowClass::GameOptions);
	ReInitAllWindows(false);
}

static void InvalidateCompanyLiveryWindow(int32_t)
{
	InvalidateWindowClassesData(WindowClass::CompanyLivery, -1);
	ResetVehicleColourMap();
}

static void DifficultyNoiseChange(int32_t)
{
	if (_game_mode == GameMode::Normal) {
		UpdateAirportsNoise();
		if (_settings_game.economy.station_noise_level) {
			InvalidateWindowClassesData(WindowClass::TownView, 0);
		}
	}
}

static void MaxNoAIsChange(int32_t)
{
	if (GetGameSettings().difficulty.max_no_competitors != 0 &&
			AI::GetInfoList()->empty() &&
			(!_networking || _network_server)) {
		ShowErrorMessage(GetEncodedString(STR_WARNING_NO_SUITABLE_AI), {}, WarningLevel::Critical);
	}

	InvalidateWindowClassesData(WindowClass::GameOptions, 0);
}

/**
 * Check whether the road side may be changed.
 * @return true if the road side may be changed.
 */
static bool CheckRoadSide(int32_t &)
{
	return _game_mode == GameMode::Menu || !RoadVehiclesAreBuilt();
}

/**
 * Conversion callback for _gameopt_settings_game.landscape
 * It converts (or try) between old values and the new ones,
 * without losing initial setting of the user
 * @param value that was read from config file
 * @return the "hopefully" converted value
 */
static std::optional<uint32_t> ConvertLandscape(std::string_view value)
{
	/* try with the old values */
	static constexpr std::initializer_list<std::string_view> _old_landscape_values{"normal"sv, "hilly"sv, "desert"sv, "candy"sv};
	return OneOfManySettingDesc::ParseSingleValue(value, _old_landscape_values);
}

static bool CheckFreeformEdges(int32_t &new_value)
{
	if (_game_mode == GameMode::Menu) return true;
	if (new_value != 0) {
		for (Ship *s : Ship::Iterate()) {
			/* Check if there is a ship on the northern border. */
			if (TileX(s->tile) == 0 || TileY(s->tile) == 0) {
				ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_EDGES_NOT_EMPTY), {}, WarningLevel::Error);
				return false;
			}
		}
		for (const BaseStation *st : BaseStation::Iterate()) {
			/* Check if there is a non-deleted buoy on the northern border. */
			if (st->IsInUse() && (TileX(st->xy) == 0 || TileY(st->xy) == 0)) {
				ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_EDGES_NOT_EMPTY), {}, WarningLevel::Error);
				return false;
			}
		}
	} else {
		for (uint i = 0; i < Map::MaxX(); i++) {
			if (TileHeight(TileXY(i, 1)) != 0) {
				ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_EDGES_NOT_WATER), {}, WarningLevel::Error);
				return false;
			}
		}
		for (uint i = 1; i < Map::MaxX(); i++) {
			if (!IsTileType(TileXY(i, Map::MaxY() - 1), TileType::Water) || TileHeight(TileXY(1, Map::MaxY())) != 0) {
				ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_EDGES_NOT_WATER), {}, WarningLevel::Error);
				return false;
			}
		}
		for (uint i = 0; i < Map::MaxY(); i++) {
			if (TileHeight(TileXY(1, i)) != 0) {
				ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_EDGES_NOT_WATER), {}, WarningLevel::Error);
				return false;
			}
		}
		for (uint i = 1; i < Map::MaxY(); i++) {
			if (!IsTileType(TileXY(Map::MaxX() - 1, i), TileType::Water) || TileHeight(TileXY(Map::MaxX(), i)) != 0) {
				ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_EDGES_NOT_WATER), {}, WarningLevel::Error);
				return false;
			}
		}
	}
	return true;
}

static void UpdateFreeformEdges(int32_t new_value)
{
	if (_game_mode == GameMode::Menu) return;

	if (new_value != 0) {
		for (uint x = 0; x < Map::SizeX(); x++) MakeVoid(TileXY(x, 0));
		for (uint y = 0; y < Map::SizeY(); y++) MakeVoid(TileXY(0, y));
	} else {
		/* Make tiles at the border water again. */
		for (uint i = 0; i < Map::MaxX(); i++) {
			SetTileHeight(TileXY(i, 0), 0);
			SetTileType(TileXY(i, 0), TileType::Water);
		}
		for (uint i = 0; i < Map::MaxY(); i++) {
			SetTileHeight(TileXY(0, i), 0);
			SetTileType(TileXY(0, i), TileType::Water);
		}
	}
	MarkWholeScreenDirty();
}

/**
 * Changing the setting "allow multiple NewGRF sets" is not allowed
 * if there are vehicles.
 * @copydoc IntSettingDesc::PreChangeCheck
 */
static bool CheckDynamicEngines([[maybe_unused]] int32_t &value)
{
	if (_game_mode == GameMode::Menu) return true;

	if (!EngineOverrideManager::ResetToCurrentNewGRFConfig()) {
		ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_DYNAMIC_ENGINES_EXISTING_VEHICLES), {}, WarningLevel::Error);
		return false;
	}

	return true;
}

static bool CheckMaxHeightLevel(int32_t &new_value)
{
	if (_game_mode == GameMode::Normal) return false;
	if (_game_mode != GameMode::Editor) return true;

	/* Check if at least one mountain on the map is higher than the new value.
	 * If yes, disallow the change. */
	for (const auto t : Map::Iterate()) {
		if ((int32_t)TileHeight(t) > new_value) {
			ShowErrorMessage(GetEncodedString(STR_CONFIG_SETTING_TOO_HIGH_MOUNTAIN), {}, WarningLevel::Error);
			/* Return old, unchanged value */
			return false;
		}
	}

	return true;
}

static void StationCatchmentChanged(int32_t)
{
	Station::RecomputeCatchmentForAll();
	MarkWholeScreenDirty();
}

static void MaxVehiclesChanged(int32_t)
{
	InvalidateWindowClassesData(WindowClass::BuildToolbar);
	MarkWholeScreenDirty();
}

/**
 * Replace a passwords that are a literal asterisk with an empty string.
 * @param newval The new string value for this password field.
 * @return Always true.
 */
static bool ReplaceAsteriskWithEmptyPassword(std::string &newval)
{
	if (newval == "*") newval.clear();
	return true;
}

/** Update the game info, and send it to the clients when we are running as a server. */
static void UpdateClientConfigValues()
{
	NetworkServerUpdateGameInfo();

	InvalidateWindowData(WindowClass::NetworkClientList, 0);

	if (_network_server) {
		NetworkServerSendConfigUpdate();
	}
}

/**
 * Callback for when the player changes the timekeeping units.
 */
static void ChangeTimekeepingUnits()
{
	/* If service intervals are in time units (calendar days or real-world minutes), reset them to the correct defaults. */
	if (!_settings_client.company.vehicle.servint_ispercent) {
		UpdateAllServiceInterval(0);
	}

	/* If we are using calendar timekeeping, "minutes per year" must be default. */
	if (!TimerGameEconomy::UsingWallclockUnits(true)) {
		_settings_newgame.economy.minutes_per_calendar_year = CalendarTime::DEF_MINUTES_PER_YEAR;
	}

	InvalidateWindowClassesData(WindowClass::GameOptions, 0);

	/* It is possible to change these units in Scenario Editor. We must set the economy date appropriately. */
	if (_game_mode == GameMode::Editor) {
		TimerGameEconomy::Date new_economy_date;
		TimerGameEconomy::DateFract new_economy_date_fract;

		if (TimerGameEconomy::UsingWallclockUnits()) {
			/* If the new mode is wallclock units, set the economy year back to 1. */
			new_economy_date = TimerGameEconomy::ConvertYMDToDate(TimerGameEconomy::Year{1}, 0, 1);
			new_economy_date_fract = 0;
		} else {
			/* If the new mode is calendar units, sync the economy year with the calendar year. */
			new_economy_date = TimerGameEconomy::Date{TimerGameCalendar::date.base()};
			new_economy_date_fract = TimerGameCalendar::date_fract;
		}

		/* If you open a savegame as a scenario, there may already be link graphs and/or vehicles. These use economy date. */
		LinkGraphSchedule::instance.ShiftDates(new_economy_date - TimerGameEconomy::date);
		for (auto v : Vehicle::Iterate()) v->ShiftDates(new_economy_date - TimerGameEconomy::date);

		/* Only change the date after changing cached values above. */
		TimerGameEconomy::SetDate(new_economy_date, new_economy_date_fract);
	}
}

/**
 * Callback after the player changes the minutes per year.
 * @param new_value The intended new value of the setting, used for clamping.
 */
static void ChangeMinutesPerYear(int32_t new_value)
{
	/* We don't allow setting Minutes Per Year below default, unless it's to 0 for frozen calendar time. */
	if (new_value < CalendarTime::DEF_MINUTES_PER_YEAR) {
		int clamped;

		/* If the new value is 1, we're probably at 0 and trying to increase the value, so we should jump up to default. */
		if (new_value == 1) {
			clamped = CalendarTime::DEF_MINUTES_PER_YEAR;
		} else {
			clamped = CalendarTime::FROZEN_MINUTES_PER_YEAR;
		}

		/* Override the setting with the clamped value. */
		if (_game_mode == GameMode::Menu) {
			_settings_newgame.economy.minutes_per_calendar_year = clamped;
		} else {
			_settings_game.economy.minutes_per_calendar_year = clamped;
		}
	}

	/* If the setting value is not the default, force the game to use wallclock timekeeping units.
	 * This can only happen in the menu, since the pre_cb ensures this setting can only be changed there, or if we're already using wallclock units.
	 */
	if (_game_mode == GameMode::Menu && (_settings_newgame.economy.minutes_per_calendar_year != CalendarTime::DEF_MINUTES_PER_YEAR)) {
		if (_settings_newgame.economy.timekeeping_units != TimekeepingUnits::Wallclock) {
			_settings_newgame.economy.timekeeping_units = TimekeepingUnits::Wallclock;
			ChangeTimekeepingUnits();
		}
	}
}

/* Get the valid range of the "minutes per calendar year" setting. */
static std::tuple<int32_t, uint32_t> GetMinutesPerYearRange(const IntSettingDesc &)
{
	/* Allow a non-default value only if using Wallclock timekeeping units. */
	if (TimerGameEconomy::UsingWallclockUnits(_game_mode == GameMode::Menu)) return { CalendarTime::FROZEN_MINUTES_PER_YEAR, CalendarTime::MAX_MINUTES_PER_YEAR };

	return { CalendarTime::DEF_MINUTES_PER_YEAR, CalendarTime::DEF_MINUTES_PER_YEAR };
}

/**
 * Pre-callback check when trying to change the timetable mode. This is locked to Seconds when using wallclock units.
 * @return True if we allow changing the timetable mode.
 */
static bool CanChangeTimetableMode(int32_t &)
{
	return !TimerGameEconomy::UsingWallclockUnits();
}

/* End - Callback Functions */
