2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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/>.
8 /** @file console.cpp Handling of the in-game console. */
11 #include "console_internal.h"
12 #include "network/network.h"
13 #include "network/network_func.h"
14 #include "network/network_admin.h"
16 #include "console_func.h"
17 #include "settings_type.h"
19 #include "safeguards.h"
21 static const uint ICON_TOKEN_COUNT
= 20; ///< Maximum number of tokens in one command
22 static const uint ICON_MAX_RECURSE
= 10; ///< Maximum number of recursion
25 /* static */ IConsole::CommandList
&IConsole::Commands()
27 static IConsole::CommandList cmds
;
31 /* static */ IConsole::AliasList
&IConsole::Aliases()
33 static IConsole::AliasList aliases
;
37 std::optional
<FileHandle
> _iconsole_output_file
;
41 _iconsole_output_file
= std::nullopt
;
42 _redirect_console_to_client
= INVALID_CLIENT_ID
;
43 _redirect_console_to_admin
= INVALID_ADMIN_ID
;
47 IConsoleStdLibRegister();
50 static void IConsoleWriteToLogFile(const std::string
&string
)
52 if (_iconsole_output_file
.has_value()) {
53 /* if there is an console output file ... also print it there */
55 fmt::print(*_iconsole_output_file
, "{}{}\n", GetLogPrefix(), string
);
56 } catch (const std::system_error
&) {
57 _iconsole_output_file
.reset();
58 IConsolePrint(CC_ERROR
, "Cannot write to console log file; closing the log file.");
63 bool CloseConsoleLogIfActive()
65 if (_iconsole_output_file
.has_value()) {
66 IConsolePrint(CC_INFO
, "Console log file closed.");
67 _iconsole_output_file
.reset();
77 CloseConsoleLogIfActive();
81 * Handle the printing of text entered into the console or redirected there
82 * by any other means. Text can be redirected to other clients in a network game
83 * as well as to a logfile. If the network server is a dedicated server, all activities
84 * are also logged. All lines to print are added to a temporary buffer which can be
85 * used as a history to print them onscreen
86 * @param colour_code The colour of the command.
87 * @param string The message to output on the console (notice, error, etc.)
89 void IConsolePrint(TextColour colour_code
, const std::string
&string
)
91 assert(IsValidConsoleColour(colour_code
));
93 if (_redirect_console_to_client
!= INVALID_CLIENT_ID
) {
94 /* Redirect the string to the client */
95 NetworkServerSendRcon(_redirect_console_to_client
, colour_code
, string
);
99 if (_redirect_console_to_admin
!= INVALID_ADMIN_ID
) {
100 NetworkServerSendAdminRcon(_redirect_console_to_admin
, colour_code
, string
);
104 /* Create a copy of the string, strip it of colours and invalid
105 * characters and (when applicable) assign it to the console buffer */
106 std::string str
= StrMakeValid(string
, SVS_NONE
);
108 if (_network_dedicated
) {
109 NetworkAdminConsole("console", str
);
110 fmt::print("{}{}\n", GetLogPrefix(), str
);
112 IConsoleWriteToLogFile(str
);
116 IConsoleWriteToLogFile(str
);
117 IConsoleGUIPrint(colour_code
, str
);
121 * Change a string into its number representation. Supports
122 * decimal and hexadecimal numbers as well as 'on'/'off' 'true'/'false'
123 * @param *value the variable a successful conversion will be put in
124 * @param *arg the string to be converted
125 * @return Return true on success or false on failure
127 bool GetArgumentInteger(uint32_t *value
, const char *arg
)
131 if (strcmp(arg
, "on") == 0 || strcmp(arg
, "true") == 0) {
135 if (strcmp(arg
, "off") == 0 || strcmp(arg
, "false") == 0) {
140 *value
= std::strtoul(arg
, &endptr
, 0);
141 return arg
!= endptr
;
145 * Creates a copy of a string with underscores removed from it
146 * @param name String to remove the underscores from.
147 * @return A copy of \a name, without underscores.
149 static std::string
RemoveUnderscores(std::string name
)
151 name
.erase(std::remove(name
.begin(), name
.end(), '_'), name
.end());
156 * Register a new command to be used in the console
157 * @param name name of the command that will be used
158 * @param proc function that will be called upon execution of command
160 /* static */ void IConsole::CmdRegister(const std::string
&name
, IConsoleCmdProc
*proc
, IConsoleHook
*hook
)
162 IConsole::Commands().try_emplace(RemoveUnderscores(name
), name
, proc
, hook
);
166 * Find the command pointed to by its string
167 * @param name command to be found
168 * @return return Cmdstruct of the found command, or nullptr on failure
170 /* static */ IConsoleCmd
*IConsole::CmdGet(const std::string
&name
)
172 auto item
= IConsole::Commands().find(RemoveUnderscores(name
));
173 if (item
!= IConsole::Commands().end()) return &item
->second
;
178 * Register a an alias for an already existing command in the console
179 * @param name name of the alias that will be used
180 * @param cmd name of the command that 'name' will be alias of
182 /* static */ void IConsole::AliasRegister(const std::string
&name
, const std::string
&cmd
)
184 auto result
= IConsole::Aliases().try_emplace(RemoveUnderscores(name
), name
, cmd
);
185 if (!result
.second
) IConsolePrint(CC_ERROR
, "An alias with the name '{}' already exists.", name
);
189 * Find the alias pointed to by its string
190 * @param name alias to be found
191 * @return return Aliasstruct of the found alias, or nullptr on failure
193 /* static */ IConsoleAlias
*IConsole::AliasGet(const std::string
&name
)
195 auto item
= IConsole::Aliases().find(RemoveUnderscores(name
));
196 if (item
!= IConsole::Aliases().end()) return &item
->second
;
201 * An alias is just another name for a command, or for more commands
202 * Execute it as well.
203 * @param *alias is the alias of the command
204 * @param tokencount the number of parameters passed
205 * @param *tokens are the parameters given to the original command (0 is the first param)
207 static void IConsoleAliasExec(const IConsoleAlias
*alias
, uint8_t tokencount
, char *tokens
[ICON_TOKEN_COUNT
], const uint recurse_count
)
209 std::string alias_buffer
;
211 Debug(console
, 6, "Requested command is an alias; parsing...");
213 if (recurse_count
> ICON_MAX_RECURSE
) {
214 IConsolePrint(CC_ERROR
, "Too many alias expansions, recursion limit reached.");
218 for (const char *cmdptr
= alias
->cmdline
.c_str(); *cmdptr
!= '\0'; cmdptr
++) {
220 case '\'': // ' will double for ""
221 alias_buffer
+= '\"';
224 case ';': // Cmd separator; execute previous and start new command
225 IConsoleCmdExec(alias_buffer
, recurse_count
);
227 alias_buffer
.clear();
232 case '%': // Some or all parameters
235 case '+': { // All parameters separated: "[param 1]" "[param 2]"
236 for (uint i
= 0; i
!= tokencount
; i
++) {
237 if (i
!= 0) alias_buffer
+= ' ';
238 alias_buffer
+= '\"';
239 alias_buffer
+= tokens
[i
];
240 alias_buffer
+= '\"';
245 case '!': { // Merge the parameters to one: "[param 1] [param 2] [param 3...]"
246 alias_buffer
+= '\"';
247 for (uint i
= 0; i
!= tokencount
; i
++) {
248 if (i
!= 0) alias_buffer
+= " ";
249 alias_buffer
+= tokens
[i
];
251 alias_buffer
+= '\"';
255 default: { // One specific parameter: %A = [param 1] %B = [param 2] ...
256 int param
= *cmdptr
- 'A';
258 if (param
< 0 || param
>= tokencount
) {
259 IConsolePrint(CC_ERROR
, "Too many or wrong amount of parameters passed to alias.");
260 IConsolePrint(CC_HELP
, "Usage of alias '{}': '{}'.", alias
->name
, alias
->cmdline
);
264 alias_buffer
+= '\"';
265 alias_buffer
+= tokens
[param
];
266 alias_buffer
+= '\"';
273 alias_buffer
+= *cmdptr
;
277 if (alias_buffer
.size() >= ICON_MAX_STREAMSIZE
- 1) {
278 IConsolePrint(CC_ERROR
, "Requested alias execution would overflow execution buffer.");
283 IConsoleCmdExec(alias_buffer
, recurse_count
);
287 * Execute a given command passed to us. First chop it up into
288 * individual tokens (separated by spaces), then execute it if possible
289 * @param command_string string to be parsed and executed
291 void IConsoleCmdExec(const std::string
&command_string
, const uint recurse_count
)
294 char *tokens
[ICON_TOKEN_COUNT
], tokenstream
[ICON_MAX_STREAMSIZE
];
295 uint t_index
, tstream_i
;
297 bool longtoken
= false;
298 bool foundtoken
= false;
300 if (command_string
[0] == '#') return; // comments
302 for (cmdptr
= command_string
.c_str(); *cmdptr
!= '\0'; cmdptr
++) {
303 if (!IsValidChar(*cmdptr
, CS_ALPHANUMERAL
)) {
304 IConsolePrint(CC_ERROR
, "Command '{}' contains malformed characters.", command_string
);
309 Debug(console
, 4, "Executing cmdline: '{}'", command_string
);
311 memset(&tokens
, 0, sizeof(tokens
));
312 memset(&tokenstream
, 0, sizeof(tokenstream
));
314 /* 1. Split up commandline into tokens, separated by spaces, commands
315 * enclosed in "" are taken as one token. We can only go as far as the amount
316 * of characters in our stream or the max amount of tokens we can handle */
317 for (cmdptr
= command_string
.c_str(), t_index
= 0, tstream_i
= 0; *cmdptr
!= '\0'; cmdptr
++) {
318 if (tstream_i
>= lengthof(tokenstream
)) {
319 IConsolePrint(CC_ERROR
, "Command line too long.");
324 case ' ': // Token separator
325 if (!foundtoken
) break;
328 tokenstream
[tstream_i
] = *cmdptr
;
330 tokenstream
[tstream_i
] = '\0';
336 case '"': // Tokens enclosed in "" are one token
337 longtoken
= !longtoken
;
339 if (t_index
>= lengthof(tokens
)) {
340 IConsolePrint(CC_ERROR
, "Command line too long.");
343 tokens
[t_index
++] = &tokenstream
[tstream_i
];
347 case '\\': // Escape character for ""
348 if (cmdptr
[1] == '"' && tstream_i
+ 1 < lengthof(tokenstream
)) {
349 tokenstream
[tstream_i
++] = *++cmdptr
;
353 default: // Normal character
354 tokenstream
[tstream_i
++] = *cmdptr
;
357 if (t_index
>= lengthof(tokens
)) {
358 IConsolePrint(CC_ERROR
, "Command line too long.");
361 tokens
[t_index
++] = &tokenstream
[tstream_i
- 1];
368 for (uint i
= 0; i
< lengthof(tokens
) && tokens
[i
] != nullptr; i
++) {
369 Debug(console
, 8, "Token {} is: '{}'", i
, tokens
[i
]);
372 if (StrEmpty(tokens
[0])) return; // don't execute empty commands
373 /* 2. Determine type of command (cmd or alias) and execute
374 * First try commands, then aliases. Execute
375 * the found action taking into account its hooking code
377 IConsoleCmd
*cmd
= IConsole::CmdGet(tokens
[0]);
378 if (cmd
!= nullptr) {
379 ConsoleHookResult chr
= (cmd
->hook
== nullptr ? CHR_ALLOW
: cmd
->hook(true));
382 if (!cmd
->proc(t_index
, tokens
)) { // index started with 0
383 cmd
->proc(0, nullptr); // if command failed, give help
387 case CHR_DISALLOW
: return;
388 case CHR_HIDE
: break;
393 IConsoleAlias
*alias
= IConsole::AliasGet(tokens
[0]);
394 if (alias
!= nullptr) {
395 IConsoleAliasExec(alias
, t_index
, &tokens
[1], recurse_count
+ 1);
399 IConsolePrint(CC_ERROR
, "Command '{}' not found.", tokens
[0]);