diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index e474ffdc17..1b30bb215b 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -75,11 +75,26 @@ static IntervalTimer _scheduled_monthly_timer = {{TimerGameCa /** * Change a string into its number representation. Supports decimal and hexadecimal numbers. * @param arg The string to be converted. + * @param base The base for parsing the number, defaults to only decimal numbers. Use 0 to also allow hexadecimal. * @return The number, or std::nullopt when it could not be parsed. */ -static std::optional ParseInteger(std::string_view arg) +static std::optional ParseInteger(std::string_view arg, int base = 10) { - return StringConsumer{arg}.TryReadIntegerBase(0); + return StringConsumer{arg}.TryReadIntegerBase(base); +} + +/** + * Parse an integer using #ParseInteger and convert it to the requested type. + * @param arg The string to be converted. + * @tparam T The type to return. + * @return The number in the given type, or std::nullopt when it could not be parsed. + */ +template +static std::optional ParseType(std::string_view arg) +{ + auto i = ParseInteger(arg); + if (i.has_value()) return static_cast(*i); + return std::nullopt; } /** File list storage for the console, for caching the last 'ls' command. */ @@ -287,7 +302,7 @@ static bool ConResetTile([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *a } if (argc == 2) { - auto result = ParseInteger(argv[1]); + auto result = ParseInteger(argv[1], 0); if (result.has_value() && IsValidTile(*result)) { DoClearSquare(TileIndex{*result}); return true; @@ -382,7 +397,7 @@ static bool ConScrollToTile([[maybe_unused]] uint8_t argc, [[maybe_unused]] char switch (argc - arg_index) { case 1: { - auto result = ParseInteger(argv[arg_index]); + auto result = ParseInteger(argv[arg_index], 0); if (result.has_value()) { if (*result >= Map::Size()) { IConsolePrint(CC_ERROR, "Tile does not exist."); @@ -395,8 +410,8 @@ static bool ConScrollToTile([[maybe_unused]] uint8_t argc, [[maybe_unused]] char } case 2: { - auto x = ParseInteger(argv[arg_index]); - auto y = ParseInteger(argv[arg_index + 1]); + auto x = ParseInteger(argv[arg_index], 0); + auto y = ParseInteger(argv[arg_index + 1], 0); if (x.has_value() && y.has_value()) { if (*x >= Map::SizeX() || *y >= Map::SizeY()) { IConsolePrint(CC_ERROR, "Tile does not exist."); @@ -670,38 +685,42 @@ static bool ConClearBuffer([[maybe_unused]] uint8_t argc, [[maybe_unused]] char * Network Core Console Commands **********************************/ -static bool ConKickOrBan(const char *argv, bool ban, const std::string &reason) +static bool ConKickOrBan(std::string_view arg, bool ban, const std::string_view reason) { uint n; - if (strchr(argv, '.') == nullptr && strchr(argv, ':') == nullptr) { // banning with ID - ClientID client_id = (ClientID)atoi(argv); + if (arg.find_first_of(".:") == std::string::npos) { // banning with ID + auto client_id = ParseType(arg); + if (!client_id.has_value()) { + IConsolePrint(CC_ERROR, "The given client-id is not a valid number."); + return true; + } /* 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) { + 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); + NetworkClientInfo *ci = NetworkClientInfo::GetByClientID(*client_id); if (ci == nullptr) { - IConsolePrint(CC_ERROR, "Invalid client ID."); + IConsolePrint(CC_ERROR, "Invalid client-id."); return true; } if (!ban) { /* Kick only this client, not all clients with that IP */ - NetworkServerKickClient(client_id, reason); + NetworkServerKickClient(*client_id, reason); return true; } /* When banning, kick+ban all clients with that IP */ - n = NetworkServerKickOrBanIP(client_id, ban, reason); + n = NetworkServerKickOrBanIP(*client_id, ban, reason); } else { - n = NetworkServerKickOrBanIP(argv, ban, reason); + n = NetworkServerKickOrBanIP(arg, ban, reason); } if (n == 0) { @@ -778,7 +797,7 @@ static bool ConUnBan([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *argv[ /* Try by index. */ if (index >= _network_ban_list.size()) { - index = atoi(argv[1]) - 1U; // let it wrap + index = ParseInteger(argv[1]).value_or(0) - 1U; // let it wrap } if (index < _network_ban_list.size()) { @@ -912,15 +931,19 @@ static bool ConClientNickChange([[maybe_unused]] uint8_t argc, [[maybe_unused]] return true; } - ClientID client_id = (ClientID)atoi(argv[1]); + auto client_id = ParseType(argv[1]); + if (!client_id.has_value()) { + IConsolePrint(CC_ERROR, "The given client-id is not a valid number."); + return true; + } - if (client_id == CLIENT_ID_SERVER) { + 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."); + if (NetworkClientInfo::GetByClientID(*client_id) == nullptr) { + IConsolePrint(CC_ERROR, "Invalid client-id."); return true; } @@ -931,13 +954,20 @@ static bool ConClientNickChange([[maybe_unused]] uint8_t argc, [[maybe_unused]] return true; } - if (!NetworkServerChangeClientName(client_id, client_name)) { + if (!NetworkServerChangeClientName(*client_id, client_name)) { IConsolePrint(CC_ERROR, "Cannot give a client a duplicate name."); } return true; } +static std::optional ParseCompanyID(std::string_view arg) +{ + auto company_id = ParseType(arg); + if (company_id.has_value() && *company_id <= MAX_COMPANIES) return static_cast(*company_id - 1); + return company_id; +} + static bool ConJoinCompany([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *argv[]) { if (argc < 2) { @@ -946,7 +976,11 @@ static bool ConJoinCompany([[maybe_unused]] uint8_t argc, [[maybe_unused]] char return true; } - CompanyID company_id = (CompanyID)(atoi(argv[1]) <= MAX_COMPANIES ? atoi(argv[1]) - 1 : atoi(argv[1])); + auto company_id = ParseCompanyID(argv[1]); + if (!company_id.has_value()) { + IConsolePrint(CC_ERROR, "The given company-id is not a valid number."); + return true; + } const NetworkClientInfo *info = NetworkClientInfo::GetByClientID(_network_own_client_id); if (info == nullptr) { @@ -955,31 +989,31 @@ static bool ConJoinCompany([[maybe_unused]] uint8_t argc, [[maybe_unused]] char } /* Check we have a valid company id! */ - if (!Company::IsValidID(company_id) && company_id != COMPANY_SPECTATOR) { + 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 (info->client_playas == company_id) { + if (info->client_playas == *company_id) { IConsolePrint(CC_ERROR, "You are already there!"); return true; } - if (company_id != COMPANY_SPECTATOR && !Company::IsHumanID(company_id)) { + if (*company_id != COMPANY_SPECTATOR && !Company::IsHumanID(*company_id)) { IConsolePrint(CC_ERROR, "Cannot join AI company."); return true; } - if (!info->CanJoinCompany(company_id)) { + if (!info->CanJoinCompany(*company_id)) { IConsolePrint(CC_ERROR, "You are not allowed to join this company."); return true; } /* non-dedicated server may just do the move! */ if (_network_server) { - NetworkServerDoMove(CLIENT_ID_SERVER, company_id); + NetworkServerDoMove(CLIENT_ID_SERVER, *company_id); } else { - NetworkClientRequestMove(company_id); + NetworkClientRequestMove(*company_id); } return true; @@ -993,8 +1027,18 @@ static bool ConMoveClient([[maybe_unused]] uint8_t argc, [[maybe_unused]] char * 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])); + auto client_id = ParseType(argv[1]); + if (!client_id.has_value()) { + IConsolePrint(CC_ERROR, "The given client-id is not a valid number."); + return true; + } + const NetworkClientInfo *ci = NetworkClientInfo::GetByClientID(*client_id); + + auto company_id = ParseCompanyID(argv[2]); + if (!company_id.has_value()) { + IConsolePrint(CC_ERROR, "The given company-id is not a valid number."); + return true; + } /* check the client exists */ if (ci == nullptr) { @@ -1002,12 +1046,12 @@ static bool ConMoveClient([[maybe_unused]] uint8_t argc, [[maybe_unused]] char * return true; } - if (!Company::IsValidID(company_id) && company_id != COMPANY_SPECTATOR) { + 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)) { + if (*company_id != COMPANY_SPECTATOR && !Company::IsHumanID(*company_id)) { IConsolePrint(CC_ERROR, "You cannot move clients to AI companies."); return true; } @@ -1017,13 +1061,13 @@ static bool ConMoveClient([[maybe_unused]] uint8_t argc, [[maybe_unused]] char * return true; } - if (ci->client_playas == company_id) { + 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); + NetworkServerDoMove(ci->client_id, *company_id); return true; } @@ -1038,32 +1082,36 @@ static bool ConResetCompany([[maybe_unused]] uint8_t argc, [[maybe_unused]] char 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); + auto index = ParseCompanyID(argv[1]); + if (!index.has_value()) { + IConsolePrint(CC_ERROR, "The given company-id is not a valid number."); return true; } - if (!Company::IsHumanID(index)) { + /* 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)) { + 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) { + 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); + Command::Post(CCA_DELETE, *index, CRR_MANUAL, INVALID_CLIENT_ID); IConsolePrint(CC_DEFAULT, "Company deleted."); return true; @@ -1084,21 +1132,21 @@ static bool ConNetworkClients([[maybe_unused]] uint8_t argc, [[maybe_unused]] ch static bool ConNetworkReconnect([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *argv[]) { 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, "Reconnect to server to which you were connected last time. Usage: 'reconnect []'."); + IConsolePrint(CC_HELP, "Company 255 is spectator (default, if not specified), 254 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.base()) { - case 0: playas = COMPANY_NEW_COMPANY; break; - case COMPANY_SPECTATOR.base(): /* 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 < CompanyID::Begin() + 1 || playas > MAX_COMPANIES + 1) return false; - break; + CompanyID playas = COMPANY_SPECTATOR; + if (argc >= 2) { + auto company_id = ParseCompanyID(argv[1]); + if (!company_id.has_value()) { + IConsolePrint(CC_ERROR, "The given company-id is not a valid number."); + return true; + } + if (*company_id >= MAX_COMPANIES && *company_id != COMPANY_NEW_COMPANY && *company_id != COMPANY_SPECTATOR) return false; + playas = *company_id; } if (_settings_client.network.last_joined.empty()) { @@ -1272,7 +1320,14 @@ static bool ConEchoC([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *argv[ } if (argc < 3) return false; - IConsolePrint((TextColour)Clamp(atoi(argv[1]), TC_BEGIN, TC_END - 1), argv[2]); + + auto colour = ParseInteger(argv[1]); + if (!colour.has_value() || !IsInsideMM(*colour, TC_BEGIN, TC_END)) { + IConsolePrint(CC_ERROR, "The colour must be a number between {} and {}.", TC_BEGIN, TC_END - 1); + return true; + } + + IConsolePrint(static_cast(*colour), argv[2]); return true; } @@ -1445,8 +1500,12 @@ static bool ConStartAI([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *arg size_t name_length = e - argv[1]; e++; - int version = atoi(e); - config->Change(std::string(argv[1], name_length), version, true); + auto version = ParseInteger(e); + if (!version.has_value()) { + IConsolePrint(CC_ERROR, "The version is not a valid number."); + return true; + } + config->Change(std::string(argv[1], name_length), *version, true); } } @@ -1483,21 +1542,26 @@ static bool ConReloadAI([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *ar return true; } - CompanyID company_id = (CompanyID)(atoi(argv[1]) - 1); - if (!Company::IsValidID(company_id)) { + auto company_id = ParseCompanyID(argv[1]); + if (!company_id.has_value()) { + IConsolePrint(CC_ERROR, "The given company-id is not a valid number."); + return true; + } + + if (!Company::IsValidID(*company_id)) { IConsolePrint(CC_ERROR, "Unknown company. Company range is between 1 and {}.", MAX_COMPANIES); return true; } /* In singleplayer mode the player can be in an AI company, after cheating or loading network save with an AI in first slot. */ - if (Company::IsHumanID(company_id) || company_id == _local_company) { + if (Company::IsHumanID(*company_id) || *company_id == _local_company) { IConsolePrint(CC_ERROR, "Company is not controlled by an AI."); return true; } /* First kill the company of the AI, then start a new one. This should start the current AI again */ - Command::Post(CCA_DELETE, company_id, CRR_MANUAL, INVALID_CLIENT_ID); - Command::Post(CCA_NEW_AI, company_id, CRR_NONE, INVALID_CLIENT_ID); + Command::Post(CCA_DELETE, *company_id, CRR_MANUAL, INVALID_CLIENT_ID); + Command::Post(CCA_NEW_AI, *company_id, CRR_NONE, INVALID_CLIENT_ID); IConsolePrint(CC_DEFAULT, "AI reloaded."); return true; @@ -1521,20 +1585,25 @@ static bool ConStopAI([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *argv return true; } - CompanyID company_id = (CompanyID)(atoi(argv[1]) - 1); - if (!Company::IsValidID(company_id)) { + auto company_id = ParseCompanyID(argv[1]); + if (!company_id.has_value()) { + IConsolePrint(CC_ERROR, "The given company-id is not a valid number."); + return true; + } + + if (!Company::IsValidID(*company_id)) { IConsolePrint(CC_ERROR, "Unknown company. Company range is between 1 and {}.", MAX_COMPANIES); return true; } /* In singleplayer mode the player can be in an AI company, after cheating or loading network save with an AI in first slot. */ - if (Company::IsHumanID(company_id) || company_id == _local_company) { + if (Company::IsHumanID(*company_id) || *company_id == _local_company) { IConsolePrint(CC_ERROR, "Company is not controlled by an AI."); return true; } /* Now kill the company of the AI. */ - Command::Post(CCA_DELETE, company_id, CRR_MANUAL, INVALID_CLIENT_ID); + Command::Post(CCA_DELETE, *company_id, CRR_MANUAL, INVALID_CLIENT_ID); IConsolePrint(CC_DEFAULT, "AI stopped, company deleted."); return true; @@ -1934,17 +2003,22 @@ static bool ConSayCompany([[maybe_unused]] uint8_t argc, [[maybe_unused]] char * if (argc != 3) return false; - CompanyID company_id = (CompanyID)(atoi(argv[1]) - 1); - if (!Company::IsValidID(company_id)) { + auto company_id = ParseCompanyID(argv[1]); + if (!company_id.has_value()) { + IConsolePrint(CC_ERROR, "The given company-id is not a valid number."); + return true; + } + + if (!Company::IsValidID(*company_id)) { IConsolePrint(CC_DEFAULT, "Unknown company. Company range is between 1 and {}.", MAX_COMPANIES); return true; } if (!_network_server) { - NetworkClientSendChat(NETWORK_ACTION_CHAT_COMPANY, DESTTYPE_TEAM, company_id.base(), argv[2]); + NetworkClientSendChat(NETWORK_ACTION_CHAT_COMPANY, DESTTYPE_TEAM, company_id->base(), argv[2]); } else { bool from_admin = (_redirect_console_to_admin < AdminID::Invalid()); - NetworkServerSendChat(NETWORK_ACTION_CHAT_COMPANY, DESTTYPE_TEAM, company_id.base(), argv[2], CLIENT_ID_SERVER, from_admin); + NetworkServerSendChat(NETWORK_ACTION_CHAT_COMPANY, DESTTYPE_TEAM, company_id->base(), argv[2], CLIENT_ID_SERVER, from_admin); } return true; @@ -1953,18 +2027,24 @@ static bool ConSayCompany([[maybe_unused]] uint8_t argc, [[maybe_unused]] char * static bool ConSayClient([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *argv[]) { if (argc == 0) { - IConsolePrint(CC_HELP, "Chat to a certain client in a multiplayer game. Usage: 'say_client \"\"'."); + IConsolePrint(CC_HELP, "Chat to a certain client in a multiplayer game. Usage: 'say_client \"\"'."); IConsolePrint(CC_HELP, "For client-id's, see the command 'clients'."); return true; } if (argc != 3) return false; + auto client_id = ParseType(argv[1]); + if (!client_id.has_value()) { + IConsolePrint(CC_ERROR, "The given client-id is not a valid number."); + return true; + } + if (!_network_server) { - NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, atoi(argv[1]), argv[2]); + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, *client_id, argv[2]); } else { bool from_admin = (_redirect_console_to_admin < AdminID::Invalid()); - NetworkServerSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, atoi(argv[1]), argv[2], CLIENT_ID_SERVER, from_admin); + NetworkServerSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, *client_id, argv[2], CLIENT_ID_SERVER, from_admin); } return true; @@ -2203,8 +2283,10 @@ static bool ConContent([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *arg * the spirit of this service. Additionally, these few people were * good for 70% of the consumed bandwidth of BaNaNaS. */ IConsolePrint(CC_ERROR, "'select all' is no longer supported since 1.11."); + } else if (auto content_id = ParseType(argv[2]); content_id.has_value()) { + _network_content_client.Select(*content_id); } else { - _network_content_client.Select((ContentID)atoi(argv[2])); + IConsolePrint(CC_ERROR, "The given content-id is not a number or 'all'"); } return true; } @@ -2216,8 +2298,10 @@ static bool ConContent([[maybe_unused]] uint8_t argc, [[maybe_unused]] char *arg } if (StrEqualsIgnoreCase(argv[2], "all")) { _network_content_client.UnselectAll(); + } else if (auto content_id = ParseType(argv[2]); content_id.has_value()) { + _network_content_client.Unselect(*content_id); } else { - _network_content_client.Unselect((ContentID)atoi(argv[2])); + IConsolePrint(CC_ERROR, "The given content-id is not a number or 'all'"); } return true; } @@ -2486,14 +2570,14 @@ static bool ConNewGRFProfile([[maybe_unused]] uint8_t argc, [[maybe_unused]] cha /* "select" sub-command */ if (StrStartsWithIgnoreCase(argv[1], "sel") && argc >= 3) { for (size_t argnum = 2; argnum < argc; ++argnum) { - int grfnum = atoi(argv[argnum]); - if (grfnum < 1 || grfnum > (int)files.size()) { // safe cast, files.size() should not be larger than a few hundred in the most extreme cases - IConsolePrint(CC_WARNING, "GRF number {} out of range, not added.", grfnum); + auto grfnum = ParseInteger(argv[argnum]); + if (!grfnum.has_value() || *grfnum < 1 || static_cast(*grfnum) > files.size()) { + IConsolePrint(CC_WARNING, "GRF number {} out of range, not added.", *grfnum); continue; } - const GRFFile *grf = &files[grfnum - 1]; + const GRFFile *grf = &files[*grfnum - 1]; if (std::any_of(_newgrf_profilers.begin(), _newgrf_profilers.end(), [&](NewGRFProfiler &pr) { return pr.grffile == grf; })) { - IConsolePrint(CC_WARNING, "GRF number {} [{:08X}] is already selected for profiling.", grfnum, std::byteswap(grf->grfid)); + IConsolePrint(CC_WARNING, "GRF number {} [{:08X}] is already selected for profiling.", *grfnum, std::byteswap(grf->grfid)); continue; } _newgrf_profilers.emplace_back(grf); @@ -2508,12 +2592,12 @@ static bool ConNewGRFProfile([[maybe_unused]] uint8_t argc, [[maybe_unused]] cha _newgrf_profilers.clear(); break; } - int grfnum = atoi(argv[argnum]); - if (grfnum < 1 || grfnum > (int)files.size()) { - IConsolePrint(CC_WARNING, "GRF number {} out of range, not removing.", grfnum); + auto grfnum = ParseInteger(argv[argnum]); + if (!grfnum.has_value() || *grfnum < 1 || static_cast(*grfnum) > files.size()) { + IConsolePrint(CC_WARNING, "GRF number {} out of range, not removing.", *grfnum); continue; } - const GRFFile *grf = &files[grfnum - 1]; + const GRFFile *grf = &files[*grfnum - 1]; _newgrf_profilers.erase(std::ranges::find(_newgrf_profilers, grf, &NewGRFProfiler::grffile)); } return true; @@ -2536,9 +2620,13 @@ static bool ConNewGRFProfile([[maybe_unused]] uint8_t argc, [[maybe_unused]] cha IConsolePrint(CC_DEBUG, "Started profiling for GRFID{} {}.", (started > 1) ? "s" : "", grfids); if (argc >= 3) { - uint64_t ticks = std::max(atoi(argv[2]), 1); - NewGRFProfiler::StartTimer(ticks); - IConsolePrint(CC_DEBUG, "Profiling will automatically stop after {} ticks.", ticks); + auto ticks = StringConsumer{std::string_view{argv[2]}}.TryReadIntegerBase(0); + if (!ticks.has_value()) { + IConsolePrint(CC_ERROR, "No valid amount of ticks was given, profiling will not stop automatically."); + } else { + NewGRFProfiler::StartTimer(*ticks); + IConsolePrint(CC_DEBUG, "Profiling will automatically stop after {} ticks.", *ticks); + } } } else if (_newgrf_profilers.empty()) { IConsolePrint(CC_ERROR, "No GRFs selected for profiling, did not start.");