From 3bc4af09a2e55fd1e592b6ccac0730bf96b5a0fb Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol Date: Mon, 26 Oct 2026 19:50:26 +0300 Subject: [PATCH 1/3] fix: mel cm trigger OnLevelEnd on non-flag finish Mel CM doesn't handle runs ending on elevator properly. Replace TransitionFromMap() event with OnLevelEnd, which SAR already triggers the CM flag for. With this, elevaator behaviour should match base game, and the demo should contain ScoreboardTempUpdate user message so LPHUB can grab portal count and time taken. --- src/Features/ChallengeMode.cpp | 8 ++++++++ src/Features/ChallengeMode.hpp | 1 + src/Modules/Server.cpp | 15 +++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/src/Features/ChallengeMode.cpp b/src/Features/ChallengeMode.cpp index 910cc0fa3..0869bf3d0 100644 --- a/src/Features/ChallengeMode.cpp +++ b/src/Features/ChallengeMode.cpp @@ -15,6 +15,14 @@ ChallengeMode *cm; +bool ChallengeMode::EnsureStatsController() { + if (entityList->GetEntityInfoByClassName("portal_stats_controller")) { + return true; + } + + return server->Create && server->Create("portal_stats_controller", {0, 0, 0}, {0, 0, 0}, nullptr); +} + void ChallengeMode::CreateNode() { if (!sv_bonus_challenge.GetBool() || engine->demoplayer->IsPlaying()) { return; diff --git a/src/Features/ChallengeMode.hpp b/src/Features/ChallengeMode.hpp index 6a2300491..ea68742d4 100644 --- a/src/Features/ChallengeMode.hpp +++ b/src/Features/ChallengeMode.hpp @@ -12,6 +12,7 @@ class ChallengeMode : public Feature { public: void CreateNode(); + bool EnsureStatsController(); void LoadNodes(SourceGameVersion version); }; diff --git a/src/Modules/Server.cpp b/src/Modules/Server.cpp index 12634d6c1..332ffeacb 100644 --- a/src/Modules/Server.cpp +++ b/src/Modules/Server.cpp @@ -616,6 +616,21 @@ static void __cdecl AcceptInput_Hook(void *thisptr, const char *inputName, void console->Print("%.4d %s.%s(%s)\n", session->GetTick(), entName, inputName, parameter.ToString()); } + if (sar.game->Is(SourceGame_PortalStoriesMel) + && client->GetChallengeStatus() == CMStatus::CHALLENGE + && !engine->demoplayer->IsPlaying() + && !strcmp(entName, "@transition_script") + && !strcmp(inputName, "RunScriptCode") + && !strcmp(parameter.ToString(), "TransitionFromMap()")) { + if (cm->EnsureStatsController()) { + // Replace Mel's map change with queued native completion, which also reaches our OnLevelEnd hook. + parameter.fieldType = FIELD_STRING; + parameter.iszVal = "EntFire(\"portal_stats_controller\", \"OnLevelEnd\", 5.0, 0);"; + } else { + console->Print("Could not obtain portal_stats_controller; keeping Mel's original elevator transition.\n"); + } + } + // HACKHACK // Deals with maps where you hit a normal transition trigger if (!strcmp(className, "portal_stats_controller") && !strcmp(inputName, "OnLevelEnd") && client->GetChallengeStatus() == CMStatus::CHALLENGE) { From dbf29a12d33700dd1f849ff7f348594d03e36df9 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol Date: Mon, 26 Oct 2026 23:05:47 +0300 Subject: [PATCH 2/3] trigger hook on changelevel --- src/Modules/Server.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Modules/Server.cpp b/src/Modules/Server.cpp index 332ffeacb..94d9f74ac 100644 --- a/src/Modules/Server.cpp +++ b/src/Modules/Server.cpp @@ -619,15 +619,20 @@ static void __cdecl AcceptInput_Hook(void *thisptr, const char *inputName, void if (sar.game->Is(SourceGame_PortalStoriesMel) && client->GetChallengeStatus() == CMStatus::CHALLENGE && !engine->demoplayer->IsPlaying() - && !strcmp(entName, "@transition_script") - && !strcmp(inputName, "RunScriptCode") - && !strcmp(parameter.ToString(), "TransitionFromMap()")) { - if (cm->EnsureStatsController()) { - // Replace Mel's map change with queued native completion, which also reaches our OnLevelEnd hook. - parameter.fieldType = FIELD_STRING; - parameter.iszVal = "EntFire(\"portal_stats_controller\", \"OnLevelEnd\", 5.0, 0);"; - } else { - console->Print("Could not obtain portal_stats_controller; keeping Mel's original elevator transition.\n"); + && (!strcasecmp(className, "point_clientcommand") || !strcasecmp(className, "point_servercommand")) + && !strcasecmp(inputName, "Command")) { + const char *command = parameter.ToString(); + command += strspn(command, " \t\r\n"); + if (!strncasecmp(command, "changelevel", 11) && strspn(command + 11, " \t\r\n") > 0) { + if (cm->EnsureStatsController()) { + // Complete after the map's fade, replacing the changelevel command entirely. + // RunScriptCode is a base entity input; queue OnLevelEnd so it reaches our hook. + inputName = "RunScriptCode"; + parameter.fieldType = FIELD_STRING; + parameter.iszVal = "EntFire(\"portal_stats_controller\", \"OnLevelEnd\", 5.0, 0);"; + } else { + console->Print("Could not obtain portal_stats_controller; keeping Mel's original elevator transition.\n"); + } } } From c3ccd66cb64aa65a9907e52b292b61a2fd859dfb Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol Date: Mon, 26 Oct 2026 23:11:53 +0300 Subject: [PATCH 3/3] strncasecmp alias for win --- src/Utils.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Utils.hpp b/src/Utils.hpp index b1d449567..4ec6e5b11 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -9,6 +9,7 @@ # include # define strdup _strdup # define strcasecmp _stricmp +# define strncasecmp _strnicmp #else # include #endif