Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified engine/argus/progs.dat
Binary file not shown.
Binary file modified game/argus/progs.dat
Binary file not shown.
2,317 changes: 2,317 additions & 0 deletions runs/ab_dm2_unstick.log

Large diffs are not rendered by default.

2,353 changes: 2,353 additions & 0 deletions runs/ab_dm2_unstick_ctl.log

Large diffs are not rendered by default.

4,144 changes: 2,068 additions & 2,076 deletions runs/ab_dm4_unstick.log

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions src/argus.qc
Original file line number Diff line number Diff line change
Expand Up @@ -5373,6 +5373,90 @@ float() Argus_ActEscort =
// ============================================================================
// Performs a multi-point verified search around human teammate to find a safe,
// non-embedded, dry, reachable ground location. NEVER teleports inside player!
// EMBEDDED IN THE WORLD. A bot whose origin is inside solid cannot
// walkmove in any direction, so it stands still until the match ends
// with no hold flag set to explain it. Two dm2 tapes caught it: Joe
// Rogan for 45 s at '2556 -414 24', reading SOLID in hull 0 at both
// origin and eye height while every bot moving normally in the same
// dumps read EMPTY at both (#257, diagnosed with the edict dump).
// Knockback is the likely delivery: he was on 22 health.
//
// pointcontents reads hull 0, which is exactly the test that
// separated him from the walkers. It cannot see a hull 1 wedge, where
// the world is empty but the player box does not fit, and that is a
// different bug with a different victim in the same pair of tapes.
//
// Recovery is the catchup warp's pattern. The destination is a nav
// node, because navgen seats those at hull 1 standable origins, so a
// node is by construction a place a player fits. Nearest by distance
// ONLY: Argus_NearestNode prefers nodes it can see, and a traceline
// that STARTS in solid returns fraction 1, so from in here every node
// in the map would look visible and it could hand back one across the
// level.
float() Argus_Unstick =
{
local entity n, best;
local float d, bd;

if (pointcontents (self.origin) != CONTENT_SOLID)
{
self.ar_stucktime = 0;
return FALSE;
}

// BEING INSIDE SOLID FOR AN INSTANT IS NORMAL. The first cut of
// this fired on contents plus a low speed, and dm4 promptly
// teleported Romero 192 units down into the pit while he was
// running the walkway at 320 u/s: a bot clips a thin edge mid
// step and reads solid for a tick, and instantaneous velocity at
// an apex is not evidence of anything. Being inside solid and
// NOT HAVING MOVED is the real signature, because a bot that is
// genuinely embedded never gets out on its own.
if (self.ar_stucktime == 0
|| vlen (self.origin - self.ar_stuckpos) > 32)
{
self.ar_stucktime = time;
self.ar_stuckpos = self.origin;
return FALSE;
}
if (time < self.ar_stucktime + 3)
return FALSE;

best = world;
bd = 999999;
n = find (world, classname, "argus_node");
while (n != world)
{
d = vlen (n.origin - self.origin);
if (d < bd && pointcontents (n.origin) != CONTENT_SOLID)
{
bd = d;
best = n;
}
n = find (n, classname, "argus_node");
}
if (best == world)
return FALSE;

spawn_tfog (self.origin);
setorigin (self, best.origin);
spawn_tfog (best.origin);
self.velocity = '0 0 0';
self.ar_node = world;
self.ar_pending = 0;
self.ar_mode = 0;
Comment on lines +5445 to +5447

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild routing from the recovery node

When an embedded bot has a normal valid item goal, this preserves ar_goal but clears both its node and pending route request. The later Argus_AI fallback treats mode 0 as direct seeking, so the rescued bot immediately beelines toward the goal through walls or hazards instead of routing from best; moreover, an existing an_busy search for this bot can subsequently install its pre-warp start node because the goal did not change. Cancel any in-flight search and re-arm routing after the teleport so recovery actually starts at the destination node.

Useful? React with 👍 / 👎.

self.ar_failstreak = 0;
self.ar_stucktime = 0;
// plain console line: the ARGEVT verb set is closed, so this rides
// beside shove and routecache adopt and gets grepped the same way
dprint ("ARGUS ");
dprint (self.netname);
dprint (" unstick ");
dprint (vtos (best.origin));
dprint ("\n");
return TRUE;
};

