diff --git a/extras/conf/udb/Includes/SRB222_linedefs.cfg b/extras/conf/udb/Includes/SRB222_linedefs.cfg
index 68077862392a0ca2668d296f051203cd250e93ed..152cafe211fe27ae8b2169e7fe795c253e8fb182 100644
--- a/extras/conf/udb/Includes/SRB222_linedefs.cfg
+++ b/extras/conf/udb/Includes/SRB222_linedefs.cfg
@@ -10,6 +10,45 @@ udmf
 			prefix = "(0)";
 		}
 
+		6
+		{
+			title = "Sector Set Portal";
+			prefix = "(6)";
+			arg0
+			{
+				title = "Target sector tag";
+				type = 13;
+			}
+			arg1
+			{
+				title = "Portal type";
+				type = 11;
+				enum
+				{
+					0 = "Link to portal with same tag";
+					1 = "Copy portal from second tag";
+					2 = "Skybox portal";
+					3 = "Plane portal";
+					4 = "Horizon portal";
+					5 = "Copy portal to line";
+					6 = "Interactive portal (unimplemented)";
+					7 = "Link to sector with second tag";
+					8 = "Link to object with second tag";
+				}
+			}
+			arg2
+			{
+				title = "Affected planes";
+				type = 11;
+				enum = "floorceiling";
+			}
+			arg3
+			{
+				title = "Misc";
+				tooltip = "For type 0 portal: specifies whether the line belongs to the sector viewed\nthrough the portal (1) or the sector in which the portal is seen (0).\nFor type 1 portal: specifies the sector tag of the portal to copy.\nFor type 7 portal: specifies the sector tag to make a portal to.\nFor type 8 portal: specifies the object tag to make a portal to.";
+			}
+		}
+
 		7
 		{
 			title = "Sector Flat Alignment";
diff --git a/src/deh_tables.c b/src/deh_tables.c
index b53cd00c8fc23149a9a9310776335d59e7edd27a..6d29fa2c77e51b27649cc253c11c1905fe992382 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -4291,7 +4291,7 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	"MT_POLYANCHOR",
 	"MT_POLYSPAWN",
 
-	// Skybox objects
+	// Portal objects
 	"MT_SKYBOX",
 
 	// Debris
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 60a5b256245367858a736ae56342ba34b6bc8b7d..d448aa55e847015fa953d5cf6b9401f0bc62eb83 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -566,7 +566,7 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 
 		for (i = 0; i < subsector->numlines; i++, line++)
 		{
-			if (!line->glseg && line->linedef->special == HORIZONSPECIAL && R_PointOnSegSide(dup_viewx, dup_viewy, line) == 0)
+			if (!line->glseg && line->linedef->special == SPECIAL_HORIZON_LINE && R_PointOnSegSide(dup_viewx, dup_viewy, line) == 0)
 			{
 				P_ClosestPointOnLine(viewx, viewy, line->linedef, &v);
 				dist = FIXED_TO_FLOAT(R_PointToDist(v.x, v.y));
@@ -1139,9 +1139,6 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 		INT32 gl_toptexture = 0, gl_bottomtexture = 0;
 		fixed_t texturevpeg;
 
-		boolean bothceilingssky = false; // turned on if both back and front ceilings are sky
-		boolean bothfloorssky = false; // likewise, but for floors
-
 		SLOPEPARAMS(gl_backsector->c_slope, worldhigh, worldhighslope, gl_backsector->ceilingheight)
 		SLOPEPARAMS(gl_backsector->f_slope, worldlow,  worldlowslope,  gl_backsector->floorheight)
 
@@ -1506,7 +1503,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 	else
 	{
 		// Single sided line... Deal only with the middletexture (if one exists)
-		if (gl_midtexture && gl_linedef->special != HORIZONSPECIAL) // (Ignore horizon line for OGL)
+		if (gl_midtexture && gl_linedef->special != SPECIAL_HORIZON_LINE) // (Ignore horizon line for OGL)
 		{
 			grTex = HWR_GetTexture(gl_midtexture);
 			xscale = FixedToFloat(gl_sidedef->scalex_mid);
@@ -1885,12 +1882,6 @@ static boolean CheckClip(seg_t * seg, sector_t * afrontsector, sector_t * abacks
 {
 	fixed_t frontf1,frontf2, frontc1, frontc2; // front floor/ceiling ends
 	fixed_t backf1, backf2, backc1, backc2; // back floor ceiling ends
-	boolean bothceilingssky = false, bothfloorssky = false;
-
-	if (abacksector->ceilingpic == skyflatnum && afrontsector->ceilingpic == skyflatnum)
-		bothceilingssky = true;
-	if (abacksector->floorpic == skyflatnum && afrontsector->floorpic == skyflatnum)
-		bothfloorssky = true;
 
 	// GZDoom method of sloped line clipping
 
@@ -2409,6 +2400,7 @@ static void HWR_AddLine(seg_t * line)
 #endif
 
 	gl_backsector = line->backsector;
+	bothceilingssky = bothfloorssky = false;
 
 #ifdef NEWCLIP
 	if (!line->backsector)
@@ -2417,13 +2409,14 @@ static void HWR_AddLine(seg_t * line)
     }
     else
     {
-		boolean bothceilingssky = false, bothfloorssky = false;
-
 		gl_backsector = R_FakeFlat(gl_backsector, &tempsec, NULL, NULL, true);
 
-		if (gl_backsector->ceilingpic == skyflatnum && gl_frontsector->ceilingpic == skyflatnum)
+		if (gl_backsector->ceilingpic == skyflatnum && gl_frontsector->ceilingpic == skyflatnum
+		&& !(P_SectorHasCeilingPortal(gl_backsector) || P_SectorHasCeilingPortal(gl_frontsector)))
 			bothceilingssky = true;
-		if (gl_backsector->floorpic == skyflatnum && gl_frontsector->floorpic == skyflatnum)
+
+		if (gl_backsector->floorpic == skyflatnum && gl_frontsector->floorpic == skyflatnum
+		&& !(P_SectorHasFloorPortal(gl_backsector) || P_SectorHasFloorPortal(gl_frontsector)))
 			bothfloorssky = true;
 
 		if (bothceilingssky && bothfloorssky) // everything's sky? let's save us a bit of time then
@@ -3020,64 +3013,16 @@ static void HWR_Subsector(size_t num)
 	}
 
 	//SoM: 4/7/2000: Test to make Boom water work in Hardware mode.
-	gl_frontsector = R_FakeFlat(gl_frontsector, &tempsec, &floorlightlevel,
-								&ceilinglightlevel, false);
-	//FIXME: Use floorlightlevel and ceilinglightlevel insted of lightlevel.
+	gl_frontsector = R_FakeFlat(gl_frontsector, &tempsec, &floorlightlevel, &ceilinglightlevel, false);
 
 	floorcolormap = ceilingcolormap = gl_frontsector->extra_colormap;
 
-	// ------------------------------------------------------------------------
-	// sector lighting, DISABLED because it's done in HWR_StoreWallRange
-	// ------------------------------------------------------------------------
-	/// \todo store a RGBA instead of just intensity, allow coloured sector lighting
-	//light = (FUBYTE)(sub->sector->lightlevel & 0xFF) / 255.0f;
-	//gl_cursectorlight.red   = light;
-	//gl_cursectorlight.green = light;
-	//gl_cursectorlight.blue  = light;
-	//gl_cursectorlight.alpha = light;
-
-// ----- end special tricks -----
 	cullFloorHeight   = P_GetSectorFloorZAt  (gl_frontsector, viewx, viewy);
 	cullCeilingHeight = P_GetSectorCeilingZAt(gl_frontsector, viewx, viewy);
 	locFloorHeight    = P_GetSectorFloorZAt  (gl_frontsector, gl_frontsector->soundorg.x, gl_frontsector->soundorg.y);
 	locCeilingHeight  = P_GetSectorCeilingZAt(gl_frontsector, gl_frontsector->soundorg.x, gl_frontsector->soundorg.y);
 
-	if (gl_frontsector->ffloors)
-	{
-		boolean anyMoved = gl_frontsector->moved;
-
-		if (anyMoved == false)
-		{
-			for (rover = gl_frontsector->ffloors; rover; rover = rover->next)
-			{
-				sector_t *controlSec = &sectors[rover->secnum];
-				if (controlSec->moved == true)
-				{
-					anyMoved = true;
-					break;
-				}
-			}
-		}
-
-		if (anyMoved == true)
-		{
-			gl_frontsector->numlights = sub->sector->numlights = 0;
-			R_Prep3DFloors(gl_frontsector);
-			sub->sector->lightlist = gl_frontsector->lightlist;
-			sub->sector->numlights = gl_frontsector->numlights;
-			sub->sector->moved = gl_frontsector->moved = false;
-		}
-
-		light = R_GetPlaneLight(gl_frontsector, locFloorHeight, false);
-		if (gl_frontsector->floorlightsec == -1 && !gl_frontsector->floorlightabsolute)
-			floorlightlevel = max(0, min(255, *gl_frontsector->lightlist[light].lightlevel + gl_frontsector->floorlightlevel));
-		floorcolormap = *gl_frontsector->lightlist[light].extra_colormap;
-
-		light = R_GetPlaneLight(gl_frontsector, locCeilingHeight, false);
-		if (gl_frontsector->ceilinglightsec == -1 && !gl_frontsector->ceilinglightabsolute)
-			ceilinglightlevel = max(0, min(255, *gl_frontsector->lightlist[light].lightlevel + gl_frontsector->ceilinglightlevel));
-		ceilingcolormap = *gl_frontsector->lightlist[light].extra_colormap;
-	}
+	R_CheckSectorLightLists(sub->sector, gl_frontsector, &floorlightlevel, &ceilinglightlevel, &floorcolormap, &ceilingcolormap);
 
 	sub->sector->extra_colormap = gl_frontsector->extra_colormap;
 
diff --git a/src/info.h b/src/info.h
index a2d87dbdc10644edaaf34187d45495d70ba49f85..20a0a4b8a5823114ad99265d72876fdf5e606c1d 100644
--- a/src/info.h
+++ b/src/info.h
@@ -5123,7 +5123,7 @@ typedef enum mobj_type
 	MT_POLYANCHOR,
 	MT_POLYSPAWN,
 
-	// Skybox objects
+	// Portal objects
 	MT_SKYBOX,
 
 	// Debris
diff --git a/src/p_map.c b/src/p_map.c
index e01798054970ee3364fa65883fa25f577c700ed7..1888d92af36b149ccb96065934c6cfe9c8ec65a8 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -3477,7 +3477,7 @@ static void PTR_GlideClimbTraverse(line_t *li)
 	}
 
 	// see about climbing on the wall
-	if (!(checkline->flags & ML_NOCLIMB) && checkline->special != HORIZONSPECIAL)
+	if (!(checkline->flags & ML_NOCLIMB) && checkline->special != SPECIAL_HORIZON_LINE)
 	{
 		boolean canclimb;
 		angle_t climbangle, climbline;
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 0f3668f5afb5dca5258eda7c9970c6059fce1c4a..41d7e3c80d1a02c0737c1c3eed032eaaee58cf67 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -41,20 +41,19 @@ UINT8 *save_p;
 
 // Block UINT32s to attempt to ensure that the correct data is
 // being sent and received
-#define ARCHIVEBLOCK_MISC     0x7FEEDEED
-#define ARCHIVEBLOCK_PLAYERS  0x7F448008
-#define ARCHIVEBLOCK_WORLD    0x7F8C08C0
-#define ARCHIVEBLOCK_POBJS    0x7F928546
-#define ARCHIVEBLOCK_THINKERS 0x7F37037C
-#define ARCHIVEBLOCK_SPECIALS 0x7F228378
-#define ARCHIVEBLOCK_EMBLEMS  0x7F4A5445
+#define ARCHIVEBLOCK_MISC       0x7FEEDEED
+#define ARCHIVEBLOCK_PLAYERS    0x7F448008
+#define ARCHIVEBLOCK_WORLD      0x7F8C08C0
+#define ARCHIVEBLOCK_POBJS      0x7F928546
+#define ARCHIVEBLOCK_THINKERS   0x7F37037C
+#define ARCHIVEBLOCK_SPECIALS   0x7F228378
+#define ARCHIVEBLOCK_EMBLEMS    0x7F4A5445
+#define ARCHIVEBLOCK_SECPORTALS 0x7FBE34C9
 
 // Note: This cannot be bigger
 // than an UINT16
 typedef enum
 {
-//	RFLAGPOINT = 0x01,
-//	BFLAGPOINT = 0x02,
 	CAPSULE    = 0x04,
 	AWAYVIEW   = 0x08,
 	FIRSTAXIS  = 0x10,
@@ -853,33 +852,43 @@ static void P_NetUnArchiveWaypoints(void)
 #define SD_DIFF3     0x80
 
 // diff3 flags
-#define SD_TAGLIST   0x01
-#define SD_COLORMAP  0x02
+#define SD_TAGLIST      0x01
+#define SD_COLORMAP     0x02
 #define SD_CRUMBLESTATE 0x04
-#define SD_FLOORLIGHT 0x08
-#define SD_CEILLIGHT 0x10
-#define SD_FLAG      0x20
-#define SD_SPECIALFLAG 0x40
-#define SD_DIFF4     0x80
+#define SD_FLOORLIGHT   0x08
+#define SD_CEILLIGHT    0x10
+#define SD_FLAG         0x20
+#define SD_SPECIALFLAG  0x40
+#define SD_DIFF4        0x80
 
-//diff4 flags
+// diff4 flags
 #define SD_DAMAGETYPE 0x01
 #define SD_TRIGGERTAG 0x02
 #define SD_TRIGGERER 0x04
-#define SD_GRAVITY   0x08
-#define SD_FXSCALE   0x10
-#define SD_FYSCALE   0x20
-#define SD_CXSCALE   0x40
-#define SD_CYSCALE   0x80
-
+#define SD_FXSCALE   0x08
+#define SD_FYSCALE   0x10
+#define SD_CXSCALE   0x20
+#define SD_CYSCALE   0x40
+#define SD_DIFF5     0x80
+
+// diff5 flags
+#define SD_GRAVITY     0x01
+#define SD_FLOORPORTAL 0x02
+#define SD_CEILPORTAL  0x04
+
+// diff1 flags
 #define LD_FLAG          0x01
 #define LD_SPECIAL       0x02
 #define LD_CLLCOUNT      0x04
 #define LD_ARGS          0x08
 #define LD_STRINGARGS    0x10
-#define LD_EXECUTORDELAY 0x20
-#define LD_SIDE1         0x40
-#define LD_SIDE2         0x80
+#define LD_SIDE1         0x20
+#define LD_SIDE2         0x40
+#define LD_DIFF2         0x80
+
+// diff2 flags
+#define LD_EXECUTORDELAY 0x01
+#define LD_TRANSFPORTAL  0x02
 
 // sidedef flags
 enum
@@ -1028,11 +1037,11 @@ static void ArchiveSectors(void)
 	size_t i, j;
 	const sector_t *ss = sectors;
 	const sector_t *spawnss = spawnsectors;
-	UINT8 diff, diff2, diff3, diff4;
+	UINT8 diff, diff2, diff3, diff4, diff5;
 
 	for (i = 0; i < numsectors; i++, ss++, spawnss++)
 	{
-		diff = diff2 = diff3 = diff4 = 0;
+		diff = diff2 = diff3 = diff4 = diff5 = 0;
 		if (ss->floorheight != spawnss->floorheight)
 			diff |= SD_FLOORHT;
 		if (ss->ceilingheight != spawnss->ceilingheight)
@@ -1094,11 +1103,18 @@ static void ArchiveSectors(void)
 		if (ss->triggerer != spawnss->triggerer)
 			diff4 |= SD_TRIGGERER;
 		if (ss->gravity != spawnss->gravity)
-			diff4 |= SD_GRAVITY;
+			diff5 |= SD_GRAVITY;
+		if (ss->portal_floor != spawnss->portal_floor)
+			diff5 |= SD_FLOORPORTAL;
+		if (ss->portal_ceiling != spawnss->portal_ceiling)
+			diff5 |= SD_CEILPORTAL;
 
 		if (ss->ffloors && CheckFFloorDiff(ss))
 			diff |= SD_FFLOORS;
 
+		if (diff5)
+			diff4 |= SD_DIFF5;
+
 		if (diff4)
 			diff3 |= SD_DIFF4;
 
@@ -1118,6 +1134,8 @@ static void ArchiveSectors(void)
 				WRITEUINT8(save_p, diff3);
 			if (diff3 & SD_DIFF4)
 				WRITEUINT8(save_p, diff4);
+			if (diff4 & SD_DIFF5)
+				WRITEUINT8(save_p, diff5);
 			if (diff & SD_FLOORHT)
 				WRITEFIXED(save_p, ss->floorheight);
 			if (diff & SD_CEILHT)
@@ -1174,8 +1192,6 @@ static void ArchiveSectors(void)
 				WRITEINT16(save_p, ss->triggertag);
 			if (diff4 & SD_TRIGGERER)
 				WRITEUINT8(save_p, ss->triggerer);
-			if (diff4 & SD_GRAVITY)
-				WRITEFIXED(save_p, ss->gravity);
 			if (diff4 & SD_FXSCALE)
 				WRITEFIXED(save_p, ss->floorxscale);
 			if (diff4 & SD_FYSCALE)
@@ -1184,6 +1200,12 @@ static void ArchiveSectors(void)
 				WRITEFIXED(save_p, ss->ceilingxscale);
 			if (diff4 & SD_CYSCALE)
 				WRITEFIXED(save_p, ss->ceilingyscale);
+			if (diff5 & SD_GRAVITY)
+				WRITEFIXED(save_p, ss->gravity);
+			if (diff5 & SD_FLOORPORTAL)
+				WRITEUINT32(save_p, ss->portal_floor);
+			if (diff5 & SD_CEILPORTAL)
+				WRITEUINT32(save_p, ss->portal_ceiling);
 			if (diff & SD_FFLOORS)
 				ArchiveFFloors(ss);
 		}
@@ -1196,7 +1218,7 @@ static void UnArchiveSectors(void)
 {
 	UINT32 i;
 	UINT16 j;
-	UINT8 diff, diff2, diff3, diff4;
+	UINT8 diff, diff2, diff3, diff4, diff5;
 	for (;;)
 	{
 		i = READUINT32(save_p);
@@ -1220,6 +1242,10 @@ static void UnArchiveSectors(void)
 			diff4 = READUINT8(save_p);
 		else
 			diff4 = 0;
+		if (diff4 & SD_DIFF5)
+			diff5 = READUINT8(save_p);
+		else
+			diff5 = 0;
 
 		if (diff & SD_FLOORHT)
 			sectors[i].floorheight = READFIXED(save_p);
@@ -1303,8 +1329,6 @@ static void UnArchiveSectors(void)
 			sectors[i].triggertag = READINT16(save_p);
 		if (diff4 & SD_TRIGGERER)
 			sectors[i].triggerer = READUINT8(save_p);
-		if (diff4 & SD_GRAVITY)
-			sectors[i].gravity = READFIXED(save_p);
 		if (diff4 & SD_FXSCALE)
 			sectors[i].floorxscale = READFIXED(save_p);
 		if (diff4 & SD_FYSCALE)
@@ -1313,6 +1337,12 @@ static void UnArchiveSectors(void)
 			sectors[i].ceilingxscale = READFIXED(save_p);
 		if (diff4 & SD_CYSCALE)
 			sectors[i].ceilingyscale = READFIXED(save_p);
+		if (diff5 & SD_GRAVITY)
+			sectors[i].gravity = READFIXED(save_p);
+		if (diff5 & SD_FLOORPORTAL)
+			sectors[i].portal_floor = READUINT32(save_p);
+		if (diff5 & SD_CEILPORTAL)
+			sectors[i].portal_ceiling = READUINT32(save_p);
 
 		if (diff & SD_FFLOORS)
 			UnArchiveFFloors(&sectors[i]);
@@ -1409,13 +1439,14 @@ static void ArchiveLines(void)
 	size_t i;
 	const line_t *li = lines;
 	const line_t *spawnli = spawnlines;
-	UINT8 diff;
-	UINT32 diff2;
-	UINT32 diff3;
+	UINT8 diff, diff2;
+	UINT32 side1diff;
+	UINT32 side2diff;
 
 	for (i = 0; i < numlines; i++, spawnli++, li++)
 	{
-		diff = diff2 = diff3 = 0;
+		diff = diff2 = 0;
+		side1diff = side2diff = 0;
 
 		if (li->special != spawnli->special)
 			diff |= LD_SPECIAL;
@@ -1430,25 +1461,33 @@ static void ArchiveLines(void)
 			diff |= LD_STRINGARGS;
 
 		if (li->executordelay != spawnli->executordelay)
-			diff |= LD_EXECUTORDELAY;
+			diff2 |= LD_EXECUTORDELAY;
+
+		if (li->secportal != spawnli->secportal)
+			diff2 |= LD_TRANSFPORTAL;
 
 		if (li->sidenum[0] != NO_SIDEDEF)
 		{
-			diff2 = GetSideDiff(&sides[li->sidenum[0]], &spawnsides[li->sidenum[0]]);
-			if (diff2)
+			side1diff = GetSideDiff(&sides[li->sidenum[0]], &spawnsides[li->sidenum[0]]);
+			if (side1diff)
 				diff |= LD_SIDE1;
 		}
 		if (li->sidenum[1] != NO_SIDEDEF)
 		{
-			diff3 = GetSideDiff(&sides[li->sidenum[1]], &spawnsides[li->sidenum[1]]);
-			if (diff3)
+			side2diff = GetSideDiff(&sides[li->sidenum[1]], &spawnsides[li->sidenum[1]]);
+			if (side2diff)
 				diff |= LD_SIDE2;
 		}
 
+		if (diff2)
+			diff |= LD_DIFF2;
+
 		if (diff)
 		{
 			WRITEUINT32(save_p, i);
 			WRITEUINT8(save_p, diff);
+			if (diff & LD_DIFF2)
+				WRITEUINT8(save_p, diff2);
 			if (diff & LD_FLAG)
 				WRITEINT16(save_p, li->flags);
 			if (diff & LD_SPECIAL)
@@ -1480,12 +1519,14 @@ static void ArchiveLines(void)
 						WRITECHAR(save_p, li->stringargs[j][k]);
 				}
 			}
-			if (diff & LD_EXECUTORDELAY)
-				WRITEINT32(save_p, li->executordelay);
 			if (diff & LD_SIDE1)
-				ArchiveSide(&sides[li->sidenum[0]], diff2);
+				ArchiveSide(&sides[li->sidenum[0]], side1diff);
 			if (diff & LD_SIDE2)
-				ArchiveSide(&sides[li->sidenum[1]], diff3);
+				ArchiveSide(&sides[li->sidenum[1]], side2diff);
+			if (diff2 & LD_EXECUTORDELAY)
+				WRITEINT32(save_p, li->executordelay);
+			if (diff2 & LD_TRANSFPORTAL)
+				WRITEUINT32(save_p, li->secportal);
 		}
 	}
 	WRITEUINT32(save_p, 0xffffffff);
@@ -1537,7 +1578,7 @@ static void UnArchiveLines(void)
 {
 	UINT32 i;
 	line_t *li;
-	UINT8 diff;
+	UINT8 diff, diff2;
 
 	for (;;)
 	{
@@ -1549,6 +1590,10 @@ static void UnArchiveLines(void)
 			I_Error("Invalid line number %u from server", i);
 
 		diff = READUINT8(save_p);
+		if (diff & LD_DIFF2)
+			diff2 = READUINT8(save_p);
+		else
+			diff2 = 0;
 		li = &lines[i];
 
 		if (diff & LD_FLAG)
@@ -1584,12 +1629,14 @@ static void UnArchiveLines(void)
 				li->stringargs[j][len] = '\0';
 			}
 		}
-		if (diff & LD_EXECUTORDELAY)
-			li->executordelay = READINT32(save_p);
 		if (diff & LD_SIDE1)
 			UnArchiveSide(&sides[li->sidenum[0]]);
 		if (diff & LD_SIDE2)
 			UnArchiveSide(&sides[li->sidenum[1]]);
+		if (diff2 & LD_EXECUTORDELAY)
+			li->executordelay = READINT32(save_p);
+		if (diff2 & LD_TRANSFPORTAL)
+			li->secportal = READUINT32(save_p);
 	}
 }
 
@@ -4851,6 +4898,86 @@ static inline void P_NetUnArchiveEmblems(void)
 	}
 }
 
+static void P_NetArchiveSectorPortals(void)
+{
+	WRITEUINT32(save_p, ARCHIVEBLOCK_SECPORTALS);
+
+	WRITEUINT32(save_p, secportalcount);
+
+	for (size_t i = 0; i < secportalcount; i++)
+	{
+		UINT8 type = secportals[i].type;
+
+		WRITEUINT8(save_p, type);
+		WRITEFIXED(save_p, secportals[i].origin.x);
+		WRITEFIXED(save_p, secportals[i].origin.y);
+
+		switch (type)
+		{
+		case SECPORTAL_LINE:
+			WRITEUINT32(save_p, SaveLine(secportals[i].line.start));
+			WRITEUINT32(save_p, SaveLine(secportals[i].line.dest));
+			break;
+		case SECPORTAL_PLANE:
+		case SECPORTAL_HORIZON:
+		case SECPORTAL_FLOOR:
+		case SECPORTAL_CEILING:
+			WRITEUINT32(save_p, SaveSector(secportals[i].sector));
+			break;
+		case SECPORTAL_OBJECT:
+			if (secportals[i].mobj && !P_MobjWasRemoved(secportals[i].mobj))
+				SaveMobjnum(secportals[i].mobj);
+			else
+				WRITEUINT32(save_p, 0);
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+static void P_NetUnArchiveSectorPortals(void)
+{
+	if (READUINT32(save_p) != ARCHIVEBLOCK_SECPORTALS)
+		I_Error("Bad $$$.sav at archive block Secportals");
+
+	Z_Free(secportals);
+	P_InitSectorPortals();
+
+	UINT32 count = READUINT32(save_p);
+
+	for (UINT32 i = 0; i < count; i++)
+	{
+		UINT32 id = P_NewSectorPortal();
+
+		sectorportal_t *secportal = &secportals[id];
+
+		secportal->type = READUINT8(save_p);
+		secportal->origin.x = READFIXED(save_p);
+		secportal->origin.y = READFIXED(save_p);
+
+		switch (secportal->type)
+		{
+		case SECPORTAL_LINE:
+			secportal->line.start = LoadLine(READUINT32(save_p));
+			secportal->line.dest = LoadLine(READUINT32(save_p));
+			break;
+		case SECPORTAL_PLANE:
+		case SECPORTAL_HORIZON:
+		case SECPORTAL_FLOOR:
+		case SECPORTAL_CEILING:
+			secportal->sector = LoadSector(READUINT32(save_p));
+			break;
+		case SECPORTAL_OBJECT:
+			id = READUINT32(save_p);
+			secportal->mobj = (id == 0) ? NULL : P_FindNewPosition(id);
+			break;
+		default:
+			break;
+		}
+	}
+}
+
 static inline void P_ArchiveLuabanksAndConsistency(void)
 {
 	UINT8 i, banksinuse = NUM_LUABANKS;
@@ -4937,6 +5064,7 @@ void P_SaveNetGame(boolean resending)
 		P_NetArchiveSpecials();
 		P_NetArchiveColormaps();
 		P_NetArchiveWaypoints();
+		P_NetArchiveSectorPortals();
 	}
 	LUA_Archive();
 
@@ -4977,6 +5105,7 @@ boolean P_LoadNetGame(boolean reloading)
 		P_NetUnArchiveSpecials();
 		P_NetUnArchiveColormaps();
 		P_NetUnArchiveWaypoints();
+		P_NetUnArchiveSectorPortals();
 		P_RelinkPointers();
 		P_FinishMobjs();
 	}
diff --git a/src/p_setup.c b/src/p_setup.c
index 9ae73c18bff49703d043825dbbf841a252aef127..1c0315847b1097d83bdb598d1e4d365c4c0f1a8d 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -993,6 +993,9 @@ static void P_InitializeSector(sector_t *ss)
 	ss->lightingdata = NULL;
 	ss->fadecolormapdata = NULL;
 
+	ss->portal_floor = UINT32_MAX;
+	ss->portal_ceiling = UINT32_MAX;
+
 	ss->heightsec = -1;
 	ss->camsec = -1;
 
@@ -1110,6 +1113,7 @@ static void P_InitializeLinedef(line_t *ld)
 	ld->polyobj = NULL;
 
 	ld->callcount = 0;
+	ld->secportal = UINT32_MAX;
 
 	// cph 2006/09/30 - fix sidedef errors right away.
 	// cph 2002/07/20 - these errors are fatal if not fixed, so apply them
@@ -7883,6 +7887,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	P_InitThinkers();
 	R_InitMobjInterpolators();
 	P_InitCachedActions();
+	P_InitSectorPortals();
 
 	// internal game map
 	maplumpname = G_BuildMapName(gamemap);
diff --git a/src/p_spec.c b/src/p_spec.c
index 3dbbc5352784bcbdf56800a5f450af35fc089d50..7bec07c92730128796558e508718f0449bfe798f 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -52,6 +52,10 @@ mobj_t *skyboxmo[2]; // current skybox mobjs: 0 = viewpoint, 1 = centerpoint
 mobj_t *skyboxviewpnts[16]; // array of MT_SKYBOX viewpoint mobjs
 mobj_t *skyboxcenterpnts[16]; // array of MT_SKYBOX centerpoint mobjs
 
+size_t secportalcount;
+size_t secportalcapacity;
+sectorportal_t *secportals;
+
 /** Animated texture descriptor
   * This keeps track of an animated texture or an animated flat.
   * \sa P_UpdateSpecials, P_InitPicAnims, animdef_t
@@ -6199,6 +6203,196 @@ fixed_t P_GetSectorGravityFactor(sector_t *sec)
 		return sec->gravity;
 }
 
+void P_InitSectorPortals(void)
+{
+	secportalcount = 0;
+	secportalcapacity = 0;
+	secportals = NULL;
+}
+
+UINT32 P_NewSectorPortal(void)
+{
+	size_t i = secportalcount++;
+	if (i == UINT32_MAX)
+		I_Error("Too many sector portals");
+
+	if (secportalcapacity == 0 || secportalcount == secportalcapacity)
+	{
+		secportalcapacity = secportalcapacity ? (secportalcapacity * 2) : 16;
+		secportals = Z_Realloc(secportals, secportalcapacity * sizeof(sectorportal_t), PU_LEVEL, NULL);
+	}
+
+	secportals[i].type = SECPORTAL_NONE;
+
+	return (UINT32)i;
+}
+
+boolean P_IsSectorPortalValid(sectorportal_t *secportal)
+{
+	if (secportal == NULL)
+		return false;
+
+	switch (secportal->type)
+	{
+	case SECPORTAL_LINE:
+	case SECPORTAL_FLOOR:
+	case SECPORTAL_CEILING:
+		return true;
+	case SECPORTAL_OBJECT:
+		return secportal->mobj && !P_MobjWasRemoved(secportal->mobj);
+	case SECPORTAL_SKYBOX:
+		return skyboxmo[0] && !P_MobjWasRemoved(skyboxmo[0]);
+	case SECPORTAL_PLANE:
+	case SECPORTAL_HORIZON:
+		return true;
+	default:
+		return false;
+	}
+}
+
+boolean P_SectorHasPortal(sector_t *sector)
+{
+	return P_SectorHasFloorPortal(sector) || P_SectorHasCeilingPortal(sector);
+}
+
+sectorportal_t *P_SectorGetFloorPortal(sector_t *sector)
+{
+	UINT32 num = sector->portal_floor;
+	if (num >= secportalcount)
+		return NULL;
+
+	return &secportals[num];
+}
+
+sectorportal_t *P_SectorGetCeilingPortal(sector_t *sector)
+{
+	UINT32 num = sector->portal_ceiling;
+	if (num >= secportalcount)
+		return NULL;
+
+	return &secportals[num];
+}
+
+boolean P_SectorHasFloorPortal(sector_t *sector)
+{
+	return P_IsSectorPortalValid(P_SectorGetFloorPortal(sector));
+}
+
+boolean P_SectorHasCeilingPortal(sector_t *sector)
+{
+	return P_IsSectorPortalValid(P_SectorGetCeilingPortal(sector));
+}
+
+boolean P_CompareSectorPortals(sectorportal_t *a, sectorportal_t *b)
+{
+	if (a == NULL && b == NULL)
+		return true;
+	else if (!a || !b)
+		return false;
+	else if (a->type != b->type)
+		return false;
+
+	switch (a->type)
+	{
+	case SECPORTAL_LINE:
+		return a->line.start == b->line.start && a->line.dest == b->line.dest;
+	case SECPORTAL_FLOOR:
+	case SECPORTAL_CEILING:
+		return a->sector == b->sector;
+	case SECPORTAL_OBJECT:
+		return a->mobj == b->mobj;
+	default:
+		return true;
+	}
+}
+
+static mobj_t *P_GetMobjByTag(INT32 tag)
+{
+	INT32 mtnum = -1;
+
+	TAG_ITER_THINGS(tag, mtnum)
+	{
+		mobj_t *mo = mapthings[mtnum].mobj;
+		if (mo)
+			return mo;
+	}
+
+	return NULL;
+}
+
+static void P_DoPortalCopyFromLine(sector_t *dest_sector, int plane_type, int tag)
+{
+	INT32 secnum = -1;
+	TAG_ITER_SECTORS(tag, secnum)
+	{
+		sector_t *src_sector = &sectors[secnum];
+		if (plane_type == TMP_FLOOR || plane_type == TMP_BOTH)
+			dest_sector->portal_floor = src_sector->portal_floor;
+		if (plane_type == TMP_CEILING || plane_type == TMP_BOTH)
+			dest_sector->portal_ceiling = src_sector->portal_ceiling;
+	}
+}
+
+static sectorportal_t *P_SectorGetPortalOrCreate(sector_t *sector, UINT32 *num, UINT32 *result)
+{
+	sectorportal_t *secportal = NULL;
+
+	if (*num >= secportalcount)
+	{
+		*num = P_NewSectorPortal();
+		secportal = &secportals[*num];
+		secportal->origin.x = sector->soundorg.x;
+		secportal->origin.y = sector->soundorg.y;
+		*result = *num;
+	}
+	else
+	{
+		*result = *num;
+		secportal = &secportals[*num];
+	}
+
+	return secportal;
+}
+
+static sectorportal_t *P_SectorGetFloorPortalOrCreate(sector_t *sector, UINT32 *result)
+{
+	return P_SectorGetPortalOrCreate(sector, &sector->portal_floor, result);
+}
+
+static sectorportal_t *P_SectorGetCeilingPortalOrCreate(sector_t *sector, UINT32 *result)
+{
+	return P_SectorGetPortalOrCreate(sector, &sector->portal_ceiling, result);
+}
+
+static void P_CopySectorPortalToLines(UINT32 portal_num, int sector_tag)
+{
+	for (size_t i = 0; i < numlines; i++)
+	{
+		if (lines[i].special != SPECIAL_SECTOR_SETPORTAL)
+			continue;
+
+		if (lines[i].args[1] != TMSECPORTAL_COPY_PORTAL_TO_LINE)
+			continue;
+
+		if (lines[i].args[3] != sector_tag)
+			continue;
+
+		if (lines[i].args[0] != 0)
+		{
+			INT32 linenum = -1;
+			TAG_ITER_LINES(lines[i].args[0], linenum)
+			{
+				lines[linenum].secportal = portal_num;
+			}
+		}
+		else
+		{
+			// Just transfer it to this line
+			lines[i].secportal = portal_num;
+		}
+	}
+}
+
 /** After the map has loaded, scans for specials that spawn 3Dfloors and
   * thinkers.
   *
@@ -6365,6 +6559,146 @@ void P_SpawnSpecials(boolean fromnetsave)
 					P_AddCameraScanner(&sectors[sec], &sectors[s], R_PointToAngle2(lines[i].v2->x, lines[i].v2->y, lines[i].v1->x, lines[i].v1->y));
 				break;
 
+			case SPECIAL_SECTOR_SETPORTAL: // Sector portal
+			{
+				int target_sector_tag = lines[i].args[0];
+				int portal_type = lines[i].args[1];
+				int plane_type = lines[i].args[2];
+				int misc = lines[i].args[3];
+
+				boolean floor, ceiling;
+				if (plane_type == TMP_BOTH)
+					floor = ceiling = true;
+				else
+				{
+					floor = plane_type == TMP_FLOOR;
+					ceiling = plane_type == TMP_CEILING;
+				}
+
+				UINT32 portal_num = UINT32_MAX;
+
+				// Eternity's floor and horizon portal types
+				if (portal_type == TMSECPORTAL_PLANE || portal_type == TMSECPORTAL_HORIZON)
+				{
+					secportaltype_e type = portal_type == TMSECPORTAL_HORIZON ? SECPORTAL_HORIZON : SECPORTAL_PLANE;
+					if (floor)
+					{
+						sectorportal_t *floorportal = P_SectorGetFloorPortalOrCreate(lines[i].frontsector, &portal_num);
+						floorportal->type = type;
+						floorportal->sector = lines[i].frontsector;
+						P_CopySectorPortalToLines(portal_num, target_sector_tag);
+					}
+					if (ceiling)
+					{
+						sectorportal_t *ceilportal = P_SectorGetCeilingPortalOrCreate(lines[i].frontsector, &portal_num);
+						ceilportal->type = type;
+						ceilportal->sector = lines[i].frontsector;
+						P_CopySectorPortalToLines(portal_num, target_sector_tag);
+					}
+					break;
+				}
+
+				INT32 s1 = -1;
+
+				TAG_ITER_SECTORS(target_sector_tag, s1)
+				{
+					sector_t *target_sector = &sectors[s1];
+
+					// Line portal
+					if (portal_type == TMSECPORTAL_NORMAL)
+					{
+						INT32 linenum = -1;
+						TAG_ITER_LINES(misc, linenum)
+						{
+							if (lines[linenum].special == SPECIAL_SECTOR_SETPORTAL
+							&& lines[linenum].args[0] == target_sector_tag
+							&& lines[linenum].args[1] == portal_type
+							&& lines[linenum].args[2] == plane_type
+							&& lines[linenum].args[3] == 1)
+							{
+								if (floor)
+								{
+									sectorportal_t *floorportal = P_SectorGetFloorPortalOrCreate(target_sector, &portal_num);
+									floorportal->type = SECPORTAL_LINE;
+									floorportal->line.start = &lines[i];
+									floorportal->line.dest = &lines[linenum];
+									P_CopySectorPortalToLines(portal_num, target_sector_tag);
+								}
+								if (ceiling)
+								{
+									sectorportal_t *ceilportal = P_SectorGetCeilingPortalOrCreate(target_sector, &portal_num);
+									ceilportal->type = SECPORTAL_LINE;
+									ceilportal->line.start = &lines[i];
+									ceilportal->line.dest = &lines[linenum];
+									P_CopySectorPortalToLines(portal_num, target_sector_tag);
+								}
+							}
+						}
+					}
+					// Skybox portal
+					else if (portal_type == TMSECPORTAL_SKYBOX)
+					{
+						if (floor)
+						{
+							sectorportal_t *floorportal = P_SectorGetFloorPortalOrCreate(target_sector, &portal_num);
+							floorportal->type = SECPORTAL_SKYBOX;
+							P_CopySectorPortalToLines(portal_num, target_sector_tag);
+						}
+						if (ceiling)
+						{
+							sectorportal_t *ceilportal = P_SectorGetCeilingPortalOrCreate(target_sector, &portal_num);
+							ceilportal->type = SECPORTAL_SKYBOX;
+							P_CopySectorPortalToLines(portal_num, target_sector_tag);
+						}
+					}
+					// Plane portal
+					else if (portal_type == TMSECPORTAL_SECTOR)
+					{
+						INT32 s2 = -1;
+						TAG_ITER_SECTORS(misc, s2) // Sector tag to make a portal to
+						{
+							sector_t *view_sector = &sectors[s2];
+							if (floor)
+							{
+								sectorportal_t *floorportal = P_SectorGetFloorPortalOrCreate(target_sector, &portal_num);
+								floorportal->type = SECPORTAL_CEILING;
+								floorportal->sector = view_sector;
+								P_CopySectorPortalToLines(portal_num, target_sector_tag);
+							}
+							if (ceiling)
+							{
+								sectorportal_t *ceilportal = P_SectorGetCeilingPortalOrCreate(target_sector, &portal_num);
+								ceilportal->type = SECPORTAL_FLOOR;
+								ceilportal->sector = view_sector;
+								P_CopySectorPortalToLines(portal_num, target_sector_tag);
+							}
+						}
+					}
+					// Use mobj as viewpoint
+					else if (portal_type == TMSECPORTAL_OBJECT)
+					{
+						mobj_t *mobj = P_GetMobjByTag(misc);
+						if (!mobj)
+							break;
+						if (floor)
+						{
+							sectorportal_t *floorportal = P_SectorGetFloorPortalOrCreate(target_sector, &portal_num);
+							floorportal->type = SECPORTAL_OBJECT;
+							P_SetTarget(&floorportal->mobj, mobj);
+							P_CopySectorPortalToLines(portal_num, target_sector_tag);
+						}
+						if (ceiling)
+						{
+							sectorportal_t *ceilportal = P_SectorGetCeilingPortalOrCreate(target_sector, &portal_num);
+							ceilportal->type = SECPORTAL_OBJECT;
+							P_SetTarget(&ceilportal->mobj, mobj);
+							P_CopySectorPortalToLines(portal_num, target_sector_tag);
+						}
+					}
+				}
+				break;
+			}
+
 			case 7: // Flat alignment - redone by toast
 			{
 				// Set calculated offsets such that line's v1 is the apparent origin
@@ -7140,10 +7474,6 @@ void P_SpawnSpecials(boolean fromnetsave)
 		}
 	}
 
-
-
-
-
 	// Allocate each list
 	for (i = 0; i < numsectors; i++)
 		if(secthinkers[i].thinkers)
@@ -7172,6 +7502,33 @@ void P_SpawnSpecials(boolean fromnetsave)
 		}
 	}
 
+	// Copy portals
+	for (i = 0; i < numlines; i++)
+	{
+		if (lines[i].special != SPECIAL_SECTOR_SETPORTAL)
+			continue;
+
+		int portal_type = lines[i].args[1];
+		if (portal_type != TMSECPORTAL_COPIED)
+			continue;
+
+		int target_sector_tag = lines[i].args[0];
+		int plane_type = lines[i].args[2];
+		int tag_to_copy = lines[i].args[3];
+
+		if (plane_type == 3)
+			plane_type = TMP_BOTH;
+
+		if (target_sector_tag == 0)
+			P_DoPortalCopyFromLine(lines[i].frontsector, plane_type, tag_to_copy);
+		else
+		{
+			INT32 s1 = -1;
+			TAG_ITER_SECTORS(target_sector_tag, s1)
+				P_DoPortalCopyFromLine(&sectors[s1], plane_type, tag_to_copy);
+		}
+	}
+
 	if (!fromnetsave)
 		P_RunLevelLoadExecutors();
 }
diff --git a/src/p_spec.h b/src/p_spec.h
index 50ab6410f145b0d2ad180b492525cec51d786365..3bbaba58b69858cfc0d8627bfb2971e8628e6b40 100644
--- a/src/p_spec.h
+++ b/src/p_spec.h
@@ -21,6 +21,10 @@ extern mobj_t *skyboxmo[2]; // current skybox mobjs: 0 = viewpoint, 1 = centerpo
 extern mobj_t *skyboxviewpnts[16]; // array of MT_SKYBOX viewpoint mobjs
 extern mobj_t *skyboxcenterpnts[16]; // array of MT_SKYBOX centerpoint mobjs
 
+extern size_t secportalcount;
+extern size_t secportalcapacity;
+extern sectorportal_t *secportals;
+
 // Amount (dx, dy) vector linedef is shifted right to get scroll amount
 #define SCROLL_SHIFT 5
 
@@ -472,6 +476,20 @@ typedef enum
 	TMB_MODULATE        = 4,
 } textmapblendmodes_t;
 
+typedef enum
+{
+	TMSECPORTAL_NORMAL = 0,
+	TMSECPORTAL_COPIED = 1,
+	TMSECPORTAL_SKYBOX = 2,
+	TMSECPORTAL_PLANE = 3,
+	TMSECPORTAL_HORIZON = 4,
+	TMSECPORTAL_COPY_PORTAL_TO_LINE = 5,
+	TMSECPORTAL_INTERACTIVE = 6, // unimplemented
+	// The two portal types below are new to SRB2
+	TMSECPORTAL_SECTOR = 7,
+	TMSECPORTAL_OBJECT = 8
+} textmapsecportaltype_t;
+
 // GETSECSPECIAL (specialval, section)
 //
 // Pulls out the special # from a particular section.
@@ -521,6 +539,19 @@ INT32 P_FindMinSurroundingLight(sector_t *sector, INT32 max);
 void P_SetupSignExit(player_t *player);
 boolean P_IsFlagAtBase(mobjtype_t flag);
 
+void P_InitSectorPortals(void);
+UINT32 P_NewSectorPortal(void);
+
+boolean P_IsSectorPortalValid(sectorportal_t *secportal);
+
+sectorportal_t *P_SectorGetFloorPortal(sector_t *sector);
+sectorportal_t *P_SectorGetCeilingPortal(sector_t *sector);
+
+boolean P_SectorHasPortal(sector_t *sector);
+boolean P_SectorHasFloorPortal(sector_t *sector);
+boolean P_SectorHasCeilingPortal(sector_t *sector);
+boolean P_CompareSectorPortals(sectorportal_t *a, sectorportal_t *b);
+
 boolean P_IsMobjTouchingSectorPlane(mobj_t *mo, sector_t *sec);
 boolean P_IsMobjTouching3DFloor(mobj_t *mo, ffloor_t *ffloor, sector_t *sec);
 boolean P_IsMobjTouchingPolyobj(mobj_t *mo, polyobj_t *po, sector_t *polysec);
diff --git a/src/r_bsp.c b/src/r_bsp.c
index d99de5981ba789395347a01ae28ad14f4c68bb0d..918dc40b0f721a73badb9549d8e656a98fedf54c 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -36,6 +36,11 @@ drawseg_t *curdrawsegs = NULL; /**< This is used to handle multiple lists for ma
 drawseg_t *drawsegs = NULL;
 drawseg_t *ds_p = NULL;
 
+boolean bothceilingssky = false; // turned on if both back and front ceilings are sky
+boolean bothfloorssky = false; // likewise, but for floors
+
+boolean horizonline = false;
+
 // indicates doors closed wrt automap bugfix:
 INT32 doorclosed;
 
@@ -366,6 +371,11 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 
 boolean R_IsEmptyLine(seg_t *line, sector_t *front, sector_t *back)
 {
+	if (P_SectorHasPortal(front) && !P_SectorHasPortal(back))
+		return false;
+	else if (!P_SectorHasPortal(front) && P_SectorHasPortal(back))
+		return false;
+
 	return (
 		!line->polyseg &&
 		back->ceilingpic == front->ceilingpic
@@ -407,7 +417,6 @@ static void R_AddLine(seg_t *line)
 	INT32 x1, x2;
 	angle_t angle1, angle2, span, tspan;
 	static sector_t tempsec;
-	boolean bothceilingssky = false, bothfloorssky = false;
 
 	portalline = false;
 
@@ -465,6 +474,8 @@ static void R_AddLine(seg_t *line)
 		return;
 
 	backsector = line->backsector;
+	horizonline = line->linedef->special == SPECIAL_HORIZON_LINE;
+	bothceilingssky = bothfloorssky = false;
 
 	// Portal line
 	if (line->linedef->special == 40 && line->side == 0)
@@ -489,6 +500,15 @@ static void R_AddLine(seg_t *line)
 			}
 		}
 	}
+	// Transferred portal
+	else if (line->linedef->secportal != UINT32_MAX && line->side == 0)
+	{
+		if (portalrender < cv_maxportals.value)
+		{
+			Portal_AddTransferred(line->linedef->secportal, x1, x2);
+			goto clipsolid;
+		}
+	}
 
 	// Single sided line?
 	if (!backsector)
@@ -498,9 +518,15 @@ static void R_AddLine(seg_t *line)
 
 	doorclosed = 0;
 
-	if (backsector->ceilingpic == skyflatnum && frontsector->ceilingpic == skyflatnum)
+	// hack to allow height changes in outdoor areas
+	// This is what gets rid of the upper textures if there should be sky
+	if (backsector->ceilingpic == skyflatnum && frontsector->ceilingpic == skyflatnum
+	&& !(P_SectorHasCeilingPortal(backsector) || P_SectorHasCeilingPortal(frontsector)))
 		bothceilingssky = true;
-	if (backsector->floorpic == skyflatnum && frontsector->floorpic == skyflatnum)
+
+	// likewise, but for floors and upper textures
+	if (backsector->floorpic == skyflatnum && frontsector->floorpic == skyflatnum
+	&& !(P_SectorHasFloorPortal(backsector) || P_SectorHasFloorPortal(frontsector)))
 		bothfloorssky = true;
 
 	if (bothceilingssky && bothfloorssky) // everything's sky? let's save us a bit of time then
@@ -585,7 +611,6 @@ static void R_AddLine(seg_t *line)
 	// Reject empty lines used for triggers and special events.
 	// Identical floor and ceiling on both sides, identical light levels on both sides,
 	// and no middle texture.
-
 	if (R_IsEmptyLine(line, frontsector, backsector))
 		return;
 
@@ -880,66 +905,32 @@ static void R_Subsector(size_t num)
 	floorcenterz   = P_GetSectorFloorZAt  (frontsector, frontsector->soundorg.x, frontsector->soundorg.y);
 	ceilingcenterz = P_GetSectorCeilingZAt(frontsector, frontsector->soundorg.x, frontsector->soundorg.y);
 
-	// Check and prep all 3D floors. Set the sector floor/ceiling light levels and colormaps.
-	if (frontsector->ffloors)
-	{
-		boolean anyMoved = frontsector->moved;
-
-		if (anyMoved == false)
-		{
-			for (rover = frontsector->ffloors; rover; rover = rover->next)
-			{
-				sector_t *controlSec = &sectors[rover->secnum];
-
-				if (controlSec->moved == true)
-				{
-					anyMoved = true;
-					break;
-				}
-			}
-		}
-
-		if (anyMoved == true)
-		{
-			frontsector->numlights = sub->sector->numlights = 0;
-			R_Prep3DFloors(frontsector);
-			sub->sector->lightlist = frontsector->lightlist;
-			sub->sector->numlights = frontsector->numlights;
-			sub->sector->moved = frontsector->moved = false;
-		}
-
-		light = R_GetPlaneLight(frontsector, floorcenterz, false);
-		if (frontsector->floorlightsec == -1 && !frontsector->floorlightabsolute)
-			floorlightlevel = max(0, min(255, *frontsector->lightlist[light].lightlevel + frontsector->floorlightlevel));
-		floorcolormap = *frontsector->lightlist[light].extra_colormap;
-		light = R_GetPlaneLight(frontsector, ceilingcenterz, false);
-		if (frontsector->ceilinglightsec == -1 && !frontsector->ceilinglightabsolute)
-			ceilinglightlevel = max(0, min(255, *frontsector->lightlist[light].lightlevel + frontsector->ceilinglightlevel));
-		ceilingcolormap = *frontsector->lightlist[light].extra_colormap;
-	}
+	R_CheckSectorLightLists(sub->sector, frontsector, &floorlightlevel, &ceilinglightlevel, &floorcolormap, &ceilingcolormap);
 
 	sub->sector->extra_colormap = frontsector->extra_colormap;
 
 	if (P_GetSectorFloorZAt(frontsector, viewx, viewy) < viewz
 		|| frontsector->floorpic == skyflatnum
+		|| P_SectorHasFloorPortal(frontsector)
 		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].ceilingpic == skyflatnum))
 	{
-		floorplane = R_FindPlane(frontsector->floorheight, frontsector->floorpic, floorlightlevel,
+		floorplane = R_FindPlane(frontsector, frontsector->floorheight, frontsector->floorpic, floorlightlevel,
 			frontsector->floorxoffset, frontsector->flooryoffset,
 			frontsector->floorxscale, frontsector->flooryscale, frontsector->floorangle,
-			floorcolormap, NULL, NULL, frontsector->f_slope);
+			floorcolormap, NULL, NULL, frontsector->f_slope, P_SectorGetFloorPortal(frontsector));
 	}
 	else
 		floorplane = NULL;
 
 	if (P_GetSectorCeilingZAt(frontsector, viewx, viewy) > viewz
 		|| frontsector->ceilingpic == skyflatnum
+		|| P_SectorHasCeilingPortal(frontsector)
 		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].floorpic == skyflatnum))
 	{
-		ceilingplane = R_FindPlane(frontsector->ceilingheight, frontsector->ceilingpic, ceilinglightlevel,
+		ceilingplane = R_FindPlane(frontsector, frontsector->ceilingheight, frontsector->ceilingpic, ceilinglightlevel,
 			frontsector->ceilingxoffset, frontsector->ceilingyoffset,
 			frontsector->ceilingxscale, frontsector->ceilingyscale, frontsector->ceilingangle,
-			ceilingcolormap, NULL, NULL, frontsector->c_slope);
+			ceilingcolormap, NULL, NULL, frontsector->c_slope, P_SectorGetCeilingPortal(frontsector));
 	}
 	else
 		ceilingplane = NULL;
@@ -980,10 +971,10 @@ static void R_Subsector(size_t num)
 				light = R_GetPlaneLight(frontsector, planecenterz,
 					viewz < heightcheck);
 
-				ffloor[numffloors].plane = R_FindPlane(*rover->bottomheight, *rover->bottompic,
+				ffloor[numffloors].plane = R_FindPlane(rover->master->frontsector, *rover->bottomheight, *rover->bottompic,
 					*frontsector->lightlist[light].lightlevel, *rover->bottomxoffs, *rover->bottomyoffs,
 					*rover->bottomxscale, *rover->bottomyscale, *rover->bottomangle,
-					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->b_slope);
+					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->b_slope, NULL);
 
 				ffloor[numffloors].slope = *rover->b_slope;
 
@@ -1010,10 +1001,10 @@ static void R_Subsector(size_t num)
 			{
 				light = R_GetPlaneLight(frontsector, planecenterz, viewz < heightcheck);
 
-				ffloor[numffloors].plane = R_FindPlane(*rover->topheight, *rover->toppic,
+				ffloor[numffloors].plane = R_FindPlane(rover->master->frontsector, *rover->topheight, *rover->toppic,
 					*frontsector->lightlist[light].lightlevel, *rover->topxoffs, *rover->topyoffs,
 					*rover->topxscale, *rover->topyscale, *rover->topangle,
-					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->t_slope);
+					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->t_slope, NULL);
 
 				ffloor[numffloors].slope = *rover->t_slope;
 
@@ -1053,18 +1044,17 @@ static void R_Subsector(size_t num)
 				&& (viewz < polysec->floorheight))
 			{
 				light = R_GetPlaneLight(frontsector, polysec->floorheight, viewz < polysec->floorheight);
-				ffloor[numffloors].plane = R_FindPlane(polysec->floorheight, polysec->floorpic,
+				ffloor[numffloors].plane = R_FindPlane(polysec, polysec->floorheight, polysec->floorpic,
 					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel),
 					polysec->floorxoffset, polysec->flooryoffset,
 					polysec->floorxscale, polysec->flooryscale,
 					polysec->floorangle-po->angle,
 					(light == -1 ? frontsector->extra_colormap : *frontsector->lightlist[light].extra_colormap), NULL, po,
-					NULL); // will ffloors be slopable eventually?
+					NULL, NULL);
 
 				ffloor[numffloors].height = polysec->floorheight;
 				ffloor[numffloors].polyobj = po;
 				ffloor[numffloors].slope = NULL;
-				//ffloor[numffloors].ffloor = rover;
 				po->visplane = ffloor[numffloors].plane;
 				numffloors++;
 			}
@@ -1079,18 +1069,17 @@ static void R_Subsector(size_t num)
 				&& (viewz > polysec->ceilingheight))
 			{
 				light = R_GetPlaneLight(frontsector, polysec->floorheight, viewz < polysec->floorheight);
-				ffloor[numffloors].plane = R_FindPlane(polysec->ceilingheight, polysec->ceilingpic,
+				ffloor[numffloors].plane = R_FindPlane(polysec, polysec->ceilingheight, polysec->ceilingpic,
 					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel),
 					polysec->ceilingxoffset, polysec->ceilingyoffset,
 					polysec->ceilingxscale, polysec->ceilingyscale,
 					polysec->ceilingangle-po->angle,
 					(light == -1 ? frontsector->extra_colormap : *frontsector->lightlist[light].extra_colormap), NULL, po,
-					NULL); // will ffloors be slopable eventually?
+					NULL, NULL);
 
 				ffloor[numffloors].polyobj = po;
 				ffloor[numffloors].height = polysec->ceilingheight;
 				ffloor[numffloors].slope = NULL;
-				//ffloor[numffloors].ffloor = rover;
 				po->visplane = ffloor[numffloors].plane;
 				numffloors++;
 			}
@@ -1099,18 +1088,18 @@ static void R_Subsector(size_t num)
 		}
 	}
 
-   // killough 9/18/98: Fix underwater slowdown, by passing real sector
-   // instead of fake one. Improve sprite lighting by basing sprite
-   // lightlevels on floor & ceiling lightlevels in the surrounding area.
-   //
-   // 10/98 killough:
-   //
-   // NOTE: TeamTNT fixed this bug incorrectly, messing up sprite lighting!!!
-   // That is part of the 242 effect!!!  If you simply pass sub->sector to
-   // the old code you will not get correct lighting for underwater sprites!!!
-   // Either you must pass the fake sector and handle validcount here, on the
-   // real sector, or you must account for the lighting in some other way,
-   // like passing it as an argument.
+	// killough 9/18/98: Fix underwater slowdown, by passing real sector
+	// instead of fake one. Improve sprite lighting by basing sprite
+	// lightlevels on floor & ceiling lightlevels in the surrounding area.
+	//
+	// 10/98 killough:
+	//
+	// NOTE: TeamTNT fixed this bug incorrectly, messing up sprite lighting!!!
+	// That is part of the 242 effect!!!  If you simply pass sub->sector to
+	// the old code you will not get correct lighting for underwater sprites!!!
+	// Either you must pass the fake sector and handle validcount here, on the
+	// real sector, or you must account for the lighting in some other way,
+	// like passing it as an argument.
 	R_AddSprites(sub->sector, (floorlightlevel+ceilinglightlevel)/2);
 
 	firstseg = NULL;
@@ -1121,7 +1110,6 @@ static void R_Subsector(size_t num)
 
 	while (count--)
 	{
-//		CONS_Debug(DBG_GAMELOGIC, "Adding normal line %d...(%d)\n", line->linedef-lines, leveltime);
 		if (!line->glseg && !line->polyseg) // ignore segs that belong to polyobjects
 			R_AddLine(line);
 		line++;
@@ -1129,6 +1117,51 @@ static void R_Subsector(size_t num)
 	}
 }
 
+void R_CheckSectorLightLists(sector_t *sector, sector_t *fakeflat, INT32 *floorlightlevel, INT32 *ceilinglightlevel, extracolormap_t **floorcolormap, extracolormap_t **ceilingcolormap)
+{
+	// Check and prep all 3D floors. Set the sector floor/ceiling light levels and colormaps.
+	if (fakeflat->ffloors)
+	{
+		fixed_t floorcenterz   = P_GetSectorFloorZAt  (fakeflat, fakeflat->soundorg.x, fakeflat->soundorg.y);
+		fixed_t ceilingcenterz = P_GetSectorCeilingZAt(fakeflat, fakeflat->soundorg.x, fakeflat->soundorg.y);
+
+		boolean anyMoved = fakeflat->moved;
+
+		if (anyMoved == false)
+		{
+			for (ffloor_t *rover = fakeflat->ffloors; rover; rover = rover->next)
+			{
+				sector_t *controlSec = &sectors[rover->secnum];
+
+				if (controlSec->moved == true)
+				{
+					anyMoved = true;
+					break;
+				}
+			}
+		}
+
+		if (anyMoved == true)
+		{
+			fakeflat->numlights = sector->numlights = 0;
+			R_Prep3DFloors(fakeflat);
+			sector->lightlist = fakeflat->lightlist;
+			sector->numlights = fakeflat->numlights;
+			sector->moved = fakeflat->moved = false;
+		}
+
+		INT32 light = R_GetPlaneLight(fakeflat, floorcenterz, false);
+		if (fakeflat->floorlightsec == -1 && !fakeflat->floorlightabsolute)
+			*floorlightlevel = max(0, min(255, *fakeflat->lightlist[light].lightlevel + fakeflat->floorlightlevel));
+		*floorcolormap = *fakeflat->lightlist[light].extra_colormap;
+
+		light = R_GetPlaneLight(fakeflat, ceilingcenterz, false);
+		if (fakeflat->ceilinglightsec == -1 && !fakeflat->ceilinglightabsolute)
+			*ceilinglightlevel = max(0, min(255, *fakeflat->lightlist[light].lightlevel + fakeflat->ceilinglightlevel));
+		*ceilingcolormap = *fakeflat->lightlist[light].extra_colormap;
+	}
+}
+
 //
 // R_Prep3DFloors
 //
@@ -1314,3 +1347,59 @@ void R_RenderBSPNode(INT32 bspnum)
 
 	R_Subsector(bspnum == -1 ? 0 : bspnum & ~NF_SUBSECTOR);
 }
+
+void R_RenderPortalHorizonLine(sector_t *sector)
+{
+	INT32 floorlightlevel, ceilinglightlevel;
+	static sector_t tempsec; // Deep water hack
+	extracolormap_t *floorcolormap;
+	extracolormap_t *ceilingcolormap;
+
+	frontsector = sector;
+	backsector = NULL;
+
+	// Deep water/fake ceiling effect.
+	frontsector = R_FakeFlat(frontsector, &tempsec, &floorlightlevel, &ceilinglightlevel, false);
+
+	floorcolormap = ceilingcolormap = frontsector->extra_colormap;
+
+	R_CheckSectorLightLists(sector, frontsector, &floorlightlevel, &ceilinglightlevel, &floorcolormap, &ceilingcolormap);
+
+	sector->extra_colormap = frontsector->extra_colormap;
+
+	if (P_GetSectorFloorZAt(frontsector, viewx, viewy) < viewz
+		|| frontsector->floorpic == skyflatnum
+		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].ceilingpic == skyflatnum))
+	{
+		floorplane = R_FindPlane(frontsector, frontsector->floorheight, frontsector->floorpic, floorlightlevel,
+			frontsector->floorxoffset, frontsector->flooryoffset, frontsector->floorxscale, frontsector->flooryscale,
+			frontsector->floorangle, floorcolormap, NULL, NULL, NULL, NULL);
+	}
+	else
+		floorplane = NULL;
+
+	if (P_GetSectorCeilingZAt(frontsector, viewx, viewy) > viewz
+		|| frontsector->ceilingpic == skyflatnum
+		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].floorpic == skyflatnum))
+	{
+		ceilingplane = R_FindPlane(frontsector, frontsector->ceilingheight, frontsector->ceilingpic,
+			ceilinglightlevel, frontsector->ceilingxoffset, frontsector->ceilingxscale, frontsector->ceilingyscale,
+			frontsector->ceilingyoffset, frontsector->ceilingangle,
+			ceilingcolormap, NULL, NULL, NULL, NULL);
+	}
+	else
+		ceilingplane = NULL;
+
+	numffloors = 0;
+	portalline = false;
+	doorclosed = 0;
+	bothceilingssky = bothfloorssky = false;
+	horizonline = true;
+
+	firstseg = NULL;
+	curline = &segs[0];
+
+	R_ClipSolidWallSegment(portalclipstart, portalclipend);
+
+	curline = NULL;
+}
diff --git a/src/r_bsp.h b/src/r_bsp.h
index 55199405ae0d59deba6e540df5e04650e1a67042..44ddd0b1bfbf175f5a5b5efb07263b995c7d1cfb 100644
--- a/src/r_bsp.h
+++ b/src/r_bsp.h
@@ -25,13 +25,17 @@ extern sector_t *frontsector;
 extern sector_t *backsector;
 extern boolean portalline; // is curline a portal seg?
 
-// drawsegs are allocated on the fly... see r_segs.c
-
 extern INT32 checkcoord[12][4];
 
 extern drawseg_t *curdrawsegs;
 extern drawseg_t *drawsegs;
 extern drawseg_t *ds_p;
+
+extern boolean bothceilingssky;
+extern boolean bothfloorssky;
+
+extern boolean horizonline;
+
 extern INT32 doorclosed;
 
 // BSP?
@@ -39,6 +43,7 @@ void R_ClearClipSegs(void);
 void R_PortalClearClipSegs(INT32 start, INT32 end);
 void R_ClearDrawSegs(void);
 void R_RenderBSPNode(INT32 bspnum);
+void R_RenderPortalHorizonLine(sector_t *sector);
 
 void R_SortPolyObjects(subsector_t *sub);
 
@@ -52,4 +57,5 @@ boolean R_IsEmptyLine(seg_t *line, sector_t *front, sector_t *back);
 
 INT32 R_GetPlaneLight(sector_t *sector, fixed_t planeheight, boolean underside);
 void R_Prep3DFloors(sector_t *sector);
+void R_CheckSectorLightLists(sector_t *sector, sector_t *fakeflat, INT32 *floorlightlevel, INT32 *ceilinglightlevel, extracolormap_t **floorcolormap, extracolormap_t **ceilingcolormap);
 #endif
diff --git a/src/r_defs.h b/src/r_defs.h
index bce045ab47d910513ef98313e6b4ed24b36026ef..2931eb1c8afca3c2b70bcb58c880c86003e8a65e 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -211,6 +211,34 @@ typedef enum
 	BT_STRONG,
 } busttype_e;
 
+typedef enum
+{
+	SECPORTAL_LINE,    // Works similar to a line portal
+	SECPORTAL_SKYBOX,  // Uses the skybox object as the reference view
+	SECPORTAL_PLANE,   // Eternity Engine's plane portal type
+	SECPORTAL_HORIZON, // Eternity Engine's horizon portal type
+	SECPORTAL_OBJECT,  // Uses an object as the reference view
+	SECPORTAL_FLOOR,   // Uses a sector as the reference view; the view height is aligned with the sector's floor
+	SECPORTAL_CEILING, // Uses a sector as the reference view; the view height is aligned with the sector's ceiling
+	SECPORTAL_NONE = 0xFF
+} secportaltype_e;
+
+typedef struct sectorportal_s
+{
+	secportaltype_e type;
+	union {
+		struct {
+			struct line_s *start;
+			struct line_s *dest;
+		} line;
+		struct sector_s *sector;
+		struct mobj_s *mobj;
+	};
+	struct {
+		fixed_t x, y;
+	} origin;
+} sectorportal_t;
+
 typedef struct ffloor_s
 {
 	fixed_t *topheight;
@@ -505,6 +533,10 @@ typedef struct sector_s
 
 	// colormap structure
 	extracolormap_t *spawn_extra_colormap;
+
+	// portals
+	UINT32 portal_floor;
+	UINT32 portal_ceiling;
 } sector_t;
 
 //
@@ -518,7 +550,9 @@ typedef enum
 	ST_NEGATIVE
 } slopetype_t;
 
-#define HORIZONSPECIAL 41
+#define SPECIAL_HORIZON_LINE 41
+
+#define SPECIAL_SECTOR_SETPORTAL 6
 
 #define NUMLINEARGS 10
 #define NUMLINESTRINGARGS 2
@@ -561,6 +595,8 @@ typedef struct line_s
 	polyobj_t *polyobj; // Belongs to a polyobject?
 
 	INT16 callcount; // no. of calls left before triggering, for the "X calls" linedef specials, defaults to 0
+
+	UINT32 secportal; // transferred sector portal
 } line_t;
 
 typedef struct
diff --git a/src/r_main.c b/src/r_main.c
index 125f80b7849cad91786cc394f1da8a958ad4f8cc..94116f8fd8af91d24a12b8caa363d2de34c0324e 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -1534,9 +1534,8 @@ void R_RenderPlayerView(player_t *player)
 
 	ps_numsprites.value.i = numvisiblesprites;
 
-	// Add skybox portals caused by sky visplanes.
-	if (cv_skybox.value && skyboxmo[0])
-		Portal_AddSkyboxPortals();
+	// Add portals caused by visplanes.
+	Portal_AddPlanePortals(cv_skybox.value);
 
 	// Portal rendering. Hijacks the BSP traversal.
 	PS_START_TIMING(ps_sw_portaltime);
@@ -1569,9 +1568,21 @@ void R_RenderPlayerView(player_t *player)
 			Mask_Pre(&masks[nummasks - 1]);
 			curdrawsegs = ds_p;
 
-			// Render the BSP from the new viewpoint, and clip
-			// any sprites with the new clipsegs and window.
-			R_RenderBSPNode((INT32)numnodes - 1);
+			if (portal->is_horizon)
+			{
+				// If the portal is a plane or a horizon portal, then we just render a horizon line
+				R_RenderPortalHorizonLine(portal->horizon_sector);
+			}
+			else
+			{
+				// Render the BSP from the new viewpoint, and clip
+				// any sprites with the new clipsegs and window.
+				R_RenderBSPNode((INT32)numnodes - 1);
+			}
+
+			// Don't add skybox portals while already rendering a skybox view, because that'll cause an infinite loop
+			Portal_AddPlanePortals(cv_skybox.value && !portal->is_skybox);
+
 			Mask_Post(&masks[nummasks - 1]);
 
 			R_ClipSprites(ds_p - (masks[nummasks - 1].drawsegs[1] - masks[nummasks - 1].drawsegs[0]), portal);
diff --git a/src/r_plane.c b/src/r_plane.c
index 08e147c89b8f8a7e290af44b4272630aa6109a98..612c650fcb0dd8c66cab3dcfaaefbbe15477b446 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -368,10 +368,10 @@ static visplane_t *new_visplane(unsigned hash)
 //              Same height, same flattexture, same lightlevel.
 //              If not, allocates another of them.
 //
-visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
+visplane_t *R_FindPlane(sector_t *sector, fixed_t height, INT32 picnum, INT32 lightlevel,
 	fixed_t xoff, fixed_t yoff, fixed_t xscale, fixed_t yscale,
 	angle_t plangle, extracolormap_t *planecolormap,
-	ffloor_t *pfloor, polyobj_t *polyobj, pslope_t *slope)
+	ffloor_t *pfloor, polyobj_t *polyobj, pslope_t *slope, sectorportal_t *portalsector)
 {
 	visplane_t *check;
 	unsigned hash;
@@ -430,7 +430,8 @@ visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
 				&& check->viewangle == viewangle
 				&& check->plangle == plangle
 				&& check->slope == slope
-				&& check->polyobj == polyobj)
+				&& check->polyobj == polyobj
+				&& P_CompareSectorPortals(check->portalsector, portalsector))
 			{
 				return check;
 			}
@@ -459,6 +460,8 @@ visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
 	check->viewz = viewz;
 	check->viewangle = viewangle;
 	check->plangle = plangle;
+	check->sector = sector;
+	check->portalsector = portalsector;
 	check->polyobj = polyobj;
 	check->slope = slope;
 
@@ -537,8 +540,10 @@ visplane_t *R_CheckPlane(visplane_t *pl, INT32 start, INT32 stop)
 		new_pl->viewz = pl->viewz;
 		new_pl->viewangle = pl->viewangle;
 		new_pl->plangle = pl->plangle;
+		new_pl->sector = pl->sector;
 		new_pl->polyobj = pl->polyobj;
 		new_pl->slope = pl->slope;
+		new_pl->portalsector = pl->portalsector;
 		pl = new_pl;
 		pl->minx = start;
 		pl->maxx = stop;
diff --git a/src/r_plane.h b/src/r_plane.h
index 5ce49e3cc9f2789b113076b64677125f68aca2c1..69620f25e07cadefee8242f23ad3a26a57ffbc9d 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -51,9 +51,11 @@ typedef struct visplane_s
 	fixed_t xoffs, yoffs; // Scrolling flats.
 	fixed_t xscale, yscale;
 
+	sector_t *sector;
 	struct ffloor_s *ffloor;
 	polyobj_t *polyobj;
 	pslope_t *slope;
+	sectorportal_t *portalsector;
 } visplane_t;
 
 extern visplane_t *visplanes[MAXVISPLANES];
@@ -71,8 +73,9 @@ void R_ClearPlanes(void);
 void R_ClearFFloorClips (void);
 
 void R_DrawPlanes(void);
-visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel, fixed_t xoff, fixed_t yoff, fixed_t xscale, fixed_t yscale,
-	angle_t plangle, extracolormap_t *planecolormap, ffloor_t *ffloor, polyobj_t *polyobj, pslope_t *slope);
+visplane_t *R_FindPlane(sector_t *sector, fixed_t height, INT32 picnum, INT32 lightlevel,
+	fixed_t xoff, fixed_t yoff, fixed_t xscale, fixed_t yscale, angle_t plangle,
+	extracolormap_t *planecolormap, ffloor_t *ffloor, polyobj_t *polyobj, pslope_t *slope, sectorportal_t *portalsector);
 visplane_t *R_CheckPlane(visplane_t *pl, INT32 start, INT32 stop);
 void R_ExpandPlane(visplane_t *pl, INT32 start, INT32 stop);
 void R_PlaneBounds(visplane_t *plane);
diff --git a/src/r_portal.c b/src/r_portal.c
index e594f960aa5bef06c4627aa096a96d555ef5d530..4d042cae38574bd54165d5b5850e8e18c5f31f33 100644
--- a/src/r_portal.c
+++ b/src/r_portal.c
@@ -16,6 +16,8 @@
 #include "r_main.h"
 #include "doomstat.h"
 #include "p_spec.h" // Skybox viewpoints
+#include "p_slopes.h" // P_GetSectorFloorZAt and P_GetSectorCeilingZAt
+#include "p_local.h"
 #include "z_zone.h"
 #include "r_things.h"
 #include "r_sky.h"
@@ -140,30 +142,17 @@ void Portal_Remove (portal_t* portal)
 	Z_Free(portal);
 }
 
-/** Creates a portal out of two lines and a determined screen range.
- *
- * line1 determines the entrance, and line2 the exit.
- * x1 and x2 determine the screen's column bounds.
-
- * The view's offset from the entry line center is obtained,
- * and then rotated&translated to the exit line's center.
- * When the portal renders, it will create the illusion of
- * the two lines being seamed together.
- */
-void Portal_Add2Lines (const INT32 line1, const INT32 line2, const INT32 x1, const INT32 x2)
+static void Portal_GetViewpointForLine(portal_t *portal, line_t *start, line_t *dest)
 {
-	portal_t* portal = Portal_Add(x1, x2);
-
 	// Offset the portal view by the linedef centers
-	line_t* start	= &lines[line1];
-	line_t* dest	= &lines[line2];
-
 	angle_t dangle = R_PointToAngle2(0,0,dest->dx,dest->dy) - R_PointToAngle2(start->dx,start->dy,0,0);
 
 	fixed_t disttopoint;
 	angle_t angtopoint;
 
-	vertex_t dest_c, start_c;
+	struct {
+		fixed_t x, y;
+	} dest_c, start_c;
 
 	// looking glass center
 	start_c.x = (start->v1->x + start->v2->x) / 2;
@@ -181,8 +170,31 @@ void Portal_Add2Lines (const INT32 line1, const INT32 line2, const INT32 x1, con
 	portal->viewy = dest_c.y + FixedMul(FINESINE(angtopoint>>ANGLETOFINESHIFT), disttopoint);
 	portal->viewz = viewz + dest->frontsector->floorheight - start->frontsector->floorheight;
 	portal->viewangle = viewangle + dangle;
+}
+
+/** Creates a portal out of two lines and a determined screen range.
+ *
+ * line1 determines the entrance, and line2 the exit.
+ * x1 and x2 determine the screen's column bounds.
+
+ * The view's offset from the entry line center is obtained,
+ * and then rotated&translated to the exit line's center.
+ * When the portal renders, it will create the illusion of
+ * the two lines being seamed together.
+ */
+void Portal_Add2Lines (const INT32 line1, const INT32 line2, const INT32 x1, const INT32 x2)
+{
+	portal_t* portal = Portal_Add(x1, x2);
+
+	line_t* start	= &lines[line1];
+	line_t* dest	= &lines[line2];
+
+	Portal_GetViewpointForLine(portal, start, dest);
 
 	portal->clipline = line2;
+	portal->is_skybox = false;
+	portal->is_horizon = false;
+	portal->horizon_sector = NULL;
 
 	Portal_ClipRange(portal);
 
@@ -252,30 +264,14 @@ static boolean TrimVisplaneBounds (const visplane_t* plane, INT16* start, INT16*
 	return false;
 }
 
-/** Creates a skybox portal out of a visplane.
- *
- * Applies the necessary offsets and rotation to give
- * a depth illusion to the skybox.
- */
-void Portal_AddSkybox (const visplane_t* plane)
+static void Portal_GetViewpointForSkybox(portal_t *portal)
 {
-	INT16 start, end;
-	mapheader_t *mh;
-	portal_t* portal;
-
-	if (TrimVisplaneBounds(plane, &start, &end))
-		return;
-
-	portal = Portal_Add(start, end);
-
-	Portal_ClipVisplane(plane, portal);
-
 	portal->viewx = skyboxmo[0]->x;
 	portal->viewy = skyboxmo[0]->y;
 	portal->viewz = skyboxmo[0]->z;
 	portal->viewangle = viewangle + skyboxmo[0]->angle;
 
-	mh = mapheaderinfo[gamemap-1];
+	mapheader_t *mh = mapheaderinfo[gamemap-1];
 
 	// If a relative viewpoint exists, offset the viewpoint.
 	if (skyboxmo[1])
@@ -302,34 +298,192 @@ void Portal_AddSkybox (const visplane_t* plane)
 		portal->viewz += viewz / mh->skybox_scalez;
 	else if (mh->skybox_scalez < 0)
 		portal->viewz += viewz * -mh->skybox_scalez;
+}
+
+/** Creates a skybox portal out of a visplane.
+ *
+ * Applies the necessary offsets and rotation to give
+ * a depth illusion to the skybox.
+ */
+static boolean Portal_AddSkybox (const visplane_t* plane)
+{
+	INT16 start, end;
+	portal_t* portal;
+
+	if (TrimVisplaneBounds(plane, &start, &end))
+		return false;
+
+	portal = Portal_Add(start, end);
+
+	Portal_ClipVisplane(plane, portal);
 
 	portal->clipline = -1;
+	portal->is_skybox = true;
+	portal->is_horizon = false;
+	portal->horizon_sector = NULL;
+
+	Portal_GetViewpointForSkybox(portal);
+
+	return true;
 }
 
-/** Creates portals for the currently existing sky visplanes.
+static void Portal_GetViewpointForSecPortal(portal_t *portal, sectorportal_t *secportal)
+{
+	fixed_t x, y, z;
+	angle_t angle;
+
+	switch (secportal->type)
+	{
+	case SECPORTAL_LINE:
+		Portal_GetViewpointForLine(portal, secportal->line.start, secportal->line.dest);
+		return;
+	case SECPORTAL_OBJECT:
+		if (!secportal->mobj || P_MobjWasRemoved(secportal->mobj))
+			return;
+		x = secportal->mobj->x;
+		y = secportal->mobj->y;
+		z = secportal->mobj->z;
+		angle = secportal->mobj->angle;
+		break;
+	case SECPORTAL_FLOOR:
+		x = secportal->sector->soundorg.x;
+		y = secportal->sector->soundorg.y;
+		z = P_GetSectorFloorZAt(secportal->sector, x, y);
+		angle = 0;
+		break;
+	case SECPORTAL_CEILING:
+		x = secportal->sector->soundorg.x;
+		y = secportal->sector->soundorg.y;
+		z = P_GetSectorCeilingZAt(secportal->sector, x, y);
+		angle = 0;
+		break;
+	case SECPORTAL_PLANE:
+	case SECPORTAL_HORIZON:
+		portal->is_horizon = true;
+		portal->horizon_sector = secportal->sector;
+		x = secportal->sector->soundorg.x;
+		y = secportal->sector->soundorg.y;
+		if (secportal->type == SECPORTAL_PLANE)
+			z = -viewz;
+		else
+			z = 0;
+		angle = 0;
+		break;
+	default:
+		return;
+	}
+
+	fixed_t refx = secportal->origin.x - viewx;
+	fixed_t refy = secportal->origin.y - viewy;
+
+	// Rotate the X/Y to match the target angle
+	if (angle != 0)
+	{
+		fixed_t tr_x = refx, tr_y = refy;
+		angle_t ang = angle >> ANGLETOFINESHIFT;
+		refx = FixedMul(tr_x, FINECOSINE(ang)) - FixedMul(tr_y, FINESINE(ang));
+		refy = FixedMul(tr_x, FINESINE(ang)) + FixedMul(tr_y, FINECOSINE(ang));
+	}
+
+	portal->viewx = x - refx;
+	portal->viewy = y - refy;
+	portal->viewz = z + viewz;
+	portal->viewangle = angle + viewangle;
+}
+
+/** Creates a sector portal out of a visplane.
+ */
+static boolean Portal_AddSectorPortal (const visplane_t* plane)
+{
+	INT16 start, end;
+	sectorportal_t *secportal = plane->portalsector;
+
+	// Shortcut
+	if (secportal->type == SECPORTAL_SKYBOX)
+	{
+		if (cv_skybox.value && skyboxmo[0])
+			return Portal_AddSkybox(plane);
+		return false;
+	}
+
+	if (TrimVisplaneBounds(plane, &start, &end))
+		return false;
+
+	portal_t* portal = Portal_Add(start, end);
+
+	Portal_ClipVisplane(plane, portal);
+
+	portal->clipline = -1;
+	portal->is_horizon = false;
+	portal->is_skybox = false;
+	portal->horizon_sector = NULL;
+
+	Portal_GetViewpointForSecPortal(portal, secportal);
+
+	return true;
+}
+
+/** Creates a transferred sector portal.
+ */
+void Portal_AddTransferred (UINT32 secportalnum, const INT32 x1, const INT32 x2)
+{
+	if (secportalnum >= secportalcount)
+		return;
+
+	sectorportal_t *secportal = &secportals[secportalnum];
+	if (!P_IsSectorPortalValid(secportal))
+		return;
+
+	portal_t* portal = Portal_Add(x1, x2);
+	portal->is_skybox = false;
+	portal->is_horizon = false;
+	portal->horizon_sector = NULL;
+
+	if (secportal->type == SECPORTAL_SKYBOX)
+		Portal_GetViewpointForSkybox(portal);
+	else
+		Portal_GetViewpointForSecPortal(portal, secportal);
+
+	if (secportal->type == SECPORTAL_LINE)
+		portal->clipline = secportal->line.dest - lines;
+	else
+		portal->clipline = -1;
+
+	Portal_ClipRange(portal);
+
+	portalline = true;
+}
+
+/** Creates portals for the currently existing portal visplanes.
  * The visplanes are also removed and cleared from the list.
  */
-void Portal_AddSkyboxPortals (void)
+void Portal_AddPlanePortals (boolean add_skyboxes)
 {
 	visplane_t *pl;
-	INT32 i;
-	UINT16 count = 0;
 
-	for (i = 0; i < MAXVISPLANES; i++, pl++)
+	for (INT32 i = 0; i < MAXVISPLANES; i++, pl++)
 	{
 		for (pl = visplanes[i]; pl; pl = pl->next)
 		{
-			if (pl->picnum == skyflatnum)
-			{
-				Portal_AddSkybox(pl);
+			if (pl->minx >= pl->maxx)
+				continue;
+
+			boolean added_portal = false;
+
+			// Render sector portal if recursiveness limit hasn't been reached
+			if (pl->portalsector && portalrender < cv_maxportals.value)
+				added_portal = Portal_AddSectorPortal(pl);
+
+			// Render skybox portal
+			if (!added_portal && pl->picnum == skyflatnum && add_skyboxes && skyboxmo[0])
+				added_portal = Portal_AddSkybox(pl);
 
+			// don't render this visplane anymore
+			if (added_portal)
+			{
 				pl->minx = 0;
 				pl->maxx = -1;
-
-				count++;
 			}
 		}
 	}
-
-	CONS_Debug(DBG_RENDER, "Skybox portals: %d\n", count);
 }
diff --git a/src/r_portal.h b/src/r_portal.h
index f90f05fbce861e12428b2f11185bea22521fdeea..2485e45a71afb5bc189525f140527c5bb03e2a21 100644
--- a/src/r_portal.h
+++ b/src/r_portal.h
@@ -30,6 +30,12 @@ typedef struct portal_s
 	fixed_t viewz;
 	angle_t viewangle;
 
+	// For horizon portals
+	boolean is_horizon;
+	sector_t *horizon_sector;
+
+	boolean is_skybox;
+
 	UINT8 pass;			/**< Keeps track of the portal's recursion depth. */
 	INT32 clipline;		/**< Optional clipline for line-based portals. */
 
@@ -49,13 +55,13 @@ extern line_t *portalclipline;
 extern sector_t *portalcullsector;
 extern INT32 portalclipstart, portalclipend;
 
-void Portal_InitList	(void);
-void Portal_Remove		(portal_t* portal);
-void Portal_Add2Lines	(const INT32 line1, const INT32 line2, const INT32 x1, const INT32 x2);
-void Portal_AddSkybox	(const visplane_t* plane);
+void Portal_InitList		(void);
+void Portal_Remove			(portal_t* portal);
+void Portal_Add2Lines		(const INT32 line1, const INT32 line2, const INT32 x1, const INT32 x2);
+void Portal_AddTransferred	(UINT32 secportalnum, const INT32 x1, const INT32 x2);
 
 void Portal_ClipRange (portal_t* portal);
 void Portal_ClipApply (const portal_t* portal);
 
-void Portal_AddSkyboxPortals (void);
+void Portal_AddPlanePortals (boolean add_skyboxes);
 #endif
diff --git a/src/r_segs.c b/src/r_segs.c
index ecc49fbc23ec80e7100c82dbd957fb453735f643..62fea352fca5d4b3b9b0a39d07b4d562481f5aa2 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -1867,9 +1867,6 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 	else
 	{
 		// two sided line
-		boolean bothceilingssky = false; // turned on if both back and front ceilings are sky
-		boolean bothfloorssky = false; // likewise, but for floors
-
 		SLOPEPARAMS(backsector->c_slope, worldhigh, worldhighslope, backsector->ceilingheight)
 		SLOPEPARAMS(backsector->f_slope, worldlow,  worldlowslope,  backsector->floorheight)
 		worldhigh -= viewz;
@@ -1877,21 +1874,6 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		worldlow -= viewz;
 		worldlowslope -= viewz;
 
-		// hack to allow height changes in outdoor areas
-		// This is what gets rid of the upper textures if there should be sky
-		if (frontsector->ceilingpic == skyflatnum
-			&& backsector->ceilingpic == skyflatnum)
-		{
-			bothceilingssky = true;
-		}
-
-		// likewise, but for floors and upper textures
-		if (frontsector->floorpic == skyflatnum
-			&& backsector->floorpic == skyflatnum)
-		{
-			bothfloorssky = true;
-		}
-
 		ds_p->sprtopclip = ds_p->sprbottomclip = NULL;
 		ds_p->silhouette = 0;
 
@@ -1991,6 +1973,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			|| backsector->floorlightsec != frontsector->floorlightsec
 			//SoM: 4/3/2000: Check for colormaps
 			|| frontsector->extra_colormap != backsector->extra_colormap
+			|| !P_CompareSectorPortals(P_SectorGetFloorPortal(frontsector), P_SectorGetFloorPortal(backsector))
 			|| (frontsector->ffloors != backsector->ffloors && !Tag_Compare(&frontsector->tags, &backsector->tags)))
 		{
 			markfloor = true;
@@ -2026,9 +2009,10 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			|| backsector->ceilinglightsec != frontsector->ceilinglightsec
 			//SoM: 4/3/2000: Check for colormaps
 			|| frontsector->extra_colormap != backsector->extra_colormap
+			|| !P_CompareSectorPortals(P_SectorGetCeilingPortal(frontsector), P_SectorGetCeilingPortal(backsector))
 			|| (frontsector->ffloors != backsector->ffloors && !Tag_Compare(&frontsector->tags, &backsector->tags)))
 		{
-				markceiling = true;
+			markceiling = true;
 		}
 		else
 		{
@@ -2478,7 +2462,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 	worldtopslope >>= 4;
 	worldbottomslope >>= 4;
 
-	if (linedef->special == HORIZONSPECIAL) { // HORIZON LINES
+	if (horizonline) { // HORIZON LINES
 		topstep = bottomstep = 0;
 		topfrac = bottomfrac = (centeryfrac>>4);
 		topfrac++; // Prevent 1px HOM
@@ -2579,7 +2563,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		{
 			ffloor[i].f_pos >>= 4;
 			ffloor[i].f_pos_slope >>= 4;
-			if (linedef->special == HORIZONSPECIAL) // Horizon lines extend FOFs in contact with them too.
+			if (horizonline) // Horizon lines extend FOFs in contact with them too.
 			{
 				ffloor[i].f_step = 0;
 				ffloor[i].f_frac = (centeryfrac>>4);