logistics.py (17281B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\behaviors\actions\logistics.py 2 from evemath import get_center_point 3 from behaviors.blackboards import BlackboardDeletedError 4 from behaviors.groups.mixin import GroupTaskMixin 5 from behaviors.tasks import Task 6 from behaviors.utility.ballparks import get_ball_position, is_ball_in_park, get_distance_between 7 from behaviors.utility.dogmatic import get_my_attribute_value, is_assistance_disallowed, get_range_falloff_multiplier_at_distance 8 from behaviors.utility.logistics import get_logistics_priority, get_remote_repair_effect_id, get_damage_state_function_by_effect_id 9 from behaviors.utility.math import compute_rear_fleet_position, compute_forward_fleet_position 10 from ccpProfile import TimedFunction 11 from collections import Counter 12 from dogma.const import attributeNPCAssistanceRange, effectBehaviorNPCRemoteArmorRepair 13 from dogma.const import attributeMaxTargetRange, attributeMaxLockedTargets 14 from dogma.effects.restricted.util import get_effect_max_effective_range_with_falloff 15 from eveexceptions import UserError 16 from inventorycommon.const import categoryEntity, categoryDrone, categoryShip 17 from inventorycommon import const as invConst, WrongInventoryLocation, FakeItemNotHere 18 import logging 19 from threadutils.be_nice import be_nice 20 import uthread2 21 logger = logging.getLogger(__name__) 22 LOGISTICS_CATEGORIES_TO_CONSIDER = [categoryEntity, categoryShip, categoryDrone] 23 LOGISTICS_GROUPS_TO_IGNORE = {invConst.groupSpawnContainer, 24 invConst.groupSpewContainer, 25 invConst.groupBillboard, 26 invConst.groupAgentsinSpace, 27 invConst.groupCapturePointTower, 28 invConst.groupControlBunker, 29 invConst.groupLargeCollidableShip, 30 invConst.groupLargeCollidableObject, 31 invConst.groupLargeCollidableStructure, 32 invConst.groupTemporaryCollidableStructure, 33 invConst.groupMissionContainer, 34 invConst.groupTemporaryCloud, 35 invConst.groupSentryGun, 36 invConst.groupIndustrialSupportFacility} 37 LOGISTICS_EFFECTS = (effectBehaviorNPCRemoteArmorRepair,) 38 39 class SelectDesiredAnchorPosition(Task, GroupTaskMixin): 40 41 @TimedFunction('behaviors::actions::logistics::SelectDesiredAnchorPosition::OnEnter') 42 def OnEnter(self): 43 friendlyPositionList, hostilePositionList = self.CollectFriendlyAndHostilePositions() 44 try: 45 if not hostilePositionList: 46 desiredPosition = get_center_point(friendlyPositionList) 47 else: 48 desiredPosition = self.ComputeDesiredPosition(friendlyPositionList, hostilePositionList) 49 self.SendBlackboardValue(self.attributes.desiredPositionAddress, tuple(desiredPosition)) 50 self.SetStatusToSuccess() 51 except ValueError: 52 self.SendBlackboardValue(self.attributes.desiredPositionAddress, None) 53 self.SetStatusToFailed() 54 55 @TimedFunction('behaviors::actions::logistics::SelectDesiredAnchorPosition::OnEnter::CollectFriendlyAndHostilePositions') 56 def CollectFriendlyAndHostilePositions(self): 57 ballIds = self.GetBallIdsToConsider() 58 friendlyPositionList = [] 59 hostilePositionList = [] 60 slims = self.context.ballpark.slims 61 balls = self.context.ballpark.balls 62 for itemId in ballIds: 63 self.ClassifyItemIdAndCollect(itemId, balls, slims, friendlyPositionList, hostilePositionList) 64 65 return (friendlyPositionList, hostilePositionList) 66 67 def ClassifyItemIdAndCollect(self, itemId, balls, slims, friendlyPositionList, hostilePositionList): 68 slimItem = slims.get(itemId) 69 if not slimItem: 70 return 71 if slimItem.categoryID not in LOGISTICS_CATEGORIES_TO_CONSIDER: 72 return 73 if slimItem.groupID in LOGISTICS_GROUPS_TO_IGNORE: 74 return 75 if slimItem.ownerID in self.attributes.friendlyOwners: 76 ball = balls[itemId] 77 friendlyPositionList.append((ball.x, ball.y, ball.z)) 78 if slimItem.ownerID in self.attributes.hostileOwners: 79 ball = balls[itemId] 80 hostilePositionList.append((ball.x, ball.y, ball.z)) 81 82 def GetBallIdsToConsider(self): 83 ballpark = self.context.ballpark 84 ballIds = [ ballId for ballId in ballpark.bubbles[ballpark.balls[self.context.myItemId].newBubbleId] if ballId > 0 ] 85 return ballIds 86 87 @TimedFunction('behaviors::actions::logistics::SelectDesiredAnchorPosition::OnEnter::ComputeDesiredPosition') 88 def ComputeDesiredPosition(self, friendlyPositionList, hostilePositionList): 89 friendlyCentroid = get_center_point(friendlyPositionList) 90 hostileCentroid = get_center_point(hostilePositionList) 91 logisticsRange = self.GetLogisticsRange() 92 desiredRearPosition = compute_rear_fleet_position(friendlyCentroid, hostileCentroid, logisticsRange * self.attributes.proximityFraction) 93 return desiredRearPosition 94 95 def GetLogisticsRange(self): 96 ranges = self.GetGroupLogisticsRanges() 97 if not ranges: 98 raise ValueError('no valid logistics range found') 99 return min(ranges) 100 101 @TimedFunction('behaviors::actions::logistics::SelectDesiredAnchorPosition::OnEnter::GetGroupLogisticsRanges') 102 def GetGroupLogisticsRanges(self): 103 ranges = [] 104 for memberId in self.GetMemberIdList(): 105 repairRange = self.GetRepairRange(memberId) 106 if repairRange > 0: 107 ranges.append(repairRange) 108 109 return ranges 110 111 def GetRepairRange(self, memberId): 112 return self.context.dogmaLM.GetAttributeValue(memberId, attributeNPCAssistanceRange) 113 114 115 class SelectGroupMemberBasedOnLowestAttributeValue(Task, GroupTaskMixin): 116 117 @TimedFunction('behaviors::actions::logistics::SelectGroupMemberBasedOnLowestAttributeValue::OnEnter') 118 def OnEnter(self): 119 memberList = self.GetLogisticsMembers() 120 if memberList: 121 self.SortMemberByAttributeValue(memberList) 122 self.SendBlackboardValue(self.attributes.selectedIdAddress, memberList[0]) 123 self.SetStatusToSuccess() 124 else: 125 self.SetStatusToFailed() 126 127 @TimedFunction('behaviors::actions::logistics::SelectGroupMemberBasedOnLowestAttributeValue::SortMemberByAttributeValue') 128 def SortMemberByAttributeValue(self, memberList): 129 GetAttributeValue = self.context.dogmaLM.GetAttributeValue 130 attributeId = self.attributes.attributeId 131 memberList.sort(key=lambda memberId: GetAttributeValue(memberId, attributeId)) 132 133 @TimedFunction('behaviors::actions::logistics::SelectGroupMemberBasedOnLowestAttributeValue::GetLogisticsMembers') 134 def GetLogisticsMembers(self): 135 return [ memberId for memberId in self.GetMemberIdList() if self.HasLogisticsEffects(memberId) ] 136 137 def HasLogisticsEffects(self, memberId): 138 slimItem = self.context.ballpark.slims.get(memberId) 139 if not slimItem: 140 return False 141 else: 142 return self.HasAnyLogisticsEffects(slimItem.typeID) 143 144 def HasAnyLogisticsEffects(self, typeID): 145 for effectId in LOGISTICS_EFFECTS: 146 if self.HasEffect(typeID, effectId): 147 return True 148 149 return False 150 151 def HasEffect(self, typeID, effectId): 152 return self.context.dogmaLM.dogmaStaticMgr.TypeHasEffect(typeID, effectId) 153 154 155 class SetRearFleetPosition(Task): 156 157 @TimedFunction('behaviors::actions::logistics::SetRearFleetPosition::OnEnter') 158 def OnEnter(self): 159 self.SetStatusToFailed() 160 hostilePosition = self.GetLastBlackboardValue(self.attributes.hostilePositionAddress) 161 if not hostilePosition: 162 return 163 anchorBallId = self.GetLastBlackboardValue(self.attributes.anchorBallIdAddress) 164 if not anchorBallId or not is_ball_in_park(self, anchorBallId): 165 return 166 friendlyPosition = get_ball_position(self, anchorBallId) 167 distance = self.GetLastBlackboardValue(self.attributes.distanceAddress) 168 rear_position = self.GetTargetPosition(friendlyPosition, hostilePosition, distance) 169 self.SendBlackboardValue(self.attributes.targetPositionAddress, rear_position) 170 self.SetStatusToSuccess() 171 172 def GetTargetPosition(self, friendlyPosition, hostilePosition, distance): 173 return compute_rear_fleet_position(friendlyPosition, hostilePosition, distance) 174 175 176 class SetForwardFleetPosition(SetRearFleetPosition): 177 178 def GetTargetPosition(self, friendlyPosition, hostilePosition, distance): 179 return compute_forward_fleet_position(friendlyPosition, hostilePosition, distance) 180 181 182 class PrioritizeLogisticsWatchlist(Task, GroupTaskMixin): 183 184 def __init__(self, attributes = None): 185 super(PrioritizeLogisticsWatchlist, self).__init__(attributes=attributes) 186 187 @TimedFunction('behaviors::actions::logistics::PrioritizeLogisticsWatchlist::OnEnter') 188 def OnEnter(self): 189 self.SetStatusToSuccess() 190 damage_state_function = self.get_damage_state_function() 191 if damage_state_function is None: 192 return 193 watchlist = self.GetLastBlackboardValue(self.attributes.watchlistAddress) 194 if watchlist: 195 prioritized_watchlist = self._GetPrioritizedWatchlist(watchlist) 196 else: 197 prioritized_watchlist = [] 198 self.SendBlackboardValue(self.attributes.prioritizedWatchlistAddress, prioritized_watchlist) 199 200 def get_repair_effect_id(self): 201 remote_repair_effect_by_type = self.GetLastBlackboardValue(self.attributes.remoteRepairEffectRegistryAddress) 202 if not remote_repair_effect_by_type: 203 return None 204 most_common = Counter(remote_repair_effect_by_type.values()).most_common(1)[0] 205 return most_common[0] 206 207 @TimedFunction('behaviors::actions::logistics::PrioritizeLogisticsWatchlist::_GetPrioritizedWatchlist') 208 def _GetPrioritizedWatchlist(self, watchlist): 209 repair_effect_id = self.get_repair_effect_id() 210 if repair_effect_id is None: 211 return [] 212 get_damage_state = get_damage_state_function_by_effect_id(repair_effect_id) 213 prioritized_watchlist = [] 214 for item_id in watchlist: 215 self.try_add_item_with_priority(item_id, prioritized_watchlist, get_damage_state) 216 217 prioritized_watchlist.sort(reverse=True) 218 return prioritized_watchlist 219 220 def try_add_item_with_priority(self, item_id, prioritized_watchlist, get_damage_state): 221 if not is_ball_in_park(self, item_id): 222 return 223 if is_assistance_disallowed(self, item_id): 224 return 225 try: 226 priority = get_logistics_priority(self, item_id, get_damage_state) 227 prioritized_watchlist.append([priority, item_id]) 228 except (WrongInventoryLocation, 229 FakeItemNotHere, 230 ZeroDivisionError, 231 BlackboardDeletedError) as e: 232 logger.debug('Behavior=%s for group=%s error while getting logistic priority: %s', self.behaviorTree.GetBehaviorId, self.context.myEntityGroupId, e) 233 234 def get_damage_state_function(self): 235 if not self.HasContextValue('get_damage_state'): 236 remote_repair_effect_id = self.get_repair_effect_id() 237 if remote_repair_effect_id is not None: 238 self.SetContextValue('get_damage_state', get_damage_state_function_by_effect_id(remote_repair_effect_id)) 239 return self.GetContextValue('get_damage_state') 240 241 242 class LockPrioritizedWatchlistMembers(Task): 243 244 def __init__(self, attributes = None): 245 super(LockPrioritizedWatchlistMembers, self).__init__(attributes=attributes) 246 self.is_updating_locks = False 247 248 def _initialize(self): 249 if not self.HasContextValue('remote_repair_effect_id'): 250 if self.HasAttribute('remoteRepairEffectIdAddress'): 251 remote_repair_effect_id = self.GetLastBlackboardValue(self.attributes.remoteRepairEffectIdAddress) 252 else: 253 remote_repair_effect_id = get_remote_repair_effect_id(self) 254 self.SetContextValue('remote_repair_effect_id', remote_repair_effect_id) 255 if self._has_combat_target(): 256 self.SetContextValue('reserved_combat_target_count', 1) 257 else: 258 self.SetContextValue('reserved_combat_target_count', 0) 259 260 @TimedFunction('behaviors::actions::logistics::LockPrioritizedWatchlistMembers::OnEnter') 261 def OnEnter(self): 262 self._initialize() 263 self.SetStatusToSuccess() 264 prioritized_watchlist = self.GetLastBlackboardValue(self.attributes.prioritizedWatchlistAddress) 265 if not prioritized_watchlist: 266 return 267 best_candidates_in_range = self._FilterItemsInTargetingRange(prioritized_watchlist) 268 self._UpdateLockedTargets(best_candidates_in_range) 269 270 def _get_effective_range(self): 271 return get_effect_max_effective_range_with_falloff(self.context.dogmaLM, self.GetContextValue('remote_repair_effect_id'), self.context.myItemId) 272 273 def get_max_targets(self): 274 max_total_targets = int(get_my_attribute_value(self, attributeMaxLockedTargets)) 275 available_logistic_target_count = max_total_targets - self.GetContextValue('reserved_combat_target_count') 276 return max(0, available_logistic_target_count) 277 278 def _get_current_combat_target(self): 279 if self._has_combat_target(): 280 return self.GetLastBlackboardValue(self.attributes.combatTargetAddress) 281 282 def _has_combat_target(self): 283 return self.HasAttribute('combatTargetAddress') 284 285 @TimedFunction('behaviors::actions::logistics::LockPrioritizedWatchlistMembers::_FilterItemsInTargetingRange') 286 def _FilterItemsInTargetingRange(self, prioritized_watchlist): 287 my_item_id = self.context.myItemId 288 target_range = get_my_attribute_value(self, attributeMaxTargetRange) 289 effective_range = self._get_effective_range() 290 useful_range = min(target_range, effective_range) 291 new_prioritized_watchlist = [] 292 for p, item_id in prioritized_watchlist: 293 if item_id == my_item_id: 294 continue 295 distance = get_distance_between(self, self.context.myItemId, item_id) 296 if distance < useful_range: 297 falloff_multiplier = get_range_falloff_multiplier_at_distance(self, self.GetContextValue('remote_repair_effect_id'), distance) 298 new_prioritized_watchlist.append((p * falloff_multiplier, item_id)) 299 300 new_prioritized_watchlist.sort(reverse=True) 301 max_targets = self.get_max_targets() 302 return [ item_id for _, item_id in new_prioritized_watchlist[:max_targets] ] 303 304 @TimedFunction('behaviors::actions::logistics::LockPrioritizedWatchlistMembers::_UpdateLockedTargets') 305 def _UpdateLockedTargets(self, items_to_target): 306 locked_targets = set(self.context.dogmaLM.GetTargets(self.context.myItemId)) 307 self._remove_combat_target(locked_targets) 308 targets_to_unlock = locked_targets.difference(items_to_target) 309 targets_to_lock = set(items_to_target).difference(locked_targets) 310 uthread2.start_tasklet(self._UnlockAndLockTargets_Thread, targets_to_unlock, targets_to_lock) 311 312 def _remove_combat_target(self, locked_targets): 313 combat_target = self._get_current_combat_target() 314 if combat_target: 315 locked_targets.discard(combat_target) 316 317 @TimedFunction('behaviors::actions::logistics::LockPrioritizedWatchlistMembers::_UnlockAndLockTargets_Thread') 318 def _UnlockAndLockTargets_Thread(self, targets_to_unlock, targets_to_lock): 319 if self.is_updating_locks: 320 return 321 try: 322 self.is_updating_locks = True 323 for item_id in targets_to_unlock: 324 self.context.dogmaLM.RemoveTarget(self.context.myItemId, item_id) 325 be_nice() 326 327 for item_id in targets_to_lock: 328 if not self.context.dogmaLM.HasPendingTarget(self.context.myItemId, item_id): 329 self._TryLockTarget(item_id) 330 be_nice() 331 332 finally: 333 self.is_updating_locks = False 334 335 def _TryLockTarget(self, item_id): 336 try: 337 self.context.dogmaLM.AddTargetEx(self.context.myItemId, item_id, tasklet=True) 338 except (UserError, RuntimeError) as e: 339 logger.debug('LockPrioritizedWatchlistMembers: %s targeting %s: Unable to acquire target lock: %s', self.context.myItemId, item_id, e) 340 except TaskletExit: 341 logger.debug('LockPrioritizedWatchlistMembers: %s targeting %s: The target locking thread got killed prematurely', self.context.myItemId, item_id) 342 except Exception as e: 343 logger.debug('LockPrioritizedWatchlistMembers: %s targeting %s: something else happened: %s', self.context.myItemId, item_id, e) 344 345 346 class RegisterRepairEffectForType(Task): 347 348 def OnEnter(self): 349 self.SetStatusToSuccess() 350 remote_repair_effect_registry = self.GetLastBlackboardValue(self.attributes.remoteRepairEffectRegistryAddress) or {} 351 type_id = self.context.mySlimItem.typeID 352 if type_id in remote_repair_effect_registry: 353 return 354 remote_repair_effect_id = get_remote_repair_effect_id(self) 355 remote_repair_effect_registry[type_id] = remote_repair_effect_id 356 self.SendBlackboardValue(self.attributes.remoteRepairEffectRegistryAddress, remote_repair_effect_registry)