123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- // 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)
- {
- struct skipped_projectile
- {
- edict_t *projectile;
- int32_t spawn_count;
- };
- // a bit ugly, but we'll store projectiles we are ignoring here.
- static std::vector<skipped_projectile> 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, tr.ent->spawn_count });
- // 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)
- if (skip.projectile->inuse && skip.projectile->spawn_count == skip.spawn_count)
- skip.projectile->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 || hit->solid == SOLID_BSP)
- 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;
- }
- // [Paril-KEX] don't allow telefragging of friends in coop.
- // the player that is about to be telefragged will have collision
- // disabled until another time.
- if (ent->client && hit->client && coop->integer)
- {
- hit->clipmask &= ~CONTENTS_PLAYER;
- ent->clipmask &= ~CONTENTS_PLAYER;
- continue;
- }
- T_Damage(hit, ent, ent, vec3_origin, ent->s.origin, vec3_origin, 100000, 0, DAMAGE_NO_PROTECTION, mod);
- }
- return true; // all clear
- }
|