float() Argus_CoopSafeWarp =
{
local vector p, fwd, rt, cand, best_pos;
Expand Down Expand Up @@ -5813,6 +5897,11 @@ void() Argus_AI =
{
self.ar_nextai = time + 0.2;

// before anything else: if we are inside the world, nothing
// below can help, because every steering decision ends in a
// walkmove that cannot succeed from in there
Argus_Unstick ();

// introductions, once the host has had time to connect
if (!self.ar_greeted && time > 5)
{
Expand Down
2 changes: 2 additions & 0 deletions src/defs.qc
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,8 @@ void(entity door) Argus_CoopCommitKey;
.float ar_escform; // 1 = holding escort formation (hysteresis)
.float ar_coop_seektime; // Next objective (key/door) seek, throttled
.float ar_fetchcool; // Rest after a co-op FETCH routefail
.float ar_stucktime; // First tick seen inside world solid
.vector ar_stuckpos; // Where that was, to prove we have not moved
.float ar_coop_local; // Flag: local escort steering handled heading this frame


27 changes: 22 additions & 5 deletions tools/argus_edicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"ar_padcooltime", "ar_goal_count", "ar_swim", "waterlevel"]


def parse(text):
def parse(text, which=None):
"""last dump in the file -> {index: {field: value}}

The server keeps printing while it dumps, so ARGLOG and ARGEVT
Expand All @@ -57,7 +57,10 @@ def parse(text):
starts = [m.start() for m in re.finditer(r"^EDICT \d+:", text, re.M)]
if not starts:
return {}
body = text[starts[-1]:]
if which is None or which >= len(starts):
which = len(starts) - 1
end = starts[which + 1] if which + 1 < len(starts) else len(text)
body = text[starts[which]:end]
Comment on lines +60 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject dump indexes outside the available range

When --dump is greater than the last index, this silently substitutes the final dump while the CLI still reports that it read the requested nonexistent index (for example, two dumps plus --dump 2 prints reading #2 but returns dump 1). Negative indexes are also accepted and can produce an empty reversed slice and a misleading no edicts dump error. Validate the requested zero-based index and fail explicitly instead of returning data from a different dump.

Useful? React with 👍 / 👎.

out, cur = {}, None
for line in body.splitlines():
h = HEAD.match(line)
Expand Down Expand Up @@ -99,6 +102,13 @@ def main():
ap.add_argument("--edict", type=int)
ap.add_argument("--field")
ap.add_argument("--all", action="store_true")
ap.add_argument("--dump", type=int, metavar="N",
help="which dump to read when a log holds several, "
"0 based. Default is the last.")
ap.add_argument("--repeat", type=int, default=1, metavar="N",
help="with --make-cfg, take N dumps that far apart. A "
"stochastic freeze needs several throws to be caught "
"in one.")
ap.add_argument("--make-cfg", type=float, metavar="SECS",
help="write engine/argus/edump.cfg: wait this long, then "
"dump. Run the engine with +exec edump.cfg when you "
Expand All @@ -113,14 +123,18 @@ def main():
# where every ar_ field still holds its default.
n = max(1, int(a.make_cfg * 10))
f = ROOT / "engine" / "argus" / "edump.cfg"
f.write_text("wait\n" * n + "edicts\n")
print(f"wrote {f} ({n} waits, about {n/10:.0f}s in)")
body = ("wait\n" * n + "edicts\n") * max(1, a.repeat)
f.write_text(body)
at = ", ".join(f"{(i+1)*n/10:.0f}s" for i in range(max(1, a.repeat)))
print(f"wrote {f} ({max(1, a.repeat)} dump(s) at about {at})")
print("then: quakespasm ... +map <map> +exec edump.cfg (needs -condebug)")
return

if not a.log:
sys.exit("give a log to read, or --make-cfg SECS to arm a dump")
eds = parse(Path(a.log).read_text(errors="replace"))
text = Path(a.log).read_text(errors="replace")
ndumps = len(re.findall(r"^EDICT 0:", text, re.M)) or 1
eds = parse(text, a.dump)
lost = eds.pop("lost", 0)
if not eds:
sys.exit("no edicts dump in that log. Inject `edicts` during a match "
Expand All @@ -129,6 +143,9 @@ def main():
used = {i: f for i, f in eds.items() if "FREE" not in f}
print(f"{len(eds)} edicts dumped, {len(used)} in use, {len(eds)-len(used)} free "
f"(the lab ceiling is max_edicts 600)")
if ndumps > 1:
print(f"log holds {ndumps} dumps; reading "
f"{'the last' if a.dump is None else f'#{a.dump}'}")
if lost:
print(f"note: {lost} edict header(s) missing from the log. A dump this "
f"size outruns the console, so counts are approximate.")
Expand Down