1from logging import debug, error, warning
2
3from EnumsAI import ShipRoleType, MissionType
4import EspionageAI
5import FleetUtilsAI
6import freeOrionAIInterface as fo  # pylint: disable=import-error
7from aistate_interface import get_aistate
8import MilitaryAI
9import MoveUtilsAI
10import CombatRatingsAI
11from target import TargetFleet, TargetSystem, TargetPlanet
12
13
14def trooper_move_reqs_met(main_fleet_mission, order, verbose):
15    """
16    Indicates whether or not move requirements specific to invasion troopers are met for the provided mission and order.
17    :type main_fleet_mission: AIFleetMission.AIFleetMission
18    :type order: OrderMove
19    :param verbose: whether to print verbose decision details
20    :type verbose: bool
21    :rtype: bool
22    """
23    # Don't advance outside of our fleet-supply zone unless the target either has no shields at all or there
24    # is already a military fleet assigned to secure the target, and don't take final jump unless the planet is
25    # (to the AI's knowledge) down to zero shields.  Additional checks will also be done by the later
26    # generic movement code
27    invasion_target = main_fleet_mission.target
28    invasion_planet = invasion_target.get_object()
29    invasion_system = invasion_target.get_system()
30    supplied_systems = fo.getEmpire().fleetSupplyableSystemIDs
31    # if about to leave supply lines
32    if order.target.id not in supplied_systems or fo.getUniverse().jumpDistance(order.fleet.id, invasion_system.id) < 5:
33        if invasion_planet.currentMeterValue(fo.meterType.maxShield):
34            military_support_fleets = MilitaryAI.get_military_fleets_with_target_system(invasion_system.id)
35            if not military_support_fleets:
36                if verbose:
37                    debug("trooper_move_reqs_met() holding Invasion fleet %d before leaving supply "
38                          "because target (%s) has nonzero max shields and there is not yet a military fleet "
39                          "assigned to secure the target system." % (order.fleet.id, invasion_planet))
40                return False
41
42            # if there is a threat in the enemy system, do give military ships at least 1 turn to clear it
43            delay_to_move_troops = 1 if MilitaryAI.get_system_local_threat(order.target.id) else 0
44
45            def eta(fleet_id):
46                return FleetUtilsAI.calculate_estimated_time_of_arrival(fleet_id, invasion_system.id)
47
48            eta_this_fleet = eta(order.fleet.id)
49            if all(((eta_this_fleet - delay_to_move_troops) <= eta(fid) and eta(fid))
50                   for fid in military_support_fleets):
51                if verbose:
52                    debug("trooper_move_reqs_met() holding Invasion fleet %d before leaving supply "
53                          "because target (%s) has nonzero max shields and no assigned military fleet would arrive"
54                          "at least %d turn earlier than the invasion fleet" % (order.fleet.id, invasion_planet,
55                                                                                delay_to_move_troops))
56                return False
57
58        if verbose:
59            debug("trooper_move_reqs_met() allowing Invasion fleet %d to leave supply "
60                  "because target (%s) has zero max shields or there is a military fleet assigned to secure "
61                  "the target system which will arrive at least 1 turn before the invasion fleet.",
62                  order.fleet.id, invasion_planet)
63    return True
64
65
66class AIFleetOrder:
67    """Stores information about orders which can be executed."""
68    TARGET_TYPE = None
69    ORDER_NAME = ''
70    fleet = None  # type: target.TargetFleet
71    target = None  # type: target.Target
72
73    def __init__(self, fleet, target):
74        """
75        :param fleet: fleet to execute order
76        :type fleet: target.TargetFleet
77        :param target: fleet target, depends of order type
78        :type target: target.Target
79        """
80        if not isinstance(fleet, TargetFleet):
81            error("Order required fleet got %s" % type(fleet))
82
83        if not isinstance(target, self.TARGET_TYPE):
84            error("Target is not allowed, got %s expect %s" % (type(target), self.TARGET_TYPE))
85
86        self.fleet = fleet
87        self.target = target
88        self.executed = False
89        self.order_issued = False
90
91    def __setstate__(self, state):
92        # construct the universe objects from stored ids
93        state["fleet"] = TargetFleet(state["fleet"])
94        target_type = state.pop("target_type")
95        if state["target"] is not None:
96            assert self.TARGET_TYPE.object_name == target_type
97            state["target"] = self.TARGET_TYPE(state["target"])
98        self.__dict__ = state
99
100    def __getstate__(self):
101        retval = dict(self.__dict__)
102        # do not store the universe object but only the fleet id
103        retval['fleet'] = self.fleet.id
104        if self.target is not None:
105            retval["target"] = self.target.id
106            retval["target_type"] = self.target.object_name
107        else:
108            retval["target_type"] = None
109        return retval
110
111    def is_valid(self):
112        """Check if FleetOrder could be somehow in future issued = is valid."""
113        if self.executed and self.order_issued:
114            debug("\t\t order not valid because already executed and completed")
115            return False
116        if self.fleet and self.target:
117            return True
118        else:
119            debug("\t\t order not valid: fleet validity: %s and target validity %s" % (
120                bool(self.fleet), bool(self.target)))
121            return False
122
123    def can_issue_order(self, verbose=False):
124        """If FleetOrder can be issued now."""
125        # for some orders, may need to re-issue if invasion/outposting/colonization was interrupted
126        if self.executed and not isinstance(self, (OrderOutpost, OrderColonize, OrderInvade)):
127            return False
128        if not self.is_valid():
129            return False
130
131        if verbose:
132            sys1 = self.fleet.get_system()
133            main_fleet_mission = get_aistate().get_fleet_mission(self.fleet.id)
134            debug("  Can issue %s - Mission Type %s (%s), current loc sys %d - %s" % (
135                self, main_fleet_mission.type,
136                main_fleet_mission.type, self.fleet.id, sys1))
137        return True
138
139    def issue_order(self):
140        if not self.can_issue_order():  # appears to be redundant with check in IAFleetMission?
141            debug("  can't issue %s" % self)
142            return False
143        # by default we now set the order as issue and executed.  For any subclass where order issuence and execution
144        # is not necessarily sure, these values can be reset after any appropriate checks in the respective
145        # subclass issue_order()
146        self.order_issued = True
147        self.executed = True
148        return True
149
150    def __str__(self):
151        execute_status = 'in progress'
152        if self.executed:
153            execute_status = 'executed'
154        elif self.order_issued:
155            execute_status = 'order issued'
156        return "[%s] of %s to %s %s" % (self.ORDER_NAME, self.fleet.get_object(),
157                                        self.target.get_object(), execute_status)
158
159    def __eq__(self, other):
160        return type(self) == type(other) and self.fleet == other.fleet and self.target == other.target
161
162    def __hash__(self):
163        return hash(self.fleet)
164
165
166class OrderMove(AIFleetOrder):
167    ORDER_NAME = 'move'
168    TARGET_TYPE = TargetSystem
169
170    def can_issue_order(self, verbose=False):
171        if not super(OrderMove, self).can_issue_order(verbose=verbose):
172            return False
173        # TODO: figure out better way to have invasions (& possibly colonizations)
174        #       require visibility on target without needing visibility of all intermediate systems
175        # if False and main_mission_type not in [MissionType.ATTACK,  # TODO: consider this later
176        #                                        MissionType.MILITARY,
177        #                                        MissionType.SECURE,
178        #                                        MissionType.HIT_AND_RUN,
179        #                                        MissionType.EXPLORATION]:
180        #     if not universe.getVisibility(target_id, get_aistate().empireID) >= fo.visibility.partial:
181        #         #if not target_id in interior systems
182        #         get_aistate().needsEmergencyExploration.append(fleet.systemID)
183        #         return False
184        system_id = self.fleet.get_system().id
185        if system_id == self.target.get_system().id:
186            return True  # TODO: already there, but could consider retreating
187
188        aistate = get_aistate()
189        main_fleet_mission = aistate.get_fleet_mission(self.fleet.id)
190
191        # TODO: Rate against specific enemies here
192        fleet_rating = CombatRatingsAI.get_fleet_rating(self.fleet.id)
193        fleet_rating_vs_planets = CombatRatingsAI.get_fleet_rating_against_planets(self.fleet.id)
194        target_sys_status = aistate.systemStatus.get(self.target.id, {})
195        f_threat = target_sys_status.get('fleetThreat', 0)
196        m_threat = target_sys_status.get('monsterThreat', 0)
197        p_threat = target_sys_status.get('planetThreat', 0)
198        threat = f_threat + m_threat + p_threat
199        safety_factor = aistate.character.military_safety_factor()
200        universe = fo.getUniverse()
201        if main_fleet_mission.type == MissionType.INVASION and not trooper_move_reqs_met(main_fleet_mission,
202                                                                                         self, verbose):
203            return False
204        if fleet_rating >= safety_factor * threat and fleet_rating_vs_planets >= p_threat:
205            return True
206        elif not p_threat and self.target.id in fo.getEmpire().supplyUnobstructedSystems:
207            return True
208        else:
209            sys1 = universe.getSystem(system_id)
210            sys1_name = sys1 and sys1.name or "unknown"
211            target_system = self.target.get_system()
212            target_system_name = (target_system and target_system.get_object().name) or "unknown"
213            # TODO: adjust calc for any departing fleets
214            my_other_fleet_rating = aistate.systemStatus.get(self.target.id, {}).get('myFleetRating', 0)
215            my_other_fleet_rating_vs_planets = aistate.systemStatus.get(self.target.id, {}).get(
216                'myFleetRatingVsPlanets', 0)
217            is_military = aistate.get_fleet_role(self.fleet.id) == MissionType.MILITARY
218
219            total_rating = CombatRatingsAI.combine_ratings(my_other_fleet_rating, fleet_rating)
220            total_rating_vs_planets = CombatRatingsAI.combine_ratings(my_other_fleet_rating_vs_planets,
221                                                                      fleet_rating_vs_planets)
222            if (my_other_fleet_rating > 3 * safety_factor * threat or
223                    (is_military and total_rating_vs_planets > 2.5*p_threat and total_rating > safety_factor * threat)):
224                debug(("\tAdvancing fleet %d (rating %d) at system %d (%s) into system %d (%s) with threat %d"
225                       " because of sufficient empire fleet strength already at destination" %
226                      (self.fleet.id, fleet_rating, system_id, sys1_name, self.target.id, target_system_name, threat)))
227                return True
228            elif (threat == p_threat and
229                  not self.fleet.get_object().aggressive and
230                  not my_other_fleet_rating and
231                  not target_sys_status.get('localEnemyFleetIDs', [-1])):
232                if verbose:
233                    debug("\tAdvancing fleet %d (rating %d) at system %d (%s) "
234                          "into system %d (%s) with planet threat %d because non aggressive"
235                          " and no other fleets present to trigger combat" % (
236                              self.fleet.id, fleet_rating, system_id, sys1_name, self.target.id, target_system_name,
237                              threat))
238                return True
239            else:
240                if verbose:
241                    _info = (self.fleet.id, fleet_rating, system_id, sys1_name,
242                             self.target.id, target_system_name, threat)
243                    debug("\tHolding fleet %d (rating %d) at system %d (%s) "
244                          "before travelling to system %d (%s) with threat %d" % _info)
245                needs_vis = aistate.misc.setdefault('needs_vis', [])
246                if self.target.id not in needs_vis:
247                    needs_vis.append(self.target.id)
248                return False
249
250    def issue_order(self):
251        if not super(OrderMove, self).issue_order():
252            return False
253        fleet_id = self.fleet.id
254        system_id = self.target.get_system().id
255        fleet = self.fleet.get_object()
256        if system_id not in [fleet.systemID, fleet.nextSystemID]:
257            dest_id = system_id
258            fo.issueFleetMoveOrder(fleet_id, dest_id)
259            debug("Order issued: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
260        aistate = get_aistate()
261        if system_id == fleet.systemID:
262            if aistate.get_fleet_role(fleet_id) == MissionType.EXPLORATION:
263                if system_id in aistate.needsEmergencyExploration:
264                    aistate.needsEmergencyExploration.remove(system_id)
265        return True
266
267
268class OrderPause(AIFleetOrder):
269    """Ensure Fleet at least temporarily halts movement at the target system."""
270    ORDER_NAME = 'pause'
271    TARGET_TYPE = TargetSystem
272
273    def is_valid(self):
274        if not super(OrderPause, self).is_valid():
275            return False
276        return bool(self.target.get_system().get_object())
277
278    def issue_order(self):
279        if not super(OrderPause, self).issue_order():
280            return False
281        # not executed until actually arrives at target system
282        self.executed = self.fleet.get_current_system_id() == self.target.get_system().id
283
284
285class OrderResupply(AIFleetOrder):
286    ORDER_NAME = 'resupply'
287    TARGET_TYPE = TargetSystem
288
289    def is_valid(self):
290        if not super(OrderResupply, self).is_valid():
291            return False
292        return self.target.id in fo.getEmpire().fleetSupplyableSystemIDs
293
294    def issue_order(self):
295        if not super(OrderResupply, self).issue_order():
296            return False
297        fleet_id = self.fleet.id
298        system_id = self.target.get_system().id
299        fleet = self.fleet.get_object()
300        aistate = get_aistate()
301        if system_id == fleet.systemID:
302            if aistate.get_fleet_role(fleet_id) == MissionType.EXPLORATION:
303                if system_id in aistate.needsEmergencyExploration:
304                    aistate.needsEmergencyExploration.remove(system_id)
305            return True
306        if system_id != fleet.nextSystemID:
307            self.executed = False
308            start_id = FleetUtilsAI.get_fleet_system(fleet)
309            dest_id = MoveUtilsAI.get_safe_path_leg_to_dest(fleet_id, start_id, system_id)
310            universe = fo.getUniverse()
311            debug("fleet %d with order type(%s) sent to safe leg dest %s and ultimate dest %s" % (
312                fleet_id, self.ORDER_NAME, universe.getSystem(dest_id), universe.getSystem(system_id)))
313            fo.issueFleetMoveOrder(fleet_id, dest_id)
314            debug("Order issued: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
315        return True
316
317
318class OrderOutpost(AIFleetOrder):
319    ORDER_NAME = 'outpost'
320    TARGET_TYPE = TargetPlanet
321
322    def is_valid(self):
323        if not super(OrderOutpost, self).is_valid():
324            return False
325        planet = self.target.get_object()
326        if not planet.unowned:
327            # terminate early
328            self.executed = True
329            self.order_issued = True
330            return False
331        else:
332            return self.fleet.get_object().hasOutpostShips
333
334    def can_issue_order(self, verbose=False):
335        # TODO: check for separate fleet holding outpost ships
336        if not super(OrderOutpost, self).can_issue_order(verbose=verbose):
337            return False
338        universe = fo.getUniverse()
339        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.CIVILIAN_OUTPOST)
340        if ship_id is None:
341            ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.BASE_OUTPOST)
342        ship = universe.getShip(ship_id)
343        return ship is not None and self.fleet.get_object().systemID == self.target.get_system().id and ship.canColonize
344
345    def issue_order(self):
346        if not super(OrderOutpost, self).issue_order():
347            return False
348        # we can't know yet if the order will actually execute; instead, rely on the fact that if the order does get
349        # executed, then next turn it will be invalid
350        self.executed = False
351        planet = self.target.get_object()
352        if not planet.unowned:
353            return False
354        fleet_id = self.fleet.id
355        ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.CIVILIAN_OUTPOST)
356        if ship_id is None:
357            ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.BASE_OUTPOST)
358        if fo.issueColonizeOrder(ship_id, self.target.id):
359            debug("Order issued: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
360            return True
361        else:
362            self.order_issued = False
363            warning("Order issuance failed: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
364            return False
365
366
367class OrderColonize(AIFleetOrder):
368    ORDER_NAME = 'colonize'
369    TARGET_TYPE = TargetPlanet
370
371    def issue_order(self):
372        if not super(OrderColonize, self).issue_order():
373            return False
374        # we can't know yet if the order will actually execute; instead, rely on the fact that if the order does get
375        # executed, then next turn it will be invalid
376        self.executed = False
377
378        fleet_id = self.fleet.id
379        ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.CIVILIAN_COLONISATION)
380        if ship_id is None:
381            ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.BASE_COLONISATION)
382
383        if fo.issueColonizeOrder(ship_id, self.target.id):
384            debug("Order issued: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
385            return True
386        else:
387            self.order_issued = False
388            warning("Order issuance failed: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
389            return False
390
391    def is_valid(self):
392        if not super(OrderColonize, self).is_valid():
393            return False
394        planet = self.target.get_object()
395        if (planet.unowned or planet.ownedBy(fo.empireID())) and not planet.currentMeterValue(fo.meterType.population):
396            return self.fleet.get_object().hasColonyShips
397        # Otherwise, terminate early
398        self.executed = True
399        self.order_issued = True
400        return False
401
402    def can_issue_order(self, verbose=False):
403        if not super(OrderColonize, self).is_valid():
404            return False
405        # TODO: check for separate fleet holding colony ships
406        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.CIVILIAN_COLONISATION)
407        if ship_id is None:
408            ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.BASE_COLONISATION)
409        universe = fo.getUniverse()
410        ship = universe.getShip(ship_id)
411        if ship and not ship.canColonize:
412            warning("colonization fleet %d has no colony ship" % self.fleet.id)
413        return ship is not None and self.fleet.get_object().systemID == self.target.get_system().id and ship.canColonize
414
415
416class OrderDefend(AIFleetOrder):
417    """
418        Used for orbital defense, have no real orders.
419    """
420    ORDER_NAME = 'defend'
421    TARGET_TYPE = TargetSystem
422
423
424class OrderInvade(AIFleetOrder):
425    ORDER_NAME = 'invade'
426    TARGET_TYPE = TargetPlanet
427
428    def is_valid(self):
429        if not super(OrderInvade, self).is_valid():
430            return False
431        planet = self.target.get_object()
432        planet_population = planet.currentMeterValue(fo.meterType.population)
433        if planet.unowned and not planet_population:
434            debug("\t\t invasion order not valid due to target planet status-- owned: %s and population %.1f" % (
435                not planet.unowned, planet_population))
436            # terminate early
437            self.executed = True
438            self.order_issued = True
439            return False
440        else:
441            return self.fleet.get_object().hasTroopShips
442
443    def can_issue_order(self, verbose=False):
444        if not super(OrderInvade, self).is_valid():
445            return False
446        # TODO: check for separate fleet holding invasion ships
447        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.MILITARY_INVASION, False)
448        if ship_id is None:
449            ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.BASE_INVASION)
450        universe = fo.getUniverse()
451        ship = universe.getShip(ship_id)
452        planet = self.target.get_object()
453        return all((
454            ship is not None,
455            self.fleet.get_object().systemID == planet.systemID,
456            ship.canInvade,
457            not planet.initialMeterValue(fo.meterType.shield)
458        ))
459
460    def issue_order(self):
461        if not super(OrderInvade, self).can_issue_order():
462            return False
463
464        universe = fo.getUniverse()
465        planet_id = self.target.id
466        planet = self.target.get_object()
467        fleet = self.fleet.get_object()
468
469        invasion_roles = (ShipRoleType.BASE_INVASION,
470                          ShipRoleType.MILITARY_INVASION)
471
472        debug("Issuing order: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
473        # will track if at least one invasion troops successfully deployed
474        result = True
475        aistate = get_aistate()
476        overkill_margin = 2  # TODO: get from character module; allows a handful of extra troops to be immediately
477        #                            defending planet
478        # invasion orders processed before regen takes place, so use initialMeterValue() here
479        troops_wanted = planet.initialMeterValue(fo.meterType.troops) + overkill_margin
480        troops_already_assigned = 0  # TODO: get from other fleets in same system
481        troops_assigned = 0
482        # Todo: evaluate all local troop ships (including other fleets) before using any, make sure base invasion troops
483        #       are used first, and that not too many altogether are used (choosing an optimal collection to use).
484        for invasion_role in invasion_roles:  # first checks base troops, then regular
485            if not result:
486                break
487            for ship_id in fleet.shipIDs:
488                if troops_already_assigned + troops_assigned >= troops_wanted:
489                    break
490                ship = universe.getShip(ship_id)
491                if aistate.get_ship_role(ship.design.id) != invasion_role:
492                    continue
493
494                debug("Ordering troop ship %d to invade %s" % (ship_id, planet))
495                result = fo.issueInvadeOrder(ship_id, planet_id) and result
496                if not result:
497                    shields = planet.currentMeterValue(fo.meterType.shield)
498                    planet_stealth = planet.currentMeterValue(fo.meterType.stealth)
499                    pop = planet.currentMeterValue(fo.meterType.population)
500                    warning("Invasion order failed!")
501                    debug(" -- planet has %.1f stealth, shields %.1f, %.1f population and "
502                          "is owned by empire %d" % (planet_stealth, shields, pop, planet.owner))
503                    if 'needsEmergencyExploration' not in dir(aistate):
504                        aistate.needsEmergencyExploration = []
505                    if fleet.systemID not in aistate.needsEmergencyExploration:
506                        aistate.needsEmergencyExploration.append(fleet.systemID)
507                        debug("Due to trouble invading, added system %d to Emergency Exploration List" % fleet.systemID)
508                    self.executed = False
509                    # debug(universe.getPlanet(planet_id).dump())  # TODO: fix fo.UniverseObject.dump()
510                    break
511                troops_assigned += ship.troopCapacity
512        # TODO: split off unused troop ships into new fleet and give new orders this cycle
513        if result:
514            debug("Successfully ordered %d troopers to invade %s" % (troops_assigned, planet))
515            return True
516        else:
517            return False
518
519
520class OrderMilitary(AIFleetOrder):
521    ORDER_NAME = 'military'
522    TARGET_TYPE = TargetSystem
523
524    def is_valid(self):
525        if not super(OrderMilitary, self).is_valid():
526            return False
527        fleet = self.fleet.get_object()
528        # TODO: consider bombardment-only fleets/orders
529        return fleet is not None and (fleet.hasArmedShips or fleet.hasFighterShips)
530
531    def can_issue_order(self, verbose=False):
532        # TODO: consider bombardment
533        # TODO: consider simmply looking at fleet characteristics, as is done for is_valid()
534        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.MILITARY)
535        universe = fo.getUniverse()
536        ship = universe.getShip(ship_id)
537        return (ship is not None and
538                self.fleet.get_object().systemID == self.target.id and
539                (ship.isArmed or ship.hasFighters))
540
541    def issue_order(self):
542        if not super(OrderMilitary, self).issue_order():
543            return False
544        target_sys_id = self.target.id
545        fleet = self.target.get_object()
546        system_status = get_aistate().systemStatus.get(target_sys_id, {})
547        total_threat = sum(system_status.get(threat, 0) for threat in ('fleetThreat', 'planetThreat', 'monsterThreat'))
548        combat_trigger = system_status.get('fleetThreat', 0) or system_status.get('monsterThreat', 0)
549        if not combat_trigger and system_status.get('planetThreat', 0):
550            universe = fo.getUniverse()
551            system = universe.getSystem(target_sys_id)
552            for planet_id in system.planetIDs:
553                planet = universe.getPlanet(planet_id)
554                if planet.ownedBy(fo.empireID()):  # TODO: also exclude at-peace planets
555                    continue
556                if planet.unowned and not EspionageAI.colony_detectable_by_empire(planet_id, empire=fo.empireID()):
557                    continue
558                if sum([planet.currentMeterValue(meter_type) for meter_type in
559                        [fo.meterType.defense, fo.meterType.shield, fo.meterType.construction]]):
560                    combat_trigger = True
561                    break
562        if not all((
563                    fleet,
564                    fleet.systemID == target_sys_id,
565                    system_status.get('currently_visible', False),
566                    not (total_threat and combat_trigger)
567        )):
568            self.executed = False
569        return True
570
571
572class OrderRepair(AIFleetOrder):
573    ORDER_NAME = 'repair'
574    TARGET_TYPE = TargetSystem
575
576    def is_valid(self):
577        if not super(OrderRepair, self).is_valid():
578            return False
579        return self.target.id in fo.getEmpire().fleetSupplyableSystemIDs  # TODO: check for drydock still there/owned
580
581    def issue_order(self):
582        if not super(OrderRepair, self).issue_order():
583            return False
584        fleet_id = self.fleet.id
585        system_id = self.target.get_system().id
586        fleet = self.fleet.get_object()  # type: fo.fleet
587        if system_id == fleet.systemID:
588            aistate = get_aistate()
589            if aistate.get_fleet_role(fleet_id) == MissionType.EXPLORATION:
590                if system_id in aistate.needsEmergencyExploration:
591                    aistate.needsEmergencyExploration.remove(system_id)
592        elif system_id != fleet.nextSystemID:
593            fo.issueAggressionOrder(fleet_id, False)
594            start_id = FleetUtilsAI.get_fleet_system(fleet)
595            dest_id = MoveUtilsAI.get_safe_path_leg_to_dest(fleet_id, start_id, system_id)
596            universe = fo.getUniverse()
597            debug("fleet %d with order type(%s) sent to safe leg dest %s and ultimate dest %s" % (
598                fleet_id, self.ORDER_NAME, universe.getSystem(dest_id), universe.getSystem(system_id)))
599            fo.issueFleetMoveOrder(fleet_id, dest_id)
600            debug("Order issued: %s fleet: %s target: %s" % (self.ORDER_NAME, self.fleet, self.target))
601        ships_cur_health, ships_max_health = FleetUtilsAI.get_current_and_max_structure(fleet_id)
602        self.executed = (ships_cur_health == ships_max_health)
603        return True
604