From 6103c213e2700e48c90f68326edfcc90ee74cda4 Mon Sep 17 00:00:00 2001 From: chief1983 Date: Mon, 6 Apr 2026 15:26:36 -0500 Subject: [PATCH] Fix standalone crash when client disconnects before mission loads When a client connects to a standalone server and backs out before a mission is loaded, multi_standalone_reset_all() calls game_level_close() which fires OnMissionAboutToEnd/OnMissionEnd scripting hooks. If any mod script calls mn.evaluateSEXP() from those hooks, alloc_sexp() crashes because the SEXP system was never initialized (Locked_sexp_true/false are still -1, Sexp_nodes is nullptr). Guard multi_standalone_reset_all() to only call game_level_close() when GM_IN_MISSION is set. Additionally, guard evaluateSEXP, evaluateNumericSEXP, and runSEXP to gracefully return with a warning when the SEXP system is not initialized, rather than crashing. Fixes #7353 Co-Authored-By: Claude Opus 4.6 (1M context) --- code/network/multi.cpp | 9 +++++++-- code/scripting/api/libs/mission.cpp | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/code/network/multi.cpp b/code/network/multi.cpp index d30f7017cce..258e8e377a2 100644 --- a/code/network/multi.cpp +++ b/code/network/multi.cpp @@ -1671,8 +1671,13 @@ void multi_standalone_reset_all() // NETLOG ml_string(NOX("Standalone resetting")); - // shut all game stuff down - game_level_close(); + // shut all game stuff down -- but only if a mission was actually in progress, + // otherwise scripting hooks in game_level_close() may fire into uninitialized + // subsystems (e.g. SEXP nodes never allocated because no mission was loaded) + if (Game_mode & GM_IN_MISSION) { + game_level_close(); + Game_mode &= ~GM_IN_MISSION; + } // reinitialize the gui std_reset_standalone_gui(); diff --git a/code/scripting/api/libs/mission.cpp b/code/scripting/api/libs/mission.cpp index 395c254fda6..2a2ac722bc7 100644 --- a/code/scripting/api/libs/mission.cpp +++ b/code/scripting/api/libs/mission.cpp @@ -201,6 +201,11 @@ ADE_FUNC(evaluateSEXP, l_Mission, "string", "Runs the defined SEXP script, and r if(!ade_get_args(L, "s", &s)) return ADE_RETURN_FALSE; + if (Sexp_nodes == nullptr) { + Warning(LOCATION, "evaluateSEXP called before SEXP system initialized; returning false"); + return ADE_RETURN_FALSE; + } + r_val = run_sexp(s); if (r_val == SEXP_TRUE) @@ -218,6 +223,11 @@ ADE_FUNC(evaluateNumericSEXP, l_Mission, "string", "Runs the defined SEXP script if (!ade_get_args(L, "s", &s)) return ade_set_args(L, "i", 0); + if (Sexp_nodes == nullptr) { + Warning(LOCATION, "evaluateNumericSEXP called before SEXP system initialized; returning NaN"); + return ade_set_args(L, "f", std::numeric_limits::quiet_NaN()); + } + r_val = run_sexp(s, true, &got_nan); if (got_nan) @@ -235,6 +245,11 @@ ADE_FUNC(runSEXP, l_Mission, "string", "Runs the defined SEXP script within a `w if(!ade_get_args(L, "s", &s)) return ADE_RETURN_FALSE; + if (Sexp_nodes == nullptr) { + Warning(LOCATION, "runSEXP called before SEXP system initialized; returning false"); + return ADE_RETURN_FALSE; + } + while (is_white_space(*s)) s++; if (*s != '(')