// BeamMP, the BeamNG.drive multiplayer mod. // Copyright (C) 2026 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 "TLuaResult.h" #include #include #include #include void TLuaResult::MarkReadySuccess(sol::object Res) { std::unique_lock Lock(mMutex); mError = false; mResult = Res; mDetachedResult = Freeze(Res); MarkAsReady(); } void TLuaResult::MarkReadyError(sol::protected_function_result Res) { std::unique_lock Lock(mMutex); mError = true; SetErrorMessageFromResult(Res); MarkAsReady(); } void TLuaResult::MarkReadyError(std::string Res) { std::unique_lock Lock(mMutex); mError = true; mErrorMessage = std::move(Res); MarkAsReady(); } bool TLuaResult::IsReady() const { std::unique_lock Lock(mMutex); return mReady; } bool TLuaResult::IsError() const { std::unique_lock Lock(mMutex); return mError; } TLuaStateId TLuaResult::OwnerState() const { std::unique_lock Lock(mMutex); // copy return mStateId; } void TLuaResult::SetOwnerState(TLuaStateId StateId) { std::unique_lock Lock(mMutex); mStateId = std::move(StateId); } TLuaResult::Snapshot TLuaResult::GetSnapshot(TLuaStateId CallingState) const { std::unique_lock Lock(mMutex); if (CallingState != mStateId) { throw std::logic_error("Tried to get snapshot from non-owning state (use a detached snapshot instead)"); } Snapshot snapshot { .Error = mError, .Ready = mReady, .ErrorMessage = mErrorMessage, .Result = mResult, .StateId = mStateId, .Function = mFunction, }; return snapshot; } TLuaResult::DetachedSnapshot TLuaResult::GetDetachedSnapshot() const { std::unique_lock Lock(mMutex); DetachedSnapshot snapshot { .Error = mError, .Ready = mReady, .ErrorMessage = mErrorMessage, .Result = mDetachedResult, .StateId = mStateId, .Function = mFunction, }; return snapshot; } TDetachedLuaValue TLuaResult::Freeze(const sol::object& o, int depth) { if (depth > 64) throw std::runtime_error("max depth (64) reached"); switch (o.get_type()) { case sol::type::lua_nil: return { { std::monostate { } } }; case sol::type::boolean: return { { o.as() } }; case sol::type::number: { if (o.is()) { return { { o.as() } }; } else { return { { o.as() } }; } } case sol::type::string: return { { o.as() } }; case sol::type::table: { TDetachedLuaValue::Object out; for (auto&& [k, v] : o.as()) { if (!k.is()) continue; // no numeric-key handling, don't need it out.emplace(k.as(), Freeze(v, depth + 1)); } return { { std::move(out) } }; } default: throw std::runtime_error("unsupported Lua type for cross-thread snapshot"); } } void TLuaResult::MarkAsReady() { mReady = true; mReadyCondition.notify_all(); } void TLuaResult::WaitUntilReady() { std::unique_lock Lock(mMutex); while (!mReady) mReadyCondition.wait_for(Lock, std::chrono::milliseconds(50), [this] { return mReady; }); } TEST_CASE("TLuaResult MarkReadyError(string) marks ready and wakes waiters") { TLuaResult result("state_a", "fn_a"); std::atomic waiterDone { false }; auto waiter = std::thread([&] { result.WaitUntilReady(); waiterDone.store(true, std::memory_order_release); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); CHECK_FALSE(waiterDone.load(std::memory_order_acquire)); result.MarkReadyError(std::string("boom")); waiter.join(); CHECK(result.IsReady()); const auto snapshot = result.GetDetachedSnapshot(); CHECK(snapshot.Ready); CHECK(snapshot.Error); CHECK(snapshot.ErrorMessage == "boom"); CHECK(snapshot.StateId == "state_a"); CHECK(snapshot.Function == "fn_a"); } TEST_CASE("TLuaResult GetSnapshot enforces owner state id") { TLuaResult result("owner_state", "fn_owner"); sol::state lua; lua.open_libraries(sol::lib::base); result.MarkReadySuccess(sol::make_object(lua.lua_state(), std::string("ok"))); CHECK_NOTHROW(result.GetSnapshot("owner_state")); CHECK_THROWS_AS(result.GetSnapshot("different_state"), std::logic_error); } TEST_CASE("TLuaResult detached snapshot freezes nested string-keyed tables") { TLuaResult result("state_table", "fn_table"); sol::state lua; lua.open_libraries(sol::lib::base); auto outer = lua.create_table(); auto inner = lua.create_table(); inner["k"] = std::string("v"); outer["flag"] = true; outer["msg"] = std::string("hello"); outer["inner"] = inner; outer[1] = std::string("ignored_numeric_key"); result.MarkReadySuccess(sol::make_object(lua.lua_state(), outer)); const auto detached = result.GetDetachedSnapshot(); CHECK(detached.Ready); CHECK_FALSE(detached.Error); const auto* object = std::get_if(&detached.Result.V); REQUIRE(object != nullptr); CHECK(object->contains("flag")); CHECK(object->contains("msg")); CHECK(object->contains("inner")); CHECK_FALSE(object->contains("1")); const auto* flag = std::get_if(&object->at("flag").V); REQUIRE(flag != nullptr); CHECK(*flag); const auto* msg = std::get_if(&object->at("msg").V); REQUIRE(msg != nullptr); CHECK(*msg == "hello"); const auto* innerObj = std::get_if(&object->at("inner").V); REQUIRE(innerObj != nullptr); REQUIRE(innerObj->contains("k")); const auto* innerValue = std::get_if(&innerObj->at("k").V); REQUIRE(innerValue != nullptr); CHECK(*innerValue == "v"); } TEST_CASE("TLuaResult MarkReadySuccess throws on unsupported Lua function value") { TLuaResult result("state_fn", "fn_fn"); sol::state lua; lua.open_libraries(sol::lib::base); lua["f"] = [] { return 1; }; const sol::table globals = lua.globals(); const sol::protected_function fn = globals.get("f"); const sol::object fnObj = sol::make_object(lua.lua_state(), fn); CHECK_THROWS_AS(result.MarkReadySuccess(fnObj), std::runtime_error); CHECK_FALSE(result.IsReady()); }