diff --git a/src/g_game.c b/src/g_game.c
index 02b32bd48b4fac6ba4fc2f5af423a91c7e64d96d..f8482938638acea8a2ae686766765eae5968ed8a 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -1956,6 +1956,16 @@ boolean G_Responder(event_t *ev)
 				if (!playeringame[displayplayer])
 					continue;
 
+#ifdef HAVE_BLUA
+				{
+					UINT8 canSwitchView = LUAh_ViewpointSwitch(&players[consoleplayer], &players[displayplayer]);
+					if (canSwitchView == 1) // Set viewpoint to this player
+						break;
+					else if (canSwitchView == 2) // Skip this player
+						continue;
+				}
+#endif
+
 				if (players[displayplayer].spectator)
 					continue;
 
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 6617bca93a34c2db6742f40814d74eb079279f32..592a93acca7d0a007b838030d1eec1433c257b66 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -51,6 +51,7 @@ enum hook {
 	hook_PlayerCanDamage,
 	hook_PlayerQuit,
 	hook_IntermissionThinker,
+	hook_ViewpointSwitch,
 
 	hook_MAX // last hook
 };
@@ -93,5 +94,6 @@ boolean LUAh_FollowMobj(player_t *player, mobj_t *mobj); // Hook for P_PlayerAft
 UINT8 LUAh_PlayerCanDamage(player_t *player, mobj_t *mobj); // Hook for P_PlayerCanDamage
 void LUAh_PlayerQuit(player_t *plr, int reason); // Hook for player quitting
 void LUAh_IntermissionThinker(void); // Hook for Y_Ticker
+UINT8 LUAh_ViewpointSwitch(player_t *player, player_t *newdisplayplayer); // Hook for spy mode in G_Responder
 
 #endif
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index ef87d0b6f7a9cc592609be76cbeb59f770710ee1..b1f702f7cf5d4cdff29cd3868ee1842790ba9b26 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -62,6 +62,7 @@ const char *const hookNames[hook_MAX+1] = {
 	"PlayerCanDamage",
 	"PlayerQuit",
 	"IntermissionThinker",
+	"ViewpointSwitch",
 	NULL
 };
 
@@ -203,6 +204,7 @@ static int lib_addHook(lua_State *L)
 	case hook_PlayerSpawn:
 	case hook_FollowMobj:
 	case hook_PlayerCanDamage:
+	case hook_ViewpointSwitch:
 	case hook_ShieldSpawn:
 	case hook_ShieldSpecial:
 		lastp = &playerhooks;
@@ -1346,4 +1348,49 @@ void LUAh_IntermissionThinker(void)
 	}
 }
 
+// Hook for spy mode in G_Responder
+UINT8 LUAh_ViewpointSwitch(player_t *player, player_t *newdisplayplayer)
+{
+	hook_p hookp;
+	UINT8 canSwitchView = 0; // 0 = default, 1 = force yes, 2 = force no.
+	if (!gL || !(hooksAvailable[hook_ViewpointSwitch/8] & (1<<(hook_ViewpointSwitch%8))))
+		return 0;
+
+	lua_settop(gL, 0);
+
+	for (hookp = playerhooks; hookp; hookp = hookp->next)
+	{
+		if (hookp->type != hook_ViewpointSwitch)
+			continue;
+
+		if (lua_gettop(gL) == 0)
+		{
+			LUA_PushUserdata(gL, player, META_PLAYER);
+			LUA_PushUserdata(gL, newdisplayplayer, META_PLAYER);
+		}
+		lua_pushfstring(gL, FMT_HOOKID, hookp->id);
+		lua_gettable(gL, LUA_REGISTRYINDEX);
+		lua_pushvalue(gL, -3);
+		lua_pushvalue(gL, -3);
+		if (lua_pcall(gL, 2, 1, 0)) {
+			if (!hookp->error || cv_debug & DBG_LUA)
+				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
+			lua_pop(gL, 1);
+			hookp->error = true;
+			continue;
+		}
+		if (!lua_isnil(gL, -1))
+		{ // if nil, leave canSwitchView = 0.
+			if (lua_toboolean(gL, -1))
+				canSwitchView = 1; // Force viewpoint switch
+			else
+				canSwitchView = 2; // Skip viewpoint switch
+		}
+		lua_pop(gL, 1);
+	}
+
+	lua_settop(gL, 0);
+	return canSwitchView;
+}
+
 #endif