diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index eaa8d7449ca707f003a81e9fb7d76acbff2fbd3b..c4b2630c0b42c00dd6363cd6460782fb44b3cc8a 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -28,6 +28,10 @@
 #include "r_state.h"
 #include "r_defs.h"
 
+
+#define POLYOBJECTS
+
+
 #ifdef POLYOBJECTS
 
 /*
@@ -2265,7 +2269,7 @@ void T_PolyDoorSwing(polyswingdoor_t *th)
 	}
 }
 
-// T_PolyObjDisplace: shift a polyobject based on a control sector's heights. -Red
+// T_PolyObjDisplace: shift a polyobject based on a control sector's heights.
 void T_PolyObjDisplace(polydisplace_t *th)
 {
 	polyobj_t *po = Polyobj_GetForNum(th->polyObjNum);
@@ -2274,10 +2278,10 @@ void T_PolyObjDisplace(polydisplace_t *th)
 
 	if (!po)
 #ifdef RANGECHECK
-		I_Error("T_PolyDoorSwing: thinker has invalid id %d\n", th->polyObjNum);
+		I_Error("T_PolyObjDisplace: thinker has invalid id %d\n", th->polyObjNum);
 #else
 	{
-		CONS_Debug(DBG_POLYOBJ, "T_PolyDoorSwing: thinker with invalid id %d removed.\n", th->polyObjNum);
+		CONS_Debug(DBG_POLYOBJ, "T_PolyObjDisplace: thinker with invalid id %d removed.\n", th->polyObjNum);
 		P_RemoveThinkerDelayed(&th->thinker);
 		return;
 	}
@@ -2305,6 +2309,45 @@ void T_PolyObjDisplace(polydisplace_t *th)
 		th->oldHeights = newheights;
 }
 
+// T_PolyObjRotDisplace: rotate a polyobject based on a control sector's heights.
+void T_PolyObjRotDisplace(polyrotdisplace_t *th)
+{
+	polyobj_t *po = Polyobj_GetForNum(th->polyObjNum);
+	fixed_t newheights, delta;
+	fixed_t rotangle;
+
+	if (!po)
+#ifdef RANGECHECK
+		I_Error("T_PolyObjRotDisplace: thinker has invalid id %d\n", th->polyObjNum);
+#else
+	{
+		CONS_Debug(DBG_POLYOBJ, "T_PolyObjRotDisplace: thinker with invalid id %d removed.\n", th->polyObjNum);
+		P_RemoveThinkerDelayed(&th->thinker);
+		return;
+	}
+#endif
+
+	// check for displacement due to override and reattach when possible
+	if (po->thinker == NULL)
+	{
+		po->thinker = &th->thinker;
+
+		// reset polyobject's thrust
+		po->thrust = FRACUNIT;
+	}
+
+	newheights = th->controlSector->floorheight+th->controlSector->ceilingheight;
+	delta = newheights-th->oldHeights;
+
+	if (!delta)
+		return;
+
+	rotangle = FixedMul(th->rotscale, delta);
+
+	if (Polyobj_rotate(po, FixedAngle(rotangle), th->turnobjs))
+		th->oldHeights = newheights;
+}
+
 static inline INT32 Polyobj_AngSpeed(INT32 speed)
 {
 	return (speed*ANG1)>>3; // no FixedAngle()
@@ -2764,6 +2807,52 @@ INT32 EV_DoPolyObjDisplace(polydisplacedata_t *prdata)
 	return 1;
 }
 
+INT32 EV_DoPolyObjRotDisplace(polyrotdisplacedata_t *prdata)
+{
+	polyobj_t *po;
+	polyobj_t *oldpo;
+	polyrotdisplace_t *th;
+	INT32 start;
+
+	if (!(po = Polyobj_GetForNum(prdata->polyObjNum)))
+	{
+		CONS_Debug(DBG_POLYOBJ, "EV_DoPolyObjRotate: bad polyobj %d\n", prdata->polyObjNum);
+		return 0;
+	}
+
+	// don't allow line actions to affect bad polyobjects
+	if (po->isBad)
+		return 0;
+
+	// create a new thinker
+	th = Z_Malloc(sizeof(polyrotdisplace_t), PU_LEVSPEC, NULL);
+	th->thinker.function.acp1 = (actionf_p1)T_PolyObjRotDisplace;
+	PolyObj_AddThinker(&th->thinker);
+	po->thinker = &th->thinker;
+
+	// set fields
+	th->polyObjNum = prdata->polyObjNum;
+
+	th->controlSector = prdata->controlSector;
+	th->oldHeights = th->controlSector->floorheight+th->controlSector->ceilingheight;
+
+	th->rotscale = prdata->rotscale;
+	th->turnobjs = prdata->turnobjs;
+
+	oldpo = po;
+
+	// apply action to mirroring polyobjects as well
+	start = 0;
+	while ((po = Polyobj_GetChild(oldpo, &start)))
+	{
+		prdata->polyObjNum = po->id; // change id to match child polyobject's
+		EV_DoPolyObjRotDisplace(prdata);
+	}
+
+	// action was successful
+	return 1;
+}
+
 void T_PolyObjFlag(polymove_t *th)
 {
 	polyobj_t *po = Polyobj_GetForNum(th->polyObjNum);
diff --git a/src/p_polyobj.h b/src/p_polyobj.h
index b871ec83780e241a18550a534eaf782efae459dd..73a11d237fcaf611005c95154b3be6e671d728c8 100644
--- a/src/p_polyobj.h
+++ b/src/p_polyobj.h
@@ -207,6 +207,17 @@ typedef struct polydisplace_s
 	fixed_t oldHeights;
 } polydisplace_t;
 
+typedef struct polyrotdisplace_s
+{
+	thinker_t thinker; // must be first
+
+	INT32 polyObjNum;
+	struct sector_s *controlSector;
+	fixed_t rotscale;
+	UINT8 turnobjs;
+	fixed_t oldHeights;
+} polyrotdisplace_t;
+
 typedef struct polyfade_s
 {
 	thinker_t thinker; // must be first
@@ -280,6 +291,14 @@ typedef struct polydisplacedata_s
 	fixed_t dy;
 } polydisplacedata_t;
 
+typedef struct polyrotdisplacedata_s
+{
+	INT32 polyObjNum;
+	struct sector_s *controlSector;
+	fixed_t rotscale;
+	UINT8 turnobjs;
+} polyrotdisplacedata_t;
+
 typedef struct polyfadedata_s
 {
 	INT32 polyObjNum;
@@ -310,6 +329,7 @@ void T_PolyObjWaypoint (polywaypoint_t *);
 void T_PolyDoorSlide(polyslidedoor_t *);
 void T_PolyDoorSwing(polyswingdoor_t *);
 void T_PolyObjDisplace  (polydisplace_t *);
+void T_PolyObjRotDisplace  (polyrotdisplace_t *);
 void T_PolyObjFlag  (polymove_t *);
 void T_PolyObjFade  (polyfade_t *);
 
@@ -318,6 +338,7 @@ INT32 EV_DoPolyObjMove(polymovedata_t *);
 INT32 EV_DoPolyObjWaypoint(polywaypointdata_t *);
 INT32 EV_DoPolyObjRotate(polyrotdata_t *);
 INT32 EV_DoPolyObjDisplace(polydisplacedata_t *);
+INT32 EV_DoPolyObjRotDisplace(polyrotdisplacedata_t *);
 INT32 EV_DoPolyObjFlag(struct line_s *);
 INT32 EV_DoPolyObjFade(polyfadedata_t *);
 
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 7a31fecfcb4f1fa3de471b360a1d582dd34af611..0d58387b9e6be03187077ebf3642cda67cd7365b 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -1312,6 +1312,7 @@ typedef enum
 	tc_polyswingdoor,
 	tc_polyflag,
 	tc_polydisplace,
+	tc_polyrotdisplace,
 	tc_polyfade,
 #endif
 	tc_end
@@ -2088,6 +2089,17 @@ static void SavePolydisplaceThinker(const thinker_t *th, const UINT8 type)
 	WRITEFIXED(save_p, ht->oldHeights);
 }
 
+static void SavePolyrotdisplaceThinker(const thinker_t *th, const UINT8 type)
+{
+	const polyrotdisplace_t *ht = (const void *)th;
+	WRITEUINT8(save_p, type);
+	WRITEINT32(save_p, ht->polyObjNum);
+	WRITEUINT32(save_p, SaveSector(ht->controlSector));
+	WRITEFIXED(save_p, ht->rotscale);
+	WRITEUINT8(save_p, ht->turnobjs);
+	WRITEFIXED(save_p, ht->oldHeights);
+}
+
 static void SavePolyfadeThinker(const thinker_t *th, const UINT8 type)
 {
 	const polyfade_t *ht = (const void *)th;
@@ -2333,6 +2345,11 @@ static void P_NetArchiveThinkers(void)
 			SavePolydisplaceThinker(th, tc_polydisplace);
 			continue;
 		}
+		else if (th->function.acp1 == (actionf_p1)T_PolyObjRotDisplace)
+		{
+			SavePolyrotdisplaceThinker(th, tc_polyrotdisplace);
+			continue;
+		}
 		else if (th->function.acp1 == (actionf_p1)T_PolyObjFade)
 		{
 			SavePolyfadeThinker(th, tc_polyfade);
@@ -3217,6 +3234,18 @@ static inline void LoadPolydisplaceThinker(actionf_p1 thinker)
 	P_AddThinker(&ht->thinker);
 }
 
+static inline void LoadPolyrotdisplaceThinker(actionf_p1 thinker)
+{
+	polyrotdisplace_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
+	ht->thinker.function.acp1 = thinker;
+	ht->polyObjNum = READINT32(save_p);
+	ht->controlSector = LoadSector(READUINT32(save_p));
+	ht->rotscale = READFIXED(save_p);
+	ht->turnobjs = READUINT8(save_p);
+	ht->oldHeights = READFIXED(save_p);
+	P_AddThinker(&ht->thinker);
+}
+
 //
 // LoadPolyfadeThinker
 //
@@ -3446,6 +3475,10 @@ static void P_NetUnArchiveThinkers(void)
 				LoadPolydisplaceThinker((actionf_p1)T_PolyObjDisplace);
 				break;
 
+			case tc_polyrotdisplace:
+				LoadPolyrotdisplaceThinker((actionf_p1)T_PolyObjRotDisplace);
+				break;
+
 			case tc_polyfade:
 				LoadPolyfadeThinker((actionf_p1)T_PolyObjFade);
 				break;
diff --git a/src/p_spec.c b/src/p_spec.c
index 0f5a502afce08d0421b0898fd65892451670326d..f8ce313eac6bca2bca405b7aed51c9b37eeeb377 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -1412,6 +1412,34 @@ static boolean PolyDisplace(line_t *line)
 	return EV_DoPolyObjDisplace(&pdd);
 }
 
+
+/** Similar to PolyDisplace().
+ */
+static boolean PolyRotDisplace(line_t *line)
+{
+	polyrotdisplacedata_t pdd;
+	fixed_t anginter, distinter;
+
+	pdd.polyObjNum = line->tag;
+	pdd.controlSector = line->frontsector;
+
+	// Rotate 'anginter' interval for each 'distinter' interval from the control sector.
+	// Use default values if not provided as fallback.
+	anginter	= sides[line->sidenum[0]].rowoffset ? sides[line->sidenum[0]].rowoffset : 90*FRACUNIT;
+	distinter	= sides[line->sidenum[0]].textureoffset ? sides[line->sidenum[0]].textureoffset : 128*FRACUNIT;
+	pdd.rotscale = FixedDiv(anginter, distinter);
+
+	// Same behavior as other rotators when carrying things.
+	if (line->flags & ML_NOCLIMB)
+		pdd.turnobjs = 0;
+	else if (line->flags & ML_EFFECT4)
+		pdd.turnobjs = 2;
+	else
+		pdd.turnobjs = 1;
+
+	return EV_DoPolyObjRotDisplace(&pdd);
+}
+
 #endif // ifdef POLYOBJECTS
 
 /** Changes a sector's tag.
@@ -7363,6 +7391,10 @@ void P_SpawnSpecials(INT32 fromnetsave)
 			case 31: // Polyobj_Displace
 				PolyDisplace(&lines[i]);
 				break;
+
+			case 32: // Polyobj_RotDisplace
+				PolyRotDisplace(&lines[i]);
+				break;
 		}
 	}
 #endif