Skip to content

Commit c075c4e

Browse files
committed
bot_navmesh: m_isBlocked team check, avoidance obstacle penalty, class-aware JUMP cost, AVOID penalty, shortcut obstacle check, frustration memory
1 parent f5b1ae8 commit c075c4e

5 files changed

Lines changed: 189 additions & 1 deletion

File tree

rcbot2/bot_cvars.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ ConVar *rcbot_debug_navmesh = nullptr;
9393
ConVar *rcbot_navmesh_jump_obstacle_min = nullptr;
9494
ConVar *rcbot_navmesh_jump_obstacle_max = nullptr;
9595
ConVar *rcbot_navmesh_jump_obstacle_range = nullptr;
96+
ConVar *rcbot_navmesh_max_jump_height = nullptr;
9697
ConVar *rcbot_use_navmesh = nullptr;
9798
ConVar *rcbot_tf2_autoupdate_point_time = nullptr;
9899
ConVar *rcbot_tf2_payload_dist_retreat = nullptr;

rcbot2/bot_cvars.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ extern ConVar *rcbot_debug_navmesh;
8989
extern ConVar *rcbot_navmesh_jump_obstacle_min;
9090
extern ConVar *rcbot_navmesh_jump_obstacle_max;
9191
extern ConVar *rcbot_navmesh_jump_obstacle_range;
92+
extern ConVar *rcbot_navmesh_max_jump_height;
9293
extern ConVar *rcbot_use_navmesh;
9394
extern ConVar *rcbot_tf2_autoupdate_point_time;
9495
extern ConVar *rcbot_tf2_payload_dist_retreat;

rcbot2/bot_cvars_register.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ void RCBOT2_Cvar_Register(ICvar *cvar)
165165
rcbot_navmesh_jump_obstacle_min = new ConVar("rcbot_navmesh_jump_obstacle_min", "18", 0, "Min obstacle height to trigger a preemptive jump");
166166
rcbot_navmesh_jump_obstacle_max = new ConVar("rcbot_navmesh_jump_obstacle_max", "72", 0, "Max obstacle height to trigger a preemptive jump");
167167
rcbot_navmesh_jump_obstacle_range = new ConVar("rcbot_navmesh_jump_obstacle_range", "160", 0, "Max distance at which to preemptively jump obstacles");
168+
rcbot_navmesh_max_jump_height = new ConVar("rcbot_navmesh_max_jump_height", "72", 0, "Baseline max jump height for non-enhanced classes in navmesh A*");
168169

169170
rcbot_use_navmesh = new ConVar("rcbot_use_navmesh", "1", 0, "");
170171

@@ -369,6 +370,8 @@ void RCBOT2_Cvar_Unlink(ICvar *cvar)
369370
cvar->UnregisterConCommand(rcbot_navmesh_jump_obstacle_max);
370371
if (rcbot_navmesh_jump_obstacle_range)
371372
cvar->UnregisterConCommand(rcbot_navmesh_jump_obstacle_range);
373+
if (rcbot_navmesh_max_jump_height)
374+
cvar->UnregisterConCommand(rcbot_navmesh_max_jump_height);
372375
if (rcbot_use_navmesh)
373376
cvar->UnregisterConCommand(rcbot_use_navmesh);
374377
if (rcbot_tf2_autoupdate_point_time)
@@ -573,6 +576,8 @@ void RCBOT2_Cvar_Unregister()
573576
rcbot_navmesh_jump_obstacle_max = nullptr;
574577
delete rcbot_navmesh_jump_obstacle_range;
575578
rcbot_navmesh_jump_obstacle_range = nullptr;
579+
delete rcbot_navmesh_max_jump_height;
580+
rcbot_navmesh_max_jump_height = nullptr;
576581
delete rcbot_use_navmesh;
577582
rcbot_use_navmesh = nullptr;
578583
delete rcbot_tf2_autoupdate_point_time;

rcbot2/bot_navmesh.cpp

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ void CNavMeshAccessor::scanGrid()
282282
}
283283

284284
detectConnectOffset();
285+
detectBlockedOffset();
286+
detectAvoidanceObstacleOffset();
285287

286288
m_teleportLinks.clear();
287289

@@ -488,6 +490,67 @@ bool CNavMeshAccessor::detectConnectOffset()
488490
return false;
489491
}
490492

