Things block floors and ceilings when they should not

From DoomWiki.org

Floors and ceilings in motion may be blocked to prevent them from crushing actors such as players and monsters, but they are also blocked for a variety of other reasons due to oversights or otherwise imprecise checks, sometimes causing bugs in maps. This is therefore a group of engine bugs contributing towards the same type of behavior, and they can interact with each other and other bugs to cause surprising glitches in maps.

Map designers for vanilla Doom can avoid this category of bugs entirely by ensuring that monsters have no access to teleporters that would lead into a low ceiling, and that they have full height-accommodating clearance where they are placed, with no overlapping ledges (or touching ledges, due to point-on-line bias in the engine) that would bring the monster into the ceiling if walked on or otherwise transported up on.

Some of these situations are fixed in Boom and its derived source ports.

Types and interactions[edit]

When a sector is in motion it will attempt to predict if a shootable thing (specifically a mobj bearing the MF_SHOOTABLE flag), such as a player, monster, or barrel, has vertical overlap with a ceiling after the sector has moved. This is to detect crushing, so if this happens, depending on the linedef type the sector may continue and crush, or stop its movement. A sector has a bounding box made of blockmap blocks in which it checks for stuck things, but it does not check if these things have actual contact with the sector, meaning that a sector has an invisible area around it where stuck things will block its movement without them even needing to touch that moving sector. Additionally, having the blockmap shift (either from using a different blockmap builder, or adding geometry to the south or west of the map) may fix or break instances of this bug elsewhere on the map.

In Boom, things overlapping the moving sector are checked instead of all things in the blockmap bounding box, greatly reducing the impact of this bug, though not eliminating it.

The most significant oversight aside from the blockmap overreach is that gradually descending floors are mistakenly programmed to be blocked if things are being crushed. This of course makes no sense, because there can be nothing under a floor in the Doom engine. The inverse, ceilings that gradually ascend, do not have this issue—gradually rising ceilings are the only non-crushing moving sectors immune to being blocked. A simple case of a blocked descending floor causing a bug can be found in the first release of MAP32 of Arrival, where monsters placed on a descending floor trap are initially stuck in the ceiling, preventing the floor from moving.

Sectors instantly moving in a given direction, and sectors gradually moving, are programmed separately. Gradually moving sectors refers to the usual behavior where e.g. a lift that moves down to the lowest neighboring floor will do so gradually in the span of some amount of frames. Instantly moving sectors refers to when this is not possible, such as if the lift is in a recess in the floor so that it is lower than any neighboring floor, so when it tries to move to the lowest neighboring floor it will instantly snap up to that floor instead of either staying still or gradually rising. The code that performs these actions has oversights making ceilings that instantly rise and floors that instantly lower have this nonsense check for things being crushed, making it possible for them to get stuck too.

This bug sometimes happens in conjunction with things snapping up to ledges when sectors move, where a monster seemingly placed with enough clearance has a slight overlap with a ledge. When a sector movement happens nearby, that monster snaps up to the height of the ledge, making it clip through the ceiling to get stuck, blocking floors (such as monster closet covers) from descending. An example of this happening can be found in E3M4 of Doom the Way id Did.

If a thing is stuck in both a ceiling (not at its center point), and another thing, it is not considered to be stuck in the ceiling, causing floors and ceilings to move without issue. This is caused by the function P_CheckPosition being responsible for checking if a thing is stuck in another thing, and tracking the lowest ceiling and highest floor levels of the linedefs that the thing touches, but it stops immediately and neglects to check any linedefs if the thing is stuck in a thing.

Technical explanation[edit]

There are two general reasons for why floors and ceilings are blocked when they should not be, these reasons sometimes working in tandem:

  • Shootable things standing in an area with too low a ceiling, within a sector's bounding blockmap box, when that sector has a blocking-prone movement, will block that movement regardless of actual contact with the sector.
  • Oversights in the sector movement function cause movements to be blocking-prone when they should not be.

Both of these causes start with the function T_MovePlane of p_floor.c, where the latter cause can be found directly. The function is split into floor and ceiling behavior, each branch being split into upwards and downwards logic, and each of those branches are split into instant and gradual motion. The floor branch is as follows, modified for brevity and clarity:

switch(floor_direction) {
    case DOWN:
        if (sector->floorheight - speed < dest) { // INSTANT MOVE
            lastpos = sector->floorheight;
            sector->floorheight = dest;
            something_stuck = P_ChangeSector(sector,crush);
            if (something_stuck) {...} // Undo
        } else { // GRADUAL MOVE
            lastpos = sector->floorheight;
            sector->floorheight -= speed;
            something_stuck = P_ChangeSector(sector,crush);
            if (something_stuck) {...} // Undo
        }
        break;
    case UP:
        if (sector->floorheight + speed > dest) { // INSTANT MOVE
            lastpos = sector->floorheight;
            sector->floorheight = dest;
            something_stuck = P_ChangeSector(sector,crush);
            if (something_stuck) {...} // Undo
        } else { // GRADUAL MOVE, COULD GET CRUSHED
            lastpos = sector->floorheight;
            sector->floorheight += speed;
            something_stuck = P_ChangeSector(sector,crush);
            if (something_stuck && crush) return crushed;
            if (something_stuck) {...} // Undo
        }
        break;
}

The logic for floor movement here has a stuck-checking condition in every situation, even though it does not make sense in the case of floors gradually or instantly moving down. Floors instantly moving down are handled in the case UP, INSTANT MOVE branches, as this happens by attempting to move the floor up when there is no upper destination, so its position is set down to the otherwise highest destination instead.

In Boom, the stuck check for gradually descending floors is deleted, avoiding them getting pointlessly stuck. Instantly descending floors were not fixed, however.

The logic for ceilings with similar alterations to the code is as follows:

switch(ceiling_direction) {
    case DOWN:
        if (sector->ceilingheight - speed < dest) { // INSTANT MOVE
            lastpos = sector->ceilingheight;
            sector->ceilingheight = dest;
            something_stuck = P_ChangeSector(sector,crush);
            if (something_stuck) {...} // Undo
        } else { // GRADUAL MOVE, COULD GET CRUSHED
            lastpos = sector->ceilingheight;
            sector->ceilingheight -= speed;
            something_stuck = P_ChangeSector(sector,crush);
            if (something_stuck && crush) return crushed;
            if (something_stuck) {...} // Undo
        }
        break;
    case UP:
        if (sector->ceilingheight + speed > dest) { // INSTANT MOVE
            lastpos = sector->ceilingheight;
            sector->ceilingheight = dest;
            something_stuck = P_ChangeSector(sector,crush);
            if (something_stuck) {...} // Undo
        } else { // GRADUAL MOVE
            lastpos = sector->ceilingheight;
            sector->ceilingheight += speed;
            P_ChangeSector(sector,crush);
        }
        break;
}

Ceilings gradually moving up are lacking the stuck check completely, making them incapable of being stuck. Every other case, including ceilings instantly moving up, can get stuck however.

The check for crushed things happens inside P_ChangeSector, which only checks for things in the blockmap bounding box standing inside low ceilings. This means that this is the only potential stuck condition for a thing that will block sectors from moving; things stuck in eachother or walls are ignored.