diff --git a/source/core/coreplayer.h b/source/core/coreplayer.h
index 8ffe91443..9f3503fba 100644
--- a/source/core/coreplayer.h
+++ b/source/core/coreplayer.h
@@ -40,6 +40,7 @@ public:
 	// All overridable methods.
 	virtual DCoreActor* GetActor() = 0;
 	virtual const bool canSlopeTilt() const { return false; }
+	virtual const unsigned getCrouchFlags() const = 0;
 	virtual const DVector2& GetInputVelocity() const { return actor->vel.XY(); }
 	virtual const double GetMaxInputVel() const = 0;
 
diff --git a/source/core/gameinput.cpp b/source/core/gameinput.cpp
index 116faaf62..0b9180c25 100644
--- a/source/core/gameinput.cpp
+++ b/source/core/gameinput.cpp
@@ -48,18 +48,6 @@ GameInput gameInput{};
 bool crouch_toggle = false;
 
 
-//---------------------------------------------------------------------------
-//
-// Clears crouch toggle state for new games.
-//
-//---------------------------------------------------------------------------
-
-void GameInput::resetCrouchToggle()
-{
-	crouch_toggle = false;
-}
-
-
 //---------------------------------------------------------------------------
 //
 // Default player movement function for the games. Can be overridden.
@@ -72,6 +60,18 @@ void GameInterface::doPlayerMovement()
 }
 
 
+//---------------------------------------------------------------------------
+//
+// Clears crouch toggle state for new games.
+//
+//---------------------------------------------------------------------------
+
+void GameInput::resetCrouchToggle()
+{
+	crouch_toggle = false;
+}
+
+
 //---------------------------------------------------------------------------
 //
 // Player's movement function, called from game's ticker or from gi->doPlayerMovement() as required.
@@ -248,7 +248,7 @@ void GameInput::processInputBits()
 	}
 	else dpad_lock = 0;
 
-	const auto crouchState = gi->getCrouchState();
+	const auto crouchFlags = PlayerArray[myconnectindex]->getCrouchFlags();
 	inputBuffer.actions |= ActionsToSend;
 	ActionsToSend = 0;
 
@@ -266,12 +266,12 @@ void GameInput::processInputBits()
 
 	if (buttonMap.ButtonDown(gamefunc_Toggle_Crouch))
 	{
-		const bool canCrouch = crouchState & CS_CANCROUCH;
+		const bool canCrouch = crouchFlags & CS_CANCROUCH;
 		crouch_toggle = !crouch_toggle && canCrouch;
 		if (canCrouch) buttonMap.ClearButton(gamefunc_Toggle_Crouch);
 	}
 
-	if (buttonMap.ButtonDown(gamefunc_Crouch) || buttonMap.ButtonDown(gamefunc_Jump) || (crouchState & CS_DISABLETOGGLE))
+	if (buttonMap.ButtonDown(gamefunc_Crouch) || buttonMap.ButtonDown(gamefunc_Jump) || (crouchFlags & CS_DISABLETOGGLE))
 		crouch_toggle = false;
 
 	if (buttonMap.ButtonDown(gamefunc_Crouch) || buttonMap.ButtonDown(gamefunc_Toggle_Crouch) || crouch_toggle)
diff --git a/source/core/gamestruct.h b/source/core/gamestruct.h
index 567ad3653..426496761 100644
--- a/source/core/gamestruct.h
+++ b/source/core/gamestruct.h
@@ -111,7 +111,6 @@ struct GameInterface
 	virtual bool WantEscape() { return false; }
 	virtual void StartSoundEngine() = 0;
 	virtual void doPlayerMovement();
-	virtual unsigned getCrouchState() = 0;
 };
 
 extern GameInterface* gi;
diff --git a/source/games/blood/src/blood.h b/source/games/blood/src/blood.h
index 4f7dd36c4..e48dcfc4d 100644
--- a/source/games/blood/src/blood.h
+++ b/source/games/blood/src/blood.h
@@ -135,7 +135,6 @@ struct GameInterface : public ::GameInterface
 	void AddQAVInterpProps(const int res_id, const FString& interptype, const bool loopable, const TMap<int, TArray<int>>&& ignoredata) override;
 	void RemoveQAVInterpProps(const int res_id) override;
 	void StartSoundEngine() override;
-	unsigned getCrouchState() override;
 };
 
 END_BLD_NS
