mirror of
https://github.com/id-Software/quake2-rerelease-dll.git
synced 2025-02-14 15:51:42 +00:00
546 lines
12 KiB
C++
546 lines
12 KiB
C++
// Copyright (c) ZeniMax Media Inc.
|
|
// Licensed under the GNU General Public License 2.0.
|
|
// g_utils.c -- misc utility functions for game module
|
|
|
|
#include "g_local.h"
|
|
|
|
/*
|
|
=============
|
|
G_Find
|
|
|
|
Searches all active entities for the next one that validates the given callback.
|
|
|
|
Searches beginning at the edict after from, or the beginning if nullptr
|
|
nullptr will be returned if the end of the list is reached.
|
|
=============
|
|
*/
|
|
edict_t *G_Find(edict_t *from, std::function<bool(edict_t *e)> matcher)
|
|
{
|
|
if (!from)
|
|
from = g_edicts;
|
|
else
|
|
from++;
|
|
|
|
for (; from < &g_edicts[globals.num_edicts]; from++)
|
|
{
|
|
if (!from->inuse)
|
|
continue;
|
|
if (matcher(from))
|
|
return from;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
findradius
|
|
|
|
Returns entities that have origins within a spherical area
|
|
|
|
findradius (origin, radius)
|
|
=================
|
|
*/
|
|
edict_t *findradius(edict_t *from, const vec3_t &org, float rad)
|
|
{
|
|
vec3_t eorg;
|
|
int j;
|
|
|
|
if (!from)
|
|
from = g_edicts;
|
|
else
|
|
from++;
|
|
for (; from < &g_edicts[globals.num_edicts]; from++)
|
|
{
|
|
if (!from->inuse)
|
|
continue;
|
|
if (from->solid == SOLID_NOT)
|
|
continue;
|
|
for (j = 0; j < 3; j++)
|
|
eorg[j] = org[j] - (from->s.origin[j] + (from->mins[j] + from->maxs[j]) * 0.5f);
|
|
if (eorg.length() > rad)
|
|
continue;
|
|
return from;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
G_PickTarget
|
|
|
|
Searches all active entities for the next one that holds
|
|
the matching string at fieldofs in the structure.
|
|
|
|
Searches beginning at the edict after from, or the beginning if nullptr
|
|
nullptr will be returned if the end of the list is reached.
|
|
|
|
=============
|
|
*/
|
|
constexpr size_t MAXCHOICES = 8;
|
|
|
|
edict_t *G_PickTarget(const char *targetname)
|
|
{
|
|
edict_t *ent = nullptr;
|
|
int num_choices = 0;
|
|
edict_t *choice[MAXCHOICES];
|
|
|
|
if (!targetname)
|
|
{
|
|
gi.Com_Print("G_PickTarget called with nullptr targetname\n");
|
|
return nullptr;
|
|
}
|
|
|
|
while (1)
|
|
{
|
|
ent = G_FindByString<&edict_t::targetname>(ent, targetname);
|
|
if (!ent)
|
|
break;
|
|
choice[num_choices++] = ent;
|
|
if (num_choices == MAXCHOICES)
|
|
break;
|
|
}
|
|
|
|
if (!num_choices)
|
|
{
|
|
gi.Com_PrintFmt("G_PickTarget: target {} not found\n", targetname);
|
|
return nullptr;
|
|
}
|
|
|
|
return choice[irandom(num_choices)];
|
|
}
|
|
|
|
THINK(Think_Delay) (edict_t *ent) -> void
|
|
{
|
|
G_UseTargets(ent, ent->activator);
|
|
G_FreeEdict(ent);
|
|
}
|
|
|
|
void G_PrintActivationMessage(edict_t *ent, edict_t *activator, bool coop_global)
|
|
{
|
|
//
|
|
// print the message
|
|
//
|
|
if ((ent->message) && !(activator->svflags & SVF_MONSTER))
|
|
{
|
|
if (coop_global && coop->integer)
|
|
gi.LocBroadcast_Print(PRINT_CENTER, "{}", ent->message);
|
|
else
|
|
gi.LocCenter_Print(activator, "{}", ent->message);
|
|
|
|
// [Paril-KEX] allow non-noisy centerprints
|
|
if (ent->noise_index >= 0)
|
|
{
|
|
if (ent->noise_index)
|
|
gi.sound(activator, CHAN_AUTO, ent->noise_index, 1, ATTN_NORM, 0);
|
|
else
|
|
gi.sound(activator, CHAN_AUTO, gi.soundindex("misc/talk1.wav"), 1, ATTN_NORM, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void G_MonsterKilled(edict_t *self);
|
|
|
|
/*
|
|
==============================
|
|
G_UseTargets
|
|
|
|
the global "activator" should be set to the entity that initiated the firing.
|
|
|
|
If self.delay is set, a DelayedUse entity will be created that will actually
|
|
do the SUB_UseTargets after that many seconds have passed.
|
|
|
|
Centerprints any self.message to the activator.
|
|
|
|
Search for (string)targetname in all entities that
|
|
match (string)self.target and call their .use function
|
|
|
|
==============================
|
|
*/
|
|
void G_UseTargets(edict_t *ent, edict_t *activator)
|
|
{
|
|
edict_t *t;
|
|
|
|
//
|
|
// check for a delay
|
|
//
|
|
if (ent->delay)
|
|
{
|
|
// create a temp object to fire at a later time
|
|
t = G_Spawn();
|
|
t->classname = "DelayedUse";
|
|
t->nextthink = level.time + gtime_t::from_sec(ent->delay);
|
|
t->think = Think_Delay;
|
|
t->activator = activator;
|
|
if (!activator)
|
|
gi.Com_Print("Think_Delay with no activator\n");
|
|
t->message = ent->message;
|
|
t->target = ent->target;
|
|
t->killtarget = ent->killtarget;
|
|
return;
|
|
}
|
|
|
|
//
|
|
// print the message
|
|
//
|
|
G_PrintActivationMessage(ent, activator, true);
|
|
|
|
//
|
|
// kill killtargets
|
|
//
|
|
if (ent->killtarget)
|
|
{
|
|
t = nullptr;
|
|
while ((t = G_FindByString<&edict_t::targetname>(t, ent->killtarget)))
|
|
{
|
|
if (t->teammaster)
|
|
{
|
|
// PMM - if this entity is part of a chain, cleanly remove it
|
|
if (t->flags & FL_TEAMSLAVE)
|
|
{
|
|
for (edict_t *master = t->teammaster; master; master = master->teamchain)
|
|
{
|
|
if (master->teamchain == t)
|
|
{
|
|
master->teamchain = t->teamchain;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// [Paril-KEX] remove teammaster too
|
|
else if (t->flags & FL_TEAMMASTER)
|
|
{
|
|
t->teammaster->flags &= ~FL_TEAMMASTER;
|
|
|
|
edict_t *new_master = t->teammaster->teamchain;
|
|
|
|
if (new_master)
|
|
{
|
|
new_master->flags |= FL_TEAMMASTER;
|
|
new_master->flags &= ~FL_TEAMSLAVE;
|
|
|
|
for (edict_t *m = new_master; m; m = m->teamchain)
|
|
m->teammaster = new_master;
|
|
}
|
|
}
|
|
}
|
|
|
|
// [Paril-KEX] if we killtarget a monster, clean up properly
|
|
if (t->svflags & SVF_MONSTER)
|
|
{
|
|
if (!t->deadflag && !(t->monsterinfo.aiflags & AI_DO_NOT_COUNT) && !(t->spawnflags & SPAWNFLAG_MONSTER_DEAD))
|
|
G_MonsterKilled(t);
|
|
}
|
|
|
|
// PMM
|
|
G_FreeEdict(t);
|
|
|
|
if (!ent->inuse)
|
|
{
|
|
gi.Com_Print("entity was removed while using killtargets\n");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// fire targets
|
|
//
|
|
if (ent->target)
|
|
{
|
|
t = nullptr;
|
|
while ((t = G_FindByString<&edict_t::targetname>(t, ent->target)))
|
|
{
|
|
// doors fire area portals in a specific way
|
|
if (!Q_strcasecmp(t->classname, "func_areaportal") &&
|
|
(!Q_strcasecmp(ent->classname, "func_door") || !Q_strcasecmp(ent->classname, "func_door_rotating")
|
|
|| !Q_strcasecmp(ent->classname, "func_door_secret") || !Q_strcasecmp(ent->classname, "func_water")))
|
|
continue;
|
|
|
|
if (t == ent)
|
|
{
|
|
gi.Com_Print("WARNING: Entity used itself.\n");
|
|
}
|
|
else
|
|
{
|
|
if (t->use)
|
|
t->use(t, ent, activator);
|
|
}
|
|
if (!ent->inuse)
|
|
{
|
|
gi.Com_Print("entity was removed while using targets\n");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
constexpr vec3_t VEC_UP = { 0, -1, 0 };
|
|
constexpr vec3_t MOVEDIR_UP = { 0, 0, 1 };
|
|
constexpr vec3_t VEC_DOWN = { 0, -2, 0 };
|
|
constexpr vec3_t MOVEDIR_DOWN = { 0, 0, -1 };
|
|
|
|
void G_SetMovedir(vec3_t &angles, vec3_t &movedir)
|
|
{
|
|
if (angles == VEC_UP)
|
|
{
|
|
movedir = MOVEDIR_UP;
|
|
}
|
|
else if (angles == VEC_DOWN)
|
|
{
|
|
movedir = MOVEDIR_DOWN;
|
|
}
|
|
else
|
|
{
|
|
AngleVectors(angles, movedir, nullptr, nullptr);
|
|
}
|
|
|
|
angles = {};
|
|
}
|
|
|
|
char *G_CopyString(const char *in, int32_t tag)
|
|
{
|
|
if(!in)
|
|
return nullptr;
|
|
const size_t amt = strlen(in) + 1;
|
|
char *const out = static_cast<char *>(gi.TagMalloc(amt, tag));
|
|
Q_strlcpy(out, in, amt);
|
|
return out;
|
|
}
|
|
|
|
void G_InitEdict(edict_t *e)
|
|
{
|
|
// ROGUE
|
|
// FIXME -
|
|
// this fixes a bug somewhere that is setting "nextthink" for an entity that has
|
|
// already been released. nextthink is being set to FRAME_TIME_S after level.time,
|
|
// since freetime = nextthink - FRAME_TIME_S
|
|
if (e->nextthink)
|
|
e->nextthink = 0_ms;
|
|
// ROGUE
|
|
|
|
e->inuse = true;
|
|
e->sv.init = false;
|
|
e->classname = "noclass";
|
|
e->gravity = 1.0;
|
|
e->s.number = e - g_edicts;
|
|
|
|
// PGM - do this before calling the spawn function so it can be overridden.
|
|
e->gravityVector[0] = 0.0;
|
|
e->gravityVector[1] = 0.0;
|
|
e->gravityVector[2] = -1.0;
|
|
// PGM
|
|
}
|
|
|
|
/*
|
|
=================
|
|
G_Spawn
|
|
|
|
Either finds a free edict, or allocates a new one.
|
|
Try to avoid reusing an entity that was recently freed, because it
|
|
can cause the client to think the entity morphed into something else
|
|
instead of being removed and recreated, which can cause interpolated
|
|
angles and bad trails.
|
|
=================
|
|
*/
|
|
edict_t *G_Spawn()
|
|
{
|
|
uint32_t i;
|
|
edict_t *e;
|
|
|
|
e = &g_edicts[game.maxclients + 1];
|
|
for (i = game.maxclients + 1; i < globals.num_edicts; i++, e++)
|
|
{
|
|
// the first couple seconds of server time can involve a lot of
|
|
// freeing and allocating, so relax the replacement policy
|
|
if (!e->inuse && (e->freetime < 2_sec || level.time - e->freetime > 500_ms))
|
|
{
|
|
G_InitEdict(e);
|
|
return e;
|
|
}
|
|
}
|
|
|
|
if (i == game.maxentities)
|
|
gi.Com_Error("ED_Alloc: no free edicts");
|
|
|
|
globals.num_edicts++;
|
|
G_InitEdict(e);
|
|
return e;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
G_FreeEdict
|
|
|
|
Marks the edict as free
|
|
=================
|
|
*/
|
|
THINK(G_FreeEdict) (edict_t *ed) -> void
|
|
{
|
|
// already freed
|
|
if (!ed->inuse)
|
|
return;
|
|
|
|
gi.unlinkentity(ed); // unlink from world
|
|
|
|
if ((ed - g_edicts) <= (ptrdiff_t) (game.maxclients + BODY_QUEUE_SIZE))
|
|
{
|
|
#ifdef _DEBUG
|
|
gi.Com_Print("tried to free special edict\n");
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
gi.Bot_UnRegisterEdict( ed );
|
|
|
|
int32_t id = ed->spawn_count + 1;
|
|
memset(ed, 0, sizeof(*ed));
|
|
ed->s.number = ed - g_edicts;
|
|
ed->classname = "freed";
|
|
ed->freetime = level.time;
|
|
ed->inuse = false;
|
|
ed->spawn_count = id;
|
|
ed->sv.init = false;
|
|
}
|
|
|
|
BoxEdictsResult_t G_TouchTriggers_BoxFilter(edict_t *hit, void *)
|
|
{
|
|
if (!hit->touch)
|
|
return BoxEdictsResult_t::Skip;
|
|
|
|
return BoxEdictsResult_t::Keep;
|
|
}
|
|
|
|
/*
|
|
============
|
|
G_TouchTriggers
|
|
|
|
============
|
|
*/
|
|
void G_TouchTriggers(edict_t *ent)
|
|
{
|
|
int i, num;
|
|
static edict_t *touch[MAX_EDICTS];
|
|
edict_t *hit;
|
|
|
|
// dead things don't activate triggers!
|
|
if ((ent->client || (ent->svflags & SVF_MONSTER)) && (ent->health <= 0))
|
|
return;
|
|
|
|
num = gi.BoxEdicts(ent->absmin, ent->absmax, touch, MAX_EDICTS, AREA_TRIGGERS, G_TouchTriggers_BoxFilter, nullptr);
|
|
|
|
// be careful, it is possible to have an entity in this
|
|
// list removed before we get to it (killtriggered)
|
|
for (i = 0; i < num; i++)
|
|
{
|
|
hit = touch[i];
|
|
if (!hit->inuse)
|
|
continue;
|
|
if (!hit->touch)
|
|
continue;
|
|
hit->touch(hit, ent, null_trace, true);
|
|
}
|
|
}
|
|
|
|
// [Paril-KEX] scan for projectiles between our movement positions
|
|
// to see if we need to collide against them
|
|
void G_TouchProjectiles(edict_t *ent, vec3_t previous_origin)
|
|
{
|
|
// a bit ugly, but we'll store projectiles we are ignoring here.
|
|
static std::vector<edict_t *> skipped;
|
|
|
|
while (true)
|
|
{
|
|
trace_t tr = gi.trace(previous_origin, ent->mins, ent->maxs, ent->s.origin, ent, ent->clipmask | CONTENTS_PROJECTILE);
|
|
|
|
if (tr.fraction == 1.0f)
|
|
break;
|
|
else if (!(tr.ent->svflags & SVF_PROJECTILE))
|
|
break;
|
|
|
|
// always skip this projectile since certain conditions may cause the projectile
|
|
// to not disappear immediately
|
|
tr.ent->svflags &= ~SVF_PROJECTILE;
|
|
skipped.push_back(tr.ent);
|
|
|
|
// if we're both players and it's coop, allow the projectile to "pass" through
|
|
if (ent->client && tr.ent->owner && tr.ent->owner->client && !G_ShouldPlayersCollide(true))
|
|
continue;
|
|
|
|
G_Impact(ent, tr);
|
|
}
|
|
|
|
for (auto &skip : skipped)
|
|
skip->svflags |= SVF_PROJECTILE;
|
|
|
|
skipped.clear();
|
|
}
|
|
|
|
/*
|
|
==============================================================================
|
|
|
|
Kill box
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
/*
|
|
=================
|
|
KillBox
|
|
|
|
Kills all entities that would touch the proposed new positioning
|
|
of ent.
|
|
=================
|
|
*/
|
|
|
|
BoxEdictsResult_t KillBox_BoxFilter(edict_t *hit, void *)
|
|
{
|
|
if (!hit->solid || !hit->takedamage || hit->solid == SOLID_TRIGGER)
|
|
return BoxEdictsResult_t::Skip;
|
|
|
|
return BoxEdictsResult_t::Keep;
|
|
}
|
|
|
|
bool KillBox(edict_t *ent, bool from_spawning, mod_id_t mod, bool bsp_clipping)
|
|
{
|
|
// don't telefrag as spectator...
|
|
if (ent->movetype == MOVETYPE_NOCLIP)
|
|
return true;
|
|
|
|
contents_t mask = CONTENTS_MONSTER | CONTENTS_PLAYER;
|
|
|
|
// [Paril-KEX] don't gib other players in coop if we're not colliding
|
|
if (from_spawning && ent->client && coop->integer && !G_ShouldPlayersCollide(false))
|
|
mask &= ~CONTENTS_PLAYER;
|
|
|
|
int i, num;
|
|
static edict_t *touch[MAX_EDICTS];
|
|
edict_t *hit;
|
|
|
|
num = gi.BoxEdicts(ent->absmin, ent->absmax, touch, MAX_EDICTS, AREA_SOLID, KillBox_BoxFilter, nullptr);
|
|
|
|
for (i = 0; i < num; i++)
|
|
{
|
|
hit = touch[i];
|
|
|
|
if (hit == ent)
|
|
continue;
|
|
else if (!hit->inuse || !hit->takedamage || !hit->solid || hit->solid == SOLID_TRIGGER)
|
|
continue;
|
|
else if (hit->client && !(mask & CONTENTS_PLAYER))
|
|
continue;
|
|
|
|
if ((ent->solid == SOLID_BSP || (ent->svflags & SVF_HULL)) && bsp_clipping)
|
|
{
|
|
trace_t clip = gi.clip(ent, hit->s.origin, hit->mins, hit->maxs, hit->s.origin, G_GetClipMask(hit));
|
|
|
|
if (clip.fraction == 1.0f)
|
|
continue;
|
|
}
|
|
|
|
T_Damage(hit, ent, ent, vec3_origin, ent->s.origin, vec3_origin, 100000, 0, DAMAGE_NO_PROTECTION, mod);
|
|
}
|
|
|
|
return true; // all clear
|
|
}
|