/** This runs in a separate thread */ static int fpathThreadFunc(void *) { wzMutexLock(fpathMutex); while (!fpathQuit) { if (pathJobs.empty()) { ASSERT(!waitingForResult, "Waiting for a result (id %u) that doesn't exist.", waitingForResultId); wzMutexUnlock(fpathMutex); wzSemaphoreWait(fpathSemaphore); // Go to sleep until needed. wzMutexLock(fpathMutex); continue; } // Copy the first job from the queue. packagedPathJob job = std::move(pathJobs.front()); pathJobs.pop_front(); wzMutexUnlock(fpathMutex); job(); wzMutexLock(fpathMutex); waitingForResult = false; objTrace(waitingForResultId, "These are the droids you are looking for."); wzSemaphorePost(waitingForResultSemaphore); } wzMutexUnlock(fpathMutex); return 0; }
// Run only from path thread PATHRESULT fpathExecute(PATHJOB job) { PATHRESULT result; result.droidID = job.droidID; memset(&result.sMove, 0, sizeof(result.sMove)); result.retval = FPR_FAILED; result.originalDest = Vector2i(job.destX, job.destY); ASR_RETVAL retval = fpathAStarRoute(&result.sMove, &job); ASSERT(retval != ASR_OK || result.sMove.asPath, "Ok result but no path in result"); ASSERT(retval == ASR_FAILED || result.sMove.numPoints > 0, "Ok result but no length of path in result"); switch (retval) { case ASR_NEAREST: if (job.acceptNearest) { objTrace(job.droidID, "** Nearest route -- accepted **"); result.retval = FPR_OK; } else { objTrace(job.droidID, "** Nearest route -- rejected **"); result.retval = FPR_FAILED; } break; case ASR_FAILED: objTrace(job.droidID, "** Failed route **"); // Is this really a good idea? Was in original code. if (job.propulsion == PROPULSION_TYPE_LIFT && (job.droidType != DROID_TRANSPORTER && job.droidType != DROID_SUPERTRANSPORTER)) { objTrace(job.droidID, "Doing fallback for non-transport VTOL"); fpathSetMove(&result.sMove, job.destX, job.destY); result.retval = FPR_OK; } else { result.retval = FPR_FAILED; } break; case ASR_OK: objTrace(job.droidID, "Got route of length %d", result.sMove.numPoints); result.retval = FPR_OK; break; } return result; }
// Run only from path thread static void fpathExecute(PATHJOB *psJob, PATHRESULT *psResult) { ASR_RETVAL retval = fpathAStarRoute(&psResult->sMove, psJob); ASSERT(retval != ASR_OK || psResult->sMove.asPath, "Ok result but no path in result"); ASSERT(retval == ASR_FAILED || psResult->sMove.numPoints > 0, "Ok result but no length of path in result"); switch (retval) { case ASR_NEAREST: if (psJob->acceptNearest) { objTrace(psJob->droidID, "** Nearest route -- accepted **"); psResult->retval = FPR_OK; } else { objTrace(psJob->droidID, "** Nearest route -- rejected **"); psResult->retval = FPR_FAILED; } break; case ASR_FAILED: objTrace(psJob->droidID, "** Failed route **"); // Is this really a good idea? Was in original code. if (psJob->propulsion == PROPULSION_TYPE_LIFT && (psJob->droidType != DROID_TRANSPORTER && psJob->droidType != DROID_SUPERTRANSPORTER)) { objTrace(psJob->droidID, "Doing fallback for non-transport VTOL"); fpathSetMove(&psResult->sMove, psJob->destX, psJob->destY); psResult->retval = FPR_OK; } else { psResult->retval = FPR_FAILED; } break; case ASR_OK: objTrace(psJob->droidID, "Got route of length %d", psResult->sMove.numPoints); psResult->retval = FPR_OK; break; } }
/** This runs in a separate thread */ static int fpathThreadFunc(void *) { wzMutexLock(fpathMutex); while (!fpathQuit) { if (pathJobs.empty()) { ASSERT(!waitingForResult, "Waiting for a result (id %u) that doesn't exist.", waitingForResultId); wzMutexUnlock(fpathMutex); wzSemaphoreWait(fpathSemaphore); // Go to sleep until needed. wzMutexLock(fpathMutex); continue; } // Copy the first job from the queue. Don't pop yet, since the main thread may want to set .deleted = true. PATHJOB job = pathJobs.front(); wzMutexUnlock(fpathMutex); // Execute path-finding for this job using our local temporaries PATHRESULT result; result.droidID = job.droidID; memset(&result.sMove, 0, sizeof(result.sMove)); result.retval = FPR_FAILED; result.originalDest = Vector2i(job.destX, job.destY); // we need to lock BEFORE we fiddle with the data, or we get ugly data race conditions. wzMutexLock(fpathMutex); fpathExecute(&job, &result); ASSERT(pathJobs.front().droidID == job.droidID, "Bug"); // The front of pathJobs may have .deleted set to true, but should not otherwise have been modified or deleted. if (!pathJobs.front().deleted) { pathResults.push_back(result); } pathJobs.pop_front(); // Unblock the main thread, if it was waiting for this particular result. if (waitingForResult && waitingForResultId == job.droidID) { waitingForResult = false; objTrace(waitingForResultId, "These are the droids you are looking for."); wzSemaphorePost(waitingForResultSemaphore); } } wzMutexUnlock(fpathMutex); return 0; }
// Find a route for an DROID to a location in world coordinates FPATH_RETVAL fpathDroidRoute(DROID *psDroid, SDWORD tX, SDWORD tY, FPATH_MOVETYPE moveType) { bool acceptNearest; PROPULSION_STATS *psPropStats = getPropulsionStats(psDroid); // override for AI to blast our way through stuff if (!isHumanPlayer(psDroid->player) && moveType == FMT_MOVE) { moveType = (psDroid->asWeaps[0].nStat == 0) ? FMT_MOVE : FMT_ATTACK; } ASSERT_OR_RETURN(FPR_FAILED, psPropStats != NULL, "invalid propulsion stats pointer"); ASSERT_OR_RETURN(FPR_FAILED, psDroid->type == OBJ_DROID, "We got passed an object that isn't a DROID!"); // Check whether the start and end points of the route are blocking tiles and find an alternative if they are. Position startPos = psDroid->pos; Position endPos = Position(tX, tY, 0); StructureBounds dstStructure = getStructureBounds(worldTile(endPos.xy)->psObject); startPos = findNonblockingPosition(startPos, getPropulsionStats(psDroid)->propulsionType, psDroid->player, moveType); if (!dstStructure.valid()) // If there's a structure over the destination, ignore it, otherwise pathfind from somewhere around the obstruction. { endPos = findNonblockingPosition(endPos, getPropulsionStats(psDroid)->propulsionType, psDroid->player, moveType); } objTrace(psDroid->id, "Want to go to (%d, %d) -> (%d, %d), going (%d, %d) -> (%d, %d)", map_coord(psDroid->pos.x), map_coord(psDroid->pos.y), map_coord(tX), map_coord(tY), map_coord(startPos.x), map_coord(startPos.y), map_coord(endPos.x), map_coord(endPos.y)); switch (psDroid->order.type) { case DORDER_BUILD: case DORDER_LINEBUILD: // build a number of structures in a row (walls + bridges) dstStructure = getStructureBounds(psDroid->order.psStats, psDroid->order.pos, psDroid->order.direction); // Just need to get close enough to build (can be diagonally), do not need to reach the destination tile. // fallthrough case DORDER_HELPBUILD: // help to build a structure case DORDER_DEMOLISH: // demolish a structure case DORDER_REPAIR: acceptNearest = false; break; default: acceptNearest = true; break; } return fpathRoute(&psDroid->sMove, psDroid->id, startPos.x, startPos.y, endPos.x, endPos.y, psPropStats->propulsionType, psDroid->droidType, moveType, psDroid->player, acceptNearest, dstStructure); }
/* Deals damage to an object * \param psObj object to deal damage to * \param damage amount of damage to deal * \param weaponClass the class of the weapon that deals the damage * \param weaponSubClass the subclass of the weapon that deals the damage * \param angle angle of impact (from the damage dealing projectile in relation to this object) * \return < 0 when the dealt damage destroys the object, > 0 when the object survives */ int32_t objDamage(BASE_OBJECT *psObj, UDWORD damage, UDWORD originalhp, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, HIT_SIDE impactSide) { int actualDamage, armour, level = 1; // If the previous hit was by an EMP cannon and this one is not: // don't reset the weapon class and hit time // (Giel: I guess we need this to determine when the EMP-"shock" is over) if (psObj->lastHitWeapon != WSC_EMP || weaponSubClass == WSC_EMP) { psObj->timeLastHit = gameTime; psObj->lastHitWeapon = weaponSubClass; } // EMP cannons do no damage, if we are one return now if (weaponSubClass == WSC_EMP) { return 0; } // apply game difficulty setting damage = modifyForDifficultyLevel(damage, psObj->player != selectedPlayer); armour = psObj->armour[impactSide][weaponClass]; debug(LOG_ATTACK, "objDamage(%d): body %d armour %d damage: %d", psObj->id, psObj->body, armour, damage); if (psObj->type == OBJ_STRUCTURE || psObj->type == OBJ_DROID) { // Force sending messages, even if messages were turned off, since a non-synchronised script will execute here. (Aaargh!) bool bMultiMessagesBackup = bMultiMessages; bMultiMessages = bMultiPlayer; clustObjectAttacked((BASE_OBJECT *)psObj); bMultiMessages = bMultiMessagesBackup; } if (psObj->type == OBJ_DROID) { DROID *psDroid = (DROID *)psObj; // Retrieve highest, applicable, experience level level = getDroidEffectiveLevel(psDroid); } // Reduce damage taken by EXP_REDUCE_DAMAGE % for each experience level actualDamage = (damage * (100 - EXP_REDUCE_DAMAGE * level)) / 100; // You always do at least a third of the experience modified damage actualDamage = MAX(actualDamage - armour, actualDamage / 3); // And at least MIN_WEAPON_DAMAGE points actualDamage = MAX(actualDamage, MIN_WEAPON_DAMAGE); objTrace(psObj->id, "objDamage: Penetrated %d", actualDamage); // for some odd reason, we have 0 hitpoints. if (!originalhp) { ASSERT(originalhp, "original hitpoints are 0 ?"); return -65536; // it is dead } // If the shell did sufficient damage to destroy the object, deal with it and return if (actualDamage >= psObj->body) { return -(int64_t)65536 * psObj->body / originalhp; } // Subtract the dealt damage from the droid's remaining body points psObj->body -= actualDamage; return (int64_t)65536 * actualDamage / originalhp; }
/* Fire a weapon at something */ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, int weapon_slot) { WEAPON_STATS *psStats; UDWORD damLevel; UDWORD firePause; SDWORD longRange; DROID *psDroid = NULL; int compIndex; CHECK_OBJECT(psAttacker); CHECK_OBJECT(psTarget); ASSERT(psWeap != NULL, "Invalid weapon pointer"); /* Watermelon:dont shoot if the weapon_slot of a vtol is empty */ if (psAttacker->type == OBJ_DROID && isVtolDroid(((DROID *)psAttacker))) { if (((DROID *)psAttacker)->sMove.iAttackRuns[weapon_slot] >= getNumAttackRuns(((DROID *)psAttacker), weapon_slot)) { objTrace(psAttacker->id, "VTOL slot %d is empty", weapon_slot); return false; } } /* Get the stats for the weapon */ compIndex = psWeap->nStat; ASSERT_OR_RETURN( false , compIndex < numWeaponStats, "Invalid range referenced for numWeaponStats, %d > %d", compIndex, numWeaponStats); psStats = asWeaponStats + compIndex; // check valid weapon/prop combination if (!validTarget(psAttacker, psTarget, weapon_slot)) { return false; } /*see if reload-able weapon and out of ammo*/ if (psStats->reloadTime && !psWeap->ammo) { if (gameTime - psWeap->lastFired < weaponReloadTime(psStats, psAttacker->player)) { return false; } //reset the ammo level psWeap->ammo = psStats->numRounds; } /* See when the weapon last fired to control it's rate of fire */ firePause = weaponFirePause(psStats, psAttacker->player); // increase the pause if heavily damaged switch (psAttacker->type) { case OBJ_DROID: psDroid = (DROID *)psAttacker; damLevel = PERCENT(psDroid->body, psDroid->originalBody); break; case OBJ_STRUCTURE: damLevel = PERCENT(((STRUCTURE *)psAttacker)->body, structureBody((STRUCTURE *)psAttacker)); break; default: damLevel = 100; break; } if (damLevel < HEAVY_DAMAGE_LEVEL) { firePause += firePause; } if (gameTime - psWeap->lastFired <= firePause) { /* Too soon to fire again */ return false; } // add a random delay to the fire // With logical updates, a good graphics gard no longer gives a better ROF. // TODO Should still replace this with something saner, such as a ±1% random deviation in reload time. int fireChance = gameTime - (psWeap->lastFired + firePause); if (gameRand(RANDOM_PAUSE) > fireChance) { return false; } if (psTarget->visible[psAttacker->player] != UBYTE_MAX) { // Can't see it - can't hit it objTrace(psAttacker->id, "combFire(%u[%s]->%u): Object has no indirect sight of target", psAttacker->id, psStats->pName, psTarget->id); return false; } /* Check we can see the target */ if (psAttacker->type == OBJ_DROID && !isVtolDroid((DROID *)psAttacker) && (proj_Direct(psStats) || actionInsideMinRange(psDroid, psTarget, psStats))) { if(!lineOfFire(psAttacker, psTarget, weapon_slot, true)) { // Can't see the target - can't hit it with direct fire objTrace(psAttacker->id, "combFire(%u[%s]->%u): Droid has no direct line of sight to target", psAttacker->id, ((DROID *)psAttacker)->aName, psTarget->id); return false; } } else if ((psAttacker->type == OBJ_STRUCTURE) && (((STRUCTURE *)psAttacker)->pStructureType->height == 1) && proj_Direct(psStats)) { // a bunker can't shoot through walls if (!lineOfFire(psAttacker, psTarget, weapon_slot, true)) { // Can't see the target - can't hit it with direct fire objTrace(psAttacker->id, "combFire(%u[%s]->%u): Structure has no direct line of sight to target", psAttacker->id, ((STRUCTURE *)psAttacker)->pStructureType->pName, psTarget->id); return false; } } else if ( proj_Direct(psStats) ) { // VTOL or tall building if (!lineOfFire(psAttacker, psTarget, weapon_slot, false)) { // Can't see the target - can't hit it with direct fire objTrace(psAttacker->id, "combFire(%u[%s]->%u): Tall object has no direct line of sight to target", psAttacker->id, psStats->pName, psTarget->id); return false; } } Vector3i deltaPos = psTarget->pos - psAttacker->pos; // if the turret doesn't turn, check if the attacker is in alignment with the target if (psAttacker->type == OBJ_DROID && !psStats->rotate) { uint16_t targetDir = iAtan2(removeZ(deltaPos)); int dirDiff = abs(angleDelta(targetDir - psAttacker->rot.direction)); if (dirDiff > FIXED_TURRET_DIR) { return false; } } /* Now see if the target is in range - also check not too near */ int dist = iHypot(removeZ(deltaPos)); longRange = proj_GetLongRange(psStats); /* modification by CorvusCorax - calculate shooting angle */ int min_angle = 0; // only calculate for indirect shots if (!proj_Direct(psStats) && dist > 0) { min_angle = arcOfFire(psAttacker,psTarget,weapon_slot,true); // prevent extremely steep shots min_angle = std::min(min_angle, DEG(PROJ_ULTIMATE_PITCH)); // adjust maximum range of unit if forced to shoot very steep if (min_angle > DEG(PROJ_MAX_PITCH)) { //do not allow increase of max range though if (iSin(2*min_angle) < iSin(2*DEG(PROJ_MAX_PITCH))) // If PROJ_MAX_PITCH == 45, then always iSin(2*min_angle) <= iSin(2*DEG(PROJ_MAX_PITCH)), and the test is redundant. { longRange = longRange * iSin(2*min_angle) / iSin(2*DEG(PROJ_MAX_PITCH)); } } } int baseHitChance = 0; if ((dist <= psStats->shortRange) && (dist >= psStats->minRange)) { // get weapon chance to hit in the short range baseHitChance = weaponShortHit(psStats,psAttacker->player); } else if ((dist <= longRange && dist >= psStats->minRange) || (psAttacker->type == OBJ_DROID && !proj_Direct(psStats) && actionInsideMinRange(psDroid, psTarget, psStats))) { // get weapon chance to hit in the long range baseHitChance = weaponLongHit(psStats,psAttacker->player); // adapt for height adjusted artillery shots if (min_angle > DEG(PROJ_MAX_PITCH)) { baseHitChance = baseHitChance * iCos(min_angle) / iCos(DEG(PROJ_MAX_PITCH)); } } else { /* Out of range */ objTrace(psAttacker->id, "combFire(%u[%s]->%u): Out of range", psAttacker->id, psStats->pName, psTarget->id); return false; } // apply experience accuracy modifiers to the base //hit chance, not to the final hit chance int resultHitChance = baseHitChance; // add the attacker's experience if (psAttacker->type == OBJ_DROID) { SDWORD level = getDroidEffectiveLevel((DROID *) psAttacker); // increase total accuracy by EXP_ACCURACY_BONUS % for each experience level resultHitChance += EXP_ACCURACY_BONUS * level * baseHitChance / 100; } // subtract the defender's experience if (psTarget->type == OBJ_DROID) { SDWORD level = getDroidEffectiveLevel((DROID *) psTarget); // decrease weapon accuracy by EXP_ACCURACY_BONUS % for each experience level resultHitChance -= EXP_ACCURACY_BONUS * level * baseHitChance / 100; } // fire while moving modifiers if (psAttacker->type == OBJ_DROID && ((DROID *)psAttacker)->sMove.Status != MOVEINACTIVE) { switch (psStats->fireOnMove) { case FOM_NO: // Can't fire while moving return false; break; case FOM_PARTIAL: resultHitChance = FOM_PARTIAL_ACCURACY_PENALTY * resultHitChance / 100; break; case FOM_YES: // can fire while moving break; } } /* -------!!! From that point we are sure that we are firing !!!------- */ /* note when the weapon fired */ psWeap->lastFired = gameTime; /* reduce ammo if salvo */ if (psStats->reloadTime) { psWeap->ammo--; } // increment the shots counter psWeap->shotsFired++; // predicted X,Y offset per sec Vector3i predict = psTarget->pos; //Watermelon:Target prediction if (isDroid(psTarget)) { DROID *psDroid = castDroid(psTarget); int32_t flightTime; if (proj_Direct(psStats) || dist <= psStats->minRange) { flightTime = dist / psStats->flightSpeed; } else { int32_t vXY, vZ; // Unused, we just want the flight time. flightTime = projCalcIndirectVelocities(dist, deltaPos.z, psStats->flightSpeed, &vXY, &vZ, min_angle); } if (psTarget->lastHitWeapon == WSC_EMP) { int empTime = EMP_DISABLE_TIME - (gameTime - psTarget->timeLastHit); CLIP(empTime, 0, EMP_DISABLE_TIME); if (empTime >= EMP_DISABLE_TIME * 9/10) { flightTime = 0; /* Just hit. Assume they'll get hit again */ } else { flightTime = MAX(0, flightTime - empTime); } } predict += Vector3i(iSinCosR(psDroid->sMove.moveDir, psDroid->sMove.speed*flightTime / GAME_TICKS_PER_SEC), 0); } /* Fire off the bullet to the miss location. The miss is only visible if the player owns the target. (Why? - Per) */ // What bVisible really does is to make the projectile audible even if it misses you. Since the target is NULL, proj_SendProjectile can't check if it was fired at you. bool bVisibleAnyway = psTarget->player == selectedPlayer; // see if we were lucky to hit the target bool isHit = gameRand(100) <= resultHitChance; if (isHit) { /* Kerrrbaaang !!!!! a hit */ objTrace(psAttacker->id, "combFire: [%s]->%u: resultHitChance=%d, visibility=%hhu : ", psStats->pName, psTarget->id, resultHitChance, psTarget->visible[psAttacker->player]); syncDebug("hit=(%d,%d,%d)", predict.x, predict.y, predict.z); } else /* Deal with a missed shot */ { const int minOffset = 5; int missDist = 2 * (100 - resultHitChance) + minOffset; Vector3i miss = Vector3i(iSinCosR(gameRand(DEG(360)), missDist), 0); predict += miss; psTarget = NULL; // Missed the target, so don't expect to hit it. objTrace(psAttacker->id, "combFire: Missed shot by (%4d,%4d)", miss.x, miss.y); syncDebug("miss=(%d,%d,%d)", predict.x, predict.y, predict.z); } // Make sure we don't pass any negative or out of bounds numbers to proj_SendProjectile CLIP(predict.x, 0, world_coord(mapWidth - 1)); CLIP(predict.y, 0, world_coord(mapHeight - 1)); proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle); return true; }
static FPATH_RETVAL fpathRoute(MOVE_CONTROL *psMove, unsigned id, int startX, int startY, int tX, int tY, PROPULSION_TYPE propulsionType, DROID_TYPE droidType, FPATH_MOVETYPE moveType, int owner, bool acceptNearest, StructureBounds const &dstStructure) { objTrace(id, "called(*,id=%d,sx=%d,sy=%d,ex=%d,ey=%d,prop=%d,type=%d,move=%d,owner=%d)", id, startX, startY, tX, tY, (int)propulsionType, (int)droidType, (int)moveType, owner); if (!worldOnMap(startX, startY) || !worldOnMap(tX, tY)) { debug(LOG_ERROR, "Droid trying to find path to/from invalid location (%d %d) -> (%d %d).", startX, startY, tX, tY); objTrace(id, "Invalid start/end."); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = FPR_FAILED", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner); return FPR_FAILED; } // don't have to do anything if already there if (startX == tX && startY == tY) { // return failed to stop them moving anywhere objTrace(id, "Tried to move nowhere"); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = FPR_FAILED", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner); return FPR_FAILED; } // Check if waiting for a result while (psMove->Status == MOVEWAITROUTE) { objTrace(id, "Checking if we have a path yet"); auto const &I = pathResults.find(id); ASSERT(I != pathResults.end(), "Missing path result promise"); PATHRESULT result = I->second.get(); ASSERT(result.retval != FPR_OK || result.sMove.asPath, "Ok result but no path in list"); // Copy over select fields - preserve others psMove->destination = result.sMove.destination; psMove->numPoints = result.sMove.numPoints; bool correctDestination = tX == result.originalDest.x && tY == result.originalDest.y; psMove->pathIndex = 0; psMove->Status = MOVENAVIGATE; free(psMove->asPath); psMove->asPath = result.sMove.asPath; FPATH_RETVAL retval = result.retval; ASSERT(retval != FPR_OK || psMove->asPath, "Ok result but no path after copy"); ASSERT(retval != FPR_OK || psMove->numPoints > 0, "Ok result but path empty after copy"); // Remove it from the result list pathResults.erase(id); objTrace(id, "Got a path to (%d, %d)! Length=%d Retval=%d", psMove->destination.x, psMove->destination.y, psMove->numPoints, (int)retval); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = %d, path[%d] = %08X->(%d, %d)", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner, retval, psMove->numPoints, ~crcSumVector2i(0, psMove->asPath, psMove->numPoints), psMove->destination.x, psMove->destination.y); if (!correctDestination) { goto queuePathfinding; // Seems we got the result of an old pathfinding job for this droid, so need to pathfind again. } return retval; } queuePathfinding: // We were not waiting for a result, and found no trivial path, so create new job and start waiting PATHJOB job; job.origX = startX; job.origY = startY; job.droidID = id; job.destX = tX; job.destY = tY; job.dstStructure = dstStructure; job.droidType = droidType; job.propulsion = propulsionType; job.moveType = moveType; job.owner = owner; job.acceptNearest = acceptNearest; job.deleted = false; fpathSetBlockingMap(&job); debug(LOG_NEVER, "starting new job for droid %d 0x%x", id, id); // Clear any results or jobs waiting already. It is a vital assumption that there is only one // job or result for each droid in the system at any time. fpathRemoveDroidData(id); packagedPathJob task([job]() { return fpathExecute(job); }); pathResults[id] = task.get_future(); // Add to end of list wzMutexLock(fpathMutex); bool isFirstJob = pathJobs.empty(); pathJobs.push_back(std::move(task)); wzMutexUnlock(fpathMutex); if (isFirstJob) { wzSemaphorePost(fpathSemaphore); // Wake up processing thread. } objTrace(id, "Queued up a path-finding request to (%d, %d), at least %d items earlier in queue", tX, tY, isFirstJob); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = FPR_WAIT", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner); return FPR_WAIT; // wait while polling result queue }
/* Fire a weapon at something */ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, int weapon_slot) { WEAPON_STATS *psStats; UDWORD firePause; SDWORD longRange; int compIndex; CHECK_OBJECT(psAttacker); CHECK_OBJECT(psTarget); ASSERT(psWeap != NULL, "Invalid weapon pointer"); /* Watermelon:dont shoot if the weapon_slot of a vtol is empty */ if (psAttacker->type == OBJ_DROID && isVtolDroid(((DROID *)psAttacker))) { if (psWeap->usedAmmo >= getNumAttackRuns(((DROID *)psAttacker), weapon_slot)) { objTrace(psAttacker->id, "VTOL slot %d is empty", weapon_slot); return false; } } /* Get the stats for the weapon */ compIndex = psWeap->nStat; ASSERT_OR_RETURN( false , compIndex < numWeaponStats, "Invalid range referenced for numWeaponStats, %d > %d", compIndex, numWeaponStats); psStats = asWeaponStats + compIndex; // check valid weapon/prop combination if (!validTarget(psAttacker, psTarget, weapon_slot)) { return false; } unsigned fireTime = gameTime - deltaGameTime + 1; // Can fire earliest at the start of the tick. // See if reloadable weapon. if (psStats->reloadTime) { unsigned reloadTime = psWeap->lastFired + weaponReloadTime(psStats, psAttacker->player); if (psWeap->ammo == 0) // Out of ammo? { fireTime = std::max(fireTime, reloadTime); // Have to wait for weapon to reload before firing. if (gameTime < fireTime) { return false; } } if (reloadTime <= fireTime) { //reset the ammo level psWeap->ammo = psStats->numRounds; } } /* See when the weapon last fired to control it's rate of fire */ firePause = weaponFirePause(psStats, psAttacker->player); firePause = std::max(firePause, 1u); // Don't shoot infinitely many shots at once. fireTime = std::max(fireTime, psWeap->lastFired + firePause); if (gameTime < fireTime) { /* Too soon to fire again */ return false; } if (psTarget->visible[psAttacker->player] != UBYTE_MAX) { // Can't see it - can't hit it objTrace(psAttacker->id, "combFire(%u[%s]->%u): Object has no indirect sight of target", psAttacker->id, psStats->pName, psTarget->id); return false; } /* Check we can hit the target */ bool tall = (psAttacker->type == OBJ_DROID && isVtolDroid((DROID *)psAttacker)) || (psAttacker->type == OBJ_STRUCTURE && ((STRUCTURE *)psAttacker)->pStructureType->height > 1); if (proj_Direct(psStats) && !lineOfFire(psAttacker, psTarget, weapon_slot, tall)) { // Can't see the target - can't hit it with direct fire objTrace(psAttacker->id, "combFire(%u[%s]->%u): No direct line of sight to target", psAttacker->id, objInfo(psAttacker), psTarget->id); return false; } Vector3i deltaPos = psTarget->pos - psAttacker->pos; // if the turret doesn't turn, check if the attacker is in alignment with the target if (psAttacker->type == OBJ_DROID && !psStats->rotate) { uint16_t targetDir = iAtan2(removeZ(deltaPos)); int dirDiff = abs(angleDelta(targetDir - psAttacker->rot.direction)); if (dirDiff > FIXED_TURRET_DIR) { return false; } } /* Now see if the target is in range - also check not too near */ int dist = iHypot(removeZ(deltaPos)); longRange = proj_GetLongRange(psStats); int min_angle = 0; // Calculate angle for indirect shots if (!proj_Direct(psStats) && dist > 0) { min_angle = arcOfFire(psAttacker,psTarget,weapon_slot,true); // prevent extremely steep shots min_angle = std::min(min_angle, DEG(PROJ_ULTIMATE_PITCH)); // adjust maximum range of unit if forced to shoot very steep if (min_angle > DEG(PROJ_MAX_PITCH)) { //do not allow increase of max range though if (iSin(2*min_angle) < iSin(2*DEG(PROJ_MAX_PITCH))) // If PROJ_MAX_PITCH == 45, then always iSin(2*min_angle) <= iSin(2*DEG(PROJ_MAX_PITCH)), and the test is redundant. { longRange = longRange * iSin(2*min_angle) / iSin(2*DEG(PROJ_MAX_PITCH)); } } } int baseHitChance = 0; if (dist <= longRange && dist >= psStats->minRange) { // get weapon chance to hit in the long range baseHitChance = weaponLongHit(psStats,psAttacker->player); // adapt for height adjusted artillery shots if (min_angle > DEG(PROJ_MAX_PITCH)) { baseHitChance = baseHitChance * iCos(min_angle) / iCos(DEG(PROJ_MAX_PITCH)); } } else { /* Out of range */ objTrace(psAttacker->id, "combFire(%u[%s]->%u): Out of range", psAttacker->id, psStats->pName, psTarget->id); return false; } // apply experience accuracy modifiers to the base //hit chance, not to the final hit chance int resultHitChance = baseHitChance; // add the attacker's experience if (psAttacker->type == OBJ_DROID) { SDWORD level = getDroidEffectiveLevel((DROID *) psAttacker); // increase total accuracy by EXP_ACCURACY_BONUS % for each experience level resultHitChance += EXP_ACCURACY_BONUS * level * baseHitChance / 100; } // subtract the defender's experience if (psTarget->type == OBJ_DROID) { SDWORD level = getDroidEffectiveLevel((DROID *) psTarget); // decrease weapon accuracy by EXP_ACCURACY_BONUS % for each experience level resultHitChance -= EXP_ACCURACY_BONUS * level * baseHitChance / 100; } // fire while moving modifiers if (psAttacker->type == OBJ_DROID && ((DROID *)psAttacker)->sMove.Status != MOVEINACTIVE) { switch (psStats->fireOnMove) { case FOM_NO: // Can't fire while moving return false; break; case FOM_PARTIAL: resultHitChance = FOM_PARTIAL_ACCURACY_PENALTY * resultHitChance / 100; break; case FOM_YES: // can fire while moving break; } } /* -------!!! From that point we are sure that we are firing !!!------- */ // Add a random delay to the next shot. // TODO Add deltaFireTime to the time it takes to fire next. If just adding to psWeap->lastFired, it might put it in the future, causing assertions. And if not sometimes putting it in the future, the fire rate would be lower than advertised. //int fireJitter = firePause/100; // ±1% variation in fire rate. //int deltaFireTime = gameRand(fireJitter*2 + 1) - fireJitter; /* note when the weapon fired */ psWeap->lastFired = fireTime; /* reduce ammo if salvo */ if (psStats->reloadTime) { psWeap->ammo--; } // increment the shots counter psWeap->shotsFired++; // predicted X,Y offset per sec Vector3i predict = psTarget->pos; //Watermelon:Target prediction if (isDroid(psTarget) && castDroid(psTarget)->sMove.bumpTime == 0) { DROID *psDroid = castDroid(psTarget); int32_t flightTime; if (proj_Direct(psStats) || dist <= psStats->minRange) { flightTime = dist * GAME_TICKS_PER_SEC / psStats->flightSpeed; } else { int32_t vXY, vZ; // Unused, we just want the flight time. flightTime = projCalcIndirectVelocities(dist, deltaPos.z, psStats->flightSpeed, &vXY, &vZ, min_angle); } if (psTarget->lastHitWeapon == WSC_EMP) { int playerEmpTime = getEmpDisableTime(psTarget->player); int empTime = playerEmpTime - (gameTime - psTarget->timeLastHit); CLIP(empTime, 0, playerEmpTime); if (empTime >= playerEmpTime * 9/10) { flightTime = 0; /* Just hit. Assume they'll get hit again */ } else { flightTime = MAX(0, flightTime - empTime); } } predict += Vector3i(iSinCosR(psDroid->sMove.moveDir, psDroid->sMove.speed*flightTime / GAME_TICKS_PER_SEC), 0); if (!isFlying(psDroid)) { predict.z = map_Height(removeZ(predict)); // Predict that the object will be on the ground. } } /* Fire off the bullet to the miss location. The miss is only visible if the player owns the target. (Why? - Per) */ // What bVisible really does is to make the projectile audible even if it misses you. Since the target is NULL, proj_SendProjectile can't check if it was fired at you. bool bVisibleAnyway = psTarget->player == selectedPlayer; // see if we were lucky to hit the target bool isHit = gameRand(100) <= resultHitChance; if (isHit) { /* Kerrrbaaang !!!!! a hit */ objTrace(psAttacker->id, "combFire: [%s]->%u: resultHitChance=%d, visibility=%d", psStats->pName, psTarget->id, resultHitChance, (int)psTarget->visible[psAttacker->player]); syncDebug("hit=(%d,%d,%d)", predict.x, predict.y, predict.z); } else /* Deal with a missed shot */ { const int minOffset = 5; int missDist = 2 * (100 - resultHitChance) + minOffset; Vector3i miss = Vector3i(iSinCosR(gameRand(DEG(360)), missDist), 0); predict += miss; psTarget = NULL; // Missed the target, so don't expect to hit it. objTrace(psAttacker->id, "combFire: Missed shot by (%4d,%4d)", miss.x, miss.y); syncDebug("miss=(%d,%d,%d)", predict.x, predict.y, predict.z); } // Make sure we don't pass any negative or out of bounds numbers to proj_SendProjectile CLIP(predict.x, 0, world_coord(mapWidth - 1)); CLIP(predict.y, 0, world_coord(mapHeight - 1)); proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle, fireTime); return true; }
/* Deals damage to an object * \param psObj object to deal damage to * \param damage amount of damage to deal * \param weaponClass the class of the weapon that deals the damage * \param weaponSubClass the subclass of the weapon that deals the damage * \return < 0 when the dealt damage destroys the object, > 0 when the object survives */ int32_t objDamage(BASE_OBJECT *psObj, unsigned damage, unsigned originalhp, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, bool isDamagePerSecond) { int actualDamage, armour, level = 1, lastHit = psObj->timeLastHit; // If the previous hit was by an EMP cannon and this one is not: // don't reset the weapon class and hit time // (Giel: I guess we need this to determine when the EMP-"shock" is over) if (psObj->lastHitWeapon != WSC_EMP || weaponSubClass == WSC_EMP) { psObj->timeLastHit = gameTime; psObj->lastHitWeapon = weaponSubClass; } // EMP cannons do no damage, if we are one return now if (weaponSubClass == WSC_EMP) { return 0; } // apply game difficulty setting damage = modifyForDifficultyLevel(damage, psObj->player != selectedPlayer); armour = psObj->armour[weaponClass]; if (psObj->type == OBJ_STRUCTURE || psObj->type == OBJ_DROID) { if (psObj->type == OBJ_STRUCTURE && ((STRUCTURE *)psObj)->status == SS_BEING_BUILT) { armour = 0; } // Force sending messages, even if messages were turned off, since a non-synchronised script will execute here. (Aaargh!) bool bMultiMessagesBackup = bMultiMessages; bMultiMessages = bMultiPlayer; clustObjectAttacked((BASE_OBJECT *)psObj); triggerEventAttacked(psObj, g_pProjLastAttacker, lastHit); bMultiMessages = bMultiMessagesBackup; } debug(LOG_ATTACK, "objDamage(%d): body %d armour %d damage: %d", psObj->id, psObj->body, armour, damage); if (psObj->type == OBJ_DROID) { DROID *psDroid = (DROID *)psObj; // Retrieve highest, applicable, experience level level = getDroidEffectiveLevel(psDroid); } // Reduce damage taken by EXP_REDUCE_DAMAGE % for each experience level actualDamage = (damage * (100 - EXP_REDUCE_DAMAGE * level)) / 100; // You always do at least a third of the experience modified damage actualDamage = MAX(actualDamage - armour, actualDamage / 3); // And at least MIN_WEAPON_DAMAGE points actualDamage = MAX(actualDamage, MIN_WEAPON_DAMAGE); if (isDamagePerSecond) { int deltaDamageRate = actualDamage - psObj->burnDamage; if (deltaDamageRate <= 0) { return 0; // Did this much damage already, this tick, so don't do more. } actualDamage = gameTimeAdjustedAverage(deltaDamageRate); psObj->burnDamage += deltaDamageRate; } objTrace(psObj->id, "objDamage: Penetrated %d", actualDamage); syncDebug("damage%u dam%u,o%u,wc%d.%d,ar%d,lev%d,aDam%d,isDps%d", psObj->id, damage, originalhp, weaponClass, weaponSubClass, armour, level, actualDamage, isDamagePerSecond); // for some odd reason, we have 0 hitpoints. if (!originalhp) { ASSERT(originalhp, "original hitpoints are 0 ?"); return -65536; // it is dead } // If the shell did sufficient damage to destroy the object, deal with it and return if (actualDamage >= psObj->body) { return -(int64_t)65536 * psObj->body / originalhp; } // Subtract the dealt damage from the droid's remaining body points psObj->body -= actualDamage; syncDebugObject(psObj, 'D'); return (int64_t)65536 * actualDamage / originalhp; }
/* See if there is a target in range for Sensor objects*/ BOOL aiChooseSensorTarget(BASE_OBJECT *psObj, BASE_OBJECT **ppsTarget) { int sensorRange = objSensorRange(psObj); unsigned int radSquared = sensorRange * sensorRange; bool radarDetector = objRadarDetector(psObj); if (!objActiveRadar(psObj) && !radarDetector) { ASSERT(false, "Only to be used for sensor turrets!"); return false; } /* See if there is something in range */ if (radarDetector) { BASE_OBJECT *psCurr, *psTemp = NULL; int tarDist = SDWORD_MAX; gridStartIterate(psObj->pos.x, psObj->pos.y, PREVIOUS_DEFAULT_GRID_SEARCH_RADIUS); psCurr = gridIterate(); while (psCurr != NULL) { if ((psCurr->type == OBJ_STRUCTURE || psCurr->type == OBJ_DROID) && !aiObjectIsProbablyDoomed(psCurr)) { if (!aiCheckAlliances(psCurr->player,psObj->player) && objActiveRadar(psCurr)) { // See if in twice *their* sensor range const int xdiff = psCurr->pos.x - psObj->pos.x; const int ydiff = psCurr->pos.y - psObj->pos.y; const unsigned int distSq = xdiff * xdiff + ydiff * ydiff; const int targetSensor = objSensorRange(psCurr) * 2; const unsigned int sensorSquared = targetSensor * targetSensor; if (distSq < sensorSquared && distSq < tarDist) { psTemp = psCurr; tarDist = distSq; } } } psCurr = gridIterate(); } if (psTemp) { objTrace(psTemp->id, "Targetted by radar detector %d", (int)psObj->id); objTrace(psObj->id, "Targetting radar %d", (int)psTemp->id); ASSERT(!psTemp->died, "aiChooseSensorTarget gave us a dead target"); *ppsTarget = psTemp; return true; } } else if (psObj->type == OBJ_DROID) { BASE_OBJECT *psTarget = NULL; if (aiBestNearestTarget((DROID *)psObj, &psTarget, 0, NULL) >= 0) { /* See if in sensor range */ const int xdiff = psTarget->pos.x - psObj->pos.x; const int ydiff = psTarget->pos.y - psObj->pos.y; const unsigned int distSq = xdiff * xdiff + ydiff * ydiff; // I do believe this will never happen, check for yourself :-) debug(LOG_NEVER, "Sensor droid(%d) found possible target(%d)!!!", psObj->id, psTarget->id); if (distSq < radSquared) { *ppsTarget = psTarget; return true; } } } else // structure { BASE_OBJECT *psCurr, *psTemp = NULL; int tarDist = SDWORD_MAX; gridStartIterate(psObj->pos.x, psObj->pos.y, PREVIOUS_DEFAULT_GRID_SEARCH_RADIUS); psCurr = gridIterate(); while (psCurr != NULL) { // Don't target features or doomed/dead objects if (psCurr->type != OBJ_FEATURE && !aiObjectIsProbablyDoomed(psCurr)) { if (!aiCheckAlliances(psCurr->player,psObj->player) && !aiObjIsWall(psCurr)) { // See if in sensor range and visible const int xdiff = psCurr->pos.x - psObj->pos.x; const int ydiff = psCurr->pos.y - psObj->pos.y; const unsigned int distSq = xdiff * xdiff + ydiff * ydiff; if (distSq < radSquared && psCurr->visible[psObj->player] && distSq < tarDist) { psTemp = psCurr; tarDist = distSq; } } } psCurr = gridIterate(); } if (psTemp) { ASSERT(!psTemp->died, "aiChooseSensorTarget gave us a dead target"); *ppsTarget = psTemp; return true; } } return false; }
// Find the best nearest target for a droid. // If extraRange is higher than zero, then this is the range it accepts for movement to target. // Returns integer representing target priority, -1 if failed int aiBestNearestTarget(DROID *psDroid, BASE_OBJECT **ppsObj, int weapon_slot, int extraRange) { SDWORD bestMod = 0, newMod, failure = -1; BASE_OBJECT *psTarget = NULL, *bestTarget = NULL, *tempTarget; bool electronic = false; STRUCTURE *targetStructure; WEAPON_EFFECT weaponEffect; TARGET_ORIGIN tmpOrigin = ORIGIN_UNKNOWN; //don't bother looking if empty vtol droid if (vtolEmpty(psDroid)) { return failure; } /* Return if have no weapons */ // The ai orders a non-combat droid to patrol = crash without it... if ((psDroid->asWeaps[0].nStat == 0 || psDroid->numWeaps == 0) && psDroid->droidType != DROID_SENSOR) { return failure; } // Check if we have a CB target to begin with if (!proj_Direct(asWeaponStats + psDroid->asWeaps[weapon_slot].nStat)) { WEAPON_STATS *psWStats = psDroid->asWeaps[weapon_slot].nStat + asWeaponStats; bestTarget = aiSearchSensorTargets((BASE_OBJECT *)psDroid, weapon_slot, psWStats, &tmpOrigin); bestMod = targetAttackWeight(bestTarget, (BASE_OBJECT *)psDroid, weapon_slot); } weaponEffect = (asWeaponStats + psDroid->asWeaps[weapon_slot].nStat)->weaponEffect; electronic = electronicDroid(psDroid); // Range was previously 9*TILE_UNITS. Increasing this doesn't seem to help much, though. Not sure why. int droidRange = std::min(aiDroidRange(psDroid, weapon_slot) + extraRange, objSensorRange(psDroid) + 6 * TILE_UNITS); static GridList gridList; // static to avoid allocations. gridList = gridStartIterate(psDroid->pos.x, psDroid->pos.y, droidRange); for (GridIterator gi = gridList.begin(); gi != gridList.end(); ++gi) { BASE_OBJECT *friendlyObj = NULL; BASE_OBJECT *targetInQuestion = *gi; /* This is a friendly unit, check if we can reuse its target */ if (aiCheckAlliances(targetInQuestion->player, psDroid->player)) { friendlyObj = targetInQuestion; targetInQuestion = NULL; /* Can we see what it is doing? */ if (friendlyObj->visible[psDroid->player] == UBYTE_MAX) { if (friendlyObj->type == OBJ_DROID) { DROID *friendlyDroid = (DROID *)friendlyObj; /* See if friendly droid has a target */ tempTarget = friendlyDroid->psActionTarget[0]; if (tempTarget && !tempTarget->died) { //make sure a weapon droid is targeting it if (friendlyDroid->numWeaps > 0) { // make sure this target wasn't assigned explicitly to this droid if (friendlyDroid->order.type != DORDER_ATTACK) { targetInQuestion = tempTarget; //consider this target } } } } else if (friendlyObj->type == OBJ_STRUCTURE) { tempTarget = ((STRUCTURE *)friendlyObj)->psTarget[0]; if (tempTarget && !tempTarget->died) { targetInQuestion = tempTarget; } } } } if (targetInQuestion != NULL && targetInQuestion != psDroid // in case friendly unit had me as target && (targetInQuestion->type == OBJ_DROID || targetInQuestion->type == OBJ_STRUCTURE || targetInQuestion->type == OBJ_FEATURE) && targetInQuestion->visible[psDroid->player] == UBYTE_MAX && !aiCheckAlliances(targetInQuestion->player, psDroid->player) && validTarget(psDroid, targetInQuestion, weapon_slot) && objPosDiffSq(psDroid, targetInQuestion) < droidRange * droidRange) { if (targetInQuestion->type == OBJ_DROID) { // in multiPlayer - don't attack Transporters with EW if (bMultiPlayer) { // if not electronic then valid target if (!electronic || (electronic && !isTransporter((DROID *)targetInQuestion))) { //only a valid target if NOT a transporter psTarget = targetInQuestion; } } else { psTarget = targetInQuestion; } } else if (targetInQuestion->type == OBJ_STRUCTURE) { STRUCTURE *psStruct = (STRUCTURE *)targetInQuestion; if (electronic) { /* don't want to target structures with resistance of zero if using electronic warfare */ if (validStructResistance((STRUCTURE *)targetInQuestion)) { psTarget = targetInQuestion; } } else if (psStruct->asWeaps[0].nStat > 0) { // structure with weapons - go for this psTarget = targetInQuestion; } else if ((psStruct->pStructureType->type != REF_WALL && psStruct->pStructureType->type != REF_WALLCORNER) || driveModeActive() || (bMultiPlayer && !isHumanPlayer(psDroid->player))) { psTarget = targetInQuestion; } } else if (targetInQuestion->type == OBJ_FEATURE && psDroid->lastFrustratedTime > 0 && gameTime - psDroid->lastFrustratedTime < FRUSTRATED_TIME && ((FEATURE *)targetInQuestion)->psStats->damageable && psDroid->player != scavengerPlayer()) // hack to avoid scavs blowing up their nice feature walls { psTarget = targetInQuestion; objTrace(psDroid->id, "considering shooting at %s in frustration", objInfo(targetInQuestion)); } /* Check if our weapon is most effective against this object */ if (psTarget != NULL && psTarget == targetInQuestion) //was assigned? { newMod = targetAttackWeight(psTarget, (BASE_OBJECT *)psDroid, weapon_slot); /* Remember this one if it's our best target so far */ if (newMod >= 0 && (newMod > bestMod || bestTarget == NULL)) { bestMod = newMod; tmpOrigin = ORIGIN_ALLY; bestTarget = psTarget; } } } } if (bestTarget) { ASSERT(!bestTarget->died, "AI gave us a target that is already dead."); targetStructure = visGetBlockingWall((BASE_OBJECT *)psDroid, bestTarget); /* See if target is blocked by a wall; only affects direct weapons */ if (proj_Direct(asWeaponStats + psDroid->asWeaps[weapon_slot].nStat) && targetStructure) { //are we any good against walls? if (asStructStrengthModifier[weaponEffect][targetStructure->pStructureType->strength] >= 100) //can attack atleast with default strength { bestTarget = (BASE_OBJECT *)targetStructure; //attack wall } } *ppsObj = bestTarget; return bestMod; } return failure; }
// Find a route for an DROID to a location in world coordinates FPATH_RETVAL fpathDroidRoute(DROID* psDroid, SDWORD tX, SDWORD tY, FPATH_MOVETYPE moveType) { bool acceptNearest; PROPULSION_STATS *psPropStats = getPropulsionStats(psDroid); // override for AI to blast our way through stuff if (!isHumanPlayer(psDroid->player) && moveType == FMT_MOVE) { moveType = (psDroid->asWeaps[0].nStat == 0) ? FMT_MOVE : FMT_ATTACK; } ASSERT_OR_RETURN(FPR_FAILED, psPropStats != NULL, "invalid propulsion stats pointer"); ASSERT_OR_RETURN(FPR_FAILED, psDroid->type == OBJ_DROID, "We got passed an object that isn't a DROID!"); // check whether the end point of the route // is a blocking tile and find an alternative if it is if (psDroid->sMove.Status != MOVEWAITROUTE && fpathDroidBlockingTile(psDroid, map_coord(tX), map_coord(tY), moveType)) { // find the nearest non blocking tile to the DROID int minDist = SDWORD_MAX; int nearestDir = NUM_DIR; int dir; objTrace(psDroid->id, "BLOCKED(%d,%d) - trying workaround", map_coord(tX), map_coord(tY)); for (dir = 0; dir < NUM_DIR; dir++) { int x = map_coord(tX) + aDirOffset[dir].x; int y = map_coord(tY) + aDirOffset[dir].y; if (tileOnMap(x, y) && !fpathDroidBlockingTile(psDroid, x, y, moveType)) { // pick the adjacent tile closest to our starting point int tileDist = fpathDistToTile(x, y, psDroid->pos.x, psDroid->pos.y); if (tileDist < minDist) { minDist = tileDist; nearestDir = dir; } } if (dir == NUM_BASIC - 1 && nearestDir != NUM_DIR) { break; // found a solution without checking at greater distance } } if (nearestDir == NUM_DIR) { // surrounded by blocking tiles, give up objTrace(psDroid->id, "route to (%d, %d) failed - target blocked", map_coord(tX), map_coord(tY)); return FPR_FAILED; } else { tX = world_coord(map_coord(tX) + aDirOffset[nearestDir].x) + TILE_UNITS / 2; tY = world_coord(map_coord(tY) + aDirOffset[nearestDir].y) + TILE_UNITS / 2; objTrace(psDroid->id, "Workaround found at (%d, %d)", map_coord(tX), map_coord(tY)); } } switch (psDroid->order) { case DORDER_BUILD: case DORDER_HELPBUILD: // help to build a structure case DORDER_LINEBUILD: // 6 - build a number of structures in a row (walls + bridges) case DORDER_DEMOLISH: // demolish a structure case DORDER_REPAIR: acceptNearest = false; break; default: acceptNearest = true; break; } return fpathRoute(&psDroid->sMove, psDroid->id, psDroid->pos.x, psDroid->pos.y, tX, tY, psPropStats->propulsionType, psDroid->droidType, moveType, psDroid->player, acceptNearest); }
static FPATH_RETVAL fpathRoute(MOVE_CONTROL *psMove, int id, int startX, int startY, int tX, int tY, PROPULSION_TYPE propulsionType, DROID_TYPE droidType, FPATH_MOVETYPE moveType, int owner, bool acceptNearest) { objTrace(id, "called(*,id=%d,sx=%d,sy=%d,ex=%d,ey=%d,prop=%d,type=%d,move=%d,owner=%d)", id, startX, startY, tX, tY, (int)propulsionType, (int)droidType, (int)moveType, owner); // don't have to do anything if already there if (startX == tX && startY == tY) { // return failed to stop them moving anywhere objTrace(id, "Tried to move nowhere"); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = FPR_FAILED", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner); return FPR_FAILED; } // Check if waiting for a result while (psMove->Status == MOVEWAITROUTE) { objTrace(id, "Checking if we have a path yet"); wzMutexLock(fpathMutex); // psNext should be _declared_ here, after the mutex lock! Used to be a race condition, thanks to -Wdeclaration-after-statement style pseudocompiler compatibility. for (std::list<PATHRESULT>::iterator psResult = pathResults.begin(); psResult != pathResults.end(); ++psResult) { if (psResult->droidID != id) { continue; // Wrong result, try next one. } ASSERT(psResult->retval != FPR_OK || psResult->sMove.asPath, "Ok result but no path in list"); // Copy over select fields - preserve others psMove->destination = psResult->sMove.destination; psMove->numPoints = psResult->sMove.numPoints; psMove->Position = 0; psMove->Status = MOVENAVIGATE; free(psMove->asPath); psMove->asPath = psResult->sMove.asPath; FPATH_RETVAL retval = psResult->retval; ASSERT(retval != FPR_OK || psMove->asPath, "Ok result but no path after copy"); ASSERT(retval != FPR_OK || psMove->numPoints > 0, "Ok result but path empty after copy"); // Remove it from the result list pathResults.erase(psResult); wzMutexUnlock(fpathMutex); objTrace(id, "Got a path to (%d, %d)! Length=%d Retval=%d", psMove->destination.x, psMove->destination.y, psMove->numPoints, (int)retval); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = %d, path[%d] = %08X->(%d, %d)", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner, retval, psMove->numPoints, ~crcSumVector2i(0, psMove->asPath, psMove->numPoints), psMove->destination.x, psMove->destination.y); return retval; } objTrace(id, "No path yet. Waiting."); waitingForResult = true; waitingForResultId = id; wzMutexUnlock(fpathMutex); wzSemaphoreWait(waitingForResultSemaphore); // keep waiting } // We were not waiting for a result, and found no trivial path, so create new job and start waiting PATHJOB job; job.origX = startX; job.origY = startY; job.droidID = id; job.destX = tX; job.destY = tY; job.droidType = droidType; job.propulsion = propulsionType; job.moveType = moveType; job.owner = owner; job.acceptNearest = acceptNearest; job.deleted = false; fpathSetBlockingMap(&job); // Clear any results or jobs waiting already. It is a vital assumption that there is only one // job or result for each droid in the system at any time. fpathRemoveDroidData(id); wzMutexLock(fpathMutex); // Add to end of list bool isFirstJob = pathJobs.empty(); pathJobs.push_back(job); if (isFirstJob) { wzSemaphorePost(fpathSemaphore); // Wake up processing thread. } wzMutexUnlock(fpathMutex); objTrace(id, "Queued up a path-finding request to (%d, %d), at least %d items earlier in queue", tX, tY, isFirstJob); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = FPR_WAIT", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner); return FPR_WAIT; // wait while polling result queue }