blankart/src/p_sight.c
Sally Coolatta 55a8643dd3 Put tm* variables into a struct
This makes it significantly easier to save/restore the state of these variables, whenever we need to do so for calling movement functions in the middle of other movement functions. This will also make it easier to move it out of global variable hell if desired later.
2024-09-13 11:29:30 -04:00

805 lines
20 KiB
C

// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file p_sight.c
/// \brief Line of sight/visibility checks, uses REJECT lookup table
#include "doomdef.h"
#include "doomstat.h"
#include "p_local.h"
#include "p_slopes.h"
#include "r_main.h"
#include "r_state.h"
#include "k_bot.h" // K_BotHatesThisSector
#include "k_kart.h" // K_TripwirePass
//
// P_CheckSight
//
// killough 4/19/98:
// Convert LOS info to struct for reentrancy and efficiency of data locality
typedef struct
{
fixed_t sightzstart, t2x, t2y; // eye z of looker
divline_t strace; // from t1 to t2
fixed_t topslope, bottomslope; // slopes to top and bottom of target
fixed_t bbox[4];
mobj_t *t1, *t2;
boolean alreadyHates; // For bot traversal, for if the bot is already in a sector it doesn't want to be
UINT8 traversed;
mobj_t *compareThing; // Original thing
} los_t;
typedef boolean (*los_init_t)(mobj_t *, mobj_t *, register los_t *);
typedef boolean (*los_valid_t)(seg_t *, divline_t *, register los_t *);
typedef boolean (*los_valid_poly_t)(polyobj_t *, divline_t *, register los_t *);
typedef struct
{
los_init_t init; // Initialization function. If true, we'll continue with checking across linedefs. If false, end early with failure.
los_valid_t validate; // Validation function. If true, continue iterating for possible success. If false, end early with failure.
los_valid_poly_t validatePolyobj; // If not NULL, then we will also check polyobject lines using this func.
} los_funcs_t;
static INT32 sightcounts[2];
#define TRAVERSE_MAX (8)
//
// P_DivlineSide
//
// Returns side 0 (front), 1 (back), or 2 (on).
//
// killough 4/19/98: made static, cleaned up
static INT32 P_DivlineSide(fixed_t x, fixed_t y, const divline_t *node)
{
fixed_t left, right;
return
!node->dx ? x == node->x ? 2 : x <= node->x ? node->dy > 0 : node->dy < 0 :
!node->dy ? y == node->y ? 2 : y <= node->y ? node->dx < 0 : node->dx > 0 :
(right = ((y - node->y) >> FRACBITS) * (node->dx >> FRACBITS)) <
(left = ((x - node->x) >> FRACBITS) * (node->dy >> FRACBITS)) ? 0 :
right == left ? 2 : 1;
}
static INT32 P_DivlineCrossed(fixed_t x1, fixed_t y1, fixed_t x2, fixed_t y2, const divline_t *node)
{
return (P_DivlineSide(x1, y1, node) == P_DivlineSide(x2, y2, node));
}
static boolean P_IsVisiblePolyObj(polyobj_t *po, divline_t *divl, register los_t *los)
{
sector_t *polysec = po->lines[0]->backsector;
fixed_t frac;
fixed_t topslope, bottomslope;
if (!(po->flags & POF_RENDERALL))
{
return true; // the polyobject isn't visible, so we can ignore it
}
// stop because it is not two sided
/*
if (!(po->flags & POF_TESTHEIGHT))
{
return false;
}
*/
frac = P_InterceptVector(&los->strace, divl);
// get slopes of top and bottom of this polyobject line
topslope = FixedDiv(polysec->ceilingheight - los->sightzstart , frac);
bottomslope = FixedDiv(polysec->floorheight - los->sightzstart , frac);
if (topslope >= los->topslope && bottomslope <= los->bottomslope)
{
// view completely blocked
return false;
}
// TODO: figure out if it's worth considering partially blocked cases or not?
// maybe to adjust los's top/bottom slopes if needed
/*
if (los->topslope <= los->bottomslope)
{
return false;
}
*/
return true;
}
static boolean P_CrossSubsecPolyObj(polyobj_t *po, register los_t *los, register los_funcs_t *funcs)
{
size_t i;
for (i = 0; i < po->numLines; ++i)
{
line_t *line = po->lines[i];
divline_t divl;
const vertex_t *v1,*v2;
// already checked other side?
if (line->validcount == validcount)
continue;
line->validcount = validcount;
// OPTIMIZE: killough 4/20/98: Added quick bounding-box rejection test
if (line->bbox[BOXLEFT ] > los->bbox[BOXRIGHT ] ||
line->bbox[BOXRIGHT ] < los->bbox[BOXLEFT ] ||
line->bbox[BOXBOTTOM] > los->bbox[BOXTOP ] ||
line->bbox[BOXTOP] < los->bbox[BOXBOTTOM])
continue;
v1 = line->v1;
v2 = line->v2;
// line isn't crossed?
if (P_DivlineCrossed(v1->x, v1->y, v2->x, v2->y, &los->strace))
continue;
divl.dx = v2->x - (divl.x = v1->x);
divl.dy = v2->y - (divl.y = v1->y);
// line isn't crossed?
if (P_DivlineCrossed(los->strace.x, los->strace.y, los->t2x, los->t2y, &divl))
continue;
if (funcs->validatePolyobj(po, &divl, los) == false)
{
return false;
}
}
return true;
}
static boolean P_IsVisible(seg_t *seg, divline_t *divl, register los_t *los)
{
line_t *line = seg->linedef;
fixed_t popentop, popenbottom;
const sector_t *front, *back;
fixed_t frac;
fixed_t fracx, fracy;
fixed_t frontf, backf, frontc, backc;
// stop because it is not two sided anyway
if (!(line->flags & ML_TWOSIDED))
{
return false;
}
// calculate fractional intercept (how far along we are divided by how far we are from t2)
frac = P_InterceptVector(&los->strace, divl);
front = seg->frontsector;
back = seg->backsector;
// calculate position at intercept
fracx = los->strace.x + FixedMul(los->strace.dx, frac);
fracy = los->strace.y + FixedMul(los->strace.dy, frac);
// calculate sector heights
frontf = P_GetSectorFloorZAt (front, fracx, fracy);
frontc = P_GetSectorCeilingZAt(front, fracx, fracy);
backf = P_GetSectorFloorZAt (back , fracx, fracy);
backc = P_GetSectorCeilingZAt(back , fracx, fracy);
// crosses a two sided line
// no wall to block sight with?
if (frontf == backf && frontc == backc
&& !front->ffloors & !back->ffloors) // (and no FOFs)
{
return true;
}
// possible occluder
// because of ceiling height differences
popentop = min(frontc, backc);
// because of floor height differences
popenbottom = max(frontf, backf);
// quick test for totally closed doors
if (popenbottom >= popentop)
{
return false;
}
if (frontf != backf)
{
fixed_t slope = FixedDiv(popenbottom - los->sightzstart , frac);
if (slope > los->bottomslope)
los->bottomslope = slope;
}
if (frontc != backc)
{
fixed_t slope = FixedDiv(popentop - los->sightzstart , frac);
if (slope < los->topslope)
los->topslope = slope;
}
if (los->topslope <= los->bottomslope)
{
return false;
}
// Monster Iestyn: check FOFs!
if (front->ffloors || back->ffloors)
{
ffloor_t *rover;
fixed_t topslope, bottomslope;
fixed_t topz, bottomz;
// check front sector's FOFs first
for (rover = front->ffloors; rover; rover = rover->next)
{
if (!(rover->fofflags & FOF_EXISTS)
|| !(rover->fofflags & FOF_RENDERSIDES) || (rover->fofflags & (FOF_TRANSLUCENT|FOF_FOG)))
{
continue;
}
topz = P_GetFFloorTopZAt (rover, fracx, fracy);
bottomz = P_GetFFloorBottomZAt(rover, fracx, fracy);
topslope = FixedDiv( topz - los->sightzstart, frac);
bottomslope = FixedDiv(bottomz - los->sightzstart, frac);
if (topslope >= los->topslope && bottomslope <= los->bottomslope)
{
return false; // view completely blocked
}
}
// check back sector's FOFs as well
for (rover = back->ffloors; rover; rover = rover->next)
{
if (!(rover->fofflags & FOF_EXISTS)
|| !(rover->fofflags & FOF_RENDERSIDES) || (rover->fofflags & (FOF_TRANSLUCENT|FOF_FOG)))
{
continue;
}
topz = P_GetFFloorTopZAt (rover, fracx, fracy);
bottomz = P_GetFFloorBottomZAt(rover, fracx, fracy);
topslope = FixedDiv( topz - los->sightzstart, frac);
bottomslope = FixedDiv(bottomz - los->sightzstart, frac);
if (topslope >= los->topslope && bottomslope <= los->bottomslope)
{
return false; // view completely blocked
}
}
// TODO: figure out if it's worth considering partially blocked cases or not?
// maybe to adjust los's top/bottom slopes if needed
}
return true;
}
static boolean P_CanTraceBlockingLine(seg_t *seg, divline_t *divl, register los_t *los)
{
line_t *line = seg->linedef;
(void)divl;
if (!(line->flags & ML_TWOSIDED))
{
// stop because it is not two sided anyway
return false;
}
if (P_IsLineBlocking(line, los->t1) == true)
{
// This line will always block us
return false;
}
if (los->t1->player != NULL)
{
if (P_IsLineTripWire(line) == true && K_TripwirePass(los->t1->player) == false)
{
// Can't go through trip wire.
return false;
}
}
return true;
}
static boolean P_CanBotTraverse(seg_t *seg, divline_t *divl, register los_t *los)
{
const boolean flip = ((los->t1->eflags & MFE_VERTICALFLIP) == MFE_VERTICALFLIP);
line_t *line = seg->linedef;
fixed_t frac = 0;
boolean canStepUp, canDropOff;
fixed_t maxstep = 0;
opening_t open = {0};
if (P_CanTraceBlockingLine(seg, divl, los) == false)
{
// Blocked, so obviously can't traverse either.
return false;
}
// calculate fractional intercept (how far along we are divided by how far we are from t2)
frac = P_InterceptVector(&los->strace, divl);
// calculate position at intercept
tm.x = los->strace.x + FixedMul(los->strace.dx, frac);
tm.y = los->strace.y + FixedMul(los->strace.dy, frac);
// set openrange, opentop, openbottom
open.fofType = (flip ? LO_FOF_CEILINGS : LO_FOF_FLOORS);
P_LineOpening(line, los->t1, &open);
maxstep = P_GetThingStepUp(los->t1, tm.x, tm.y);
if (open.range < los->t1->height)
{
// Can't fit
return false;
}
// If we can step up...
canStepUp = ((flip ? (open.highceiling - open.ceiling) : (open.floor - open.lowfloor)) <= maxstep);
// Or if we're on the higher side...
canDropOff = (flip ? (los->t1->z + los->t1->height <= open.ceiling) : (los->t1->z >= open.floor));
if (canStepUp || canDropOff)
{
if (los->t1->player != NULL && los->alreadyHates == false)
{
// Treat damage / offroad sectors like walls.
UINT8 side = P_DivlineSide(los->t2x, los->t2y, divl) & 1;
sector_t *sector = (side == 1) ? seg->backsector : seg->frontsector;
if (K_BotHatesThisSector(los->t1->player, sector, tm.x, tm.y))
{
// This line does not block us, but we don't want to cross it regardless.
return false;
}
}
return true;
}
los->traversed++;
return (los->traversed < TRAVERSE_MAX);
}
static boolean P_CanWaypointTraverse(seg_t *seg, divline_t *divl, register los_t *los)
{
const boolean flip = ((los->t1->eflags & MFE_VERTICALFLIP) == MFE_VERTICALFLIP);
line_t *line = seg->linedef;
fixed_t frac = 0;
boolean canStepUp, canDropOff;
fixed_t maxstep = 0;
opening_t open = {0};
if (P_CanTraceBlockingLine(seg, divl, los) == false)
{
// Blocked, so obviously can't traverse either.
return false;
}
if (line->special == 2001)
{
// Don't allow through the finish linedef.
// Causes some janky behavior.
return false;
}
// calculate fractional intercept (how far along we are divided by how far we are from t2)
frac = P_InterceptVector(&los->strace, divl);
// calculate position at intercept
tm.x = los->strace.x + FixedMul(los->strace.dx, frac);
tm.y = los->strace.y + FixedMul(los->strace.dy, frac);
// set openrange, opentop, openbottom
open.fofType = (flip ? LO_FOF_CEILINGS : LO_FOF_FLOORS);
P_LineOpening(line, los->t1, &open);
maxstep = P_GetThingStepUp(los->t1, tm.x, tm.y);
#if 0
if (los->t2->type == MT_WAYPOINT)
{
waypoint_t *wp = K_SearchWaypointHeapForMobj(los->t2);
if (wp != NULL)
{
CONS_Printf(
"========\nID: %d\nrange: %.2f >= %.2f\n",
K_GetWaypointID(wp),
FIXED_TO_FLOAT(open.range),
FIXED_TO_FLOAT(los->t1->height)
);
if (open.range >= los->t1->height)
{
CONS_Printf(
"floor: %.2f\nlowfloor: %.2f\nstep: %.2f <= %.2f\n",
FIXED_TO_FLOAT(open.floor),
FIXED_TO_FLOAT(open.lowfloor),
FIXED_TO_FLOAT(open.floor - open.lowfloor),
FIXED_TO_FLOAT(maxstep)
);
}
}
}
#endif
if (open.range < los->t1->height)
{
// Can't fit
return false;
}
// If we can step up...
canStepUp = ((flip ? (open.highceiling - open.ceiling) : (open.floor - open.lowfloor)) <= maxstep);
// Or if we're on the higher side...
canDropOff = (flip ? (los->t1->z + los->t1->height <= open.ceiling) : (los->t1->z >= open.floor));
if (canStepUp || canDropOff)
{
return true;
}
los->traversed++;
return (los->traversed < TRAVERSE_MAX);
}
//
// P_CrossSubsector
//
// Returns true if strace crosses the given subsector successfully.
//
static boolean P_CrossSubsector(size_t num, register los_t *los, register los_funcs_t *funcs)
{
seg_t *seg;
INT32 count;
#ifdef RANGECHECK
if (num >= numsubsectors)
I_Error("P_CrossSubsector: ss %s with numss = %s\n", sizeu1(num), sizeu2(numsubsectors));
#endif
// haleyjd 02/23/06: this assignment should be after the above check
seg = segs + subsectors[num].firstline;
// haleyjd 02/23/06: check polyobject lines
if (funcs->validatePolyobj != NULL)
{
polyobj_t *po;
if ((po = subsectors[num].polyList))
{
while (po)
{
if (po->validcount != validcount)
{
po->validcount = validcount;
if (!P_CrossSubsecPolyObj(po, los, funcs))
return false;
}
po = (polyobj_t *)(po->link.next);
}
}
}
for (count = subsectors[num].numlines; --count >= 0; seg++) // check lines
{
line_t *line = seg->linedef;
const vertex_t *v1, *v2;
divline_t divl;
if (seg->glseg)
continue;
// already checked other side?
if (line->validcount == validcount)
continue;
line->validcount = validcount;
// OPTIMIZE: killough 4/20/98: Added quick bounding-box rejection test
if (line->bbox[BOXLEFT ] > los->bbox[BOXRIGHT ] ||
line->bbox[BOXRIGHT ] < los->bbox[BOXLEFT ] ||
line->bbox[BOXBOTTOM] > los->bbox[BOXTOP ] ||
line->bbox[BOXTOP] < los->bbox[BOXBOTTOM])
continue;
v1 = line->v1;
v2 = line->v2;
// line isn't crossed?
if (P_DivlineCrossed(v1->x, v1->y, v2->x, v2->y, &los->strace))
continue;
divl.dx = v2->x - (divl.x = v1->x);
divl.dy = v2->y - (divl.y = v1->y);
// line isn't crossed?
if (P_DivlineCrossed(los->strace.x, los->strace.y, los->t2x, los->t2y, &divl))
continue;
if (funcs->validate(seg, &divl, los) == false)
{
return false;
}
}
// passed the subsector ok
return true;
}
//
// P_CrossBSPNode
// Returns true
// if strace crosses the given node successfully.
//
// killough 4/20/98: rewritten to remove tail recursion, clean up, and optimize
// cph - Made to use R_PointOnSide instead of P_DivlineSide, since the latter
// could return 2 which was ambigous, and the former is
// better optimised; also removes two casts :-)
static boolean P_CrossBSPNode(INT32 bspnum, register los_t *los, register los_funcs_t *funcs)
{
if (funcs->validate == NULL)
{
return false;
}
while (!(bspnum & NF_SUBSECTOR))
{
register node_t *bsp = nodes + bspnum;
INT32 side = R_PointOnSide(los->strace.x, los->strace.y, bsp);
INT32 side2 = R_PointOnSide(los->t2x, los->t2y, bsp);
if (side == side2)
{
// doesn't touch the other side
bspnum = bsp->children[side];
}
else
{
// the partition plane is crossed here
if (!P_CrossBSPNode(bsp->children[side], los, funcs))
{
return false; // cross the starting side
}
else
{
bspnum = bsp->children[side ^ 1]; // cross the ending side
}
}
}
return P_CrossSubsector((bspnum == -1 ? 0 : bspnum & ~NF_SUBSECTOR), los, funcs);
}
static boolean P_InitCheckSight(mobj_t *t1, mobj_t *t2, register los_t *los)
{
const sector_t *s1, *s2;
// An unobstructed LOS is possible.
// Now look from eyes of t1 to any part of t2.
sightcounts[1]++;
// Prevent SOME cases of looking through 3dfloors
//
// This WILL NOT work for things like 3d stairs with monsters behind
// them - they will still see you! TODO: Fix.
//
s1 = t1->subsector->sector;
s2 = t2->subsector->sector;
if (s1 == s2) // Both sectors are the same.
{
ffloor_t *rover;
fixed_t topz1, bottomz1; // top, bottom heights at t1's position
fixed_t topz2, bottomz2; // likewise but for t2
for (rover = s1->ffloors; rover; rover = rover->next)
{
// Allow sight through water, fog, etc.
/// \todo Improve by checking fog density/translucency
/// and setting a sight limit.
if (!(rover->fofflags & FOF_EXISTS)
|| !(rover->fofflags & FOF_RENDERPLANES) /*|| (rover->fofflags & (FOF_TRANSLUCENT|FOF_FOG))*/)
{
continue;
}
topz1 = P_GetFFloorTopZAt (rover, t1->x, t1->y);
topz2 = P_GetFFloorTopZAt (rover, t2->x, t2->y);
bottomz1 = P_GetFFloorBottomZAt(rover, t1->x, t1->y);
bottomz2 = P_GetFFloorBottomZAt(rover, t2->x, t2->y);
// Check for blocking floors here.
if ((los->sightzstart < bottomz1 && t2->z >= topz2)
|| (los->sightzstart >= topz1 && t2->z + t2->height < bottomz2))
{
// no way to see through that
return false;
}
if (rover->fofflags & FOF_SOLID)
continue; // shortcut since neither mobj can be inside the 3dfloor
if (rover->fofflags & FOF_BOTHPLANES || !(rover->fofflags & FOF_INVERTPLANES))
{
if (los->sightzstart >= topz1 && t2->z + t2->height < topz2)
return false; // blocked by upper outside plane
if (los->sightzstart < bottomz1 && t2->z >= bottomz2)
return false; // blocked by lower outside plane
}
if (rover->fofflags & FOF_BOTHPLANES || rover->fofflags & FOF_INVERTPLANES)
{
if (los->sightzstart < topz1 && t2->z >= topz2)
return false; // blocked by upper inside plane
if (los->sightzstart >= bottomz1 && t2->z + t2->height < bottomz2)
return false; // blocked by lower inside plane
}
}
}
return true;
}
static boolean P_InitTraceBotTraversal(mobj_t *t1, mobj_t *t2, register los_t *los)
{
(void)t2;
if (t1->player != NULL)
{
los->alreadyHates = K_BotHatesThisSector(
t1->player, t1->subsector->sector,
t1->x, t1->y
);
}
else
{
los->alreadyHates = false;
}
return true;
}
static boolean P_CompareMobjsAcrossLines(mobj_t *t1, mobj_t *t2, register los_funcs_t *funcs)
{
los_t los;
const sector_t *s1, *s2;
size_t pnum;
// First check for trivial rejection.
if (P_MobjWasRemoved(t1) == true || P_MobjWasRemoved(t2) == true)
{
return false;
}
if (!t1->subsector || !t2->subsector
|| !t1->subsector->sector || !t2->subsector->sector)
{
return false;
}
s1 = t1->subsector->sector;
s2 = t2->subsector->sector;
pnum = (s1-sectors)*numsectors + (s2-sectors);
if (rejectmatrix != NULL)
{
// Check in REJECT table.
if (rejectmatrix[pnum>>3] & (1 << (pnum&7))) // can't possibly be connected
{
return false;
}
}
// killough 11/98: shortcut for melee situations
// same subsector? obviously visible
// haleyjd 02/23/06: can't do this if there are polyobjects in the subsec
if (!t1->subsector->polyList &&
t1->subsector == t2->subsector)
{
return true;
}
validcount++;
los.t1 = t1;
los.t2 = t2;
los.alreadyHates = false;
los.traversed = 0;
los.topslope =
(los.bottomslope = t2->z - (los.sightzstart =
t1->z + t1->height -
(t1->height>>2))) + t2->height;
los.strace.dx = (los.t2x = t2->x) - (los.strace.x = t1->x);
los.strace.dy = (los.t2y = t2->y) - (los.strace.y = t1->y);
if (t1->x > t2->x)
los.bbox[BOXRIGHT] = t1->x, los.bbox[BOXLEFT] = t2->x;
else
los.bbox[BOXRIGHT] = t2->x, los.bbox[BOXLEFT] = t1->x;
if (t1->y > t2->y)
los.bbox[BOXTOP] = t1->y, los.bbox[BOXBOTTOM] = t2->y;
else
los.bbox[BOXTOP] = t2->y, los.bbox[BOXBOTTOM] = t1->y;
if (funcs->init != NULL)
{
if (funcs->init(t1, t2, &los) == false)
{
return false;
}
}
// The only required function.
I_Assert(funcs->validate != NULL);
// the head node is the last node output
return P_CrossBSPNode((INT32)numnodes - 1, &los, funcs);
}
//
// P_CheckSight
//
// Returns true if a straight line between t1 and t2 is unobstructed.
// Uses REJECT.
//
boolean P_CheckSight(mobj_t *t1, mobj_t *t2)
{
los_funcs_t funcs = {0};
funcs.init = &P_InitCheckSight;
funcs.validate = &P_IsVisible;
funcs.validatePolyobj = &P_IsVisiblePolyObj;
return P_CompareMobjsAcrossLines(t1, t2, &funcs);
}
boolean P_TraceBlockingLines(mobj_t *t1, mobj_t *t2)
{
los_funcs_t funcs = {0};
funcs.validate = &P_CanTraceBlockingLine;
return P_CompareMobjsAcrossLines(t1, t2, &funcs);
}
boolean P_TraceBotTraversal(mobj_t *t1, mobj_t *t2)
{
los_funcs_t funcs = {0};
funcs.init = &P_InitTraceBotTraversal;
funcs.validate = &P_CanBotTraverse;
return P_CompareMobjsAcrossLines(t1, t2, &funcs);
}
boolean P_TraceWaypointTraversal(mobj_t *t1, mobj_t *t2)
{
los_funcs_t funcs = {0};
funcs.validate = &P_CanWaypointTraverse;
return P_CompareMobjsAcrossLines(t1, t2, &funcs);
}