Things snap up to ledges when sectors move
From DoomWiki.org
When a sector moves, such as a lift activating or a door opening, things intersecting the linedef between sectors of different heights may instantly snap up to rest on top of the higher sector. This affects items and monsters alike and typically manifests innocuously as a visual error, carrying no significant effect on gameplay, however it can also make items impossible to reach and cause enemies to become stuck on top of ledges or in ceilings.
The inverse of this can also happen to things that hang from ceilings, such as hanging victims. In this case, the thing can snap down to the intersecting lower ceiling.
This is caused by the moving sector having an invisible bounding area made of blockmap tiles on and around it where things are updated when the sector moves, as they are being considered for moving up or down on a lift, and a thing intersecting the ledge of a lift should move with the height of the lift. Due to the odd implementation, whether the thing is hanging off the ledge or sitting below the ledge is not taken into consideration, nor does it have to be the lift linedef that is intersected, as any ledge in the bounding area is considered and holds a potential floor to snap up to. Due to the lift function serving multiple uses, moving ceilings also make this happen.
A map author can always prevent this bug from happening by keeping things away from high ledges, though it is safe to have things intersect impassable or monster-blocking linedefs as these will never be considered as ledges to snap onto.
Other factors[edit]
This behavior depends on the source port and complevel. For example, Boom compatibility reduces the scope of the bug to only affect things intersecting the moving sector, rather than having the original Doom behavior of affecting all things that can be found in the sector's bounding area regardless of actual contact with the sector.
This bug is sensitive to linedef ordering within the BLOCKMAP lump of a given map; when the linedefs within a blocklist are iterated to find ledges or walls for a thing to collide with, finding a blocking linedef (with the impassable or monster-blocking flags) will end the linedef-finding loop immediately, not continuing to find ledges for the thing to snap onto. If a suitable ledge to snap onto is found before that, however, the snap is made, and the loop continues to find higher potential ledges to continue snapping the thing up to. This means it is possible for a thing intersecting with a very high ledge, and a moderately high ledge, to only snap up to the moderately high ledge if a blocking linedef is found before the very high ledge. A simple example of order sensitivity is 2002 A Doom Odyssey E2M1 with thing 54 (a candle); in version 3 of the WAD, this candle snaps up, but it does not in the mostly identical 10th anniversary edition due to the latter having prioritized blocking linedefs over ledge linedefs.
When a blocking thing such as a monster, player, or solid decoration like a tree is intersecting a snapping thing, the snapping thing is instead snapped down to the floor at its center point, the same place it was placed when the map was loaded. The thing is considered snapping while a sector holding it is updating, such as a lift moving, and during this time the player can walk into the area below a snapping thing to observe that it snaps back down, and walk out to make it snap back up. When the sector stops moving, the thing can no longer snap and remains at its current location.
Snapping does not affect things that ignore the blockmap, i.e. those that hold the MF_NOBLOCKMAP flag in their mobj definition. There are three decorative thing types that, perhaps accidentally, have this flag: 79 (a pile of guts), 80 (a pool of blood), and 81 (a brain stem). Because these things completely ignore the blockmap, they also ignore lifts moving from under them, leaving them floating in the air or embedded inside the lift, as is seen in MAP03 of Overboard.
Technical explanation[edit]
Thing spawning is handled by the function P_SpawnMobj in p_mobj.c, which uses R_PointOnSide in r_main.c for determining which side of a node the thing is on, repeatedly until the subsector and associated floor under the thing's center point is found. The thing is placed at the height of that floor, with no regard for nearby ledges.
The moving sector mechanics are found in the source code file p_map.c. A moving sector has a bounding box (or blockbox) consisting of blockmap tiles such that the sector is fully enveloped, and while that sector is moving, the function P_ChangeSector will iterate through all these bounding box tiles and update all things within.
P_ChangeSector:
... for (x=sector->blockbox[BOXLEFT] ; x<= sector->blockbox[BOXRIGHT] ; x++) for (y=sector->blockbox[BOXBOTTOM];y<= sector->blockbox[BOXTOP] ; y++) P_BlockThingsIterator (x, y, PIT_ChangeSector); ...
The function PIT_ChangeSector is called with each thing in these tiles to manage crushing and moving. As part of this task, it calls P_ThingHeightClip which updates the thing's vertical position to match the height of the floor it is on.
P_ThingHeightClip:
... boolean onfloor; onfloor = (thing->z == thing->floorz); P_CheckPosition (thing, thing->x, thing->y); // what about stranding a monster partially off an edge? thing->floorz = tmfloorz; thing->ceilingz = tmceilingz; if (onfloor) { // walking monsters rise and fall with the floor thing->z = thing->floorz; } ...
A call to P_CheckPosition is made to ascertain that floor, which is where a thing intersecting a ledge will be detected. The ledge is placed in the variable tmfloorz which then becomes the new associated floor of that thing.
This floor-finding step before updating the height ensures that things can not clip through walls around a descending lift, however the same mechanic is also what causes things below ledges to snap upwards on and around moving sectors.
Of note is that blockmap tiles are rather large and can easily contain things that would never touch the sector, and single sectors consisting of unconnected parts as well as concave sectors still have only one bounding box, potentially making them redundantly update things in an enormous and irrelevant area. The blockmap is also not static, having an origin point on the southwest corner of the map. This means that adding map geometry in the southwest part of a map may remove or add instances of this bug elsewhere by shifting the origin point of the blockmap.