From ff62a0732a7f8d9f61b885dd3a3771507ca7d0d8 Mon Sep 17 00:00:00 2001
From: Louis-Antoine <lamr@free.fr>
Date: Fri, 15 Nov 2019 15:35:28 +0100
Subject: [PATCH] Let players move after completing the level

This only takes effect in co-op,
and can be disabled with "exitmove off"
---
 src/d_netcmd.c    | 16 ++++++++++++++++
 src/d_netcmd.h    |  2 +-
 src/d_player.h    |  1 +
 src/dehacked.c    |  1 +
 src/g_game.c      | 25 +++++++++++++++++++++++++
 src/g_game.h      |  1 +
 src/lua_baselib.c | 20 ++++++++++++++++++++
 src/p_local.h     |  1 +
 src/p_spec.c      |  2 +-
 src/p_user.c      | 35 ++++++++++++++++++++++++++++++++---
 10 files changed, 99 insertions(+), 5 deletions(-)

diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index b14f92b33..a5191331a 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -87,6 +87,7 @@ static void JoinTimeout_OnChange(void);
 
 static void CoopStarposts_OnChange(void);
 static void CoopLives_OnChange(void);
+static void ExitMove_OnChange(void);
 
 static void Ringslinger_OnChange(void);
 static void Gravity_OnChange(void);
@@ -355,9 +356,12 @@ consvar_t cv_cooplives = {"cooplives", "Avoid Game Over", CV_NETVAR|CV_CALL|CV_C
 
 static CV_PossibleValue_t advancemap_cons_t[] = {{0, "Off"}, {1, "Next"}, {2, "Random"}, {0, NULL}};
 consvar_t cv_advancemap = {"advancemap", "Next", CV_NETVAR, advancemap_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 static CV_PossibleValue_t playersforexit_cons_t[] = {{0, "One"}, {1, "1/4"}, {2, "Half"}, {3, "3/4"}, {4, "All"}, {0, NULL}};
 consvar_t cv_playersforexit = {"playersforexit", "All", CV_NETVAR, playersforexit_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
+consvar_t cv_exitmove = {"exitmove", "On", CV_NETVAR|CV_CALL, CV_OnOff, ExitMove_OnChange, 0, NULL, NULL, 0, 0, NULL};
+
 consvar_t cv_runscripts = {"runscripts", "Yes", 0, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 consvar_t cv_pause = {"pausepermission", "Server", CV_NETVAR, pause_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -511,6 +515,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_inttime);
 	CV_RegisterVar(&cv_advancemap);
 	CV_RegisterVar(&cv_playersforexit);
+	CV_RegisterVar(&cv_exitmove);
 	CV_RegisterVar(&cv_timelimit);
 	CV_RegisterVar(&cv_playbackspeed);
 	CV_RegisterVar(&cv_forceskin);
@@ -3634,6 +3639,17 @@ static void CoopLives_OnChange(void)
 	}
 }
 
+static void ExitMove_OnChange(void)
+{
+	if (!(netgame || multiplayer) || gametype != GT_COOP)
+		return;
+
+	if (cv_exitmove.value)
+		CONS_Printf(M_GetText("Players can now move after completing the level.\n"));
+	else
+		CONS_Printf(M_GetText("Players can no longer move after completing the level.\n"));
+}
+
 UINT32 timelimitintics = 0;
 
 /** Deals with a timelimit change by printing the change to the console.
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index e789e5b50..d3d73602c 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -96,7 +96,7 @@ extern consvar_t cv_recycler;
 
 extern consvar_t cv_itemfinder;
 
-extern consvar_t cv_inttime, cv_coopstarposts, cv_cooplives, cv_advancemap, cv_playersforexit;
+extern consvar_t cv_inttime, cv_coopstarposts, cv_cooplives, cv_advancemap, cv_playersforexit, cv_exitmove;
 extern consvar_t cv_overtime;
 extern consvar_t cv_startinglives;
 
diff --git a/src/d_player.h b/src/d_player.h
index f2fdda050..3ec46c352 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -150,6 +150,7 @@ typedef enum
 	/*** misc ***/
 	PF_FORCESTRAFE = 1<<28, // Turning inputs are translated into strafing inputs
 	PF_CANCARRY    = 1<<29, // Can carry another player?
+	PF_FINISHED    = 1<<30, // The player finished the level. NOT the same as exiting
 
 	// up to 1<<31 is free
 } pflags_t;
diff --git a/src/dehacked.c b/src/dehacked.c
index 34ee1f170..39e3dafe8 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -8298,6 +8298,7 @@ static const char *const PLAYERFLAG_LIST[] = {
 	/*** misc ***/
 	"FORCESTRAFE", // Translate turn inputs into strafe inputs
 	"CANCARRY", // Can carry?
+	"FINISHED",
 
 	NULL // stop loop here.
 };
diff --git a/src/g_game.c b/src/g_game.c
index e2f43e4f2..9efebcd68 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -2831,6 +2831,31 @@ void G_AddPlayer(INT32 playernum)
 		P_DoPlayerExit(p);
 }
 
