From 28a4db0355709bcb14554c26c6f6d74e94d80e25 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 17:18:19 +0200 Subject: [PATCH 1/4] Guard against nullptr dereference (#278) Encountered a situation where a pSoldier between bFirstID and bLastID was null and then we'd dereference it. --- Tactical/Auto Bandage.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Tactical/Auto Bandage.cpp b/Tactical/Auto Bandage.cpp index 0205376b..a70f5f0a 100644 --- a/Tactical/Auto Bandage.cpp +++ b/Tactical/Auto Bandage.cpp @@ -460,7 +460,7 @@ void AutoBandage( BOOLEAN fStart ) for ( pSoldier = MercPtrs[cnt]; cnt <= gTacticalStatus.Team[OUR_TEAM].bLastID; ++cnt, ++pSoldier ) { // 0verhaul: Make sure the merc is also in the sector before making him stand up! - if ( pSoldier->bActive && pSoldier->bInSector ) + if (pSoldier && pSoldier->bActive && pSoldier->bInSector) { ActionDone( pSoldier ); if ( pSoldier->bSlotItemTakenFrom != NO_SLOT ) @@ -483,12 +483,16 @@ void AutoBandage( BOOLEAN fStart ) ubLoop = gTacticalStatus.Team[ gbPlayerNum ].bFirstID; for ( ; ubLoop <= gTacticalStatus.Team[ gbPlayerNum ].bLastID; ++ubLoop) { - ActionDone( MercPtrs[ ubLoop ] ); - - // If anyone is still doing aid animation, stop! - if ( MercPtrs[ ubLoop ]->usAnimState == GIVING_AID || MercPtrs[ ubLoop ]->usAnimState == GIVING_AID_PRN ) + pSoldier = MercPtrs[ubLoop]; + if (pSoldier && pSoldier->bActive && pSoldier->bInSector) { - MercPtrs[ ubLoop ]->SoldierGotoStationaryStance( ); + ActionDone(pSoldier); + + // If anyone is still doing aid animation, stop! + if (pSoldier->usAnimState == GIVING_AID || pSoldier->usAnimState == GIVING_AID_PRN) + { + pSoldier->SoldierGotoStationaryStance(); + } } } From ef69f9b2e65ae0a0b35ff774c40a1948a5400d0c Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:27:14 +0200 Subject: [PATCH 2/4] Allow enemy to retreat (#279) This check was preventing enemies from retreating into another sector if tactical AI retreat was enabled, leading into AI deadlock every time AI_ACTION_RUNAWAY was the decision. --- Tactical/Overhead.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Tactical/Overhead.cpp b/Tactical/Overhead.cpp index 36e5881d..3052c2bb 100644 --- a/Tactical/Overhead.cpp +++ b/Tactical/Overhead.cpp @@ -5081,11 +5081,17 @@ BOOLEAN NewOKDestination( SOLDIERTYPE * pCurrSoldier, INT32 sGridNo, BOOLEAN fPe INT16 sDesiredLevel; BOOLEAN fOKCheckStruct; - // sevenfm: allow civilians and NPCs with profile to go off screen - if (!GridNoOnVisibleWorldTile(sGridNo) && (pCurrSoldier->bTeam != CIV_TEAM || pCurrSoldier->ubProfile == NO_PROFILE)) - { - return(FALSE); - } + // Allow civilians and NPCs with profile to go off screen, and also enemies if tactical retreat is enabled + auto destinationOffscreen = !(GridNoOnVisibleWorldTile(sGridNo)); + auto hasProfile = pCurrSoldier->ubProfile != NO_PROFILE; + auto isCivilian = pCurrSoldier->bTeam == CIV_TEAM; + auto isEnemy = pCurrSoldier->bTeam == ENEMY_TEAM; + auto retreatAllowed = gGameExternalOptions.fAITacticalRetreat == true; + + if (destinationOffscreen && !(isCivilian || hasProfile || (isEnemy && retreatAllowed))) + { + return(FALSE); + } if (fPeopleToo && ( bPerson = WhoIsThere2( sGridNo, bLevel ) ) != NOBODY ) { From 4338b5b90985e3bf7dffc48e52383d4c148230cd Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 11 Feb 2024 12:42:21 +0200 Subject: [PATCH 3/4] Fix AI tactical retreat (#282) Only perform tactical traversal if pSoldier is at the map edge --- TacticalAI/AIMain.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index a64c4fcd..00bf2902 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -750,7 +750,9 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named if (pSoldier->aiData.bAction >= FIRST_MOVEMENT_ACTION && pSoldier->aiData.bAction <= LAST_MOVEMENT_ACTION && !pSoldier->flags.fDelayedMovement) { if (pSoldier->pathing.usPathIndex == pSoldier->pathing.usPathDataSize) - { + { + INT8 bEscapeDirection = NOWHERE; + if (!TileIsOutOfBounds(pSoldier->sAbsoluteFinalDestination)) { if ( !ACTING_ON_SCHEDULE( pSoldier ) && SpacesAway( pSoldier->sGridNo, pSoldier->sAbsoluteFinalDestination ) < 4 ) @@ -791,7 +793,8 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named } } // for regular guys still have to check for leaving the map - else if (pSoldier->ubQuoteActionID >= QUOTE_ACTION_ID_TRAVERSE_EAST && pSoldier->ubQuoteActionID <= QUOTE_ACTION_ID_TRAVERSE_NORTH) + else if (pSoldier->ubQuoteActionID >= QUOTE_ACTION_ID_TRAVERSE_EAST && pSoldier->ubQuoteActionID <= QUOTE_ACTION_ID_TRAVERSE_NORTH && + GridNoOnEdgeOfMap(pSoldier->sGridNo, &bEscapeDirection) && EscapeDirectionIsValid(&bEscapeDirection)) { HandleAITacticalTraversal( pSoldier ); return; From 65698284c39f90dc90daebc5f4887692f1b7f77a Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 11 Feb 2024 14:57:53 +0200 Subject: [PATCH 4/4] Re-enable extracover check for A* (#283) Allows smarter pathfinding for AI, only done during turnbased pathing to alleviate its extra cost --- Tactical/PATHAI.cpp | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Tactical/PATHAI.cpp b/Tactical/PATHAI.cpp index 9bc43579..02a8067d 100644 --- a/Tactical/PATHAI.cpp +++ b/Tactical/PATHAI.cpp @@ -461,7 +461,7 @@ static auto canJumpFences(SOLDIERTYPE* pSoldier) -> bool { //ADB the extra cover feature is supposed to pick a path of the same distance as one calculated with the feature off, //but a safer path, usually farther away from an enemy or following behind some cover. //however it has not been tested and it may need some work, I haven't touched it in a while -//#define ASTAR_USING_EXTRACOVER +#define ASTAR_USING_EXTRACOVER using namespace std; using namespace ASTAR; @@ -1164,7 +1164,7 @@ void AStarPathfinder::ExecuteAStarLogic() #ifdef ASTAR_USING_EXTRACOVER //check if we will run out of AP while entering this node or before //if we run out, the merc will stop at the parent node for a turn and be vulnerable - if (mercsMaxAPs && APCost > mercsMaxAPs) + if (gfTurnBasedAI && mercsMaxAPs && AStarG > mercsMaxAPs) { extraGCoverCost = GetExtraGCover(ParentNode); @@ -1174,7 +1174,7 @@ void AStarPathfinder::ExecuteAStarLogic() //use the stance and cover to see how much we really want to stop at the parent node //as opposed to an equal path with different cover //because other nodes further on the path will stop here too, add this value to the F cost - extraGCoverCost = CalcGCover(ParentNodeIndex, APCost); + extraGCoverCost = CalcGCover(ParentNode, AStarG); //remember, we have run out of points to *enter* this node, so we are stuck at the *parent* node //cache the cost to stay at the parent node @@ -1512,9 +1512,9 @@ int AStarPathfinder::CalcGCover(int const NodeIndex, INT32 iMyThreatValue; INT16 sThreatLoc; UINT32 uiThreatCnt = 0; - INT16 * pusLastLoc; - INT8 * pbPersOL; - INT8 * pbPublOL; + INT32* pusLastLoc; + INT8 * pbPersOL; + INT8 * pbPublOL; //although we have run out of APs to get here, it could just mean we have some APs but not enough to enter int APLeft = this->mercsMaxAPs - APCost; @@ -1523,7 +1523,7 @@ int AStarPathfinder::CalcGCover(int const NodeIndex, pusLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]); // hang a pointer into personal opplist - pbPersOL = &(pSoldier->bOppList[0]); + pbPersOL = &(pSoldier->aiData.bOppList[0]); // hang a pointer into public opplist pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]); @@ -1535,7 +1535,7 @@ int AStarPathfinder::CalcGCover(int const NodeIndex, SOLDIERTYPE* pOpponent = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, dead, unconscious - if (!pOpponent || pOpponent->bLife < OKLIFE) { + if (!pOpponent || pOpponent->stats.bLife < OKLIFE) { continue; // next merc } @@ -1544,7 +1544,7 @@ int AStarPathfinder::CalcGCover(int const NodeIndex, continue; // next merc } - pbPersOL = pSoldier->bOppList + pOpponent->ubID; + pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID; @@ -1554,7 +1554,7 @@ int AStarPathfinder::CalcGCover(int const NodeIndex, } // Special stuff for Carmen the bounty hunter - if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != 64) { + if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != 64) { continue; // next opponent } @@ -1599,7 +1599,7 @@ int AStarPathfinder::CalcGCover(int const NodeIndex, Threats[uiThreatCnt].iOrigRange = iThreatRange; // calculate how many APs he will have at the start of the next turn - Threats[uiThreatCnt].iAPs = CalcActionPoints(pOpponent); + Threats[uiThreatCnt].iAPs = pOpponent->CalcActionPoints(); if (iThreatRange < iClosestThreatRange) { iClosestThreatRange = iThreatRange; @@ -1641,7 +1641,7 @@ int AStarPathfinder::CalcCoverValue(INT32 sMyGridNo, INT32 iMyThreat, INT32 iMyA INT32 myThreatsiValue, INT32 myThreatsiAPs, INT32 myThreatsiCertainty) { SOLDIERTYPE* pMe = this->pSoldier; - INT32 morale = pSoldier->bAIMorale; + INT32 morale = pSoldier->aiData.bAIMorale; INT32 iRange = myThreatsiOrigRange; // all 32-bit integers for max. speed @@ -1690,7 +1690,7 @@ int AStarPathfinder::CalcCoverValue(INT32 sMyGridNo, INT32 iMyThreat, INT32 iMyA else { // optimistically assume we'll be behind the best cover available at this spot - bHisActualCTGT = CalcWorstCTGTForPosition( pHim, pMe->ubID, sMyGridNo, pMe->bLevel, iMyAPsLeft ); + bHisActualCTGT = CalcWorstCTGTForPosition( pHim, pMe->ubID, sMyGridNo, pMe->pathing.bLevel, iMyAPsLeft ); } // normally, that will be the cover I'll use, unless worst case over-rides it @@ -1709,7 +1709,7 @@ int AStarPathfinder::CalcCoverValue(INT32 sMyGridNo, INT32 iMyThreat, INT32 iMyA } // calculate where my cover is worst if opponent moves just 1 tile over - bHisBestCTGT = CalcBestCTGT(pHim, pMe->ubID, sMyGridNo, pMe->bLevel, iMyAPsLeft); + bHisBestCTGT = CalcBestCTGT(pHim, pMe->ubID, sMyGridNo, pMe->pathing.bLevel, iMyAPsLeft); // if he can actually improve his CTGT by moving to a nearby gridno if (bHisBestCTGT > bHisActualCTGT) @@ -1738,7 +1738,7 @@ int AStarPathfinder::CalcCoverValue(INT32 sMyGridNo, INT32 iMyThreat, INT32 iMyA // let's not assume anything about the stance the enemy might take, so take an average // value... no cover give a higher value than partial cover - bMyCTGT = CalcAverageCTGTForPosition( pMe, pHim->ubID, sHisGridNo, pHim->bLevel, iMyAPsLeft ); + bMyCTGT = CalcAverageCTGTForPosition( pMe, pHim->ubID, sHisGridNo, pHim->pathing.bLevel, iMyAPsLeft ); // since NPCs are too dumb to shoot "blind", ie. at opponents that they // themselves can't see (mercs can, using another as a spotter!), if the @@ -1768,14 +1768,14 @@ int AStarPathfinder::CalcCoverValue(INT32 sMyGridNo, INT32 iMyThreat, INT32 iMyA // try to account for who outnumbers who: the side with the advantage thus // (hopefully) values offense more, while those in trouble will play defense - if (pHim->bOppCnt > 1) + if (pHim->aiData.bOppCnt > 1) { - HisPosValue /= pHim->bOppCnt; + HisPosValue /= pHim->aiData.bOppCnt; } - if (pMe->bOppCnt > 1) + if (pMe->aiData.bOppCnt > 1) { - MyPosValue /= pMe->bOppCnt; + MyPosValue /= pMe->aiData.bOppCnt; }