// BeamMP, the BeamNG.drive multiplayer mod. // Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors. // // BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program 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 Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include "LuaAPI.h" #include "Common.h" #include "CustomAssert.h" #include "Value.h" #include #include #include #define SOL_ALL_SAFETIES_ON 1 #include std::string LuaAPI::MP::GetOSName() { #if WIN32 return "Windows"; #elif __linux return "Linux"; #else return "Other"; #endif } std::tuple LuaAPI::MP::GetServerVersion() { return { Application::ServerVersion().major, Application::ServerVersion().minor, Application::ServerVersion().patch }; } TEST_CASE("LuaAPI::MP::GetServerVersion") { const auto [ma, mi, pa] = LuaAPI::MP::GetServerVersion(); const auto real = Application::ServerVersion(); CHECK(ma == real.major); CHECK(mi == real.minor); CHECK(pa == real.patch); } static inline std::pair InternalTriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data) { throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__)); /* std::string Packet = "E:" + EventName + ":" + Data; if (PlayerID == -1) { LuaAPI::MP::Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true); return { true, "" }; } else { auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID); if (!MaybeClient) { beammp_errorff("TriggerClientEvent invalid Player ID '{}'", PlayerID); return { false, "Invalid Player ID" }; } auto c = MaybeClient.value(); if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) { beammp_errorff("Respond failed, dropping client {}", PlayerID); LuaAPI::MP::Engine->Network().Disconnect(*c); return { false, "Respond failed, dropping client" }; } return { true, "" }; }*/ } std::pair LuaAPI::MP::TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& DataObj) { std::string Data = DataObj.as(); return InternalTriggerClientEvent(PlayerID, EventName, Data); } std::pair LuaAPI::MP::DropPlayer(int ID, std::optional MaybeReason) { throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__)); /* auto MaybeClient = GetClient(Engine->Server(), ID); if (!MaybeClient) { beammp_errorff("Tried to drop client with id {}, who doesn't exist", ID); return { false, "Player does not exist" }; } auto c = MaybeClient.value(); LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason")); return { true, "" }; */ } std::pair LuaAPI::MP::SendChatMessage(int ID, const std::string& Message) { throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__)); /* std::pair Result; std::string Packet = "C:Server: " + Message; if (ID == -1) { LogChatMessage(" (to everyone) ", -1, Message); Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true); Result.first = true; } else { auto MaybeClient = GetClient(Engine->Server(), ID); if (MaybeClient) { auto c = MaybeClient.value(); if (!c->IsSynced) { Result.first = false; Result.second = "Player still syncing data"; return Result; } LogChatMessage(" (to \"" + c->Name.get() + "\")", -1, Message); if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) { beammp_errorf("Failed to send chat message back to sender (id {}) - did the sender disconnect?", ID); beammp_infof("Disconnecting client {} for failure to receive a chat message (TCP disconnect)", c->Name.get()); Engine->Network().Disconnect(c); } Result.first = true; } else { beammp_errorf("SendChatMessage invalid argument [1] invalid ID"); Result.first = false; Result.second = "Invalid Player ID"; } return Result; } return Result; */ } std::pair LuaAPI::MP::RemoveVehicle(int PID, int VID) { throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__)); /* std::pair Result; auto MaybeClient = GetClient(Engine->Server(), PID); if (!MaybeClient) { beammp_errorf("RemoveVehicle invalid Player ID"); Result.first = false; Result.second = "Invalid Player ID"; return Result; } auto c = MaybeClient.value(); if (!c->GetCarData(VID).empty()) { std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID); Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true); c->DeleteCar(VID); Result.first = true; } else { Result.first = false; Result.second = "Vehicle does not exist"; } return Result; */ } void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) { switch (ConfigID) { case 0: // debug if (NewValue.is()) { Application::Settings.DebugModeEnabled = NewValue.as(); beammp_info(std::string("Set `Debug` to ") + (Application::Settings.DebugModeEnabled ? "true" : "false")); } else { beammp_errorf("set invalid argument [2] expected boolean"); } break; case 1: // private if (NewValue.is()) { Application::Settings.Private = NewValue.as(); beammp_info(std::string("Set `Private` to ") + (Application::Settings.Private ? "true" : "false")); } else { beammp_errorf("set invalid argument [2] expected boolean"); } break; case 2: // max cars if (NewValue.is()) { Application::Settings.MaxCars = NewValue.as(); beammp_info(std::string("Set `MaxCars` to ") + std::to_string(Application::Settings.MaxCars)); } else { beammp_errorf("set invalid argument [2] expected integer"); } break; case 3: // max players if (NewValue.is()) { Application::Settings.MaxPlayers = NewValue.as(); beammp_info(std::string("Set `MaxPlayers` to ") + std::to_string(Application::Settings.MaxPlayers)); } else { beammp_errorf("set invalid argument [2] expected integer"); } break; case 4: // Map if (NewValue.is()) { Application::Settings.MapName = NewValue.as(); beammp_info(std::string("Set `Map` to ") + Application::Settings.MapName); } else { beammp_errorf("set invalid argument [2] expected string"); } break; case 5: // Name if (NewValue.is()) { Application::Settings.ServerName = NewValue.as(); beammp_info(std::string("Set `Name` to ") + Application::Settings.ServerName); } else { beammp_errorf("set invalid argument [2] expected string"); } break; case 6: // Desc if (NewValue.is()) { Application::Settings.ServerDesc = NewValue.as(); beammp_info(std::string("Set `Description` to ") + Application::Settings.ServerDesc); } else { beammp_errorf("set invalid argument [2] expected string"); } break; default: beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this."); break; } } bool LuaAPI::MP::IsPlayerConnected(int ID) { throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__)); /* auto MaybeClient = GetClient(Engine->Server(), ID); if (MaybeClient) { return MaybeClient.value()->IsConnected.get(); } else { return false; }*/ } bool LuaAPI::MP::IsPlayerGuest(int ID) { throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__)); /* auto MaybeClient = GetClient(Engine->Server(), ID); if (MaybeClient) { return MaybeClient.value()->IsGuest.get(); } else { return false; } */ } template static std::pair FSWrapper(FnT Fn, ArgsT&&... Args) { std::error_code errc; std::pair Result; Fn(std::forward(Args)..., errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); } return Result; } std::pair LuaAPI::FS::CreateDirectory(const std::string& Path) { std::error_code errc; std::pair Result; fs::create_directories(Path, errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); } return Result; } TEST_CASE("LuaAPI::FS::CreateDirectory") { std::string TestDir = "beammp_test_dir"; fs::remove_all(TestDir); SUBCASE("Single level dir") { const auto [Ok, Err] = LuaAPI::FS::CreateDirectory(TestDir); CHECK(Ok); CHECK(Err == ""); CHECK(fs::exists(TestDir)); } SUBCASE("Multi level dir") { const auto [Ok, Err] = LuaAPI::FS::CreateDirectory(TestDir + "/a/b/c"); CHECK(Ok); CHECK(Err == ""); CHECK(fs::exists(TestDir + "/a/b/c")); } SUBCASE("Already exists") { const auto [Ok, Err] = LuaAPI::FS::CreateDirectory(TestDir); CHECK(Ok); CHECK(Err == ""); CHECK(fs::exists(TestDir)); const auto [Ok2, Err2] = LuaAPI::FS::CreateDirectory(TestDir); CHECK(Ok2); CHECK(Err2 == ""); } fs::remove_all(TestDir); } std::pair LuaAPI::FS::Remove(const std::string& Path) { std::error_code errc; std::pair Result; fs::remove(fs::relative(Path), errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); } return Result; } TEST_CASE("LuaAPI::FS::Remove") { const std::string TestFileOrDir = "beammp_test_thing"; SUBCASE("Remove existing directory") { fs::create_directory(TestFileOrDir); const auto [Ok, Err] = LuaAPI::FS::Remove(TestFileOrDir); CHECK(Ok); CHECK_EQ(Err, ""); CHECK(!fs::exists(TestFileOrDir)); } SUBCASE("Remove non-existing directory") { fs::remove_all(TestFileOrDir); const auto [Ok, Err] = LuaAPI::FS::Remove(TestFileOrDir); CHECK(Ok); CHECK_EQ(Err, ""); CHECK(!fs::exists(TestFileOrDir)); } // TODO: add tests for files // TODO: add tests for files and folders without access permissions (failure) } std::pair LuaAPI::FS::Rename(const std::string& Path, const std::string& NewPath) { std::error_code errc; std::pair Result; fs::rename(Path, NewPath, errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); } return Result; } TEST_CASE("LuaAPI::FS::Rename") { const auto TestDir = "beammp_test_dir"; const auto OtherTestDir = "beammp_test_dir_2"; fs::remove_all(OtherTestDir); fs::create_directory(TestDir); const auto [Ok, Err] = LuaAPI::FS::Rename(TestDir, OtherTestDir); CHECK(Ok); CHECK_EQ(Err, ""); CHECK(!fs::exists(TestDir)); CHECK(fs::exists(OtherTestDir)); fs::remove_all(OtherTestDir); fs::remove_all(TestDir); } std::pair LuaAPI::FS::Copy(const std::string& Path, const std::string& NewPath) { std::error_code errc; std::pair Result; fs::copy(Path, NewPath, fs::copy_options::recursive, errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); } return Result; } TEST_CASE("LuaAPI::FS::Copy") { const auto TestDir = "beammp_test_dir"; const auto OtherTestDir = "beammp_test_dir_2"; fs::remove_all(OtherTestDir); fs::create_directory(TestDir); const auto [Ok, Err] = LuaAPI::FS::Copy(TestDir, OtherTestDir); CHECK(Ok); CHECK_EQ(Err, ""); CHECK(fs::exists(TestDir)); CHECK(fs::exists(OtherTestDir)); fs::remove_all(OtherTestDir); fs::remove_all(TestDir); } bool LuaAPI::FS::Exists(const std::string& Path) { return fs::exists(Path); } TEST_CASE("LuaAPI::FS::Exists") { const auto TestDir = "beammp_test_dir"; const auto OtherTestDir = "beammp_test_dir_2"; fs::remove_all(OtherTestDir); fs::create_directory(TestDir); CHECK(LuaAPI::FS::Exists(TestDir)); CHECK(!LuaAPI::FS::Exists(OtherTestDir)); fs::remove_all(OtherTestDir); fs::remove_all(TestDir); } std::string LuaAPI::FS::GetFilename(const std::string& Path) { return fs::path(Path).filename().string(); } TEST_CASE("LuaAPI::FS::GetFilename") { CHECK(LuaAPI::FS::GetFilename("test.txt") == "test.txt"); CHECK(LuaAPI::FS::GetFilename("/test.txt") == "test.txt"); CHECK(LuaAPI::FS::GetFilename("place/test.txt") == "test.txt"); CHECK(LuaAPI::FS::GetFilename("/some/../place/test.txt") == "test.txt"); } std::string LuaAPI::FS::GetExtension(const std::string& Path) { return fs::path(Path).extension().string(); } TEST_CASE("LuaAPI::FS::GetExtension") { CHECK(LuaAPI::FS::GetExtension("test.txt") == ".txt"); CHECK(LuaAPI::FS::GetExtension("/test.txt") == ".txt"); CHECK(LuaAPI::FS::GetExtension("place/test.txt") == ".txt"); CHECK(LuaAPI::FS::GetExtension("/some/../place/test.txt") == ".txt"); CHECK(LuaAPI::FS::GetExtension("/some/../place/test") == ""); CHECK(LuaAPI::FS::GetExtension("/some/../place/test.a.b.c") == ".c"); CHECK(LuaAPI::FS::GetExtension("/some/../place/test.") == "."); CHECK(LuaAPI::FS::GetExtension("/some/../place/test.a.b.") == "."); } std::string LuaAPI::FS::GetParentFolder(const std::string& Path) { return fs::path(Path).parent_path().string(); } TEST_CASE("LuaAPI::FS::GetParentFolder") { CHECK(LuaAPI::FS::GetParentFolder("test.txt") == ""); CHECK(LuaAPI::FS::GetParentFolder("/test.txt") == "/"); CHECK(LuaAPI::FS::GetParentFolder("place/test.txt") == "place"); CHECK(LuaAPI::FS::GetParentFolder("/some/../place/test.txt") == "/some/../place"); } // TODO: add tests bool LuaAPI::FS::IsDirectory(const std::string& Path) { return fs::is_directory(Path); } // TODO: add tests bool LuaAPI::FS::IsFile(const std::string& Path) { return fs::is_regular_file(Path); } // TODO: add tests std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) { fs::path Path; for (size_t i = 0; i < Args.size(); ++i) { auto Obj = Args[i]; if (!Obj.is()) { beammp_errorf("FS.Concat called with non-string argument"); return ""; } Path += Obj.as(); if (i < Args.size() - 1 && !Path.empty()) { Path += fs::path::preferred_separator; } } auto Result = Path.lexically_normal().string(); return Result; } static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, const sol::object& right, bool is_array, size_t depth = 0) { if (depth > 100) { beammp_errorf("json serialize will not go deeper than 100 nested tables, internal references assumed, aborted this path"); return; } std::string key {}; switch (left.get_type()) { case sol::type::lua_nil: case sol::type::none: case sol::type::poly: case sol::type::boolean: case sol::type::lightuserdata: case sol::type::userdata: case sol::type::thread: case sol::type::function: case sol::type::table: beammp_errorf("JsonEncode: left side of table field is unexpected type"); return; case sol::type::string: key = left.as(); break; case sol::type::number: key = std::to_string(left.as()); break; default: beammp_assert_not_reachable(); } nlohmann::json value; switch (right.get_type()) { case sol::type::lua_nil: case sol::type::none: return; case sol::type::poly: beammp_lua_warn("unsure what to do with poly type in JsonEncode, ignoring"); return; case sol::type::boolean: value = right.as(); break; case sol::type::lightuserdata: beammp_lua_warn("unsure what to do with lightuserdata in JsonEncode, ignoring"); return; case sol::type::userdata: beammp_lua_warn("unsure what to do with userdata in JsonEncode, ignoring"); return; case sol::type::thread: beammp_lua_warn("unsure what to do with thread in JsonEncode, ignoring"); return; case sol::type::string: value = right.as(); break; case sol::type::number: value = right.as(); break; case sol::type::function: beammp_lua_warn("unsure what to do with function in JsonEncode, ignoring"); return; case sol::type::table: { bool local_is_array = true; for (const auto& pair : right.as()) { if (pair.first.get_type() != sol::type::number) { local_is_array = false; } } for (const auto& pair : right.as()) { JsonEncodeRecursive(value, pair.first, pair.second, local_is_array, depth + 1); } break; } default: beammp_assert_not_reachable(); } if (is_array) { json.push_back(value); } else { json[key] = value; } } static std::string lua_to_json_impl(const sol::object& obj) { // used as the invalid value provider in sol_obj_to_value. auto special_stringifier = [](const sol::object& object) -> Result { beammp_debugf("Cannot convert from type {} to json, ignoring (using null)", int(object.get_type())); return { Null {} }; }; auto maybe_val = sol_obj_to_value(obj, special_stringifier); if (maybe_val) { auto result = boost::apply_visitor(ValueToJsonVisitor {}, maybe_val.move()); return boost::json::serialize(result); } else { beammp_errorf("Failed to convert an argument to json: {}", maybe_val.error); return ""; } } std::string LuaAPI::Util::JsonEncode(const sol::object& object) { return lua_to_json_impl(object); } std::string LuaAPI::Util::JsonDiff(const std::string& a, const std::string& b) { if (!nlohmann::json::accept(a)) { beammp_error("JsonDiff first argument is not valid json: `" + a + "`"); return ""; } if (!nlohmann::json::accept(b)) { beammp_error("JsonDiff second argument is not valid json: `" + b + "`"); return ""; } auto a_json = nlohmann::json::parse(a); auto b_json = nlohmann::json::parse(b); return nlohmann::json::diff(a_json, b_json).dump(); } std::string LuaAPI::Util::JsonDiffApply(const std::string& data, const std::string& patch) { if (!nlohmann::json::accept(data)) { beammp_error("JsonDiffApply first argument is not valid json: `" + data + "`"); return ""; } if (!nlohmann::json::accept(patch)) { beammp_error("JsonDiffApply second argument is not valid json: `" + patch + "`"); return ""; } auto a_json = nlohmann::json::parse(data); auto b_json = nlohmann::json::parse(patch); a_json.patch(b_json); return a_json.dump(); } std::string LuaAPI::Util::JsonPrettify(const std::string& json) { if (!nlohmann::json::accept(json)) { beammp_error("JsonPrettify argument is not valid json: `" + json + "`"); return ""; } return nlohmann::json::parse(json).dump(4); } std::string LuaAPI::Util::JsonMinify(const std::string& json) { if (!nlohmann::json::accept(json)) { beammp_error("JsonMinify argument is not valid json: `" + json + "`"); return ""; } return nlohmann::json::parse(json).dump(-1); } std::string LuaAPI::Util::JsonFlatten(const std::string& json) { if (!nlohmann::json::accept(json)) { beammp_error("JsonFlatten argument is not valid json: `" + json + "`"); return ""; } return nlohmann::json::parse(json).flatten().dump(-1); } std::string LuaAPI::Util::JsonUnflatten(const std::string& json) { if (!nlohmann::json::accept(json)) { beammp_error("JsonUnflatten argument is not valid json: `" + json + "`"); return ""; } return nlohmann::json::parse(json).unflatten().dump(-1); } std::pair LuaAPI::MP::TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data) { return InternalTriggerClientEvent(PlayerID, EventName, Util::JsonEncode(Data)); } size_t LuaAPI::MP::GetPlayerCount() { return 0; } template static void AddToTable(sol::table& table, const std::string& left, const T& value) { if (left.empty()) { table[table.size() + 1] = value; } else { table[left] = value; } } static void JsonDecodeRecursive(sol::state_view& StateView, sol::table& table, const std::string& left, const nlohmann::json& right) { switch (right.type()) { case nlohmann::detail::value_t::null: return; case nlohmann::detail::value_t::object: { auto value = table.create(); value.clear(); for (const auto& entry : right.items()) { JsonDecodeRecursive(StateView, value, entry.key(), entry.value()); } AddToTable(table, left, value); break; } case nlohmann::detail::value_t::array: { auto value = table.create(); value.clear(); for (const auto& entry : right.items()) { JsonDecodeRecursive(StateView, value, "", entry.value()); } AddToTable(table, left, value); break; } case nlohmann::detail::value_t::string: AddToTable(table, left, right.get()); break; case nlohmann::detail::value_t::boolean: AddToTable(table, left, right.get()); break; case nlohmann::detail::value_t::number_integer: AddToTable(table, left, right.get()); break; case nlohmann::detail::value_t::number_unsigned: AddToTable(table, left, right.get()); break; case nlohmann::detail::value_t::number_float: AddToTable(table, left, right.get()); break; case nlohmann::detail::value_t::binary: beammp_errorf("JsonDecode can't handle binary blob in json, ignoring"); return; case nlohmann::detail::value_t::discarded: return; default: beammp_assert_not_reachable(); } } sol::table LuaAPI::Util::JsonDecode(sol::this_state s, const std::string& str) { sol::state_view StateView(s.lua_state()); auto table = StateView.create_table(StateView); if (!nlohmann::json::accept(str)) { beammp_error("string given to JsonDecode is not valid json: `" + str + "`"); return sol::lua_nil; } nlohmann::json json = nlohmann::json::parse(str); if (json.is_object()) { for (const auto& entry : json.items()) { JsonDecodeRecursive(StateView, table, entry.key(), entry.value()); } } else if (json.is_array()) { for (const auto& entry : json) { JsonDecodeRecursive(StateView, table, "", entry); } } else { beammp_error("JsonDecode expected array or object json, instead got " + std::string(json.type_name())); return sol::lua_nil; } return table; } sol::table LuaAPI::FS::ListDirectories(sol::this_state s, const std::string& Path) { if (!std::filesystem::exists(Path)) { return sol::lua_nil; } auto table = sol::state_view(s.lua_state()).create_table(); for (const auto& entry : std::filesystem::directory_iterator(Path)) { if (entry.is_directory()) { table[table.size() + 1] = entry.path().lexically_relative(Path).string(); } } return table; } sol::table LuaAPI::FS::ListFiles(sol::this_state s, const std::string& Path) { if (!std::filesystem::exists(Path)) { return sol::lua_nil; } auto table = sol::state_view(s.lua_state()).create_table(); for (const auto& entry : std::filesystem::directory_iterator(Path)) { if (entry.is_regular_file() || entry.is_symlink()) { table[table.size() + 1] = entry.path().lexically_relative(Path).string(); } } return table; }