Brie Doom Sources Docs

Possible NPD with frontsector of line_t

Published: 18 Sep 2021
Tags: , ,

Doom uses a structure called line to define the structure of the maps. It is assumed that each line always has a front sector and an optional back sector. If a line has only the front sector, then it is a one-sided line. If it has a back sector, then it is a two-sided line. Line structure is represented in the code with struct line_t:

typedef struct line_s
{
  ...
  // Front and back sector.
  // Note: redundant? Can be retrieved from SideDefs.
  sector_t*  frontsector;
  sector_t*  backsector;
  ...
} line_t;

In the code, dereferencing the backsector is usually preceded by a check that backsector is not a null pointer. There are no such checks for the frontsector. For example, this is illustrated in P_GroupLines:

// count number of lines in each sector
li = lines;
total = 0;
for (i=0 ; i<numlines ; i++, li++)
{
  total++;
  li->frontsector->linecount++;

  if (li->backsector && li->backsector != li->frontsector)
  {
    li->backsector->linecount++;
    total++;
  }
}

However, when initializing line definitions, frontsector can be assigned a null pointer in P_LoadLineDefs:

ld->sidenum[0] = SHORT(mld->sidenum[0]);
ld->sidenum[1] = SHORT(mld->sidenum[1]);

if (ld->sidenum[0] != -1)
  ld->frontsector = sides[ld->sidenum[0]].sector;
else
  ld->frontsector = 0;

if (ld->sidenum[1] != -1)
  ld->backsector = sides[ld->sidenum[1]].sector;
else
  ld->backsector = 0;

Here, mld is the raw data read from the file and ld is the line_t being constructed. When sidenum[0] is -1, null pointer is written to ld.frontsector.

Solutions in existing projects

Other source ports handle this situation differently. For example, Mocha Doom issues a warning in one case:

/*
  * cph 2006/09/30 - our frontsector can be the second side of the
  * linedef, so must check for NO_INDEX in case we are incorrectly
  * referencing the back of a 1S line
  */
if (ldef.sidenum[side] != NO_INDEX) {
  li.frontsector = sides[ldef.sidenum[side]].sector;
} else {
  li.frontsector = null;
  System.err.printf("P_LoadZSegs: front of seg %i has no sidedef\n", i);
}

In the other case it sets null as the frontsector value:

// Front side defined without a valid frontsector.
if (ld.sidenum[0] != line_t.NO_INDEX) {
  ld.frontsector = sides[ld.sidenum[0]].sector;
  if (ld.frontsector == null) { // // Still null? Bad map. Map to dummy.
    ld.frontsector = dummy_sector;
  }
} else {
  ld.frontsector = null;
}

Delphi Doom also issues a warning and sets another sector as the frontsector:

if (ld.sidenum[0] >= 0) and (ld.sidenum[0] < numsides) then
  ld.frontsector := sides[ld.sidenum[0]].sector
else
begin
  if devparm then
    printf('P_LoadLineDefs(): Line %d does has invalid front sidedef %d'#13#10, [i, ld.sidenum[0]]);
  ld.sidenum[0] := 0;
  ld.frontsector := sides[0].sector;
end;

Our solution and Void Safety

Brie Doom’s source code is written with Void Safety enabled, so we have to decide if frontsector should indeed be attached and assigning Void to it should be banned, or should it be detached and all dereferences will be preceded with checking if frontsector is not Void. We cannot simply ignore this problem by assigning Void in one place and just assuming that it should not be Void in other places.

As a temporary solution for phase 1 we decided to leave frontsector as detached to preserve the original behaviour. All dereferences of frontsector first check that it is not Void. Example in P_GroupLines:

-- count number of lines in each sector
total := 0
from
  i := 0
until
  i >= numlines
loop
  total := total + 1
  check attached lines [i].frontsector as frontsector then
    frontsector.linecount := frontsector.linecount + 1
  end
  if attached lines [i].backsector as bs and then bs /= lines [i].frontsector then
    bs.linecount := bs.linecount + 1
  end
  i := i + 1
end

This solution is not ideal as it increases the verbosity of the code. Each access to frontsector has to be surrounded by a check.

In fact, this solution does not solve the real problem. What should the game do with a malformed map data? Maybe it should refuse working with such incorrect data altogether, maybe not. Changing this behaviour is left for phase 2 where we will revisit this problem again.