/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
*/
/** @file console_cmds.cpp Implementation of the console hooks. */
#include "stdafx.h"
#include "console_internal.h"
#include "debug.h"
#include "engine_func.h"
#include "landscape.h"
#include "saveload/saveload.h"
#include "network/core/network_game_info.h"
#include "network/network.h"
#include "network/network_func.h"
#include "network/network_base.h"
#include "network/network_admin.h"
#include "network/network_client.h"
#include "command_func.h"
#include "settings_func.h"
#include "fios.h"
#include "fileio_func.h"
#include "fontcache.h"
#include "screenshot.h"
#include "genworld.h"
#include "strings_func.h"
#include "viewport_func.h"
#include "window_func.h"
#include "timer/timer_game_calendar.h"
#include "company_func.h"
#include "gamelog.h"
#include "ai/ai.hpp"
#include "ai/ai_config.hpp"
#include "newgrf.h"
#include "newgrf_profiling.h"
#include "console_func.h"
#include "engine_base.h"
#include "road.h"
#include "rail.h"
#include "game/game.hpp"
#include "table/strings.h"
#include "3rdparty/fmt/chrono.h"
#include "company_cmd.h"
#include "misc_cmd.h"
#include
#include "safeguards.h"
/* scriptfile handling */
static uint _script_current_depth; ///< Depth of scripts running (used to abort execution when #ConReturn is encountered).
/** File list storage for the console, for caching the last 'ls' command. */
class ConsoleFileList : public FileList {
public:
ConsoleFileList() : FileList()
{
this->file_list_valid = false;
}
/** Declare the file storage cache as being invalid, also clears all stored files. */
void InvalidateFileList()
{
this->clear();
this->file_list_valid = false;
}
/**
* (Re-)validate the file storage cache. Only makes a change if the storage was invalid, or if \a force_reload.
* @param force_reload Always reload the file storage cache.
*/
void ValidateFileList(bool force_reload = false)
{
if (force_reload || !this->file_list_valid) {
this->BuildFileList(FT_SAVEGAME, SLO_LOAD);
this->file_list_valid = true;
}
}
bool file_list_valid; ///< If set, the file list is valid.
};
static ConsoleFileList _console_file_list; ///< File storage cache for the console.
/* console command defines */
#define DEF_CONSOLE_CMD(function) static bool function(byte argc, char *argv[])
#define DEF_CONSOLE_HOOK(function) static ConsoleHookResult function(bool echo)
/****************
* command hooks
****************/
/**
* Check network availability and inform in console about failure of detection.
* @return Network availability.
*/
static inline bool NetworkAvailable(bool echo)
{
if (!_network_available) {
if (echo) IConsolePrint(CC_ERROR, "You cannot use this command because there is no network available.");
return false;
}
return true;
}
/**
* Check whether we are a server.
* @return Are we a server? True when yes, false otherwise.
*/
DEF_CONSOLE_HOOK(ConHookServerOnly)
{
if (!NetworkAvailable(echo)) return CHR_DISALLOW;
if (!_network_server) {
if (echo) IConsolePrint(CC_ERROR, "This command is only available to a network server.");
return CHR_DISALLOW;
}
return CHR_ALLOW;
}
/**
* Check whether we are a client in a network game.
* @return Are we a client in a network game? True when yes, false otherwise.
*/
DEF_CONSOLE_HOOK(ConHookClientOnly)
{
if (!NetworkAvailable(echo)) return CHR_DISALLOW;
if (_network_server) {
if (echo) IConsolePrint(CC_ERROR, "This command is not available to a network server.");
return CHR_DISALLOW;
}
return CHR_ALLOW;
}
/**
* Check whether we are in a multiplayer game.
* @return True when we are client or server in a network game.
*/
DEF_CONSOLE_HOOK(ConHookNeedNetwork)
{
if (!NetworkAvailable(echo)) return CHR_DISALLOW;
if (!_networking || (!_network_server && !MyClient::IsConnected())) {
if (echo) IConsolePrint(CC_ERROR, "Not connected. This command is only available in multiplayer.");
return CHR_DISALLOW;
}
return CHR_ALLOW;
}
/**
* Check whether we are in singleplayer mode.
* @return True when no network is active.
*/
DEF_CONSOLE_HOOK(ConHookNoNetwork)
{
if (_networking) {
if (echo) IConsolePrint(CC_ERROR, "This command is forbidden in multiplayer.");
return CHR_DISALLOW;
}
return CHR_ALLOW;
}
/**
* Check if are either in singleplayer or a server.
* @return True iff we are either in singleplayer or a server.
*/
DEF_CONSOLE_HOOK(ConHookServerOrNoNetwork)
{
if (_networking && !_network_server) {
if (echo) IConsolePrint(CC_ERROR, "This command is only available to a network server.");
return CHR_DISALLOW;
}
return CHR_ALLOW;
}
DEF_CONSOLE_HOOK(ConHookNewGRFDeveloperTool)
{
if (_settings_client.gui.newgrf_developer_tools) {
if (_game_mode == GM_MENU) {
if (echo) IConsolePrint(CC_ERROR, "This command is only available in-game and in the editor.");
return CHR_DISALLOW;
}
return ConHookNoNetwork(echo);
}
return CHR_HIDE;
}
/**
* Reset status of all engines.
* @return Will always succeed.
*/
DEF_CONSOLE_CMD(ConResetEngines)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Reset status data of all engines. This might solve some issues with 'lost' engines. Usage: 'resetengines'.");
return true;
}
StartupEngines();
return true;
}
/**
* Reset status of the engine pool.
* @return Will always return true.
* @note Resetting the pool only succeeds when there are no vehicles ingame.
*/
DEF_CONSOLE_CMD(ConResetEnginePool)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Reset NewGRF allocations of engine slots. This will remove invalid engine definitions, and might make default engines available again.");
return true;
}
if (_game_mode == GM_MENU) {
IConsolePrint(CC_ERROR, "This command is only available in-game and in the editor.");
return true;
}
if (!EngineOverrideManager::ResetToCurrentNewGRFConfig()) {
IConsolePrint(CC_ERROR, "This can only be done when there are no vehicles in the game.");
return true;
}
return true;
}
#ifdef _DEBUG
/**
* Reset a tile to bare land in debug mode.
* param tile number.
* @return True when the tile is reset or the help on usage was printed (0 or two parameters).
*/
DEF_CONSOLE_CMD(ConResetTile)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Reset a tile to bare land. Usage: 'resettile '.");
IConsolePrint(CC_HELP, "Tile can be either decimal (34161) or hexadecimal (0x4a5B).");
return true;
}
if (argc == 2) {
uint32_t result;
if (GetArgumentInteger(&result, argv[1])) {
DoClearSquare((TileIndex)result);
return true;
}
}
return false;
}
#endif /* _DEBUG */
/**
* Zoom map to given level.
* param level As defined by ZoomLevel and as limited by zoom_min/zoom_max from GUISettings.
* @return True when either console help was shown or a proper amount of parameters given.
*/
DEF_CONSOLE_CMD(ConZoomToLevel)
{
switch (argc) {
case 0:
IConsolePrint(CC_HELP, "Set the current zoom level of the main viewport.");
IConsolePrint(CC_HELP, "Usage: 'zoomto '.");
IConsolePrint(
CC_HELP,
ZOOM_LVL_MIN < _settings_client.gui.zoom_min ?
"The lowest zoom-in level allowed by current client settings is {}." :
"The lowest supported zoom-in level is {}.",
std::max(ZOOM_LVL_MIN, _settings_client.gui.zoom_min)
);
IConsolePrint(
CC_HELP,
_settings_client.gui.zoom_max < ZOOM_LVL_MAX ?
"The highest zoom-out level allowed by current client settings is {}." :
"The highest supported zoom-out level is {}.",
std::min(_settings_client.gui.zoom_max, ZOOM_LVL_MAX)
);
return true;
case 2: {
uint32_t level;
if (GetArgumentInteger(&level, argv[1])) {
/* In case ZOOM_LVL_MIN is more than 0, the next if statement needs to be amended.
* A simple check for less than ZOOM_LVL_MIN does not work here because we are
* reading an unsigned integer from the console, so just check for a '-' char. */
static_assert(ZOOM_LVL_MIN == 0);
if (argv[1][0] == '-') {
IConsolePrint(CC_ERROR, "Zoom-in levels below {} are not supported.", ZOOM_LVL_MIN);
} else if (level < _settings_client.gui.zoom_min) {
IConsolePrint(CC_ERROR, "Current client settings do not allow zooming in below level {}.", _settings_client.gui.zoom_min);
} else if (level > ZOOM_LVL_MAX) {
IConsolePrint(CC_ERROR, "Zoom-in levels above {} are not supported.", ZOOM_LVL_MAX);
} else if (level > _settings_client.gui.zoom_max) {
IConsolePrint(CC_ERROR, "Current client settings do not allow zooming out beyond level {}.", _settings_client.gui.zoom_max);
} else {
Window *w = GetMainWindow();
Viewport *vp = w->viewport;
while (vp->zoom > level) DoZoomInOutWindow(ZOOM_IN, w);
while (vp->zoom < level) DoZoomInOutWindow(ZOOM_OUT, w);
}
return true;
}
break;
}
}
return false;
}
/**
* Scroll to a tile on the map.
* param x tile number or tile x coordinate.
* param y optional y coordinate.
* @note When only one argument is given it is interpreted as the tile number.
* When two arguments are given, they are interpreted as the tile's x
* and y coordinates.
* @return True when either console help was shown or a proper amount of parameters given.
*/
DEF_CONSOLE_CMD(ConScrollToTile)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Center the screen on a given tile.");
IConsolePrint(CC_HELP, "Usage: 'scrollto [instant] ' or 'scrollto [instant] '.");
IConsolePrint(CC_HELP, "Numbers can be either decimal (34161) or hexadecimal (0x4a5B).");
IConsolePrint(CC_HELP, "'instant' will immediately move and redraw viewport without smooth scrolling.");
return true;
}
if (argc < 2) return false;
uint32_t arg_index = 1;
bool instant = false;
if (strcmp(argv[arg_index], "instant") == 0) {
++arg_index;
instant = true;
}
switch (argc - arg_index) {
case 1: {
uint32_t result;
if (GetArgumentInteger(&result, argv[arg_index])) {
if (result >= Map::Size()) {
IConsolePrint(CC_ERROR, "Tile does not exist.");
return true;
}
ScrollMainWindowToTile((TileIndex)result, instant);
return true;
}
break;
}
case 2: {
uint32_t x, y;
if (GetArgumentInteger(&x, argv[arg_index]) && GetArgumentInteger(&y, argv[arg_index + 1])) {
if (x >= Map::SizeX() || y >= Map::SizeY()) {
IConsolePrint(CC_ERROR, "Tile does not exist.");
return true;
}
ScrollMainWindowToTile(TileXY(x, y), instant);
return true;
}
break;
}
}
return false;
}
/**
* Save the map to a file.
* param filename the filename to save the map to.
* @return True when help was displayed or the file attempted to be saved.
*/
DEF_CONSOLE_CMD(ConSave)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Save the current game. Usage: 'save '.");
return true;
}
if (argc == 2) {
std::string filename = argv[1];
filename += ".sav";
IConsolePrint(CC_DEFAULT, "Saving map...");
if (SaveOrLoad(filename, SLO_SAVE, DFT_GAME_FILE, SAVE_DIR) != SL_OK) {
IConsolePrint(CC_ERROR, "Saving map failed.");
} else {
IConsolePrint(CC_INFO, "Map successfully saved to '{}'.", filename);
}
return true;
}
return false;
}
/**
* Explicitly save the configuration.
* @return True.
*/
DEF_CONSOLE_CMD(ConSaveConfig)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Saves the configuration for new games to the configuration file, typically 'openttd.cfg'.");
IConsolePrint(CC_HELP, "It does not save the configuration of the current game to the configuration file.");
return true;
}
SaveToConfig();
IConsolePrint(CC_DEFAULT, "Saved config.");
return true;
}
DEF_CONSOLE_CMD(ConLoad)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Load a game by name or index. Usage: 'load '.");
return true;
}
if (argc != 2) return false;
const char *file = argv[1];
_console_file_list.ValidateFileList();
const FiosItem *item = _console_file_list.FindItem(file);
if (item != nullptr) {
if (GetAbstractFileType(item->type) == FT_SAVEGAME) {
_switch_mode = SM_LOAD_GAME;
_file_to_saveload.Set(*item);
} else {
IConsolePrint(CC_ERROR, "'{}' is not a savegame.", file);
}
} else {
IConsolePrint(CC_ERROR, "'{}' cannot be found.", file);
}
return true;
}
DEF_CONSOLE_CMD(ConRemove)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Remove a savegame by name or index. Usage: 'rm '.");
return true;
}
if (argc != 2) return false;
const char *file = argv[1];
_console_file_list.ValidateFileList();
const FiosItem *item = _console_file_list.FindItem(file);
if (item != nullptr) {
if (unlink(item->name.c_str()) != 0) {
IConsolePrint(CC_ERROR, "Failed to delete '{}'.", item->name);
}
} else {
IConsolePrint(CC_ERROR, "'{}' could not be found.", file);
}
_console_file_list.InvalidateFileList();
return true;
}
/* List all the files in the current dir via console */
DEF_CONSOLE_CMD(ConListFiles)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "List all loadable savegames and directories in the current dir via console. Usage: 'ls | dir'.");
return true;
}
_console_file_list.ValidateFileList(true);
for (uint i = 0; i < _console_file_list.size(); i++) {
IConsolePrint(CC_DEFAULT, "{}) {}", i, _console_file_list[i].title);
}
return true;
}
/* Change the dir via console */
DEF_CONSOLE_CMD(ConChangeDirectory)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Change the dir via console. Usage: 'cd '.");
return true;
}
if (argc != 2) return false;
const char *file = argv[1];
_console_file_list.ValidateFileList(true);
const FiosItem *item = _console_file_list.FindItem(file);
if (item != nullptr) {
switch (item->type) {
case FIOS_TYPE_DIR: case FIOS_TYPE_DRIVE: case FIOS_TYPE_PARENT:
FiosBrowseTo(item);
break;
default: IConsolePrint(CC_ERROR, "{}: Not a directory.", file);
}
} else {
IConsolePrint(CC_ERROR, "{}: No such file or directory.", file);
}
_console_file_list.InvalidateFileList();
return true;
}
DEF_CONSOLE_CMD(ConPrintWorkingDirectory)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Print out the current working directory. Usage: 'pwd'.");
return true;
}
/* XXX - Workaround for broken file handling */
_console_file_list.ValidateFileList(true);
_console_file_list.InvalidateFileList();
IConsolePrint(CC_DEFAULT, FiosGetCurrentPath());
return true;
}
DEF_CONSOLE_CMD(ConClearBuffer)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Clear the console buffer. Usage: 'clear'.");
return true;
}
IConsoleClearBuffer();
SetWindowDirty(WC_CONSOLE, 0);
return true;
}
/**********************************
* Network Core Console Commands
**********************************/
static bool ConKickOrBan(const char *argv, bool ban, const std::string &reason)
{
uint n;
if (strchr(argv, '.') == nullptr && strchr(argv, ':') == nullptr) { // banning with ID
ClientID client_id = (ClientID)atoi(argv);
/* Don't kill the server, or the client doing the rcon. The latter can't be kicked because
* kicking frees closes and subsequently free the connection related instances, which we
* would be reading from and writing to after returning. So we would read or write data
* from freed memory up till the segfault triggers. */
if (client_id == CLIENT_ID_SERVER || client_id == _redirect_console_to_client) {
IConsolePrint(CC_ERROR, "You can not {} yourself!", ban ? "ban" : "kick");
return true;
}
NetworkClientInfo *ci = NetworkClientInfo::GetByClientID(client_id);
if (ci == nullptr) {
IConsolePrint(CC_ERROR, "Invalid client ID.");
return true;
}
if (!ban) {
/* Kick only this client, not all clients with that IP */
NetworkServerKickClient(client_id, reason);
return true;
}
/* When banning, kick+ban all clients with that IP */
n = NetworkServerKickOrBanIP(client_id, ban, reason);
} else {
n = NetworkServerKickOrBanIP(argv, ban, reason);
}
if (n == 0) {
IConsolePrint(CC_DEFAULT, ban ? "Client not online, address added to banlist." : "Client not found.");
} else {
IConsolePrint(CC_DEFAULT, "{}ed {} client(s).", ban ? "Bann" : "Kick", n);
}
return true;
}
DEF_CONSOLE_CMD(ConKick)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Kick a client from a network game. Usage: 'kick []'.");
IConsolePrint(CC_HELP, "For client-id's, see the command 'clients'.");
return true;
}
if (argc != 2 && argc != 3) return false;
/* No reason supplied for kicking */
if (argc == 2) return ConKickOrBan(argv[1], false, {});
/* Reason for kicking supplied */
size_t kick_message_length = strlen(argv[2]);
if (kick_message_length >= 255) {
IConsolePrint(CC_ERROR, "Maximum kick message length is 254 characters. You entered {} characters.", kick_message_length);
return false;
} else {
return ConKickOrBan(argv[1], false, argv[2]);
}
}
DEF_CONSOLE_CMD(ConBan)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Ban a client from a network game. Usage: 'ban []'.");
IConsolePrint(CC_HELP, "For client-id's, see the command 'clients'.");
IConsolePrint(CC_HELP, "If the client is no longer online, you can still ban their IP.");
return true;
}
if (argc != 2 && argc != 3) return false;
/* No reason supplied for kicking */
if (argc == 2) return ConKickOrBan(argv[1], true, {});
/* Reason for kicking supplied */
size_t kick_message_length = strlen(argv[2]);
if (kick_message_length >= 255) {
IConsolePrint(CC_ERROR, "Maximum kick message length is 254 characters. You entered {} characters.", kick_message_length);
return false;
} else {
return ConKickOrBan(argv[1], true, argv[2]);
}
}
DEF_CONSOLE_CMD(ConUnBan)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Unban a client from a network game. Usage: 'unban '.");
IConsolePrint(CC_HELP, "For a list of banned IP's, see the command 'banlist'.");
return true;
}
if (argc != 2) return false;
/* Try by IP. */
uint index;
for (index = 0; index < _network_ban_list.size(); index++) {
if (_network_ban_list[index] == argv[1]) break;
}
/* Try by index. */
if (index >= _network_ban_list.size()) {
index = atoi(argv[1]) - 1U; // let it wrap
}
if (index < _network_ban_list.size()) {
IConsolePrint(CC_DEFAULT, "Unbanned {}.", _network_ban_list[index]);
_network_ban_list.erase(_network_ban_list.begin() + index);
} else {
IConsolePrint(CC_DEFAULT, "Invalid list index or IP not in ban-list.");
IConsolePrint(CC_DEFAULT, "For a list of banned IP's, see the command 'banlist'.");
}
return true;
}
DEF_CONSOLE_CMD(ConBanList)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "List the IP's of banned clients: Usage 'banlist'.");
return true;
}
IConsolePrint(CC_DEFAULT, "Banlist:");
uint i = 1;
for (const auto &entry : _network_ban_list) {
IConsolePrint(CC_DEFAULT, " {}) {}", i, entry);
i++;
}
return true;
}
DEF_CONSOLE_CMD(ConPauseGame)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Pause a network game. Usage: 'pause'.");
return true;
}
if (_game_mode == GM_MENU) {
IConsolePrint(CC_ERROR, "This command is only available in-game and in the editor.");
return true;
}
if ((_pause_mode & PM_PAUSED_NORMAL) == PM_UNPAUSED) {
Command::Post(PM_PAUSED_NORMAL, true);
if (!_networking) IConsolePrint(CC_DEFAULT, "Game paused.");
} else {
IConsolePrint(CC_DEFAULT, "Game is already paused.");
}
return true;
}
DEF_CONSOLE_CMD(ConUnpauseGame)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Unpause a network game. Usage: 'unpause'.");
return true;
}
if (_game_mode == GM_MENU) {
IConsolePrint(CC_ERROR, "This command is only available in-game and in the editor.");
return true;
}
if ((_pause_mode & PM_PAUSED_NORMAL) != PM_UNPAUSED) {
Command::Post(PM_PAUSED_NORMAL, false);
if (!_networking) IConsolePrint(CC_DEFAULT, "Game unpaused.");
} else if ((_pause_mode & PM_PAUSED_ERROR) != PM_UNPAUSED) {
IConsolePrint(CC_DEFAULT, "Game is in error state and cannot be unpaused via console.");
} else if (_pause_mode != PM_UNPAUSED) {
IConsolePrint(CC_DEFAULT, "Game cannot be unpaused manually; disable pause_on_join/min_active_clients.");
} else {
IConsolePrint(CC_DEFAULT, "Game is already unpaused.");
}
return true;
}
DEF_CONSOLE_CMD(ConRcon)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Remote control the server from another client. Usage: 'rcon '.");
IConsolePrint(CC_HELP, "Remember to enclose the command in quotes, otherwise only the first parameter is sent.");
return true;
}
if (argc < 3) return false;
if (_network_server) {
IConsoleCmdExec(argv[2]);
} else {
NetworkClientSendRcon(argv[1], argv[2]);
}
return true;
}
DEF_CONSOLE_CMD(ConStatus)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "List the status of all clients connected to the server. Usage 'status'.");
return true;
}
NetworkServerShowStatusToConsole();
return true;
}
DEF_CONSOLE_CMD(ConServerInfo)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "List current and maximum client/company limits. Usage 'server_info'.");
IConsolePrint(CC_HELP, "You can change these values by modifying settings 'network.max_clients' and 'network.max_companies'.");
return true;
}
IConsolePrint(CC_DEFAULT, "Invite code: {}", _network_server_invite_code);
IConsolePrint(CC_DEFAULT, "Current/maximum clients: {:3d}/{:3d}", _network_game_info.clients_on, _settings_client.network.max_clients);
IConsolePrint(CC_DEFAULT, "Current/maximum companies: {:3d}/{:3d}", Company::GetNumItems(), _settings_client.network.max_companies);
IConsolePrint(CC_DEFAULT, "Current spectators: {:3d}", NetworkSpectatorCount());
return true;
}
DEF_CONSOLE_CMD(ConClientNickChange)
{
if (argc != 3) {
IConsolePrint(CC_HELP, "Change the nickname of a connected client. Usage: 'client_name '.");
IConsolePrint(CC_HELP, "For client-id's, see the command 'clients'.");
return true;
}
ClientID client_id = (ClientID)atoi(argv[1]);
if (client_id == CLIENT_ID_SERVER) {
IConsolePrint(CC_ERROR, "Please use the command 'name' to change your own name!");
return true;
}
if (NetworkClientInfo::GetByClientID(client_id) == nullptr) {
IConsolePrint(CC_ERROR, "Invalid client ID.");
return true;
}
std::string client_name(argv[2]);
StrTrimInPlace(client_name);
if (!NetworkIsValidClientName(client_name)) {
IConsolePrint(CC_ERROR, "Cannot give a client an empty name.");
return true;
}
if (!NetworkServerChangeClientName(client_id, client_name)) {
IConsolePrint(CC_ERROR, "Cannot give a client a duplicate name.");
}
return true;
}
DEF_CONSOLE_CMD(ConJoinCompany)
{
if (argc < 2) {
IConsolePrint(CC_HELP, "Request joining another company. Usage: 'join []'.");
IConsolePrint(CC_HELP, "For valid company-id see company list, use 255 for spectator.");
return true;
}
CompanyID company_id = (CompanyID)(atoi(argv[1]) <= MAX_COMPANIES ? atoi(argv[1]) - 1 : atoi(argv[1]));
/* Check we have a valid company id! */
if (!Company::IsValidID(company_id) && company_id != COMPANY_SPECTATOR) {
IConsolePrint(CC_ERROR, "Company does not exist. Company-id must be between 1 and {}.", MAX_COMPANIES);
return true;
}
if (NetworkClientInfo::GetByClientID(_network_own_client_id)->client_playas == company_id) {
IConsolePrint(CC_ERROR, "You are already there!");
return true;
}
if (company_id != COMPANY_SPECTATOR && !Company::IsHumanID(company_id)) {
IConsolePrint(CC_ERROR, "Cannot join AI company.");
return true;
}
/* Check if the company requires a password */
if (NetworkCompanyIsPassworded(company_id) && argc < 3) {
IConsolePrint(CC_ERROR, "Company {} requires a password to join.", company_id + 1);
return true;
}
/* non-dedicated server may just do the move! */
if (_network_server) {
NetworkServerDoMove(CLIENT_ID_SERVER, company_id);
} else {
NetworkClientRequestMove(company_id, NetworkCompanyIsPassworded(company_id) ? argv[2] : "");
}
return true;
}
DEF_CONSOLE_CMD(ConMoveClient)
{
if (argc < 3) {
IConsolePrint(CC_HELP, "Move a client to another company. Usage: 'move '.");
IConsolePrint(CC_HELP, "For valid client-id see 'clients', for valid company-id see 'companies', use 255 for moving to spectators.");
return true;
}
const NetworkClientInfo *ci = NetworkClientInfo::GetByClientID((ClientID)atoi(argv[1]));
CompanyID company_id = (CompanyID)(atoi(argv[2]) <= MAX_COMPANIES ? atoi(argv[2]) - 1 : atoi(argv[2]));
/* check the client exists */
if (ci == nullptr) {
IConsolePrint(CC_ERROR, "Invalid client-id, check the command 'clients' for valid client-id's.");
return true;
}
if (!Company::IsValidID(company_id) && company_id != COMPANY_SPECTATOR) {
IConsolePrint(CC_ERROR, "Company does not exist. Company-id must be between 1 and {}.", MAX_COMPANIES);
return true;
}
if (company_id != COMPANY_SPECTATOR && !Company::IsHumanID(company_id)) {
IConsolePrint(CC_ERROR, "You cannot move clients to AI companies.");
return true;
}
if (ci->client_id == CLIENT_ID_SERVER && _network_dedicated) {
IConsolePrint(CC_ERROR, "You cannot move the server!");
return true;
}
if (ci->client_playas == company_id) {
IConsolePrint(CC_ERROR, "You cannot move someone to where they already are!");
return true;
}
/* we are the server, so force the update */
NetworkServerDoMove(ci->client_id, company_id);
return true;
}
DEF_CONSOLE_CMD(ConResetCompany)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Remove an idle company from the game. Usage: 'reset_company '.");
IConsolePrint(CC_HELP, "For company-id's, see the list of companies from the dropdown menu. Company 1 is 1, etc.");
return true;
}
if (argc != 2) return false;
CompanyID index = (CompanyID)(atoi(argv[1]) - 1);
/* Check valid range */
if (!Company::IsValidID(index)) {
IConsolePrint(CC_ERROR, "Company does not exist. Company-id must be between 1 and {}.", MAX_COMPANIES);
return true;
}
if (!Company::IsHumanID(index)) {
IConsolePrint(CC_ERROR, "Company is owned by an AI.");
return true;
}
if (NetworkCompanyHasClients(index)) {
IConsolePrint(CC_ERROR, "Cannot remove company: a client is connected to that company.");
return false;
}
const NetworkClientInfo *ci = NetworkClientInfo::GetByClientID(CLIENT_ID_SERVER);
assert(ci != nullptr);
if (ci->client_playas == index) {
IConsolePrint(CC_ERROR, "Cannot remove company: the server is connected to that company.");
return true;
}
/* It is safe to remove this company */
Command::Post(CCA_DELETE, index, CRR_MANUAL, INVALID_CLIENT_ID);
IConsolePrint(CC_DEFAULT, "Company deleted.");
return true;
}
DEF_CONSOLE_CMD(ConNetworkClients)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Get a list of connected clients including their ID, name, company-id, and IP. Usage: 'clients'.");
return true;
}
NetworkPrintClients();
return true;
}
DEF_CONSOLE_CMD(ConNetworkReconnect)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Reconnect to server to which you were connected last time. Usage: 'reconnect []'.");
IConsolePrint(CC_HELP, "Company 255 is spectator (default, if not specified), 0 means creating new company.");
IConsolePrint(CC_HELP, "All others are a certain company with Company 1 being #1.");
return true;
}
CompanyID playas = (argc >= 2) ? (CompanyID)atoi(argv[1]) : COMPANY_SPECTATOR;
switch (playas) {
case 0: playas = COMPANY_NEW_COMPANY; break;
case COMPANY_SPECTATOR: /* nothing to do */ break;
default:
/* From a user pov 0 is a new company, internally it's different and all
* companies are offset by one to ease up on users (eg companies 1-8 not 0-7) */
if (playas < COMPANY_FIRST + 1 || playas > MAX_COMPANIES + 1) return false;
break;
}
if (_settings_client.network.last_joined.empty()) {
IConsolePrint(CC_DEFAULT, "No server for reconnecting.");
return true;
}
/* Don't resolve the address first, just print it directly as it comes from the config file. */
IConsolePrint(CC_DEFAULT, "Reconnecting to {} ...", _settings_client.network.last_joined);
return NetworkClientConnectGame(_settings_client.network.last_joined, playas);
}
DEF_CONSOLE_CMD(ConNetworkConnect)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Connect to a remote OTTD server and join the game. Usage: 'connect '.");
IConsolePrint(CC_HELP, "IP can contain port and company: 'IP[:Port][#Company]', eg: 'server.ottd.org:443#2'.");
IConsolePrint(CC_HELP, "Company #255 is spectator all others are a certain company with Company 1 being #1.");
return true;
}
if (argc < 2) return false;
return NetworkClientConnectGame(argv[1], COMPANY_NEW_COMPANY);
}
/*********************************
* script file console commands
*********************************/
DEF_CONSOLE_CMD(ConExec)
{
if (argc == 0) {
IConsolePrint(CC_HELP, "Execute a local script file. Usage: 'exec