Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/commit-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ jobs:
tools/tests/renderer_vulkan_shadow_compatibility.py \
tools/tests/renderer_vulkan_world_interaction_compatibility.py \
tools/tests/rigid_body_recovery_diagnostics.py \
tools/tests/ai_medic_approach_watchdog.py \
tools/tests/savegame_corruption_contract.py \
tools/tests/savegame_pointer_width_safety.py \
tools/tests/savegame_v3_contract.py \
Expand Down Expand Up @@ -714,6 +715,7 @@ jobs:
python tools/tests/renderer_vulkan_shadow_compatibility.py
python tools/tests/renderer_vulkan_world_interaction_compatibility.py
python tools/tests/rigid_body_recovery_diagnostics.py
python tools/tests/ai_medic_approach_watchdog.py
python tools/tests/savegame_corruption_contract.py
python tools/tests/savegame_pointer_width_safety.py
python tools/tests/savegame_v3_contract.py
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/push-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ jobs:
tools/tests/renderer_vulkan_shadow_compatibility.py \
tools/tests/renderer_vulkan_world_interaction_compatibility.py \
tools/tests/rigid_body_recovery_diagnostics.py \
tools/tests/ai_medic_approach_watchdog.py \
tools/tests/savegame_corruption_contract.py \
tools/tests/savegame_pointer_width_safety.py \
tools/tests/savegame_v3_contract.py \
Expand Down Expand Up @@ -714,6 +715,7 @@ jobs:
python tools/tests/renderer_vulkan_shadow_compatibility.py
python tools/tests/renderer_vulkan_world_interaction_compatibility.py
python tools/tests/rigid_body_recovery_diagnostics.py
python tools/tests/ai_medic_approach_watchdog.py
python tools/tests/savegame_corruption_contract.py
python tools/tests/savegame_pointer_width_safety.py
python tools/tests/savegame_v3_contract.py
Expand Down
13 changes: 13 additions & 0 deletions docs/dev/release-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ completed release material here and publish curated notes in `releases/`.

## Post-0.14.000 corrections