493+
// m_isBlocked[2] is at offset 0x36 (54) in the SDK CNavAreaCriticalData struct.
494+
// The accessor adds +4 for the vtable, so the arena-space offset is 0x3A (58).
495+
// per the SDK comment [7/24/2008 tom], so it should be stable across engine builds.
496+
bool CNavMeshAccessor::detectBlockedOffset()
497+
{
498+
m_iBlockedOffset = -1;
499+
if (m_Areas.size() < 4) return false;
500+
int good = 0;
501+
for (int s = 0; s < 8 && (size_t)s < m_Areas.size(); s++)
502+
{
503+
unsigned char *area = m_Areas[s];
504+
if (!area) continue;
505+
unsigned char b0 = *(unsigned char*)(area + 0x3A);
506+
unsigned char b1 = *(unsigned char*)(area + 0x3B);
507+
if ((b0 <= 1) && (b1 <= 1))
508+
good++;
509+
}
510+
if (good >= 4)
511+
{
512+
m_iBlockedOffset = 0x3A;
513+
return true;
514+
}
515+
return false;
516+
}
517+
518+
// m_avoidanceObstacleHeight is a float at the end of CNavArea's non-critical
519+
// data. The exact offset varies between builds, so we probe a range.
520+
bool CNavMeshAccessor::detectAvoidanceObstacleOffset()
521+
{
522+
m_iAvoidanceObstacleOffset = -1;
523+
if (m_Areas.size() < 4) return false;
524+
525+
const int kMinOff = 0xA8, kMaxOff = 0x2FC;
526+
int bestOff = -1;
527+
float bestScore = 0.0f;
528+
529+
for (int off = kMinOff; off <= kMaxOff; off += 4)
530+
{
531+
int zeroCnt = 0, posCnt = 0;
532+
for (int s = 0; s < 16 && (size_t)s < m_Areas.size(); s++)
533+
{
534+
unsigned char *area = m_Areas[s];
535+
if (!area) continue;
536+
float v = *(float*)(area + off);
537+
if (!(v >= 0.0f && v <= 4096.0f)) { zeroCnt = posCnt = -99; break; }
538+
if (v == 0.0f) zeroCnt++;
539+
else if (v > 0.0f) posCnt++;
540+
}
541+
if (zeroCnt < 0) continue;
542+
float score = (float)zeroCnt * 0.5f + (float)posCnt * 1.5f;
543+
if (score > bestScore) { bestScore = score; bestOff = off; }
544+
}
545+
546+
if (bestOff >= 0 && bestScore >= 10.0f)
547+
{
548+
m_iAvoidanceObstacleOffset = bestOff;
549+
return true;
550+
}
551+
return false;
552+
}
553+
491554
unsigned char *CNavMeshAccessor::getAreaByIndex(int i) const
492555
{
493556
if (i < 0 || i >= (int)m_Areas.size()) return nullptr;
@@ -726,6 +789,75 @@ bool CNavMeshNavigator::workRoute(Vector vFrom, Vector vTo, bool *bFail,
726789
float stepCost = dx * dx + dy * dy + dz * dz;
727790
if (nCenter[2] < vCurCenter.z - 128.0f)
728791
stepCost += (vCurCenter.z - nCenter[2]) * (vCurCenter.z - nCenter[2]) * 3.0f;
792+
793+
// --- Blocked-area check (team-specific) ---
794+
{
795+
int blkOff = m_pAccessor->getBlockedOffset();
796+
if (blkOff >= 0)
797+
{
798+
int team = m_pBot ? m_pBot->getTeam() : 0;
799+
if (team >= 0 && team < 2)
800+
{
801+
unsigned char blocked = *(unsigned char*)((unsigned char*)neighbor + blkOff + team);
802+
if (blocked) continue;
803+
}
804+
}
805+
}
806+
807+
// --- Avoidance-obstacle penalty ---
808+
{
809+
int obsOff = m_pAccessor->getAvoidanceObstacleOffset();
810+
if (obsOff >= 0)
811+
{
812+
float obsH = *(float*)((unsigned char*)neighbor + obsOff);
813+
if (obsH > 0.0f && obsH <= 512.0f)
814+
stepCost += obsH * 10.0f;
815+
}
816+
}
817+
818+
// --- Class-aware JUMP connection cost ---
819+
int curAttr = *(int *)((unsigned char *)current + NAV_M_ATTR);
820+
if (curAttr & NAV_ATTR_JUMP)
821+
{
822+
float zDelta = fabsf(nCenter[2] - vCurCenter.z);
823+
float maxJump = 72.0f; // default baseline
824+
float enhCostMul = 2.0f;
825+
if (m_pBot && m_pBot->isTF2())
826+
{
827+
CBotTF2 *pTF2 = (CBotTF2 *)m_pBot;
828+
int tfClass = pTF2->getClass();
829+
if (tfClass == TF_CLASS_SCOUT)
830+
maxJump = 108.0f;
831+
else if (tfClass == TF_CLASS_SOLDIER && pTF2->getHealthPercent() > 0.5f)
832+
maxJump = 128.0f;
833+
else if (tfClass == TF_CLASS_DEMOMAN && rcbot_demo_jump && rcbot_demo_jump->GetInt())
834+
maxJump = 128.0f;
835+
else
836+
{
837+
if (rcbot_navmesh_max_jump_height)
838+
maxJump = rcbot_navmesh_max_jump_height->GetFloat();
839+
extern ConVar *rcbot_navmesh_max_jump_height;
840+
if (rcbot_navmesh_max_jump_height)
841+
maxJump = rcbot_navmesh_max_jump_height->GetFloat();
842+
}
843+
}
844+
if (zDelta > maxJump)
845+
continue; // too high for this class
846+
if (zDelta > 72.0f)
847+
stepCost *= (zDelta / 72.0f) * enhCostMul;
848+
}
849+
850+
// --- NAV_MESH_AVOID penalty ---
851+
if (curAttr & NAV_MESH_AVOID)
852+
stepCost *= 2.0f;
853+
854+
// --- Frustration penalty ---
855+
{
856+
auto fit = m_mapAreaFrustration.find(neighbor);
857+
if (fit != m_mapAreaFrustration.end() && fit->second > 0.01f)
858+
stepCost *= (1.0f + fit->second);
859+
}
860+
729861
float tentativeG = gScore[current] + stepCost;
730862

731863
auto gIt = gScore.find(neighbor);
@@ -1083,6 +1215,19 @@ void CNavMeshNavigator::optimizePath()
10831215
if (!isPotentiallyTraversable(posA, posC))
10841216
continue;
10851217

1218+
// Obstacle check: trace from waist height A→C.
1219+
// If a vertical obstacle sits between them, this shortcut would
1220+
// pass through a crate or low wall — reject.
1221+
{
1222+
CTraceFilterWorldAndPropsOnly trF;
1223+
Vector vTraceA = posA + Vector(0,0,18);
1224+
Vector vTraceC = posC + Vector(0,0,18);
1225+
CBotGlobals::traceLine(vTraceA, vTraceC, MASK_PLAYERSOLID, &trF);
1226+
trace_t *tr = CBotGlobals::getTraceResult();
1227+
if (tr && tr->fraction < 0.75f && tr->plane.normal.z < 0.5f)
1228+
continue;
1229+
}
1230+
10861231
// Remove node B at index i-1
10871232
m_route.erase(m_route.begin() + (i - 1));
10881233
removed = true;
@@ -1269,6 +1414,22 @@ bool CNavMeshNavigator::hasNextPoint() { return !m_route.empty(); }
12691414
void CNavMeshNavigator::updatePosition()
12701415
{
12711416
if (!m_pBot) return;
1417+
1418+
// Frustration decay: reduce all area frustration scores by 10% per frame.
1419+
// Prune entries that have decayed below 0.01.
1420+
{
1421+
auto it = m_mapAreaFrustration.begin();
1422+
while (it != m_mapAreaFrustration.end())
1423+
{
1424+
it->second *= 0.9f;
1425+
if (it->second < 0.01f)
1426+
it = m_mapAreaFrustration.erase(it);
1427+
else
1428+
++it;
1429+
}
1430+
}
1431+
1432+
if (!m_pAccessor || !m_pAccessor->ready()) { freeMapMemory(); return; }
12721433
if (!m_pAccessor || !m_pAccessor->ready()) { freeMapMemory(); return; }
12731434

12741435
// Phase 1: Escape mode (handles its own early-return if active)
@@ -1420,6 +1581,14 @@ void CNavMeshNavigator::updatePosition()
14201581
else
14211582
{
14221583
m_iConsecutiveHits++;
1584+
// Frustration memory: when stuck hitting walls in this area,
1585+
// increment its cost for future A* searches.
1586+
if (m_iConsecutiveHits >= 8 && m_pCurrentArea)
1587+
{
1588+
float &scr = m_mapAreaFrustration[m_pCurrentArea];
1589+
scr += 1.0f;
1590+
if (scr > 5.0f) scr = 5.0f;
1591+
}
14231592
if (m_iConsecutiveHits >= 60)
14241593
{
14251594
void *stuckDst = (!m_route.empty()) ? m_route.back().area : nullptr;

rcbot2/bot_navmesh.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#define NAV_ATTR_RUN 0x00000020 // NAV_MESH_RUN
3232
#define NAV_ATTR_WALK 0x00000040 // NAV_MESH_WALK
3333
#define NAV_ATTR_STAIRS 0x00001000 // NAV_MESH_STAIRS
34+
#define NAV_MESH_AVOID 0x00000080 // NAV_MESH_AVOID — avoid unless no alternative
3435

3536
// TF2-specific nav attributes (CTFNavArea subclass, offset 0x1C4)
3637
#define TF_NAV_ATTR_OFFSET 0x1C4 // CTFNavArea::m_attributeFlags
@@ -81,7 +82,7 @@ class CNavMeshAccessor : public CSignatureFunction
8182
{
8283
public:
8384
CNavMeshAccessor() : m_pNavMesh(nullptr), m_nAreaCount(0), m_bReady(false), m_pBase(nullptr),
84-
m_connectOffset(-1), m_areaMin(0), m_areaMax(0) {}
85+
m_connectOffset(-1), m_iBlockedOffset(-1), m_iAvoidanceObstacleOffset(-1), m_areaMin(0), m_areaMax(0) {}
8586
~CNavMeshAccessor() {}
8687

8788
void init(class CRCBotKeyValueList &kv, void *pBase);
@@ -92,6 +93,7 @@ class CNavMeshAccessor : public CSignatureFunction
9293
inline void *getBase() { return m_pBase; }
9394
int getAreaCount() const { return m_nAreaCount; }
9495
void invalidate() { m_Areas.clear(); m_nAreaCount = 0; m_bReady = false; m_connectOffset = -1;
96+
m_iBlockedOffset = -1; m_iAvoidanceObstacleOffset = -1;
9597
m_areaMin = 0; m_areaMax = 0; m_failedGoals.clear(); m_teleportLinks.clear(); }
9698
unsigned char *getNearestArea(float x, float y, float z);
9799
unsigned char *getNearestArea(Vector v) { return getNearestArea(v.x, v.y, v.z); }
@@ -103,6 +105,8 @@ class CNavMeshAccessor : public CSignatureFunction
103105
// CNavArea, detected at runtime (the layout differs between game builds).
104106
// Returns -1 if detection failed (navmesh should then stay inert).
105107
int getConnectOffset() const { return m_connectOffset; }
108+
int getBlockedOffset() const { return m_iBlockedOffset; }
109+
int getAvoidanceObstacleOffset() const { return m_iAvoidanceObstacleOffset; }
106110
// True if p is one of the cached nav area pointers (O(log n)).
107111
bool areaKnown(const void *p) const;
108112

@@ -129,13 +133,17 @@ class CNavMeshAccessor : public CSignatureFunction
129133

130134
private:
131135
bool detectConnectOffset();
136+
bool detectBlockedOffset();
137+
bool detectAvoidanceObstacleOffset();
132138

133139
void *m_pNavMesh;
134140
void *m_pBase;
135141
std::vector<unsigned char *> m_Areas; // sorted+unique after scanGrid()
136142
int m_nAreaCount;
137143
bool m_bReady;
138144
int m_connectOffset;
145+
int m_iBlockedOffset;
146+
int m_iAvoidanceObstacleOffset;
139147
uintptr_t m_areaMin; // min/max cached area pointer = heap arena span
140148
uintptr_t m_areaMax;
141149
std::map<const void *, float> m_failedGoals; // goal area → clear time (180–600 s)
@@ -327,6 +335,10 @@ class CNavMeshNavigator : public IBotNavigator
327335
float m_tnPhaseStartTime; // when current phase began
328336
Vector m_vTnExploreStart; // position where explore phase started
329337
float m_fTnExploreRadius; // how far we've covered from explore start
338+
339+
// Per-area frustration scores: increment when bot repeatedly hits walls
340+
// in an area, decay per frame. Used as A* cost multiplier.
341+
std::map<void*, float> m_mapAreaFrustration;
330342
};
331343

332344
// ---------- CNavMeshAccessor ----------

0 commit comments

Comments
 (0)