From 4104dc1f189e3986ea964d6850340e9bc17fc0d0 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Wed, 29 Apr 2026 18:29:02 +0000 Subject: [PATCH] fix lua result serialization causing various issues for example, loading a library which returns a table of functions would fail with an inexplicable (to the user) error about serialization. This is now fixed. We no longer use a single result type, instead there are void results and normal results, and the former simply never has to worry about the result. This was likely causing even more issues; if the `sol::object` was populated before, it hitting the dtor in another path would, again, like we've seen before, corrupt the lua stack. --- include/TLuaEngine.h | 10 +- include/TLuaResult.h | 63 +++++++++--- src/TConsole.cpp | 2 +- src/TLuaEngine.cpp | 123 ++++++++++++++--------- src/TLuaPlugin.cpp | 4 +- src/TLuaResult.cpp | 215 +++++++++++++++++++++++++++++++++++------ src/TPluginMonitor.cpp | 2 +- 7 files changed, 319 insertions(+), 100 deletions(-) diff --git a/include/TLuaEngine.h b/include/TLuaEngine.h index e0e1f9b..56555fe 100644 --- a/include/TLuaEngine.h +++ b/include/TLuaEngine.h @@ -141,7 +141,7 @@ public: const std::optional& Max = std::nullopt); void ReportErrors(const std::vector>& Results); bool HasState(TLuaStateId StateId); - [[nodiscard]] std::shared_ptr EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script); + [[nodiscard]] std::shared_ptr EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script); [[nodiscard]] std::shared_ptr EnqueueFunctionCall(TLuaStateId StateID, const std::string& FunctionName, const std::vector& Args, const std::string& EventName); void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false); void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName); @@ -202,7 +202,7 @@ public: // Debugging functions (slow) std::unordered_map /* handlers */> Debug_GetEventsForState(TLuaStateId StateId); - std::queue>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId); + std::queue>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId); std::vector Debug_GetStateFunctionQueueForState(TLuaStateId StateId); std::vector Debug_GetResultsToCheckForState(TLuaStateId StateId); @@ -217,7 +217,7 @@ private: StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine); StateThreadData(const StateThreadData&) = delete; virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); } - [[nodiscard]] std::shared_ptr EnqueueScript(const TLuaChunk& Script); + [[nodiscard]] std::shared_ptr EnqueueScript(const TLuaChunk& Script); [[nodiscard]] std::shared_ptr EnqueueFunctionCall(const std::string& FunctionName, const std::vector& Args, const std::string& EventName); [[nodiscard]] std::shared_ptr EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector& Args, const std::string& EventName, CallStrategy Strategy); void RegisterEvent(const std::string& EventName, const std::string& FunctionName); @@ -229,7 +229,7 @@ private: std::vector GetStateTableKeys(const std::vector& keys); // Debug functions, slow - std::queue>> Debug_GetStateExecuteQueue(); + std::queue>> Debug_GetStateExecuteQueue(); std::vector Debug_GetStateFunctionQueue(); private: @@ -254,7 +254,7 @@ private: TLuaStateId mStateId; lua_State* mState; std::thread mThread; - std::queue>> mStateExecuteQueue; + std::queue>> mStateExecuteQueue; std::recursive_mutex mStateExecuteQueueMutex; std::vector mStateFunctionQueue; std::mutex mStateFunctionQueueMutex; diff --git a/include/TLuaResult.h b/include/TLuaResult.h index a75da92..d5b892e 100644 --- a/include/TLuaResult.h +++ b/include/TLuaResult.h @@ -37,19 +37,54 @@ struct TDetachedLuaValue { }; std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value); -struct TLuaResult { +/// Result for operations that stay within one Lua state (e.g. script loads). +/// This type intentionally does not store any Lua references or serialized Lua values. +struct TLuaVoidResult { struct Snapshot { bool Error; bool Ready; std::string ErrorMessage; - /// CAUTION: Accessing this object causes the Lua stack of the owning state - /// to be accessed. Only call this from the owning Lua state. Otherwise, - /// use `DetachedSnapshot`. - sol::object Result; TLuaStateId StateId; - std::string Function; }; + explicit TLuaVoidResult(TLuaStateId StateId) + : mStateId(std::move(StateId)) {} + + /// Marks this result as success & sets it as ready, waking all waiting threads. + void MarkReadySuccess(); + /// Marks this result as erroneous & sets it as ready, waking all waiting threads. + void MarkReadyError(sol::protected_function_result Res); + /// Marks this result as erroneous & sets it as ready, waking all waiting threads. + void MarkReadyError(std::string Res); + /// Wait (suspend) until this result is ready. + void WaitUntilReady(); + bool IsReady() const; + bool IsError() const; + Snapshot GetSnapshot() const; + +private: + mutable std::mutex mMutex {}; + bool mReady { false }; + bool mError { false }; + std::string mErrorMessage; + TLuaStateId mStateId; + std::condition_variable mReadyCondition {}; + + void SetErrorMessageFromResult(sol::protected_function_result& Res) { + if (Res.valid()) { + beammp_lua_errorf("Error was not an error"); + } + if (Res.get_type() == sol::type::string) { + mErrorMessage = Res.get().what(); + } else { + mErrorMessage = "(unknown error; error object is not inspectable)"; + } + } + + void MarkAsReady(); +}; + +struct TLuaResult { struct DetachedSnapshot { bool Error; bool Ready; @@ -66,33 +101,29 @@ struct TLuaResult { /// Marks this result as success & sets it as ready, waking all threads waiting on it. void MarkReadySuccess(sol::object Res); + /// Marks this result as successful and ready without serializing a Lua value. + void MarkReadySuccessNoResult(); /// Marks this result as erroneous & sets it as ready, waking all threads waiting on it. void MarkReadyError(sol::protected_function_result Res); /// Marks this result as erroneous & sets it as ready, waking all threads waiting on it. void MarkReadyError(std::string Res); - /// Wait (suspend) until this result is ready. Use `GetSnapshot` to get the results. + /// Wait (suspend) until this result is ready. Use `GetDetachedSnapshot` to get the result. void WaitUntilReady(); bool IsReady() const; bool IsError() const; TLuaStateId OwnerState() const; void SetOwnerState(TLuaStateId StateId); - /// To get the result or error, while in the owning state, use this function. - /// Pass your lua state ID so that the function can verify the safety of the access. - /// If you have no state ID, use `GetDetachedSnapshot`. - /// Accesses the Lua state directly when using the result. - Snapshot GetSnapshot(TLuaStateId CallingState) const; /// Returns a snapshot with, if not an error, a serialized version of the result - /// object. In contrast to `GetSnapshot`, this does not access the Lua stack when - /// accessing `.Result`. + /// object. This never stores or returns raw Lua references and is safe to use + /// across threads. DetachedSnapshot GetDetachedSnapshot() const; private: mutable std::mutex mMutex {}; bool mReady { false }; - bool mError; + bool mError { false }; std::string mErrorMessage; - sol::object mResult { sol::lua_nil }; TDetachedLuaValue mDetachedResult; TLuaStateId mStateId; std::string mFunction; diff --git a/src/TConsole.cpp b/src/TConsole.cpp index 2aca3a0..2710914 100644 --- a/src/TConsole.cpp +++ b/src/TConsole.cpp @@ -810,7 +810,7 @@ void TConsole::InitializeCommandline() { auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared(TrimmedCmd), "", "" }); Future->WaitUntilReady(); if (Future->IsError()) { - auto Snapshot = Future->GetDetachedSnapshot(); + auto Snapshot = Future->GetSnapshot(); beammp_lua_error("error in " + mStateId + ": " + Snapshot.ErrorMessage); } } diff --git a/src/TLuaEngine.cpp b/src/TLuaEngine.cpp index c0e3a28..f92ff3c 100644 --- a/src/TLuaEngine.cpp +++ b/src/TLuaEngine.cpp @@ -209,8 +209,8 @@ std::unordered_map /* han return Result; } -std::queue>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) { - std::queue>> Result; +std::queue>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) { + std::queue>> Result; std::unique_lock Lock(mLuaStatesMutex); Result = mLuaStates.at(StateId)->Debug_GetStateExecuteQueue(); return Result; @@ -362,7 +362,7 @@ bool TLuaEngine::HasState(TLuaStateId StateId) { return mLuaStates.find(StateId) != mLuaStates.end(); } -std::shared_ptr TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) { +std::shared_ptr TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) { std::unique_lock Lock(mLuaStatesMutex); return mLuaStates.at(StateID)->EnqueueScript(Script); } @@ -444,7 +444,7 @@ void TLuaEngine::EnsureStateExists(TLuaStateId StateId, const std::string& Name, if (!DontCallOnInit) { auto Res = EnqueueFunctionCall(StateId, "onInit", { }, "onInit"); Res->WaitUntilReady(); - auto Snapshot = Res->GetSnapshot(StateId); + auto Snapshot = Res->GetDetachedSnapshot(); if (Snapshot.Error && Snapshot.ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) { beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Snapshot.ErrorMessage); } @@ -534,45 +534,50 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string } return true; }); - TLuaStateId StateId = mStateId; AsyncEventReturn.set_function("GetResults", - [StateId](const sol::table& Self, sol::this_state State) -> sol::table { + [](const sol::table& Self, sol::this_state State) -> sol::table { sol::state_view StateView(State); sol::table Result = StateView.create_table(); auto Vector = Self.get>>("ReturnValueImpl"); + auto DetachedToLuaObject = [&StateView](const auto& SelfConvert, const TDetachedLuaValue& value) -> sol::object { + return std::visit([&StateView, &SelfConvert](auto&& arg) -> sol::object { + using T = std::decay_t; + if constexpr (std::is_same_v) { + sol::table Table = StateView.create_table(static_cast(arg.size()), 0); + size_t i = 1; + for (const auto& Elem : arg) { + Table.set(i, SelfConvert(SelfConvert, Elem)); + ++i; + } + return sol::make_object(StateView, Table); + } else if constexpr (std::is_same_v) { + sol::table Table = StateView.create_table(); + for (const auto& [Key, Elem] : arg) { + Table.set(Key, SelfConvert(SelfConvert, *Elem)); + } + return sol::make_object(StateView, Table); + } + else if constexpr (std::is_same_v) + return sol::make_object(StateView, arg); + else if constexpr (std::is_same_v) + return sol::make_object(StateView, arg); + else if constexpr (std::is_same_v) + return sol::make_object(StateView, arg); + else if constexpr (std::is_same_v) + return sol::make_object(StateView, arg); + else if constexpr (std::is_same_v) + return sol::make_object(StateView, sol::lua_nil_t()); + else + static_assert(AlwaysFalseV, "non-exhaustive visitor!"); + }, value.V); + }; int i = 1; for (const auto& Value : Vector) { if (!Value->IsReady()) { return sol::lua_nil; } - // event results from this state are valid unserialized - if (Value->OwnerState() == StateId) { - auto Snapshot = Value->GetSnapshot(StateId); - Result.set(i, Snapshot.Result); - } else { - // event result from another state, goes through serialization boundary - auto Snapshot = Value->GetDetachedSnapshot(); - std::visit([i, &Result](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) - Result.set(i, arg); - else if constexpr (std::is_same_v) - Result.set(i, arg); - else if constexpr (std::is_same_v) - Result.set(i, arg); - else if constexpr (std::is_same_v) - Result.set(i, arg); - else if constexpr (std::is_same_v) - Result.set(i, arg); - else if constexpr (std::is_same_v) - Result.set(i, arg); - else if constexpr (std::is_same_v) - // monostate means no result value - Result.set(i, sol::lua_nil_t()); - else - static_assert(AlwaysFalseV, "non-exhaustive visitor!"); - }, Snapshot.Result.V); - } + auto Snapshot = Value->GetDetachedSnapshot(); + Result.set(i, DetachedToLuaObject(DetachedToLuaObject, Snapshot.Result)); ++i; } @@ -1125,10 +1130,9 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI Start(); } -std::shared_ptr TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) { +std::shared_ptr TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) { std::unique_lock Lock(mStateExecuteQueueMutex); - // explicitly passing empty string as there's no single function being called here - auto Result = std::make_shared(mStateId, std::string()); + auto Result = std::make_shared(mStateId); mStateExecuteQueue.push({ Script, Result }); return Result; } @@ -1203,11 +1207,11 @@ void TLuaEngine::StateThreadData::operator()() { sol::state_view StateView(mState); auto Res = StateView.safe_script(*S.first.Content, sol::script_pass_on_error, S.first.FileName); if (Res.valid()) { - try { - S.second->MarkReadySuccess(std::move(Res)); - } catch (const std::exception& e) { - S.second->MarkReadyError(fmt::format("Call was successful, but result could not be serialized")); - } + // Script-load completion should not serialize the script's return value. + // A loaded chunk may legally return non-serializable Lua values such as + // functions or function tables. For this reason, we don't pass anything + // to the result here. + S.second->MarkReadySuccess(); } else { S.second->MarkReadyError(std::move(Res)); } @@ -1284,7 +1288,7 @@ void TLuaEngine::StateThreadData::operator()() { } } -std::queue>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() { +std::queue>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() { std::unique_lock Lock(mStateExecuteQueueMutex); return mStateExecuteQueue; } @@ -1369,13 +1373,36 @@ function postPlayerAuth(isDenied, reason, playerName, playerRole, isGuest, ident return "post:" .. tostring(isDenied) .. ":" .. reason .. ":" .. playerName .. ":" .. playerRole .. ":" .. tostring(isGuest) .. ":" .. tostring(identifiers.ip) .. ":" .. tostring(identifiers.beammp) end +function arrayBoundaryHandler() + return { "first", "second", [4] = true } +end + +function verifyArrayBoundaryRoundtrip() + local pending = MP.TriggerGlobalEvent("arrayBoundaryEvent") + if not pending:IsDone() then + return "not_done" + end + + local results = pending:GetResults() + if type(results) ~= "table" then + return "bad_results_type:" .. type(results) + end + if type(results[1]) ~= "table" then + return "bad_item_type:" .. type(results[1]) + end + + local arr = results[1] + return tostring(arr[1]) .. "|" .. tostring(arr[2]) .. "|" .. tostring(arr[4]) .. "|" .. tostring(arr[3] == nil) +end + MP.RegisterEvent("onPlayerAuth", "onPlayerAuth") MP.RegisterEvent("postPlayerAuth", "postPlayerAuth") +MP.RegisterEvent("arrayBoundaryEvent", "arrayBoundaryHandler") )"); auto LoadResult = engine.EnqueueScript(StateId, TLuaChunk(Script, "event_contract.lua", "beammp_server_test_resources/Server/LuaEventContractTest")); LoadResult->WaitUntilReady(); - auto LoadSnapshot = LoadResult->GetDetachedSnapshot(); + auto LoadSnapshot = LoadResult->GetSnapshot(); CHECK(!LoadSnapshot.Error); const std::unordered_map Identifiers { @@ -1415,5 +1442,13 @@ MP.RegisterEvent("postPlayerAuth", "postPlayerAuth") REQUIRE(PostPlayerAuthValue != nullptr); CHECK(*PostPlayerAuthValue == "post:false::guest8133569:USER:true:410.0.24.1:123456"); + auto ArrayRoundtrip = engine.EnqueueFunctionCall(StateId, "verifyArrayBoundaryRoundtrip", {}, "verifyArrayBoundaryRoundtrip"); + ArrayRoundtrip->WaitUntilReady(); + auto ArrayRoundtripSnapshot = ArrayRoundtrip->GetDetachedSnapshot(); + CHECK(!ArrayRoundtripSnapshot.Error); + const auto* ArrayRoundtripValue = std::get_if(&ArrayRoundtripSnapshot.Result.V); + REQUIRE(ArrayRoundtripValue != nullptr); + CHECK(*ArrayRoundtripValue == "first|second|true|true"); + Application::GracefullyShutdown(); } diff --git a/src/TLuaPlugin.cpp b/src/TLuaPlugin.cpp index 0d558ac..2b86eee 100644 --- a/src/TLuaPlugin.cpp +++ b/src/TLuaPlugin.cpp @@ -44,7 +44,7 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const std::transform(secondStr.begin(), secondStr.end(), secondStr.begin(), ::tolower); return firstStr < secondStr; }); - std::vector>> ResultsToCheck; + std::vector>> ResultsToCheck; for (const auto& Entry : Entries) { // read in entire file try { @@ -63,7 +63,7 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const } for (auto& Result : ResultsToCheck) { Result.second->WaitUntilReady(); - auto Snapshot = Result.second->GetDetachedSnapshot(); + auto Snapshot = Result.second->GetSnapshot(); if (Snapshot.Error) { beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Snapshot.ErrorMessage); } diff --git a/src/TLuaResult.cpp b/src/TLuaResult.cpp index 39f4af6..e1a9d15 100644 --- a/src/TLuaResult.cpp +++ b/src/TLuaResult.cpp @@ -17,8 +17,11 @@ // along with this program. If not, see . #include "TLuaResult.h" +#include #include #include +#include +#include #include #include #include @@ -64,16 +67,79 @@ std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value) { return os; } +void TLuaVoidResult::MarkReadySuccess() { + std::unique_lock Lock(mMutex); + mError = false; + mErrorMessage.clear(); + MarkAsReady(); +} + +void TLuaVoidResult::MarkReadyError(sol::protected_function_result Res) { + std::unique_lock Lock(mMutex); + mError = true; + SetErrorMessageFromResult(Res); + MarkAsReady(); +} + +void TLuaVoidResult::MarkReadyError(std::string Res) { + std::unique_lock Lock(mMutex); + mError = true; + mErrorMessage = std::move(Res); + MarkAsReady(); +} + +bool TLuaVoidResult::IsReady() const { + std::unique_lock Lock(mMutex); + return mReady; +} + +bool TLuaVoidResult::IsError() const { + std::unique_lock Lock(mMutex); + return mError; +} + +TLuaVoidResult::Snapshot TLuaVoidResult::GetSnapshot() const { + std::unique_lock Lock(mMutex); + Snapshot snapshot { + .Error = mError, + .Ready = mReady, + .ErrorMessage = mErrorMessage, + .StateId = mStateId, + }; + return snapshot; +} + +void TLuaVoidResult::MarkAsReady() { + mReady = true; + mReadyCondition.notify_all(); +} + +void TLuaVoidResult::WaitUntilReady() { + std::unique_lock Lock(mMutex); + while (!mReady) + mReadyCondition.wait_for(Lock, std::chrono::milliseconds(50), + [this] { + return mReady; + }); +} + void TLuaResult::MarkReadySuccess(sol::object Res) { std::unique_lock Lock(mMutex); mError = false; - mResult = Res; mDetachedResult = Freeze(Res); MarkAsReady(); } +void TLuaResult::MarkReadySuccessNoResult() { + std::unique_lock Lock(mMutex); + mError = false; + mDetachedResult = { { std::monostate { } } }; + + MarkAsReady(); +} + void TLuaResult::MarkReadyError(sol::protected_function_result Res) { std::unique_lock Lock(mMutex); mError = true; @@ -109,21 +175,6 @@ void TLuaResult::SetOwnerState(TLuaStateId StateId) { 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 { @@ -155,13 +206,45 @@ TDetachedLuaValue TLuaResult::Freeze(const sol::object& o, int depth) { case sol::type::string: return { { o.as() } }; case sol::type::table: { - TDetachedLuaValue::Object out; + TDetachedLuaValue::Object ObjectOut; + // numeric stuff is for arrays + std::vector> NumericEntries; + bool HasNonStringOrNumericKey = false; + size_t MaxNumericIndex = 0; for (auto&& [k, v] : o.as()) { - if (!k.is()) - continue; // no numeric-key handling, don't need it - out.emplace(k.as(), std::make_shared(std::move(Freeze(v, depth + 1)))); + if (k.is()) { + ObjectOut.emplace(k.as(), std::make_shared(std::move(Freeze(v, depth + 1)))); + continue; + } + // Thanks to lua handling arrays in a weird way, and because they can also be sparse, this weird code is needed. + if (k.get_type() == sol::type::number) { + const double NumericKey = k.as(); + const size_t CandidateIndex = static_cast(NumericKey); + // unsure if we need to do this, or if we can do the same int check we do in other places, but this works well + const bool IsPositiveInteger = std::isfinite(NumericKey) + && NumericKey >= 1.0 + && std::fabs(NumericKey - static_cast(CandidateIndex)) <= std::numeric_limits::epsilon(); + if (IsPositiveInteger) { + const auto Index = CandidateIndex; + MaxNumericIndex = std::max(MaxNumericIndex, Index); + NumericEntries.emplace_back(Index, Freeze(v, depth + 1)); + continue; + } + } + HasNonStringOrNumericKey = true; } - return { { std::move(out) } }; + // Pure numeric-keyed tables are reconstructed as Lua arrays (1-based indexes). + // This preserves array semantics across this serialization boundary :^) + if (!NumericEntries.empty() && ObjectOut.empty() && !HasNonStringOrNumericKey) { + TDetachedLuaValue::Array ArrayOut(MaxNumericIndex); + for (const auto& [Index, Value] : NumericEntries) { + if (Index > 0 && Index <= ArrayOut.size()) { + ArrayOut[Index - 1] = Value; + } + } + return { { std::move(ArrayOut) } }; + } + return { { std::move(ObjectOut) } }; } default: throw std::runtime_error("unsupported Lua type for cross-thread snapshot"); @@ -181,6 +264,41 @@ void TLuaResult::WaitUntilReady() { }); } +TEST_CASE("TLuaInStateResult MarkReadyError(string) marks ready and wakes waiters") { + TLuaVoidResult result("state_local"); + 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.GetSnapshot(); + CHECK(snapshot.Ready); + CHECK(snapshot.Error); + CHECK(snapshot.ErrorMessage == "boom"); + CHECK(snapshot.StateId == "state_local"); +} + +TEST_CASE("TLuaInStateResult MarkReadySuccess clears error state") { + TLuaVoidResult result("state_local_success"); + result.MarkReadySuccess(); + + CHECK(result.IsReady()); + CHECK_FALSE(result.IsError()); + const auto snapshot = result.GetSnapshot(); + CHECK(snapshot.Ready); + CHECK_FALSE(snapshot.Error); + CHECK(snapshot.ErrorMessage.empty()); +} + TEST_CASE("TLuaResult MarkReadyError(string) marks ready and wakes waiters") { TLuaResult result("state_a", "fn_a"); std::atomic waiterDone { false }; @@ -205,16 +323,6 @@ TEST_CASE("TLuaResult MarkReadyError(string) marks ready and wakes waiters") { CHECK(snapshot.Function == "fn_a"); } -TEST_CASE("TLuaResult GetSnapshot enforces owner state id") { - sol::state lua; - TLuaResult result("owner_state", "fn_owner"); - 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") { sol::state lua; TLuaResult result("state_table", "fn_table"); @@ -258,6 +366,41 @@ TEST_CASE("TLuaResult detached snapshot freezes nested string-keyed tables") { CHECK(*innerValue == "v"); } +TEST_CASE("TLuaResult detached snapshot preserves numeric array tables") { + sol::state lua; + TLuaResult result("state_array", "fn_array"); + lua.open_libraries(sol::lib::base); + + auto arr = lua.create_table(); + arr[1] = std::string("a"); + arr[2] = 42; + arr[4] = true; // keep sparse indexes + + result.MarkReadySuccess(sol::make_object(lua.lua_state(), arr)); + const auto detached = result.GetDetachedSnapshot(); + + CHECK(detached.Ready); + CHECK_FALSE(detached.Error); + + const auto* array = std::get_if(&detached.Result.V); + REQUIRE(array != nullptr); + REQUIRE(array->size() == 4); + + const auto* v1 = std::get_if(&(*array)[0].V); + REQUIRE(v1 != nullptr); + CHECK(*v1 == "a"); + + const auto* v2 = std::get_if(&(*array)[1].V); + REQUIRE(v2 != nullptr); + CHECK(*v2 == 42); + + CHECK(std::holds_alternative((*array)[2].V)); + + const auto* v4 = std::get_if(&(*array)[3].V); + REQUIRE(v4 != nullptr); + CHECK(*v4); +} + TEST_CASE("TLuaResult MarkReadySuccess throws on unsupported Lua function value") { sol::state lua; TLuaResult result("state_fn", "fn_fn"); @@ -270,3 +413,13 @@ TEST_CASE("TLuaResult MarkReadySuccess throws on unsupported Lua function value" CHECK_THROWS_AS(result.MarkReadySuccess(fnObj), std::runtime_error); CHECK_FALSE(result.IsReady()); } + +TEST_CASE("TLuaResult MarkReadySuccessNoResult stores monostate") { + TLuaResult result("state_empty", "fn_empty"); + result.MarkReadySuccessNoResult(); + + CHECK(result.IsReady()); + const auto snapshot = result.GetDetachedSnapshot(); + CHECK_FALSE(snapshot.Error); + CHECK(std::holds_alternative(snapshot.Result.V)); +} diff --git a/src/TPluginMonitor.cpp b/src/TPluginMonitor.cpp index d766a70..3084e3c 100644 --- a/src/TPluginMonitor.cpp +++ b/src/TPluginMonitor.cpp @@ -70,7 +70,7 @@ void TPluginMonitor::operator()() { auto Res = mEngine->EnqueueScript(StateID, Chunk); Res->WaitUntilReady(); if (Res->IsError()) { - auto Snapshot = Res->GetDetachedSnapshot(); + auto Snapshot = Res->GetSnapshot(); beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Snapshot.ErrorMessage); } else { mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));