diff --git a/source/games/blood/src/bloodactor.h b/source/games/blood/src/bloodactor.h
index 4434086bd..a8007f792 100644
--- a/source/games/blood/src/bloodactor.h
+++ b/source/games/blood/src/bloodactor.h
@@ -283,6 +283,12 @@ public:
 		const int florhit = pActor->hit.florhit.type;
 		return pActor->xspr.height < 16 && (florhit == kHitSector || florhit == 0);
 	}
+
+	const unsigned getCrouchFlags() const override
+	{
+		const bool swimming = posture == kPostureSwim;
+		return (CS_CANCROUCH * !swimming) | (CS_DISABLETOGGLE * swimming);
+	}
 };
 
 inline DBloodPlayer* getPlayer(int index)
diff --git a/source/games/blood/src/player.cpp b/source/games/blood/src/player.cpp
index c72f5d5f2..d3a7c307f 100644
--- a/source/games/blood/src/player.cpp
+++ b/source/games/blood/src/player.cpp
@@ -1495,18 +1495,6 @@ int ActionScan(DBloodPlayer* pPlayer, HitInfo* out)
 //
 //---------------------------------------------------------------------------
 
-unsigned GameInterface::getCrouchState()
-{
-	const bool swimming = getPlayer(myconnectindex)->posture == kPostureSwim;
-	return (CS_CANCROUCH * !swimming) | (CS_DISABLETOGGLE * swimming);
-}
-
-//---------------------------------------------------------------------------
-//
-//
-//
-//---------------------------------------------------------------------------
-
 void ProcessInput(DBloodPlayer* pPlayer)
 {
 	enum
diff --git a/source/games/duke/src/duke3d.h b/source/games/duke/src/duke3d.h
index b6cc762c7..35ec9eeff 100644
--- a/source/games/duke/src/duke3d.h
+++ b/source/games/duke/src/duke3d.h
@@ -38,7 +38,6 @@ struct GameInterface : public ::GameInterface
 	void ExitFromMenu() override;
 	void DrawPlayerSprite(const DVector2& origin, bool onteam) override;
 	void doPlayerMovement() override;
-	unsigned getCrouchState() override;
 	void UpdateSounds() override;
 	void Startup() override;
 	void DrawBackground() override;
diff --git a/source/games/duke/src/input.cpp b/source/games/duke/src/input.cpp
index 183061a19..e5a10a76b 100644
--- a/source/games/duke/src/input.cpp
+++ b/source/games/duke/src/input.cpp
@@ -486,21 +486,6 @@ void hud_input(DDukePlayer* const p)
 	}
 }
 
-//---------------------------------------------------------------------------
-//
-// 
-//
-//---------------------------------------------------------------------------
-
-unsigned GameInterface::getCrouchState()
-{
-	const auto p = getPlayer(myconnectindex);
-	const int sectorLotag = p->insector() ? p->cursector->lotag : 0;
-	const int crouchable = sectorLotag != ST_2_UNDERWATER && (sectorLotag != ST_1_ABOVE_WATER || p->spritebridge) && !p->jetpack_on;
-	const int disableToggle = (!crouchable && p->on_ground) || p->jetpack_on || (isRRRA() && (p->OnMotorcycle || p->OnBoat));
-	return (CS_CANCROUCH * crouchable) | (CS_DISABLETOGGLE * disableToggle);
-}
-
 //---------------------------------------------------------------------------
 //
 // External entry point
diff --git a/source/games/duke/src/types.h b/source/games/duke/src/types.h
index 4dbd0aece..8fe39ee20 100644
--- a/source/games/duke/src/types.h
+++ b/source/games/duke/src/types.h
@@ -419,6 +419,14 @@ public:
 	{
 		cmd.ucmd.setItemUsed(num - 1);
 	}
+
+	const unsigned getCrouchFlags() const override
+	{
+		const int sectorLotag = insector() ? cursector->lotag : 0;
+		const int crouchable = sectorLotag != ST_2_UNDERWATER && (sectorLotag != ST_1_ABOVE_WATER || spritebridge) && !jetpack_on;
+		const int disableToggle = (!crouchable && on_ground) || jetpack_on || (isRRRA() && (OnMotorcycle || OnBoat));
+		return (CS_CANCROUCH * crouchable) | (CS_DISABLETOGGLE * disableToggle);
+	}
 };
 
 struct Cycler
