#include "TConsole.h" #include "Common.h" #include "Compat.h" #include "Client.h" #include "CustomAssert.h" #include "LuaAPI.h" #include "TLuaEngine.h" #include #include #include #include #include #include #include #include #include #include static inline bool StringStartsWith(const std::string& What, const std::string& StartsWith) { return What.size() >= StartsWith.size() && What.substr(0, StartsWith.size()) == StartsWith; } static inline bool StringStartsWithLower(const std::string& Name1, const std::string& Name2) { std::string Name1Lower = boost::algorithm::to_lower_copy(Name1); return StringStartsWith(Name1Lower, Name2) || StringStartsWith(Name2, Name1Lower); }; static inline bool StringStartsWithLowerBoth(const std::string& Name1, const std::string& Name2) { std::string Name1Lower = boost::algorithm::to_lower_copy(Name1); std::string Name2Lower = boost::algorithm::to_lower_copy(Name2); return StringStartsWith(Name1Lower, Name2Lower) || StringStartsWith(Name2Lower, Name1Lower); }; TEST_CASE("StringStartsWith") { CHECK(StringStartsWith("Hello, World", "Hello")); CHECK(StringStartsWith("Hello, World", "H")); CHECK(StringStartsWith("Hello, World", "")); CHECK(!StringStartsWith("Hello, World", "ello")); CHECK(!StringStartsWith("Hello, World", "World")); CHECK(StringStartsWith("", "")); CHECK(!StringStartsWith("", "hello")); } // Trims leading and trailing spaces, newlines, tabs, etc. static inline std::string TrimString(std::string S) { S.erase(S.begin(), std::find_if(S.begin(), S.end(), [](unsigned char ch) { return !std::isspace(ch); })); S.erase(std::find_if(S.rbegin(), S.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), S.end()); return S; } TEST_CASE("TrimString") { CHECK(TrimString("hel lo") == "hel lo"); CHECK(TrimString(" hel lo") == "hel lo"); CHECK(TrimString(" hel lo ") == "hel lo"); CHECK(TrimString("hel lo ") == "hel lo"); CHECK(TrimString(" hel lo") == "hel lo"); CHECK(TrimString("hel lo ") == "hel lo"); CHECK(TrimString(" hel lo ") == "hel lo"); CHECK(TrimString("\t\thel\nlo\n\n") == "hel\nlo"); CHECK(TrimString("\n\thel\tlo\n\t") == "hel\tlo"); CHECK(TrimString(" ") == ""); CHECK(TrimString(" \t\n\r ") == ""); CHECK(TrimString("") == ""); } // TODO: add unit tests to SplitString static inline void SplitString(std::string const& str, const char delim, std::vector& out) { size_t start; size_t end = 0; while ((start = str.find_first_not_of(delim, end)) != std::string::npos) { end = str.find(delim, start); out.push_back(str.substr(start, end - start)); } } static std::string GetDate() { TimeType::time_point now = TimeType::now(); time_t tt = TimeType::to_time_t(now); auto local_tm = std::localtime(&tt); char buf[30]; std::string date; if (Application::GetSettingBool(StrDebug)) { std::strftime(buf, sizeof(buf), "[%Y/%m/%d %T.", local_tm); date += buf; auto seconds = std::chrono::time_point_cast(now); auto fraction = now - seconds; size_t ms = std::chrono::duration_cast(fraction).count(); char fracstr[5]; std::sprintf(fracstr, "%03lu", ms); date += fracstr; date += "] "; } else { std::strftime(buf, sizeof(buf), "[%Y/%m/%d %T] ", local_tm); date += buf; } return date; } void TConsole::BackupOldLog() { fs::path Path = "Server.log"; if (fs::exists(Path)) { auto OldLog = Path.filename().stem().string() + ".old.log"; try { fs::rename(Path, OldLog); beammp_debug("renamed old log file to '" + OldLog + "'"); } catch (const std::exception& e) { beammp_warn(e.what()); } /* int err = 0; zip* z = zip_open("ServerLogs.zip", ZIP_CREATE, &err); if (!z) { std::cerr << GetPlatformAgnosticErrorString() << std::endl; return; } FILE* File = std::fopen(Path.string().c_str(), "r"); if (!File) { std::cerr << GetPlatformAgnosticErrorString() << std::endl; return; } std::vector Buffer; Buffer.resize(fs::file_size(Path)); std::fread(Buffer.data(), 1, Buffer.size(), File); std::fclose(File); auto s = zip_source_buffer(z, Buffer.data(), Buffer.size(), 0); auto TimePoint = fs::last_write_time(Path); auto Secs = TimePoint.time_since_epoch().count(); auto MyTimeT = std::time(&Secs); std::string NewName = Path.stem().string(); NewName += "_"; std::string Time; Time.resize(32); size_t n = strftime(Time.data(), Time.size(), "%F_%H.%M.%S", localtime(&MyTimeT)); Time.resize(n); NewName += Time; NewName += ".log"; zip_file_add(z, NewName.c_str(), s, 0); zip_close(z); */ } } void TConsole::StartLoggingToFile() { mLogFileStream.open("Server.log"); Application::Console().Internal().on_write = [this](const std::string& ToWrite) { // TODO: Sanitize by removing all ansi escape codes (vt100) std::unique_lock Lock(mLogFileStreamMtx); mLogFileStream.write(ToWrite.c_str(), ToWrite.size()); mLogFileStream.write("\n", 1); mLogFileStream.flush(); }; } void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) { if (!mIsLuaConsole) { if (!mLuaEngine) { beammp_error("Lua engine not initialized yet, please wait and try again"); return; } mLuaEngine->EnsureStateExists(mDefaultStateId, "Console"); mStateId = LuaStateId; mIsLuaConsole = true; if (mStateId != mDefaultStateId) { Application::Console().WriteRaw("Attached to Lua state '" + mStateId + "'. For help, type `:help`. To detach, type `:exit`"); mCommandline.set_prompt("lua @" + LuaStateId + "> "); } else { Application::Console().WriteRaw("Attached to Lua. For help, type `:help`. To detach, type `:exit`"); mCommandline.set_prompt("lua> "); } mCachedRegularHistory = mCommandline.history(); mCommandline.set_history(mCachedLuaHistory); } } void TConsole::ChangeToRegularConsole() { if (mIsLuaConsole) { mIsLuaConsole = false; if (mStateId != mDefaultStateId) { Application::Console().WriteRaw("Detached from Lua state '" + mStateId + "'."); } else { Application::Console().WriteRaw("Detached from Lua."); } mCachedLuaHistory = mCommandline.history(); mCommandline.set_history(mCachedRegularHistory); mCommandline.set_prompt("> "); mStateId = mDefaultStateId; } } bool TConsole::EnsureArgsCount(const std::vector& args, size_t n) { if (n == 0 && args.size() != 0) { Application::Console().WriteRaw("This command expects no arguments."); return false; } else if (args.size() != n) { Application::Console().WriteRaw("Expected " + std::to_string(n) + " argument(s), instead got " + std::to_string(args.size())); return false; } else { return true; } } bool TConsole::EnsureArgsCount(const std::vector& args, size_t min, size_t max) { if (min == max) { return EnsureArgsCount(args, min); } else { if (args.size() > max) { Application::Console().WriteRaw("Too many arguments. At most " + std::to_string(max) + " argument(s) expected, got " + std::to_string(args.size()) + " instead."); return false; } else if (args.size() < min) { Application::Console().WriteRaw("Too few arguments. At least " + std::to_string(min) + " argument(s) expected, got " + std::to_string(args.size()) + " instead."); return false; } } return true; } void TConsole::Command_Lua(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 0, 1)) { return; } if (args.size() == 1) { auto NewStateId = args.at(0); beammp_assert(!NewStateId.empty()); if (mLuaEngine->HasState(NewStateId)) { ChangeToLuaConsole(NewStateId); } else { Application::Console().WriteRaw("Lua state '" + NewStateId + "' is not a known state. Didn't switch to Lua."); } } else if (args.size() == 0) { ChangeToLuaConsole(mDefaultStateId); } } void TConsole::Command_Help(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 0)) { return; } static constexpr const char* sHelpString = R"( Commands: help displays this help exit quit shuts down the server kick [reason] kicks specified player with an optional reason list lists all players and info about them say sends the message to all players in chat lua [state id] switches to lua, optionally into a specific state id's lua settings [command] sets or gets settings for the server, run `settings help` for more info status how the server is doing and what it's up to debug internal error and debug state of the server (for development) clear clears the console window version version info)"; Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString)); } std::string TConsole::ConcatArgs(const std::vector& args, char space) { std::string Result; for (const auto& arg : args) { Result += arg + space; } Result = Result.substr(0, Result.size() - 1); // strip trailing space return Result; } void TConsole::Command_Clear(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 0, size_t(-1))) { return; } mCommandline.write("\x1b[;H\x1b[2J"); } void TConsole::Command_Debug(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 0)) { return; } Application::Console().WriteRaw(fmt::format(R"(Debug info (for developers): UDP: Malformed packets: {} Invalid packets: {})", Application::MalformedUdpPackets, Application::InvalidUdpPackets)); Application::Console().WriteRaw(fmt::format(R"( Clients: Note: All data/second rates are an average across the total time since connection and do not necessarily reflect the *current* data rate of that client. )")); mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { if (!Client.expired()) { auto Locked = Client.lock(); std::string State = ""; if (Locked->IsSyncing()) { State += "Syncing"; } if (Locked->IsSynced()) { if (!State.empty()) { State += " & "; } State += "Synced"; } if (Locked->IsConnected()) { if (!State.empty()) { State += " & "; } State += "Connected"; } if (Locked->IsDisconnected()) { if (!State.empty()) { State += " & "; } State += "Disconnected"; } auto Now = TimeType::now(); auto Seconds = std::chrono::duration_cast(Now - Locked->ConnectionTime); std::string ConnectedSince = fmt::format("{:%Y/%m/%d %H:%M:%S}, {:%H:%M:%S} ago ({} seconds)", fmt::localtime(TimeType::to_time_t(Locked->ConnectionTime)), Seconds, Seconds.count()); Application::Console().WriteRaw(fmt::format( R"( {} ('{}'): Roles: {} Cars: {} Is guest: {} Has unicycle: {} TCP: {} (on port {}) UDP: {} (on port {}) Sent via TCP: {} Received via TCP: {} Sent via UDP: {} ({} packets) Received via UDP: {} ({} packets) Status: {} Queued packets: {} Latest packet: {}s ago Connected since: {} Average send: {}/s Average receive: {}/s)", Locked->GetID(), Locked->GetName(), Locked->GetRoles(), Locked->GetCarCount(), Locked->IsGuest() ? "yes" : "no", Locked->GetUnicycleID() == -1 ? "no" : "yes", Locked->GetTCPSock().remote_endpoint().address() == ip::address::from_string("0.0.0.0") ? "not connected" : "connected", Locked->GetTCPSock().remote_endpoint().port(), Locked->GetUDPAddr().address() == ip::address::from_string("0.0.0.0") ? "NOT connected" : "connected", Locked->GetUDPAddr().port(), ToHumanReadableSize(Locked->TcpSent), ToHumanReadableSize(Locked->TcpReceived), ToHumanReadableSize(Locked->UdpSent), Locked->UdpPacketsSent, ToHumanReadableSize(Locked->UdpReceived), Locked->UdpPacketsReceived, State.empty() ? "None (likely pre-sync)" : State, Locked->MissedPacketQueueSize(), Locked->SecondsSinceLastPing(), ConnectedSince, ToHumanReadableSize((Locked->TcpSent + Locked->UdpSent) / Seconds.count()), ToHumanReadableSize((Locked->TcpReceived + Locked->UdpReceived) / Seconds.count()))); } else { Application::Console().WriteRaw(fmt::format(R"( )")); } return true; }); } void TConsole::Command_Version(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 0)) { return; } Application::Console().WriteRaw(fmt::format("BeamMP Server v{} ({})", Application::ServerVersionString(), BEAMMP_GIT_HASH)); } void TConsole::Command_Kick(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 1, size_t(-1))) { return; } auto Name = boost::algorithm::to_lower_copy(args.at(0)); std::string Reason = "Kicked by server console"; if (args.size() > 1) { Reason = ConcatArgs({ args.begin() + 1, args.end() }); } beammp_trace("attempt to kick '" + Name + "' for '" + Reason + "'"); bool Kicked = false; mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { auto Locked = Client.lock(); if (Locked) { if (StringStartsWithLower(Locked->GetName(), Name)) { mLuaEngine->Network().ClientKick(*Locked, Reason); Kicked = true; return false; } } return true; }); if (!Kicked) { Application::Console().WriteRaw("Error: No player with name matching '" + Name + "' was found."); } else { Application::Console().WriteRaw("Kicked player '" + Name + "' for reason: '" + Reason + "'."); } } std::tuple> TConsole::ParseCommand(const std::string& CommandWithArgs) { // Algorithm designed and implemented by Lion Kortlepel (c) 2022 // It correctly splits arguments, including respecting single and double quotes, as well as backticks auto End_i = CommandWithArgs.find_first_of(' '); std::string Command = CommandWithArgs.substr(0, End_i); std::string ArgsStr {}; if (End_i != std::string::npos) { ArgsStr = CommandWithArgs.substr(End_i); } std::vector Args; char* PrevPtr = ArgsStr.data(); char* Ptr = ArgsStr.data(); const char* End = ArgsStr.data() + ArgsStr.size(); while (Ptr != End) { std::string Arg = ""; // advance while space while (Ptr != End && std::isspace(*Ptr)) ++Ptr; PrevPtr = Ptr; // advance while NOT space, also handle quotes while (Ptr != End && !std::isspace(*Ptr)) { // TODO: backslash escaping quotes for (char Quote : { '"', '\'', '`' }) { if (*Ptr == Quote) { // seek if there's a closing quote // if there is, go there and continue, otherwise ignore char* Seeker = Ptr + 1; while (Seeker != End && *Seeker != Quote) ++Seeker; if (Seeker != End) { // found closing quote Ptr = Seeker; } break; // exit for loop } } ++Ptr; } // this is required, otherwise we get negative int to unsigned cast in the next operations beammp_assert(PrevPtr <= Ptr); Arg = std::string(PrevPtr, std::string::size_type(Ptr - PrevPtr)); // remove quotes if enclosed in quotes for (char Quote : { '"', '\'', '`' }) { if (!Arg.empty() && Arg.at(0) == Quote && Arg.at(Arg.size() - 1) == Quote) { Arg = Arg.substr(1, Arg.size() - 2); break; } } if (!Arg.empty()) { Args.push_back(Arg); } } return { Command, Args }; } void TConsole::Command_Settings(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 1, 100)) { return; } static const char* SETTINGS_HELP = R"(Settings: settings help Displays this help settings list Lists all settings settings get Prints the current value of the specified setting settings set Sets the specified setting to the value)"; if (args.at(0) == "help") { Application::Console().WriteRaw(SETTINGS_HELP); } else if (args.at(0) == "list") { Application::Console().WriteRaw("Available settings:"); Application::Console().WriteRaw(fmt::format("\t{:<25} {}", "", "")); for (const auto& [k, v] : Application::mSettings) { if (k == StrAuthKey) { Application::Console().WriteRaw(fmt::format("\t{:<25} ", k, Application::SettingToString(v).size())); } else { Application::Console().WriteRaw(fmt::format("\t{:<25} {}", k, Application::SettingToString(v))); } } } else if (args.at(0) == "get") { if (args.size() < 2) { Application::Console().WriteRaw("Not enough arguments: `settings get` requires a setting name."); } else { if (Application::mSettings.contains(args.at(1))) { if (args.at(1) != StrAuthKey) { Application::Console().WriteRaw(fmt::format("{} = {}", args.at(1), Application::SettingToString(Application::mSettings.at(args.at(1))))); } else { Application::Console().WriteRaw(fmt::format("{} = ", args.at(1), Application::SettingToString(Application::mSettings.at(args.at(1))).size())); } } else { Application::Console().WriteRaw(fmt::format("Setting '{}' doesn't exist.", args.at(1))); } } } else if (args.at(0) == "set") { if (args.size() < 3) { Application::Console().WriteRaw("Not enough arguments: `settings set` requires a setting name and value."); } else { if (args.at(1) == StrAuthKey) { Application::Console().WriteRaw("It's not allowed to set the AuthKey during runtime."); } else { using namespace boost::spirit; using qi::_1; std::string ValueString = args.at(2); Application::SettingValue Value; qi::rule StringRule; StringRule %= qi::lexeme['"' >> *(qi::char_ - '"') >> '"'] | +(qi::char_ - '"'); qi::rule ValueRule = qi::bool_ | qi::int_ | StringRule; auto It = ValueString.begin(); if (qi::phrase_parse(It, ValueString.end(), ValueRule[boost::phoenix::ref(Value) = _1], ascii::space) && It == ValueString.end()) { Application::SetSetting(args.at(1), Value); Application::Console().WriteRaw(fmt::format("{} := {}", args.at(1), Application::SettingToString(Application::mSettings.at(args.at(1))))); } else { Application::Console().WriteRaw(fmt::format("New value '{}' did not parse as a valid value.", ValueString)); } } } } else { Application::Console().WriteRaw(fmt::format("Unknown argument '{}' - 'settings {}' is not a valid command.", args.at(0), args.at(0))); } } void TConsole::Command_Say(const std::string& FullCmd) { if (FullCmd.size() > 3) { auto Message = FullCmd.substr(4); LuaAPI::MP::SendChatMessage(-1, Message); if (!Application::GetSettingBool(StrLogChat)) { Application::Console().WriteRaw("Chat message sent!"); } } } void TConsole::Command_List(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 0)) { return; } if (mLuaEngine->Server().ClientCount() == 0) { Application::Console().WriteRaw("No players online."); } else { std::stringstream ss; ss << std::left << std::setw(25) << "Name" << std::setw(6) << "ID" << std::setw(6) << "Cars" << std::endl; mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { if (!Client.expired()) { auto locked = Client.lock(); ss << std::left << std::setw(25) << locked->GetName() << std::setw(6) << locked->GetID() << std::setw(6) << locked->GetCarCount() << "\n"; } return true; }); auto Str = ss.str(); Application::Console().WriteRaw(Str.substr(0, Str.size() - 1)); } } void TConsole::Command_Status(const std::string&, const std::vector& args) { if (!EnsureArgsCount(args, 0)) { return; } std::stringstream Status; size_t CarCount = 0; size_t ConnectedCount = 0; size_t GuestCount = 0; size_t SyncedCount = 0; size_t SyncingCount = 0; size_t MissedPacketQueueSum = 0; int LargestSecondsSinceLastPing = 0; mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { if (!Client.expired()) { auto Locked = Client.lock(); CarCount += Locked->GetCarCount(); ConnectedCount += Locked->IsConnected() ? 1 : 0; GuestCount += Locked->IsGuest() ? 1 : 0; SyncedCount += Locked->IsSynced() ? 1 : 0; SyncingCount += Locked->IsSyncing() ? 1 : 0; MissedPacketQueueSum += Locked->MissedPacketQueueSize(); if (Locked->SecondsSinceLastPing() < LargestSecondsSinceLastPing) { LargestSecondsSinceLastPing = Locked->SecondsSinceLastPing(); } } return true; }); size_t SystemsStarting = 0; size_t SystemsGood = 0; size_t SystemsBad = 0; size_t SystemsShuttingDown = 0; size_t SystemsShutdown = 0; std::string SystemsBadList {}; std::string SystemsGoodList {}; std::string SystemsStartingList {}; std::string SystemsShuttingDownList {}; std::string SystemsShutdownList {}; auto Statuses = Application::GetSubsystemStatuses(); for (const auto& NameStatusPair : Statuses) { switch (NameStatusPair.second) { case Application::Status::Good: SystemsGood++; SystemsGoodList += NameStatusPair.first + ", "; break; case Application::Status::Bad: SystemsBad++; SystemsBadList += NameStatusPair.first + ", "; break; case Application::Status::Starting: SystemsStarting++; SystemsStartingList += NameStatusPair.first + ", "; break; case Application::Status::ShuttingDown: SystemsShuttingDown++; SystemsShuttingDownList += NameStatusPair.first + ", "; break; case Application::Status::Shutdown: SystemsShutdown++; SystemsShutdownList += NameStatusPair.first + ", "; break; default: beammp_assert_not_reachable(); } } // remove ", " at the end SystemsBadList = SystemsBadList.substr(0, SystemsBadList.size() - 2); SystemsGoodList = SystemsGoodList.substr(0, SystemsGoodList.size() - 2); SystemsStartingList = SystemsStartingList.substr(0, SystemsStartingList.size() - 2); SystemsShuttingDownList = SystemsShuttingDownList.substr(0, SystemsShuttingDownList.size() - 2); SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2); auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime(); Status << "BeamMP-Server Status:\n" << "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n" << "\tSyncing Players: " << SyncingCount << "\n" << "\tSynced Players: " << SyncedCount << "\n" << "\tConnected Players: " << ConnectedCount << "\n" << "\tGuests: " << GuestCount << "\n" << "\tCars: " << CarCount << "\n" << "\tUptime: " << ElapsedTime << "ms (~" << size_t(double(ElapsedTime) / 1000.0 / 60.0 / 60.0) << "h) \n" << "\tLua:\n" << "\t\tQueued results to check: " << mLuaEngine->GetResultsToCheckSize() << "\n" << "\t\tStates: " << mLuaEngine->GetLuaStateCount() << "\n" << "\t\tEvent timers: " << mLuaEngine->GetTimedEventsCount() << "\n" << "\t\tEvent handlers: " << mLuaEngine->GetRegisteredEventHandlerCount() << "\n" << "\tSubsystems:\n" << "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n" << "\t\tShutting down/Shut down: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n" << "\t\tGood: [ " << SystemsGoodList << " ]\n" << "\t\tStarting: [ " << SystemsStartingList << " ]\n" << "\t\tBad: [ " << SystemsBadList << " ]\n" << "\t\tShutting down: [ " << SystemsShuttingDownList << " ]\n" << "\t\tShut down: [ " << SystemsShutdownList << " ]\n" << ""; Application::Console().WriteRaw(Status.str()); } void TConsole::Autocomplete_Lua(const std::string& stub, std::vector& suggestions) { auto stateNames = mLuaEngine->GetLuaStateNames(); for (const auto& name : stateNames) { if (name.find(stub) == 0) { suggestions.push_back("lua " + name); } } } void TConsole::Autocomplete_Kick(const std::string& stub, std::vector& suggestions) { std::string stub_lower = boost::algorithm::to_lower_copy(stub); mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { auto Locked = Client.lock(); if (Locked) { if (StringStartsWithLower(Locked->GetName(), stub_lower)) { suggestions.push_back("kick " + Locked->GetName()); } } return true; }); } void TConsole::Autocomplete_Settings(const std::string& stub, std::vector& suggestions) { const std::string subcommands[] = { "help", "list", "set", "get" }; auto [command, args] = ParseCommand(stub); std::string arg; if (!args.empty()) arg = boost::algorithm::to_lower_copy(args.at(0)); // suggest setting names if (command == "set" || command == "get") { for (const auto& [k, v] : Application::mSettings) { std::string key = std::string(k); if (StringStartsWithLower(key, arg)) { suggestions.push_back("settings " + command + " " + key); } } return; } // suggest subcommands for (const auto& cmd : subcommands) { if (cmd.find(command) == 0) { suggestions.push_back("settings " + cmd); } } } void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) { auto FutureIsNonNil = [](const std::shared_ptr& Future) { if (!Future->Error && Future->Result.valid()) { auto Type = Future->Result.get_type(); return Type != sol::type::lua_nil && Type != sol::type::none; } return false; }; std::vector> NonNilFutures; { // Futures scope auto Futures = mLuaEngine->TriggerEvent("onConsoleInput", "", cmd); TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5)); size_t Count = 0; for (auto& Future : Futures) { if (!Future->Error) { ++Count; } } for (const auto& Future : Futures) { if (FutureIsNonNil(Future)) { NonNilFutures.push_back(Future); } } } if (NonNilFutures.size() == 0) { if (!IgnoreNotACommand) { Application::Console().WriteRaw("Error: Unknown command: '" + cmd + "'. Type 'help' to see a list of valid commands."); } } else { std::stringstream Reply; if (NonNilFutures.size() > 1) { for (size_t i = 0; i < NonNilFutures.size(); ++i) { Reply << NonNilFutures[i]->StateId << ": \n" << LuaAPI::LuaToString(NonNilFutures[i]->Result); if (i < NonNilFutures.size() - 1) { Reply << "\n"; } } } else { Reply << LuaAPI::LuaToString(NonNilFutures[0]->Result); } Application::Console().WriteRaw(Reply.str()); } } void TConsole::HandleLuaInternalCommand(const std::string& cmd) { if (cmd == "exit") { ChangeToRegularConsole(); } else if (cmd == "queued") { auto QueuedFunctions = LuaAPI::MP::Engine->Debug_GetStateFunctionQueueForState(mStateId); Application::Console().WriteRaw("Pending functions in State '" + mStateId + "'"); std::unordered_map FunctionsCount; std::vector FunctionsInOrder; while (!QueuedFunctions.empty()) { auto Tuple = QueuedFunctions.front(); QueuedFunctions.erase(QueuedFunctions.begin()); FunctionsInOrder.push_back(Tuple.FunctionName); FunctionsCount[Tuple.FunctionName] += 1; } std::set Uniques; for (const auto& Function : FunctionsInOrder) { if (Uniques.count(Function) == 0) { Uniques.insert(Function); if (FunctionsCount.at(Function) > 1) { Application::Console().WriteRaw(" " + Function + " (" + std::to_string(FunctionsCount.at(Function)) + "x)"); } else { Application::Console().WriteRaw(" " + Function); } } } Application::Console().WriteRaw("Executed functions waiting to be checked in State '" + mStateId + "'"); for (const auto& Function : LuaAPI::MP::Engine->Debug_GetResultsToCheckForState(mStateId)) { Application::Console().WriteRaw(" '" + Function.Function + "' (Ready? " + (Function.Ready ? "Yes" : "No") + ", Error? " + (Function.Error ? "Yes: '" + Function.ErrorMessage + "'" : "No") + ")"); } } else if (cmd == "events") { auto Events = LuaAPI::MP::Engine->Debug_GetEventsForState(mStateId); Application::Console().WriteRaw("Registered Events + Handlers for State '" + mStateId + "'"); for (const auto& EventHandlerPair : Events) { Application::Console().WriteRaw(" Event '" + EventHandlerPair.first + "'"); for (const auto& Handler : EventHandlerPair.second) { Application::Console().WriteRaw(" " + Handler); } } } else if (cmd == "help") { Application::Console().WriteRaw(R"(BeamMP Lua Debugger All commands must be prefixed with a `:`. Non-prefixed commands are interpreted as Lua. Commands :exit detaches (exits) from this Lua console :help displays this help :events shows a list of currently registered events :queued shows a list of all pending and queued functions)"); } else { beammp_error("internal command '" + cmd + "' is not known"); } } TConsole::TConsole() { mCommandline.enable_history(); mCommandline.set_history_limit(20); BackupOldLog(); mCommandline.on_command = [this](Commandline& c) { try { auto TrimmedCmd = c.get_command(); TrimmedCmd = TrimString(TrimmedCmd); auto [cmd, args] = ParseCommand(TrimmedCmd); mCommandline.write(mCommandline.prompt() + TrimmedCmd); if (mIsLuaConsole) { if (!mLuaEngine) { beammp_info("Lua not started yet, please try again in a second"); } else if (!cmd.empty() && cmd.at(0) == ':') { HandleLuaInternalCommand(cmd.substr(1)); } else { auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared(TrimmedCmd), "", "" }); while (!Future->Ready) { std::this_thread::yield(); // TODO: Add a timeout } if (Future->Error) { beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage); } } } else { if (!mLuaEngine) { beammp_error("Attempted to run a command before Lua engine started. Please wait and try again."); } else if (cmd == "exit" || cmd == "quit") { beammp_info("gracefully shutting down"); Application::GracefullyShutdown(); } else if (cmd == "say") { RunAsCommand(TrimmedCmd, true); Command_Say(TrimmedCmd); } else { if (mCommandMap.find(cmd) != mCommandMap.end()) { mCommandMap.at(cmd)(cmd, args); RunAsCommand(TrimmedCmd, true); } else { RunAsCommand(TrimmedCmd); } } } } catch (const std::exception& e) { beammp_error("Console died with: " + std::string(e.what()) + ". This could be a fatal error and could cause the server to terminate."); } }; mCommandline.on_autocomplete = [this](Commandline&, std::string stub, int) { std::vector suggestions; try { if (mIsLuaConsole) { // if lua if (!mLuaEngine) { beammp_info("Lua not started yet, please try again in a second"); } else { std::string prefix {}; // stores non-table part of input for (size_t i = stub.length(); i > 0; i--) { // separate table from input if (!std::isalnum(stub[i - 1]) && stub[i - 1] != '_' && stub[i - 1] != '.') { prefix = stub.substr(0, i); stub = stub.substr(i); break; } } // turn string into vector of keys std::vector tablekeys; SplitString(stub, '.', tablekeys); // remove last key if incomplete if (stub.rfind('.') != stub.size() - 1 && !tablekeys.empty()) { tablekeys.pop_back(); } auto keys = mLuaEngine->GetStateTableKeysForState(mStateId, tablekeys); for (const auto& key : keys) { // go through each bottom-level key auto last_dot = stub.rfind('.'); std::string last_atom; if (last_dot != std::string::npos) { last_atom = stub.substr(last_dot + 1); } std::string before_last_atom = stub.substr(0, last_dot + 1); // get last confirmed key auto last = stub.substr(stub.rfind('.') + 1); std::string::size_type n = key.find(last); if (n == 0) { suggestions.push_back(prefix + before_last_atom + key); } } } } else { // if not lua for (const auto& [cmd_name, autocomplete_fn] : mCommandAutocompleteMap) { if (stub.find(cmd_name) == 0) { // input starts with a full command (that has autocomplete) std::size_t cmd_len = cmd_name.length(); std::string trimmed = TrimString(stub.substr(cmd_len)); autocomplete_fn(trimmed, suggestions); break; } } if (suggestions.empty()) { for (const auto& [cmd_name, cmd_fn] : mCommandMap) { if (cmd_name.find(stub) == 0) { suggestions.push_back(cmd_name); } } } } } catch (const std::exception& e) { beammp_error("Console died with: " + std::string(e.what()) + ". This could be a fatal error and could cause the server to terminate."); } std::sort(suggestions.begin(), suggestions.end()); return suggestions; }; } void TConsole::Write(const std::string& str) { auto ToWrite = GetDate() + str; mCommandline.write(ToWrite); } void TConsole::WriteRaw(const std::string& str) { mCommandline.write(str); } void TConsole::InitializeLuaConsole(TLuaEngine& Engine) { mLuaEngine = &Engine; }