File: fleet_orders.py

package info (click to toggle)
freeorion 0.5.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 194,920 kB
  • sloc: cpp: 186,821; python: 40,979; ansic: 1,164; xml: 721; makefile: 32; sh: 7
file content (648 lines) | stat: -rw-r--r-- 28,763 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
import freeOrionAIInterface as fo
from logging import debug, error, warning

import CombatRatingsAI
import EspionageAI
import ExplorationAI
import FleetUtilsAI
import MilitaryAI
import MoveUtilsAI
from aistate_interface import get_aistate
from EnumsAI import MissionType, ShipRoleType
from freeorion_tools import combine_ratings
from target import Target, TargetFleet, TargetPlanet, TargetSystem


class AIFleetOrder:
    """Stores information about orders which can be executed."""

    TARGET_TYPE = None
    ORDER_NAME = ""

    def __init__(self, fleet: TargetFleet, target: Target):
        """
        :param fleet: fleet to execute order
        :param target: fleet target, depends of order type
        """
        if not isinstance(fleet, TargetFleet):
            error("Order required fleet got %s" % type(fleet))

        if not isinstance(target, self.TARGET_TYPE):
            error(f"Target is not allowed, got {type(target)} expect {self.TARGET_TYPE}")

        self.fleet = fleet
        self.target = target
        self.executed = False
        self.order_issued = False

    def __setstate__(self, state):
        # construct the universe objects from stored ids
        state["fleet"] = TargetFleet(state["fleet"])
        target_type = state.pop("target_type")
        if state["target"] is not None:
            assert self.TARGET_TYPE.object_name == target_type
            state["target"] = self.TARGET_TYPE(state["target"])
        self.__dict__ = state

    def __getstate__(self):
        retval = dict(self.__dict__)
        # do not store the universe object but only the fleet id
        retval["fleet"] = self.fleet.id
        if self.target is not None:
            retval["target"] = self.target.id
            retval["target_type"] = self.target.object_name
        else:
            retval["target_type"] = None
        return retval

    def is_valid(self):
        """Check if FleetOrder could be somehow in future issued = is valid."""
        if self.executed and self.order_issued:
            debug("\t\t order not valid because already executed and completed")
            return False
        if self.fleet and self.target:
            return True
        else:
            debug(f"\t\t order not valid: fleet validity: {bool(self.fleet)} and target validity {bool(self.target)}")
            return False

    def can_issue_order(self, verbose=False):
        """If FleetOrder can be issued now."""
        # for some orders, may need to re-issue if invasion/outposting/colonization was interrupted
        if self.executed and not isinstance(self, (OrderOutpost, OrderColonize, OrderInvade)):
            return False
        if not self.is_valid():
            return False

        if verbose:
            sys1 = self.fleet.get_system()
            main_fleet_mission = get_aistate().get_fleet_mission(self.fleet.id)
            debug(
                "  Can issue %s - Mission Type %s (%s), current loc sys %d - %s"
                % (self, main_fleet_mission.type, main_fleet_mission.type, self.fleet.id, sys1)
            )
        return True

    def issue_order(self):
        if not self.can_issue_order():  # appears to be redundant with check in IAFleetMission?
            debug("  can't issue %s" % self)
            return False
        # by default we now set the order as issue and executed.  For any subclass where order issuence and execution
        # is not necessarily sure, these values can be reset after any appropriate checks in the respective
        # subclass issue_order()
        self.order_issued = True
        self.executed = True
        return True

    def __str__(self):
        execute_status = "in progress"
        if self.executed:
            execute_status = "executed"
        elif self.order_issued:
            execute_status = "order issued"
        return f"[{self.ORDER_NAME}] of {self.fleet.get_object()} to {self.target.get_object()} {execute_status}"

    def __eq__(self, other):
        return type(self) is type(other) and self.fleet == other.fleet and self.target == other.target

    def __hash__(self):
        return hash(self.fleet)


