From b1fa8c1ad76cfa18fa73e4a8ab7cce28f2c7969a Mon Sep 17 00:00:00 2001 From: Niehztog Date: Mon, 31 Aug 2026 16:03:06 +0200 Subject: [PATCH 1/2] Random respawn: test the candidate item in the dmflag filters FindSubstituteItem chooses a replacement item for randomrespawn. Its DF_NO_SPHERES, DF_NO_NUKES and DF_NO_MINES filters test ent->classname, the item being replaced, inside the loop that is choosing the item to replace it with. The condition does not depend on the loop, so the effect is the inverse of the comment above the sphere filter: a disabled sphere respawns as itself, while every other item can still be substituted into one. SpawnItem's own DF_NO_SPHERES guard then frees that substitute, and DoRandomRespawn returns the freed edict to DoRespawn, which relinks it as a solid trigger. The third sphere classname is misspelled "item_spehre_defender" besides, so of the three the Defender is the one DF_NO_SPHERES never recognised. Both come from the original Ground Zero source. The 2023 remaster tests the candidate item in these three filters and spells the Defender correctly, which is what this commit does as well. The sphere filter only existed in the counting pass. Once the filters depend on the candidate, the counting pass and the picking pass have to apply the same ones or they disagree on how many items are eligible and the pick walks off the count, so both now share a single predicate. It identifies spheres by the pickup handler, the same test SpawnItem already uses for DF_NO_SPHERES, rather than by three more classname literals. --- src/g_newdm.c | 63 +++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/g_newdm.c b/src/g_newdm.c index 204fd78..5b7fb96 100644 --- a/src/g_newdm.c +++ b/src/g_newdm.c @@ -17,6 +17,7 @@ extern qboolean Pickup_Health(edict_t *ent, edict_t *other); extern qboolean Pickup_Adrenaline(edict_t *ent, edict_t *other); extern qboolean Pickup_Armor(edict_t *ent, edict_t *other); extern qboolean Pickup_PowerArmor(edict_t *ent, edict_t *other); +extern qboolean Pickup_Sphere(edict_t *ent, edict_t *other); extern edict_t *Sphere_Spawn(edict_t *owner, int spawnflags); extern void ED_CallSpawn(edict_t *ent); void fire_doppleganger(edict_t *ent, vec3_t start, vec3_t aimdir); @@ -68,6 +69,39 @@ InitGameRules(void) } } +/* + * These dmflag filters decide which item may be substituted IN, so they test + * the candidate item. The original tested ent->classname, the item being + * replaced, which is loop invariant and the inverse of what the comment on the + * sphere filter says: a disabled sphere respawned as itself, while every other + * item could still be substituted into one. Both passes share this predicate + * so that the counting pass and the picking pass cannot disagree on which + * items are eligible. + */ +static qboolean +SubstituteItemAllowed(const gitem_t *it) +{ + /* don't respawn spheres if they're dmflag disabled. */ + if (((int)dmflags->value & DF_NO_SPHERES) && (it->pickup == Pickup_Sphere)) + { + return false; + } + + if (((int)dmflags->value & DF_NO_NUKES) && + !strcmp(it->classname, "ammo_nuke")) + { + return false; + } + + if (((int)dmflags->value & DF_NO_MINES) && + (!strcmp(it->classname, "ammo_prox") || !strcmp(it->classname, "ammo_tesla"))) + { + return false; + } + + return true; +} + const char * FindSubstituteItem(edict_t *ent) { @@ -168,25 +202,7 @@ FindSubstituteItem(edict_t *ent) itflags = IT_AMMO; } - /* don't respawn spheres if they're dmflag disabled. */ - if ((int)dmflags->value & DF_NO_SPHERES) - { - if (!strcmp(ent->classname, "item_sphere_vengeance") || - !strcmp(ent->classname, "item_sphere_hunter") || - !strcmp(ent->classname, "item_spehre_defender")) - { - continue; - } - } - - if (((int)dmflags->value & DF_NO_NUKES) && - !strcmp(ent->classname, "ammo_nuke")) - { - continue; - } - - if (((int)dmflags->value & DF_NO_MINES) && - (!strcmp(ent->classname, "ammo_prox") || !strcmp(ent->classname, "ammo_tesla"))) + if (!SubstituteItemAllowed(it)) { continue; } @@ -223,14 +239,7 @@ FindSubstituteItem(edict_t *ent) itflags = IT_AMMO; } - if (((int)dmflags->value & DF_NO_NUKES) && - !strcmp(ent->classname, "ammo_nuke")) - { - continue; - } - - if (((int)dmflags->value & DF_NO_MINES) && - (!strcmp(ent->classname, "ammo_prox") || !strcmp(ent->classname, "ammo_tesla"))) + if (!SubstituteItemAllowed(it)) { continue; } From a559bd34fbecbcc1796052a214de772df6acb824 Mon Sep 17 00:00:00 2001 From: Niehztog Date: Mon, 31 Aug 2026 16:24:25 +0200 Subject: [PATCH 2/2] Don't retarget a monster that is already angry at a tesla SV_movestep retargets a blocked monster onto the tesla whose bad area blocks it. The original had four cases: no valid enemy, retarget; the enemy is already a tesla, do nothing; the enemy is a player, retarget only when it cannot be seen; any other enemy, retarget. The second was guarded by strcmp(ent->enemy->classname, "telsa"), a misspelling, so it never ran and its case fell through to the last one and retargeted after all. Collapsing the chain into a single condition kept the behaviour of the misspelling rather than of the case it was written for: a tesla is not a client, so !ent->enemy->client is true for a monster whose enemy is a tesla and it is retargeted on every blocked frame. TargetTesla skips the switch when the tesla is the one the monster is already angry at, but its AI_MEDIC bail runs before that test, so a medic repeatedly calls cleanupHealTarget on a tesla; and when the blocking area belongs to a different tesla the monster switches to that one and overwrites oldenemy with a tesla, losing the player it was chasing. The 2023 remaster keeps all four cases and spells the classname correctly, so restore the exclusion. --- src/monster/misc/move.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/monster/misc/move.c b/src/monster/misc/move.c index 146a62f..61b2390 100644 --- a/src/monster/misc/move.c +++ b/src/monster/misc/move.c @@ -447,8 +447,14 @@ SV_movestep(edict_t *ent, vec3_t move, qboolean relink) { if (new_bad->owner && !strcmp(new_bad->owner->classname, "tesla")) { + /* A monster that is already angry at a tesla is left alone. + The original guarded that case with a misspelled "telsa", + so the branch never ran and the monster was retargeted at + the blocking tesla instead, which is the behaviour this + condition inherited when the chain was collapsed. */ if (!ent->enemy || !ent->enemy->inuse || - !ent->enemy->client || !visible(ent, ent->enemy)) + (strcmp(ent->enemy->classname, "tesla") && + (!ent->enemy->client || !visible(ent, ent->enemy)))) { TargetTesla(ent, new_bad->owner); ent->monsterinfo.aiflags |= AI_BLOCKED;