+boolean G_EnoughPlayersFinished(void)
+{
+	UINT8 numneeded = (G_IsSpecialStage(gamemap) ? 4 : cv_playersforexit.value);
+	INT32 total = 0;
+	INT32 exiting = 0;
+	INT32 i;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i] || players[i].spectator || players[i].bot)
+			continue;
+		if (players[i].lives <= 0)
+			continue;
+
+		total++;
+		if (players[i].pflags & PF_FINISHED)
+			exiting++;
+	}
+
+	if (exiting)
+		return exiting * 4 / total >= numneeded;
+	else
+		return false;
+}
+
 void G_ExitLevel(void)
 {
 	if (gamestate == GS_LEVEL)
diff --git a/src/g_game.h b/src/g_game.h
index 198cbc396..f96d83c33 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -186,6 +186,7 @@ boolean G_GametypeHasSpectators(void);
 boolean G_RingSlingerGametype(void);
 boolean G_PlatformGametype(void);
 boolean G_TagGametype(void);
+boolean G_EnoughPlayersFinished(void);
 void G_ExitLevel(void);
 void G_NextLevel(void);
 void G_Continue(void);
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 8f173e32e..45021b5ad 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1160,6 +1160,17 @@ static int lib_pElementalFire(lua_State *L)
 	return 0;
 }
 
+static int lib_pDoPlayerFinish(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	P_DoPlayerFinish(player);
+	return 0;
+}
+
 static int lib_pDoPlayerExit(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -2632,6 +2643,13 @@ static int lib_gSetCustomExitVars(lua_State *L)
 	return 0;
 }
 
+static int lib_gEnoughPlayersFinished(lua_State *L)
+{
+	INLEVEL
+	lua_pushboolean(L, G_EnoughPlayersFinished());
+	return 1;
+}
+
 static int lib_gExitLevel(lua_State *L)
 {
 	int n = lua_gettop(L); // Num arguments
@@ -2827,6 +2845,7 @@ static luaL_Reg lib[] = {
 	{"P_DoBubbleBounce",lib_pDoBubbleBounce},
 	{"P_BlackOw",lib_pBlackOw},
 	{"P_ElementalFire",lib_pElementalFire},
+	{"P_DoPlayerFinish",lib_pDoPlayerFinish},
 	{"P_DoPlayerExit",lib_pDoPlayerExit},
 	{"P_InstaThrust",lib_pInstaThrust},
 	{"P_ReturnThrustX",lib_pReturnThrustX},
@@ -2938,6 +2957,7 @@ static luaL_Reg lib[] = {
 	{"G_BuildMapName",lib_gBuildMapName},
 	{"G_DoReborn",lib_gDoReborn},
 	{"G_SetCustomExitVars",lib_gSetCustomExitVars},
+	{"G_EnoughPlayersFinished",lib_gEnoughPlayersFinished},
 	{"G_ExitLevel",lib_gExitLevel},
 	{"G_IsSpecialStage",lib_gIsSpecialStage},
 	{"G_GametypeUsesLives",lib_gGametypeUsesLives},
diff --git a/src/p_local.h b/src/p_local.h
index 02f497850..fd41d2bfa 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -172,6 +172,7 @@ void P_ElementalFire(player_t *player, boolean cropcircle);
 void P_DoPityCheck(player_t *player);
 void P_PlayerThink(player_t *player);
 void P_PlayerAfterThink(player_t *player);
+void P_DoPlayerFinish(player_t *player);
 void P_DoPlayerExit(player_t *player);
 void P_NightserizePlayer(player_t *player, INT32 ptime);
 
diff --git a/src/p_spec.c b/src/p_spec.c
index 7b23ecbe7..5e22696b3 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -4645,7 +4645,7 @@ DoneSection2:
 			{
 				INT32 lineindex;
 
-				P_DoPlayerExit(player);
+				P_DoPlayerFinish(player);
 
 				P_SetupSignExit(player);
 				// important: use sector->tag on next line instead of player->mo->subsector->tag
diff --git a/src/p_user.c b/src/p_user.c
index 561183cd5..12d91204e 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2124,6 +2124,30 @@ void P_SpawnSpinMobj(player_t *player, mobjtype_t type)
 	P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
 }
 
+/** Called when \p player finishes the level.
+  *
+  * Only use for cases where the player should be able to move
+  * while waiting for others to finish. Otherwise, use P_DoPlayerExit().
+  *
+  * In single player or if ::cv_exitmove is disabled, this will also cause
+  * P_PlayerThink() to call P_DoPlayerExit(), so you do not need to
+  * make a special cases for those.
+  *
+  * \param player The player who finished the level.
+  * \sa P_DoPlayerExit
+  *
+  */
+void P_DoPlayerFinish(player_t *player)
+{
+	if (player->pflags & PF_FINISHED)
+		return;
+
+	player->pflags |= PF_FINISHED;
+
+	if (netgame)
+		CONS_Printf(M_GetText("%s has completed the level.\n"), player_names[player-players]);
+}
+
 //
 // P_DoPlayerExit
 //
@@ -2161,9 +2185,6 @@ void P_DoPlayerExit(player_t *player)
 	player->powers[pw_underwater] = 0;
 	player->powers[pw_spacetime] = 0;
 	P_RestoreMusic(player);
-
-	if (playeringame[player-players] && netgame && !circuitmap)
-		CONS_Printf(M_GetText("%s has completed the level.\n"), player_names[player-players]);
 }
 
 #define SPACESPECIAL 12
@@ -11285,6 +11306,14 @@ void P_PlayerThink(player_t *player)
 		}
 	}
 
+	if (player->pflags & PF_FINISHED)
+	{
+		if (cv_exitmove.value && !G_EnoughPlayersFinished())
+			player->exiting = 0;
+		else
+			P_DoPlayerExit(player);
+	}
+
 	// check water content, set stuff in mobj
 	P_MobjCheckWater(player->mo);