class OrderMove(AIFleetOrder):
    ORDER_NAME = "move"
    TARGET_TYPE = TargetSystem

    def can_issue_order(self, verbose=False):  # noqa: C901
        if not super().can_issue_order(verbose=verbose):
            return False
        # TODO: figure out better way to have invasions (& possibly colonizations)
        #       require visibility on target without needing visibility of all intermediate systems
        # if False and main_mission_type not in [MissionType.ATTACK,  # TODO: consider this later
        #                                        MissionType.MILITARY,
        #                                        MissionType.SECURE,
        #                                        MissionType.HIT_AND_RUN,
        #                                        MissionType.EXPLORATION]:
        #     if not universe.getVisibility(target_id, get_aistate().empireID) >= fo.visibility.partial:
        #         #if not target_id in interior systems
        #         get_aistate().needsEmergencyExploration.append(fleet.systemID)
        #         return False
        system_id = self.fleet.get_system().id
        if system_id == self.target.get_system().id:
            return True  # TODO: already there, but could consider retreating

        aistate = get_aistate()
        main_fleet_mission = aistate.get_fleet_mission(self.fleet.id)

        # TODO: Rate against specific enemies here
        fleet_rating = CombatRatingsAI.get_fleet_rating(self.fleet.id)
        fleet_rating_vs_planets = CombatRatingsAI.get_fleet_rating_against_planets(self.fleet.id)
        target_sys_status = aistate.systemStatus.get(self.target.id, {})
        f_threat = target_sys_status.get("fleetThreat", 0)
        m_threat = target_sys_status.get("monsterThreat", 0)
        p_threat = target_sys_status.get("planetThreat", 0)
        threat = f_threat + m_threat + p_threat
        safety_factor = aistate.character.military_safety_factor()
        universe = fo.getUniverse()
        if main_fleet_mission.type == MissionType.INVASION and not trooper_move_reqs_met(
            main_fleet_mission.target, self, verbose
        ):
            return False
        if fleet_rating >= safety_factor * threat and fleet_rating_vs_planets >= p_threat:
            return True
        elif not p_threat and self.target.id in fo.getEmpire().supplyUnobstructedSystems:
            return True
        else:
            sys1 = universe.getSystem(system_id)
            sys1_name = sys1 and sys1.name or "unknown"
            target_system = self.target.get_system()
            target_system_name = (target_system and target_system.get_object().name) or "unknown"
            # TODO: adjust calc for any departing fleets
            my_other_fleet_rating = aistate.systemStatus.get(self.target.id, {}).get("myFleetRating", 0)
            my_other_fleet_rating_vs_planets = aistate.systemStatus.get(self.target.id, {}).get(
                "myFleetRatingVsPlanets", 0
            )
            is_military = aistate.get_fleet_role(self.fleet.id) == MissionType.MILITARY

            total_rating = combine_ratings(my_other_fleet_rating, fleet_rating)
            total_rating_vs_planets = combine_ratings(my_other_fleet_rating_vs_planets, fleet_rating_vs_planets)
            if my_other_fleet_rating > 3 * safety_factor * threat or (
                is_military and total_rating_vs_planets > 2.5 * p_threat and total_rating > safety_factor * threat
            ):
                debug(
                    "\tAdvancing fleet %d (rating %d) at system %d (%s) into system %d (%s) with threat %d"
                    " because of sufficient empire fleet strength already at destination"
                    % (
                        self.fleet.id,
                        fleet_rating,
                        system_id,
                        sys1_name,
                        self.target.id,
                        target_system_name,
                        threat,
                    )
                )
                return True
            elif (
                threat == p_threat
                and not self.fleet.get_object().aggressive
                and not my_other_fleet_rating
                and not target_sys_status.get("localEnemyFleetIDs", [-1])
            ):
                if verbose:
                    debug(
                        "\tAdvancing fleet %d (rating %d) at system %d (%s) "
                        "into system %d (%s) with planet threat %d because non aggressive"
                        " and no other fleets present to trigger combat"
                        % (
                            self.fleet.id,
                            fleet_rating,
                            system_id,
                            sys1_name,
                            self.target.id,
                            target_system_name,
                            threat,
                        )
                    )
                return True
            else:
                if verbose:
                    _info = (
                        self.fleet.id,
                        fleet_rating,
                        system_id,
                        sys1_name,
                        self.target.id,
                        target_system_name,
                        threat,
                    )
                    debug(
                        "\tHolding fleet %d (rating %d) at system %d (%s) "
                        "before travelling to system %d (%s) with threat %d" % _info
                    )
                needs_vis = aistate.misc.setdefault("needs_vis", [])
                if self.target.id not in needs_vis:
                    needs_vis.append(self.target.id)
                return False

    def issue_order(self):
        if not super().issue_order():
            return False
        fleet_id = self.fleet.id
        system_id = self.target.get_system().id
        fleet = self.fleet.get_object()
        if system_id not in [fleet.systemID, fleet.nextSystemID]:
            dest_id = system_id
            fo.issueFleetMoveOrder(fleet_id, dest_id)
            debug(f"Order issued: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
        aistate = get_aistate()
        if system_id == fleet.systemID:
            if aistate.get_fleet_role(fleet_id) == MissionType.EXPLORATION:
                if system_id in aistate.needsEmergencyExploration:
                    aistate.needsEmergencyExploration.remove(system_id)
        return True


class OrderPause(AIFleetOrder):
    """Ensure Fleet at least temporarily halts movement at the target system."""

    ORDER_NAME = "pause"
    TARGET_TYPE = TargetSystem

    def is_valid(self):
        if not super().is_valid():
            return False
        return bool(self.target.get_system().get_object())

    def issue_order(self):
        if not super().issue_order():
            return False
        # not executed until actually arrives at target system
        self.executed = self.fleet.get_current_system_id() == self.target.get_system().id


class OrderResupply(AIFleetOrder):
    ORDER_NAME = "resupply"
    TARGET_TYPE = TargetSystem

    def is_valid(self):
        if not super().is_valid():
            return False
        return self.target.id in fo.getEmpire().fleetSupplyableSystemIDs

    def issue_order(self):
        if not super().issue_order():
            return False
        fleet_id = self.fleet.id
        system_id = self.target.get_system().id
        fleet = self.fleet.get_object()
        aistate = get_aistate()
        if system_id == fleet.systemID:
            if aistate.get_fleet_role(fleet_id) == MissionType.EXPLORATION:
                if system_id in aistate.needsEmergencyExploration:
                    aistate.needsEmergencyExploration.remove(system_id)
            return True
        if system_id != fleet.nextSystemID:
            self.executed = False
            start_id = FleetUtilsAI.get_fleet_system(fleet)
            dest_id = MoveUtilsAI.get_safe_path_leg_to_dest(fleet_id, start_id, system_id)
            universe = fo.getUniverse()
            debug(
                "fleet %d with order type(%s) sent to safe leg dest %s and ultimate dest %s"
                % (fleet_id, self.ORDER_NAME, universe.getSystem(dest_id), universe.getSystem(system_id))
            )
            fo.issueFleetMoveOrder(fleet_id, dest_id)
            debug(f"Order issued: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
        return True


class OrderOutpost(AIFleetOrder):
    ORDER_NAME = "outpost"
    TARGET_TYPE = TargetPlanet

    def is_valid(self):
        if not super().is_valid():
            return False
        planet = self.target.get_object()
        if not planet.unowned:
            # terminate early
            self.executed = True
            self.order_issued = True
            return False
        else:
            return self.fleet.get_object().hasOutpostShips

    def can_issue_order(self, verbose=False):
        # TODO: check for separate fleet holding outpost ships
        if not super().can_issue_order(verbose=verbose):
            return False
        universe = fo.getUniverse()
        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.CIVILIAN_OUTPOST)
        if ship_id is None:
            ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.BASE_OUTPOST)
        ship = universe.getShip(ship_id)
        return ship is not None and self.fleet.get_object().systemID == self.target.get_system().id and ship.canColonize

    def issue_order(self):
        if not super().issue_order():
            return False
        # we can't know yet if the order will actually execute; instead, rely on the fact that if the order does get
        # executed, then next turn it will be invalid
        self.executed = False
        planet = self.target.get_object()
        if not planet.unowned:
            return False
        fleet_id = self.fleet.id
        ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.CIVILIAN_OUTPOST)
        if ship_id is None:
            ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.BASE_OUTPOST)
        if fo.issueColonizeOrder(ship_id, self.target.id):
            debug(f"Order issued: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
            return True
        else:
            self.order_issued = False
            warning(f"Order issuance failed: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
            return False


class OrderColonize(AIFleetOrder):
    ORDER_NAME = "colonize"
    TARGET_TYPE = TargetPlanet

    def issue_order(self):
        if not super().issue_order():
            return False
        # we can't know yet if the order will actually execute; instead, rely on the fact that if the order does get
        # executed, then next turn it will be invalid
        self.executed = False

        fleet_id = self.fleet.id
        ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.CIVILIAN_COLONISATION)
        if ship_id is None:
            ship_id = FleetUtilsAI.get_ship_id_with_role(fleet_id, ShipRoleType.BASE_COLONISATION)

        if fo.issueColonizeOrder(ship_id, self.target.id):
            debug(f"Order issued: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
            return True
        else:
            self.order_issued = False
            warning(f"Order issuance failed: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
            return False

    def is_valid(self):
        if not super().is_valid():
            return False
        planet = self.target.get_object()
        if (planet.unowned or planet.ownedBy(fo.empireID())) and not planet.currentMeterValue(fo.meterType.population):
            return self.fleet.get_object().hasColonyShips
        # Otherwise, terminate early
        self.executed = True
        self.order_issued = True
        return False

    def can_issue_order(self, verbose=False):
        if not super().is_valid():
            return False
        # TODO: check for separate fleet holding colony ships
        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.CIVILIAN_COLONISATION)
        if ship_id is None:
            ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.BASE_COLONISATION)
        universe = fo.getUniverse()
        ship = universe.getShip(ship_id)
        if ship and not ship.canColonize:
            warning("colonization fleet %d has no colony ship" % self.fleet.id)
        return ship is not None and self.fleet.get_object().systemID == self.target.get_system().id and ship.canColonize


class OrderDefend(AIFleetOrder):
    """
    Used for orbital defense, have no real orders.
    """

    ORDER_NAME = "defend"
    TARGET_TYPE = TargetSystem


class OrderInvade(AIFleetOrder):
    ORDER_NAME = "invade"
    TARGET_TYPE = TargetPlanet

    def is_valid(self):
        if not super().is_valid():
            return False
        planet = self.target.get_object()
        planet_population = planet.currentMeterValue(fo.meterType.population)
        if planet.unowned and not planet_population:
            debug(
                f"\t\t invasion order not valid due to target planet status-- owned: {not planet.unowned} and population {planet_population:.1f}"
            )
            # terminate early
            self.executed = True
            self.order_issued = True
            return False
        else:
            return self.fleet.get_object().hasTroopShips

    def can_issue_order(self, verbose=False):
        if not super().is_valid():
            return False
        # TODO: check for separate fleet holding invasion ships
        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.MILITARY_INVASION, False)
        if ship_id is None:
            ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.BASE_INVASION)
        universe = fo.getUniverse()
        ship = universe.getShip(ship_id)
        planet = self.target.get_object()
        return all(
            (
                ship is not None,
                self.fleet.get_object().systemID == planet.systemID,
                ship.canInvade,
                not planet.initialMeterValue(fo.meterType.shield),
            )
        )

    def issue_order(self):
        if not super().can_issue_order():
            return False

        universe = fo.getUniverse()
        planet_id = self.target.id
        planet = self.target.get_object()
        fleet = self.fleet.get_object()

        invasion_roles = (ShipRoleType.BASE_INVASION, ShipRoleType.MILITARY_INVASION)

        debug(f"Issuing order: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
        # will track if at least one invasion troops successfully deployed
        result = True
        aistate = get_aistate()
        overkill_margin = 2  # TODO: get from character module; allows a handful of extra troops to be immediately
        #                            defending planet
        # invasion orders processed before regen takes place, so use initialMeterValue() here
        troops_wanted = planet.initialMeterValue(fo.meterType.troops) + overkill_margin
        troops_already_assigned = 0  # TODO: get from other fleets in same system
        troops_assigned = 0
        # Todo: evaluate all local troop ships (including other fleets) before using any, make sure base invasion troops
        #       are used first, and that not too many altogether are used (choosing an optimal collection to use).
        for invasion_role in invasion_roles:  # first checks base troops, then regular
            if not result:
                break
            for ship_id in fleet.shipIDs:
                if troops_already_assigned + troops_assigned >= troops_wanted:
                    break
                ship = universe.getShip(ship_id)
                if aistate.get_ship_role(ship.design.id) != invasion_role:
                    continue

                debug("Ordering troop ship %d to invade %s" % (ship_id, planet))
                result = fo.issueInvadeOrder(ship_id, planet_id) and result
                if not result:
                    shields = planet.currentMeterValue(fo.meterType.shield)
                    planet_stealth = planet.currentMeterValue(fo.meterType.stealth)
                    pop = planet.currentMeterValue(fo.meterType.population)
                    warning("Invasion order failed!")
                    debug(
                        " -- planet has %.1f stealth, shields %.1f, %.1f population and "
                        "is owned by empire %d" % (planet_stealth, shields, pop, planet.owner)
                    )
                    if "needsEmergencyExploration" not in dir(aistate):
                        aistate.needsEmergencyExploration = []

                    # TODO: Check if this even makes sense - to invade a planet the ship must be in the system
                    #       which should grant the same visibility as a scout ship...
                    ExplorationAI.request_emergency_exploration(fleet.systemID)
                    debug("Due to trouble invading, added system %d to Emergency Exploration List" % fleet.systemID)
                    self.executed = False
                    # debug(universe.getPlanet(planet_id).dump())  # TODO: fix fo.UniverseObject.dump()
                    break
                troops_assigned += ship.troopCapacity
        # TODO: split off unused troop ships into new fleet and give new orders this cycle
        if result:
            debug("Successfully ordered %d troopers to invade %s" % (troops_assigned, planet))
            return True
        else:
            return False


class OrderMilitary(AIFleetOrder):
    ORDER_NAME = "military"
    TARGET_TYPE = TargetSystem

    def is_valid(self):
        if not super().is_valid():
            return False
        fleet = self.fleet.get_object()
        # TODO: consider bombardment-only fleets/orders
        return fleet is not None and (fleet.hasArmedShips or fleet.hasFighterShips)

    def can_issue_order(self, verbose=False):
        # TODO: consider bombardment
        # TODO: consider simmply looking at fleet characteristics, as is done for is_valid()
        ship_id = FleetUtilsAI.get_ship_id_with_role(self.fleet.id, ShipRoleType.MILITARY)
        universe = fo.getUniverse()
        ship = universe.getShip(ship_id)
        return (
            ship is not None
            and self.fleet.get_object().systemID == self.target.id
            and (ship.isArmed or ship.hasFighters)
        )

    def issue_order(self):
        if not super().issue_order():
            return False
        target_sys_id = self.target.id
        fleet = self.target.get_object()
        system_status = get_aistate().systemStatus.get(target_sys_id, {})
        total_threat = sum(system_status.get(threat, 0) for threat in ("fleetThreat", "planetThreat", "monsterThreat"))
        combat_trigger = system_status.get("fleetThreat", 0) or system_status.get("monsterThreat", 0)
        if not combat_trigger and system_status.get("planetThreat", 0):
            universe = fo.getUniverse()
            system = universe.getSystem(target_sys_id)
            for planet_id in system.planetIDs:
                planet = universe.getPlanet(planet_id)
                if planet.ownedBy(fo.empireID()):  # TODO: also exclude at-peace planets
                    continue
                if planet.unowned and not EspionageAI.colony_detectable_by_empire(planet_id, empire=fo.empireID()):
                    continue
                if sum(
                    [
                        planet.currentMeterValue(meter_type)
                        for meter_type in [fo.meterType.defense, fo.meterType.shield, fo.meterType.construction]
                    ]
                ):
                    combat_trigger = True
                    break
        if not all(
            (
                fleet,
                fleet.systemID == target_sys_id,
                system_status.get("currently_visible", False),
                not (total_threat and combat_trigger),
            )
        ):
            self.executed = False
        return True


class OrderRepair(AIFleetOrder):
    ORDER_NAME = "repair"
    TARGET_TYPE = TargetSystem

    def is_valid(self):
        if not super().is_valid():
            return False
        return self.target.id in fo.getEmpire().fleetSupplyableSystemIDs  # TODO: check for drydock still there/owned

    def issue_order(self):
        if not super().issue_order():
            return False
        fleet_id = self.fleet.id
        system_id = self.target.get_system().id
        fleet = self.fleet.get_object()  # type: fo.fleet
        if system_id == fleet.systemID:
            aistate = get_aistate()
            if aistate.get_fleet_role(fleet_id) == MissionType.EXPLORATION:
                if system_id in aistate.needsEmergencyExploration:
                    aistate.needsEmergencyExploration.remove(system_id)
        elif system_id != fleet.nextSystemID:
            fo.issueAggressionOrder(fleet_id, False)
            start_id = FleetUtilsAI.get_fleet_system(fleet)
            dest_id = MoveUtilsAI.get_safe_path_leg_to_dest(fleet_id, start_id, system_id)
            universe = fo.getUniverse()
            debug(
                "fleet %d with order type(%s) sent to safe leg dest %s and ultimate dest %s"
                % (fleet_id, self.ORDER_NAME, universe.getSystem(dest_id), universe.getSystem(system_id))
            )
            fo.issueFleetMoveOrder(fleet_id, dest_id)
            debug(f"Order issued: {self.ORDER_NAME} fleet: {self.fleet} target: {self.target}")
        ships_cur_health, ships_max_health = FleetUtilsAI.get_current_and_max_structure(fleet_id)
        self.executed = ships_cur_health == ships_max_health
        return True


def trooper_move_reqs_met(invasion_target: Target, order: OrderMove, verbose: bool) -> bool:
    """
    Indicates whether or not move requirements specific to invasion troopers are met for the provided mission and order.

    :param verbose: whether to print verbose decision details
    """
    # Don't advance outside of our fleet-supply zone unless the target either has no shields at all or there
    # is already a military fleet assigned to secure the target, and don't take final jump unless the planet is
    # (to the AI's knowledge) down to zero shields.  Additional checks will also be done by the later
    # generic movement code
    invasion_planet = invasion_target.get_object()
    invasion_system = invasion_target.get_system()
    supplied_systems = fo.getEmpire().fleetSupplyableSystemIDs
    # if about to leave supply lines
    if order.target.id not in supplied_systems or fo.getUniverse().jumpDistance(order.fleet.id, invasion_system.id) < 5:
        if invasion_planet.currentMeterValue(fo.meterType.maxShield):
            military_support_fleets = MilitaryAI.get_military_fleets_with_target_system(invasion_system.id)
            # if there is a threat in the enemy system, do give military ships at least 1 turn to clear it
            delay_to_move_troops = 1 if MilitaryAI.get_system_local_threat(order.target.id) else 0

            def eta(fleet_id):
                return FleetUtilsAI.calculate_estimated_time_of_arrival(fleet_id, invasion_system.id)

            eta_this_fleet = eta(order.fleet.id)
            if all(
                ((eta_this_fleet - delay_to_move_troops) <= eta(fid) and eta(fid)) for fid in military_support_fleets
            ):
                if verbose:
                    debug(
                        "trooper_move_reqs_met() holding Invasion fleet %d before leaving supply "
                        "because target (%s) has nonzero max shields and no assigned military fleet would arrive"
                        "at least %d turn earlier than the invasion fleet"
                        % (order.fleet.id, invasion_planet, delay_to_move_troops)
                    )
                return False

        if verbose:
            debug(
                "trooper_move_reqs_met() allowing Invasion fleet %d to leave supply "
                "because target (%s) has zero max shields or there is a military fleet assigned to secure "
                "the target system which will arrive at least 1 turn before the invasion fleet.",
                order.fleet.id,
                invasion_planet,
            )
    return True