diff --git a/regamedll/dlls/cdll_dll.h b/regamedll/dlls/cdll_dll.h
index da6cd36df..6e495368d 100644
--- a/regamedll/dlls/cdll_dll.h
+++ b/regamedll/dlls/cdll_dll.h
@@ -74,6 +74,7 @@ const int DEFAULT_FOV      = 90;	// the default field of view
 #define PLAYER_PREVENT_DUCK     BIT(4)
 #define PLAYER_PREVENT_CLIMB    BIT(5) // The player can't climb ladder
 #define PLAYER_PREVENT_JUMP     BIT(6)
+#define PLAYER_PREVENT_DDUCK    BIT(7)
 
 #define MENU_KEY_1 BIT(0)
 #define MENU_KEY_2 BIT(1)
diff --git a/regamedll/dlls/client.cpp b/regamedll/dlls/client.cpp
index 9708fa2c8..e1b4d3221 100644
--- a/regamedll/dlls/client.cpp
+++ b/regamedll/dlls/client.cpp
@@ -3299,6 +3299,26 @@ void EXT_FUNC InternalCommand(edict_t *pEntity, const char *pcmd, const char *pa
 		}
 	}
 #endif
+
+#ifdef REGAMEDLL_ADD
+	// Request from client for the given version of player movement control, if any
+	else if (FStrEq(pcmd, "cl_pmove_version"))
+	{
+		// cl_pmove_version <num>
+		if (CMD_ARGC_() < 2)
+			return; // invalid
+
+		PlayerMovementVersion &playerMovementVersion = pPlayer->CSPlayer()->m_MovementVersion;
+		playerMovementVersion.Set(parg1);
+
+		// If the client's requested movement version is newer, enforce it to the available one
+		if (playerMovementVersion.IsGreaterThan(PM_VERSION))
+		{
+			playerMovementVersion.Set(PM_VERSION); // reset to available version
+			CLIENT_COMMAND(pEntity, "cl_pmove_version %s\n", playerMovementVersion.ToString());
+		}
+	}
+#endif
 	else
 	{
 		if (g_pGameRules->ClientCommand_DeadOrAlive(GetClassPtr<CCSPlayer>((CBasePlayer *)pev), pcmd))
@@ -3727,6 +3747,20 @@ void EXT_FUNC ServerDeactivate()
 
 void EXT_FUNC ServerActivate(edict_t *pEdictList, int edictCount, int clientMax)
 {
+#ifdef REGAMEDLL_ADD
+	//
+	// Tells clients which version of player movement (pmove) the server is using
+	//
+	// In GoldSrc, both the server and clients handle player movement using shared code.
+	// If the server changes how movement works, due to improvements or bugfixes, it can mess up
+	// the client's movement prediction, causing desync. To avoid this, the server can tell clients what
+	// version of the movement code it's using. Clients that don't recognize or respond to this version
+	// will be treated as using the previous behavior, and the server will handle them accordingly.
+	// Clients that do recognize it will let the server know, so everything stays in sync.
+	//
+	SET_KEY_VALUE(GET_INFO_BUFFER(pEdictList), "pmove", PM_ServerVersion());
+#endif
+
 	int i;
 	CBaseEntity *pClass;
 
diff --git a/regamedll/pm_shared/pm_defs.h b/regamedll/pm_shared/pm_defs.h
index 8606c587d..75fba5076 100644
--- a/regamedll/pm_shared/pm_defs.h
+++ b/regamedll/pm_shared/pm_defs.h
@@ -91,6 +91,74 @@ typedef struct physent_s
 
 } physent_t;
 
+#define PM_VERSION_MAJOR 1
+#define PM_VERSION_MINOR 0
+#define PM_VERSION_PATCH 0
+#define PM_VERSION       PM_VERSION_MAJOR, PM_VERSION_MINOR, PM_VERSION_PATCH
+
+#define PM_VERSION_STRINGIZE(x) #x
+#define PM_VERSION_STRING(major,minor,patch) \
+	(patch == 0 ?\
+		PM_VERSION_STRINGIZE(major) "." PM_VERSION_STRINGIZE(minor) :\
+		PM_VERSION_STRINGIZE(major) "." PM_VERSION_STRINGIZE(minor) "." PM_VERSION_STRINGIZE(patch))
+
+// Control version of the player movement system
+struct PlayerMovementVersion {
+	uint8_t major, minor, patch;
+	uint32_t u32;
+
+	PlayerMovementVersion() {
+		Set(0, 0, 0);
+	}
+
+	PlayerMovementVersion(uint32_t majorVersion, uint32_t minorVersion, uint32_t patchVersion = 0) {
+		Set(majorVersion, minorVersion, patchVersion);
+	}
+
+	inline void Set(uint32_t majorVersion, uint32_t minorVersion, uint32_t patchVersion = 0)
+	{
+		major = majorVersion;
+		minor = minorVersion;
+		patch = patchVersion;
+		u32   = (major << 16) | (minor << 8) | patch;
+	}
+
+	inline void Set(const char *version)
+	{
+		if (!version)
+			return;
+
+		major = minor = patch = u32 = 0;
+		int result = sscanf(version, "%hhu.%hhu.%hhu", &major, &minor, &patch);
+		if (result < 1)
+			return; // major invalid
+
+		u32 = (major << 16) | (minor << 8) | patch;
+	}
+
+	// Compares if the current version is less than the given version
+	inline bool IsLessThan(uint32_t major, uint32_t minor, uint32_t patch = 0) const {
+		return u32 < ((major << 16) | (minor << 8) | patch);
+	}
+
+	// Compares if the current version is greater than the given version
+	inline bool IsGreaterThan(uint32_t major, uint32_t minor, uint32_t patch = 0) const {
+		return u32 > ((major << 16) | (minor << 8) | patch);
+	}
+
+	// Compares if the current version is greater than or equal to the given version
+	inline bool IsAtLeast(uint32_t major, uint32_t minor, uint32_t patch = 0) const {
+		return !IsLessThan(major, minor, patch);
+	}
+
+	const char *ToString() const {
+		static char string[14];
+		int len = Q_snprintf(string, sizeof(string), "%u.%u.%u", major, minor, patch);
+		if (patch == 0 && len > 2) string[len - 2] = '\0';
+		return string;
+	}
+};
+
 typedef struct playermove_s
 {
 	int player_index;				// So we don't try to run the PM_CheckStuck nudging too quickly.
@@ -111,7 +179,7 @@ typedef struct playermove_s
 	qboolean bInDuck;				// In process of ducking or ducked already?
 	int flTimeStepSound;			// For walking/falling
 									// Next time we can play a step sound
-	int iStepLeft;
+	qboolean iStepLeft;
 	float flFallVelocity;
 	vec3_t punchangle;
 	float flSwimTime;
diff --git a/regamedll/pm_shared/pm_shared.cpp b/regamedll/pm_shared/pm_shared.cpp
index d90206bc5..6ab253970 100644
--- a/regamedll/pm_shared/pm_shared.cpp
+++ b/regamedll/pm_shared/pm_shared.cpp
@@ -898,7 +898,7 @@ void PM_WalkMove()
 
 #ifdef REGAMEDLL_ADD
 	// Player can speed up the run if '+speed' button is pressed
-	if ((pmove->cmd.buttons & IN_RUN) && pmove->fuser3 > 0)
+	if (pmoveplayer->m_MovementVersion.IsAtLeast(1, 0) && (pmove->cmd.buttons & IN_RUN) && pmove->fuser3 > 0)
 	{
 		fmove *= pmove->fuser3;
 		smove *= pmove->fuser3;
@@ -1450,7 +1450,7 @@ void PM_CategorizePosition()
 
 	// Do not stick to the ground of an OBSERVER or NOCLIP mode
 #ifdef REGAMEDLL_FIXES
-	if (pmove->movetype == MOVETYPE_NOCLIP || pmove->movetype == MOVETYPE_NONE)
+	if (pmoveplayer->m_MovementVersion.IsAtLeast(1, 0) && (pmove->movetype == MOVETYPE_NOCLIP || pmove->movetype == MOVETYPE_NONE))
 	{
 		pmove->onground = -1;
 		return;
@@ -1704,7 +1704,7 @@ void PM_SpectatorMove()
 
 #ifdef REGAMEDLL_ADD
 		// Observer can accelerate in air if '+speed' button is pressed
-		if (pmove->cmd.buttons & IN_RUN)
+		if (pmoveplayer->m_MovementVersion.IsAtLeast(1, 0) && pmove->cmd.buttons & IN_RUN)
 		{
 			float flAirAccelerate = (pmove->fuser3 > 0.0f) ? pmove->fuser3 : max(pmove->movevars->airaccelerate / 100.0f, 7.0f);
 			fmove *= flAirAccelerate;
@@ -1737,12 +1737,15 @@ void PM_SpectatorMove()
 
 		addspeed = wishspeed - currentspeed;
 
-#ifndef REGAMEDLL_FIXES
 		if (addspeed <= 0)
-			return;
-#else
-		if (addspeed > 0)
+		{
+#ifdef REGAMEDLL_FIXES
+			if (pmoveplayer->m_MovementVersion.IsLessThan(1, 0))
 #endif
+				return;
+		}
+
+		if (addspeed > 0)
 		{
 			accelspeed = pmove->movevars->accelerate * pmove->frametime * wishspeed;
 			if (accelspeed > addspeed)
@@ -1844,7 +1847,7 @@ LINK_HOOK_VOID_CHAIN2(PM_UnDuck)
 void EXT_FUNC __API_HOOK(PM_UnDuck)()
 {
 #ifdef REGAMEDLL_ADD
-	if (unduck_method.value)
+	if (unduck_method.value || (pmove->iuser3 & PLAYER_PREVENT_DDUCK))
 #endif
 	{
 #ifdef REGAMEDLL_FIXES
@@ -2365,7 +2368,7 @@ void PM_NoClip()
 
 #ifdef REGAMEDLL_ADD
 	// Player with noclip can accelerate in air if '+speed' button is pressed
-	if ((pmove->cmd.buttons & IN_RUN) && pmove->fuser3 > 0)
+	if (pmoveplayer->m_MovementVersion.IsAtLeast(1, 0) && (pmove->cmd.buttons & IN_RUN) && pmove->fuser3 > 0)
 	{
 		float flAirAccelerate = pmove->fuser3;
 		fmove *= flAirAccelerate;
@@ -3379,3 +3382,8 @@ void EXT_FUNC __API_HOOK(PM_Init)(struct playermove_s *ppmove)
 
 	pm_shared_initialized = TRUE;
 }
+
+const char *PM_ServerVersion()
+{
+	return PM_VERSION_STRING(PM_VERSION_MAJOR, PM_VERSION_MINOR, PM_VERSION_PATCH);
+}
diff --git a/regamedll/pm_shared/pm_shared.h b/regamedll/pm_shared/pm_shared.h
index 2c0385f40..50550c696 100644
--- a/regamedll/pm_shared/pm_shared.h
+++ b/regamedll/pm_shared/pm_shared.h
@@ -96,4 +96,6 @@ void PM_AirAccelerate_OrigFunc(vec_t *wishdir, float wishspeed, float accel);
 void PM_AirMove(int playerIndex = 0);
 #endif
 
+const char *PM_ServerVersion();
+
 extern struct playermove_s *pmove;
diff --git a/regamedll/public/regamedll/API/CSPlayer.h b/regamedll/public/regamedll/API/CSPlayer.h
index f2bfe028b..05b81b704 100644
--- a/regamedll/public/regamedll/API/CSPlayer.h
+++ b/regamedll/public/regamedll/API/CSPlayer.h
@@ -186,6 +186,9 @@ class CCSPlayer: public CCSMonster {
 
 	int m_iGibDamageThreshold; // negative health to reach to gib player
 	usercmd_t m_LastCmd;
+
+	// Player movement version control
+	PlayerMovementVersion m_MovementVersion;
 };
 
 // Inlines
diff --git a/regamedll/public/regamedll/regamedll_api.h b/regamedll/public/regamedll/regamedll_api.h
index d655ee656..74879aa42 100644
--- a/regamedll/public/regamedll/regamedll_api.h
+++ b/regamedll/public/regamedll/regamedll_api.h
@@ -38,7 +38,7 @@
 #include <API/CSInterfaces.h>
 
 #define REGAMEDLL_API_VERSION_MAJOR 5
-#define REGAMEDLL_API_VERSION_MINOR 27
+#define REGAMEDLL_API_VERSION_MINOR 28
 
 // CBasePlayer::Spawn hook
 typedef IHookChainClass<void, class CBasePlayer> IReGameHook_CBasePlayer_Spawn;
diff --git a/regamedll/version/version.h b/regamedll/version/version.h
index fc64686b4..62c33001b 100644
--- a/regamedll/version/version.h
+++ b/regamedll/version/version.h
@@ -6,5 +6,5 @@
 #pragma once
 
 #define VERSION_MAJOR		5
-#define VERSION_MINOR		27
+#define VERSION_MINOR		28
 #define VERSION_MAINTENANCE	0