diff --git a/source/games/exhumed/src/exhumed.h b/source/games/exhumed/src/exhumed.h
index b1e742c98..d818912d4 100644
--- a/source/games/exhumed/src/exhumed.h
+++ b/source/games/exhumed/src/exhumed.h
@@ -238,7 +238,6 @@ struct GameInterface : public ::GameInterface
     void processSprites(tspriteArray& tsprites, const DVector3& view, DAngle viewang, double interpfrac) override;
     int GetCurrentSkill() override;
     void StartSoundEngine() override;
-    unsigned getCrouchState() override;
 };
 
 
diff --git a/source/games/exhumed/src/player.cpp b/source/games/exhumed/src/player.cpp
index f66789922..f8001a8ba 100644
--- a/source/games/exhumed/src/player.cpp
+++ b/source/games/exhumed/src/player.cpp
@@ -1199,18 +1199,6 @@ static void updatePlayerWeapon(DExhumedPlayer* const pPlayer)
 //
 //---------------------------------------------------------------------------
 
-unsigned GameInterface::getCrouchState()
-{
-    const bool swimming = getPlayer(nLocalPlayer)->bUnderwater;
-    return (CS_CANCROUCH * !swimming) | (CS_DISABLETOGGLE * swimming);
-}
-
-//---------------------------------------------------------------------------
-//
-//
-//
-//---------------------------------------------------------------------------
-
 static void updatePlayerAction(DExhumedPlayer* const pPlayer)
 {
     const auto pPlayerActor = pPlayer->GetActor();
diff --git a/source/games/exhumed/src/player.h b/source/games/exhumed/src/player.h
index 6b331ce2f..c75e04afd 100644
--- a/source/games/exhumed/src/player.h
+++ b/source/games/exhumed/src/player.h
@@ -123,6 +123,11 @@ public:
     {
         return 15.25;
     }
+
+    const unsigned getCrouchFlags() const override
+    {
+        return (CS_CANCROUCH * !bUnderwater) | (CS_DISABLETOGGLE * bUnderwater);
+    }
 };
 
 extern int PlayerCount;
diff --git a/source/games/sw/src/game.h b/source/games/sw/src/game.h
index 0a99646f7..f44090835 100644
--- a/source/games/sw/src/game.h
+++ b/source/games/sw/src/game.h
@@ -1869,6 +1869,13 @@ public:
     {
         GetActor()->spr.pos.Z = val - GetActor()->viewzoffset;
     }
+
+    const unsigned getCrouchFlags() const override
+    {
+        const bool crouchable = true;
+        const bool disableToggle = (Flags & (PF_JUMPING|PF_FALLING|PF_CLIMBING|PF_DIVING|PF_DEAD)) || sop;
+        return (CS_CANCROUCH * crouchable) | (CS_DISABLETOGGLE * disableToggle);
+    }
 };
 
 inline DSWPlayer* getPlayer(int index)
@@ -1914,7 +1921,6 @@ struct GameInterface : public ::GameInterface
     void ExitFromMenu() override;
     int GetCurrentSkill() override;
     void StartSoundEngine() override;
-    unsigned getCrouchState() override;
     void doPlayerMovement() override
     {
         const auto pp = getPlayer(myconnectindex);
diff --git a/source/games/sw/src/player.cpp b/source/games/sw/src/player.cpp
index 3b2f51ddf..177500ef4 100644
--- a/source/games/sw/src/player.cpp
+++ b/source/games/sw/src/player.cpp
@@ -6393,20 +6393,6 @@ void DoPlayerRun(DSWPlayer* pp)
 //
 //---------------------------------------------------------------------------
 
-unsigned GameInterface::getCrouchState()
-{
-    const auto pp = getPlayer(myconnectindex);
-    const bool crouchable = true;
-    const bool disableToggle = (pp->Flags & (PF_JUMPING|PF_FALLING|PF_CLIMBING|PF_DIVING|PF_DEAD)) || pp->sop;
-    return (CS_CANCROUCH * crouchable) | (CS_DISABLETOGGLE * disableToggle);
-}
-
-//---------------------------------------------------------------------------
-//
-//
-//
-//---------------------------------------------------------------------------
-
 void PlayerStateControl(DSWActor* actor)
 {
     if (actor == nullptr || !actor->hasU()) return;