- [ ] Stop medics and techs waiting forever for a player they cannot reach
(#184). Choosing the player as a patient makes the healer passive and blind to
enemies, and the approach only ended beside a visible patient, so a player it
could see but not reach held it in place indefinitely, as reported for Tech Li
after hangar1's lift. `ai_medicApproachTimeout` (15 s, 0 disables) now abandons
an approach that makes no progress and holds back automatic heals for as long
again; a heal the player asks for is unaffected. No save-format change.
`ai_medic_approach_watchdog.py` keeps SP and MP aligned.

Candidate AI release wording: Medics and techs no longer freeze in place when
they cannot reach you to heal or repair you; after a short wait they carry on
with their orders.

- [x] Complete the stage-one baseline implementation and audit: use a shared
high-resolution presentation phase for each SP/MP draw and precise OpenXR
smooth-turn deltas; restore imported game regression coverage; reconcile
Expand Down
49 changes: 48 additions & 1 deletion src/game/ai/AI_Medic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ rvAIMedic::rvAIMedic ( void ) {
wasAware = false;
wasIgnoreEnemies = false;
healDebounceTime = 0;
approachDistance = 0.0f;
approachProgressTime = 0;
patientRetryTime = 0;
}

void rvAIMedic::InitSpawnArgsVariables( void )
Expand Down Expand Up @@ -143,6 +146,9 @@ void rvAIMedic::Restore( idRestoreGame *savefile ) {
mPostHealScript.Restore( savefile );

InitSpawnArgsVariables();

approachProgressTime = 0;
patientRetryTime = 0;
}

/*
Expand Down Expand Up @@ -282,6 +288,7 @@ void rvAIMedic::TakePatient( idPlayer* pPatient )
ClearEnemy();
lookTarget = patient;
healing = false;
approachProgressTime = 0;
//healedAmount = 0;

if ( pPatient && DistanceTo( pPatient ) > 250.0f && !silent )
Expand Down Expand Up @@ -444,6 +451,30 @@ bool rvAIMedic::SituationAllowsPatient( void )
return false;
}

/*
================
rvAIMedic::PatientApproachStalled

True once the medic has not come closer to its patient than its best distance
so far for ai_medicApproachTimeout seconds. Only a new best restarts the clock,
whichever of them moved: a medic re-pathing back and forth beneath a patient it
cannot reach keeps moving without ever closing in.
================
*/
bool rvAIMedic::PatientApproachStalled( void ) {
const float timeoutSec = ai_medicApproachTimeout.GetFloat();
if ( timeoutSec <= 0.0f || !patient ) {
return false;
}
const float distance = DistanceTo( patient );
if ( approachProgressTime == 0 || distance < approachDistance - 8.0f ) {
approachDistance = distance;
approachProgressTime = gameLocal.GetTime();
return false;
}
return gameLocal.GetTime() - approachProgressTime > SEC2MS( timeoutSec );
}

bool rvAIMedic::AvailableToTakePatient( void )
{
if ( !healDisabled )
Expand Down Expand Up @@ -481,7 +512,9 @@ void rvAIMedic::Think ( void ) {
{
emergencyOverride = false;
}
if ( AvailableToTakePatient() )
// A medic that gave up on an approach waits before it volunteers
// again; a heal the player asks for (TalkTo) is not held back.
if ( gameLocal.GetTime() >= patientRetryTime && AvailableToTakePatient() )
{
idPlayer* player = gameLocal.GetLocalPlayer();

Expand Down Expand Up @@ -616,6 +649,20 @@ stateResult_t rvAIMedic::State_Medic ( const stateParms_t& parms ) {

switch ( parms.stage ) {
case STAGE_MOVE:
// TakePatient makes the medic passive and blind to enemies, and this
// stage only ends on reaching the patient, so a patient it can see but
// never reach would hold it here for good. Give up once it stops closing
// the distance; the map script and its tether continue as normal.
if ( PatientApproachStalled() ) {
gameLocal.Warning( "medic '%s' made no progress toward '%s' for %g seconds (distance %.0f); giving up on the heal for now",
GetName(), patient->GetName(), ai_medicApproachTimeout.GetFloat(), DistanceTo( patient ) );
StopMove( MOVE_STATUS_DONE );
DropPatient();
patientRetryTime = gameLocal.GetTime() + SEC2MS( ai_medicApproachTimeout.GetFloat() );
ExecScriptFunction( mPostHealScript );
return SRESULT_DONE;
}

// Attack when we have either stopped moving or are within melee range
if ( move.fl.done )
{
Expand Down
6 changes: 6 additions & 0 deletions src/game/ai/AI_Medic.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,19 @@ class rvAIMedic : public rvAITactical {
bool wasAware;
bool wasIgnoreEnemies;

// Approach watchdog. Transient: a restored medic starts measuring afresh.
float approachDistance;
int approachProgressTime;
int patientRetryTime;

void SetHealValues ( idPlayer* player );

void TakePatient ( idPlayer* pPatient );
void DropPatient ( void );
bool CheckTakePatient ( idPlayer* actor );
bool SituationAllowsPatient ( void );
bool AvailableToTakePatient ( void );
bool PatientApproachStalled ( void );

stateResult_t State_Medic ( const stateParms_t& parms );

Expand Down
1 change: 1 addition & 0 deletions src/game/gamesys/SysCvar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ idCVar ai_disableCover( "ai_disableCover", "0", CVAR_GAME | CVAR_BOOL, "d
//cdr: use new master move functions
idCVar ai_useRVMasterMove( "ai_useRVMasterMove", "0", CVAR_GAME | CVAR_BOOL, "changes AI to use new master move function" );
//jshepard: allow out of date AAS files to be used, for testing
idCVar ai_medicApproachTimeout( "ai_medicApproachTimeout", "15", CVAR_GAME | CVAR_FLOAT, "seconds a medic or tech may make no progress toward the player it chose to heal before it gives up and resumes its orders; 0 disables the watchdog" );
idCVar ai_scriptedMoveTimeout( "ai_scriptedMoveTimeout", "30", CVAR_GAME | CVAR_FLOAT, "seconds a scripted move may make no forward progress before it is abandoned so the map script can continue; 0 disables the watchdog" );
idCVar ai_allowOldAAS( "ai_allowOldAAS", "0", CVAR_GAME | CVAR_BOOL, "allows AI to use most recent AAS file, even if it is not up-to-date. Enable only for testing.");
// twhitaker: debugging support for eye focus
Expand Down
1 change: 1 addition & 0 deletions src/game/gamesys/SysCvar.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ extern idCVar ai_debugHelpers;
extern idCVar ai_useRVMasterMove;
//jshepard: allow old AAS files
extern idCVar ai_scriptedMoveTimeout;
extern idCVar ai_medicApproachTimeout;
extern idCVar ai_allowOldAAS;
// twhitaker: debugging support for eye focus
extern idCVar ai_debugEyeFocus;
Expand Down
49 changes: 48 additions & 1 deletion src/mpgame/ai/AI_Medic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ rvAIMedic::rvAIMedic ( void ) {
wasAware = false;
wasIgnoreEnemies = false;
healDebounceTime = 0;
approachDistance = 0.0f;
approachProgressTime = 0;
patientRetryTime = 0;
}

void rvAIMedic::InitSpawnArgsVariables( void )
Expand Down Expand Up @@ -143,6 +146,9 @@ void rvAIMedic::Restore( idRestoreGame *savefile ) {
mPostHealScript.Restore( savefile );

InitSpawnArgsVariables();

approachProgressTime = 0;
patientRetryTime = 0;
}

/*
Expand Down Expand Up @@ -282,6 +288,7 @@ void rvAIMedic::TakePatient( idPlayer* pPatient )
ClearEnemy();
lookTarget = patient;
healing = false;
approachProgressTime = 0;
//healedAmount = 0;

if ( pPatient && DistanceTo( pPatient ) > 250.0f && !silent )
Expand Down Expand Up @@ -444,6 +451,30 @@ bool rvAIMedic::SituationAllowsPatient( void )
return false;
}

/*
================
rvAIMedic::PatientApproachStalled

True once the medic has not come closer to its patient than its best distance
so far for ai_medicApproachTimeout seconds. Only a new best restarts the clock,
whichever of them moved: a medic re-pathing back and forth beneath a patient it
cannot reach keeps moving without ever closing in.
================
*/
bool rvAIMedic::PatientApproachStalled( void ) {
const float timeoutSec = ai_medicApproachTimeout.GetFloat();
if ( timeoutSec <= 0.0f || !patient ) {
return false;
}
const float distance = DistanceTo( patient );
if ( approachProgressTime == 0 || distance < approachDistance - 8.0f ) {
approachDistance = distance;
approachProgressTime = gameLocal.GetTime();
return false;
}
return gameLocal.GetTime() - approachProgressTime > SEC2MS( timeoutSec );
}

bool rvAIMedic::AvailableToTakePatient( void )
{
if ( !healDisabled )
Expand Down Expand Up @@ -481,7 +512,9 @@ void rvAIMedic::Think ( void ) {
{
emergencyOverride = false;
}
if ( AvailableToTakePatient() )
// A medic that gave up on an approach waits before it volunteers
// again; a heal the player asks for (TalkTo) is not held back.
if ( gameLocal.GetTime() >= patientRetryTime && AvailableToTakePatient() )
{
idPlayer* player = gameLocal.GetLocalPlayer();

Expand Down Expand Up @@ -616,6 +649,20 @@ stateResult_t rvAIMedic::State_Medic ( const stateParms_t& parms ) {

switch ( parms.stage ) {
case STAGE_MOVE:
// TakePatient makes the medic passive and blind to enemies, and this
// stage only ends on reaching the patient, so a patient it can see but
// never reach would hold it here for good. Give up once it stops closing
// the distance; the map script and its tether continue as normal.
if ( PatientApproachStalled() ) {
gameLocal.Warning( "medic '%s' made no progress toward '%s' for %g seconds (distance %.0f); giving up on the heal for now",
GetName(), patient->GetName(), ai_medicApproachTimeout.GetFloat(), DistanceTo( patient ) );
StopMove( MOVE_STATUS_DONE );
DropPatient();
patientRetryTime = gameLocal.GetTime() + SEC2MS( ai_medicApproachTimeout.GetFloat() );
ExecScriptFunction( mPostHealScript );
return SRESULT_DONE;
}

// Attack when we have either stopped moving or are within melee range
if ( move.fl.done )
{
Expand Down
6 changes: 6 additions & 0 deletions src/mpgame/ai/AI_Medic.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,19 @@ class rvAIMedic : public rvAITactical {
bool wasAware;
bool wasIgnoreEnemies;

// Approach watchdog. Transient: a restored medic starts measuring afresh.
float approachDistance;
int approachProgressTime;
int patientRetryTime;

void SetHealValues ( idPlayer* player );

void TakePatient ( idPlayer* pPatient );
void DropPatient ( void );
bool CheckTakePatient ( idPlayer* actor );
bool SituationAllowsPatient ( void );
bool AvailableToTakePatient ( void );
bool PatientApproachStalled ( void );

stateResult_t State_Medic ( const stateParms_t& parms );

Expand Down
1 change: 1 addition & 0 deletions src/mpgame/gamesys/SysCvar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ idCVar ai_disableCover( "ai_disableCover", "0", CVAR_GAME | CVAR_BOOL, "d
//cdr: use new master move functions
idCVar ai_useRVMasterMove( "ai_useRVMasterMove", "0", CVAR_GAME | CVAR_BOOL, "changes AI to use new master move function" );
//jshepard: allow out of date AAS files to be used, for testing
idCVar ai_medicApproachTimeout( "ai_medicApproachTimeout", "15", CVAR_GAME | CVAR_FLOAT, "seconds a medic or tech may make no progress toward the player it chose to heal before it gives up and resumes its orders; 0 disables the watchdog" );
idCVar ai_scriptedMoveTimeout( "ai_scriptedMoveTimeout", "30", CVAR_GAME | CVAR_FLOAT, "seconds a scripted move may make no forward progress before it is abandoned so the map script can continue; 0 disables the watchdog" );
idCVar ai_allowOldAAS( "ai_allowOldAAS", "0", CVAR_GAME | CVAR_BOOL, "allows AI to use most recent AAS file, even if it is not up-to-date. Enable only for testing.");
// twhitaker: debugging support for eye focus
Expand Down
1 change: 1 addition & 0 deletions src/mpgame/gamesys/SysCvar.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ extern idCVar ai_debugHelpers;
extern idCVar ai_useRVMasterMove;
//jshepard: allow old AAS files
extern idCVar ai_scriptedMoveTimeout;
extern idCVar ai_medicApproachTimeout;
extern idCVar ai_allowOldAAS;
// twhitaker: debugging support for eye focus
extern idCVar ai_debugEyeFocus;
Expand Down
Loading
Loading