1 #include "Fleet.h"
2
3 #include "Predicates.h"
4 #include "ShipDesign.h"
5 #include "Ship.h"
6 #include "System.h"
7 #include "Pathfinder.h"
8 #include "Universe.h"
9 #include "Enums.h"
10 #include "../util/i18n.h"
11 #include "../util/Logger.h"
12 #include "../util/ScopedTimer.h"
13 #include "../util/AppInterface.h"
14 #include "../Empire/Empire.h"
15 #include "../Empire/EmpireManager.h"
16 #include "../Empire/Supply.h"
17
18 #include <boost/algorithm/cxx11/all_of.hpp>
19
20 namespace {
21 const bool ALLOW_ALLIED_SUPPLY = true;
22
23 const std::set<int> EMPTY_SET;
24 const double MAX_SHIP_SPEED = 500.0; // max allowed speed of ship movement
25 const double FLEET_MOVEMENT_EPSILON = 0.1; // how close a fleet needs to be to a system to have arrived in the system
26
SystemHasNoVisibleStarlanes(int system_id,int empire_id)27 bool SystemHasNoVisibleStarlanes(int system_id, int empire_id)
28 { return !GetPathfinder()->SystemHasVisibleStarlanes(system_id, empire_id); }
29
MoveFleetWithShips(Fleet & fleet,double x,double y)30 void MoveFleetWithShips(Fleet& fleet, double x, double y){
31 fleet.MoveTo(x, y);
32
33 for (auto& ship : Objects().find<Ship>(fleet.ShipIDs())) {
34 ship->MoveTo(x, y);
35 }
36 }
37
InsertFleetWithShips(Fleet & fleet,std::shared_ptr<System> & system)38 void InsertFleetWithShips(Fleet& fleet, std::shared_ptr<System>& system){
39 system->Insert(fleet.shared_from_this());
40
41 for (auto& ship : Objects().find<Ship>(fleet.ShipIDs())) {
42 system->Insert(ship);
43 }
44 }
45
46 /** Return \p full_route terminates at \p last_system or before the first
47 system not known to the \p empire_id. */
TruncateRouteToEndAtSystem(const std::list<int> & full_route,int empire_id,int last_system)48 std::list<int> TruncateRouteToEndAtSystem(const std::list<int>& full_route, int empire_id, int last_system) {
49
50 if (full_route.empty() || (last_system == INVALID_OBJECT_ID))
51 return std::list<int>();
52
53 auto visible_end_it = full_route.cend();
54 if (last_system != full_route.back()) {
55 visible_end_it = std::find(full_route.begin(), full_route.end(), last_system);
56
57 // if requested last system not in route, do nothing
58 if (visible_end_it == full_route.end())
59 return std::list<int>();
60
61 ++visible_end_it;
62 }
63
64 #if BOOST_VERSION >= 106000
65 using boost::placeholders::_1;
66 #endif
67
68 // Remove any extra systems from the route after the apparent destination.
69 // SystemHasNoVisibleStarlanes determines if empire_id knows about the
70 // system and/or its starlanes. It is enforced on the server in the
71 // visibility calculations that an owning empire knows about a) the
72 // system containing a fleet, b) the starlane on which a fleet is travelling
73 // and c) both systems terminating a starlane on which a fleet is travelling.
74 auto end_it = std::find_if(full_route.begin(), visible_end_it,
75 boost::bind(&SystemHasNoVisibleStarlanes, _1, empire_id));
76
77 std::list<int> truncated_route;
78 std::copy(full_route.begin(), end_it, std::back_inserter(truncated_route));
79
80 return truncated_route;
81 }
82 }
83
84 // static(s)
85 const int Fleet::ETA_UNKNOWN = (1 << 30);
86 const int Fleet::ETA_OUT_OF_RANGE = (1 << 30) - 1;
87 const int Fleet::ETA_NEVER = (1 << 30) - 2;
88
Fleet(const std::string & name,double x,double y,int owner)89 Fleet::Fleet(const std::string& name, double x, double y, int owner) :
90 UniverseObject(name, x, y)
91 {
92 UniverseObject::Init();
93 SetOwner(owner);
94 }
95
Clone(int empire_id) const96 Fleet* Fleet::Clone(int empire_id) const {
97 Visibility vis = GetUniverse().GetObjectVisibilityByEmpire(this->ID(), empire_id);
98
99 if (!(vis >= VIS_BASIC_VISIBILITY && vis <= VIS_FULL_VISIBILITY))
100 return nullptr;
101
102 Fleet* retval = new Fleet();
103 retval->Copy(shared_from_this(), empire_id);
104 return retval;
105 }
106
Copy(std::shared_ptr<const UniverseObject> copied_object,int empire_id)107 void Fleet::Copy(std::shared_ptr<const UniverseObject> copied_object, int empire_id) {
108 if (copied_object.get() == this)
109 return;
110 auto copied_fleet = std::dynamic_pointer_cast<const Fleet>(copied_object);
111 if (!copied_fleet) {
112 ErrorLogger() << "Fleet::Copy passed an object that wasn't a Fleet";
113 return;
114 }
115
116 int copied_object_id = copied_object->ID();
117 Visibility vis = GetUniverse().GetObjectVisibilityByEmpire(copied_object_id, empire_id);
118 auto visible_specials = GetUniverse().GetObjectVisibleSpecialsByEmpire(copied_object_id, empire_id);
119
120 UniverseObject::Copy(copied_object, vis, visible_specials);
121
122 if (vis >= VIS_BASIC_VISIBILITY) {
123 m_ships = copied_fleet->VisibleContainedObjectIDs(empire_id);
124
125 m_next_system = ((EmpireKnownObjects(empire_id).get<System>(copied_fleet->m_next_system))
126 ? copied_fleet->m_next_system : INVALID_OBJECT_ID);
127 m_prev_system = ((EmpireKnownObjects(empire_id).get<System>(copied_fleet->m_prev_system))
128 ? copied_fleet->m_prev_system : INVALID_OBJECT_ID);
129 m_arrived_this_turn = copied_fleet->m_arrived_this_turn;
130 m_arrival_starlane = copied_fleet->m_arrival_starlane;
131
132 if (vis >= VIS_PARTIAL_VISIBILITY) {
133 m_aggressive = copied_fleet->m_aggressive;
134 if (Unowned())
135 m_name = copied_fleet->m_name;
136
137 // Truncate the travel route to only systems known to empire_id
138 int moving_to = (vis >= VIS_FULL_VISIBILITY
139 ? (!copied_fleet->m_travel_route.empty()
140 ? copied_fleet->m_travel_route.back()
141 : INVALID_OBJECT_ID)
142 : m_next_system);
143
144 m_travel_route = TruncateRouteToEndAtSystem(copied_fleet->m_travel_route, empire_id, moving_to);
145
146
147 if (vis >= VIS_FULL_VISIBILITY)
148 m_ordered_given_to_empire_id =copied_fleet->m_ordered_given_to_empire_id;
149 }
150 }
151 }
152
HostileToEmpire(int empire_id) const153 bool Fleet::HostileToEmpire(int empire_id) const
154 {
155 if (OwnedBy(empire_id))
156 return false;
157 return empire_id == ALL_EMPIRES || Unowned() ||
158 Empires().GetDiplomaticStatus(Owner(), empire_id) == DIPLO_WAR;
159 }
160
ObjectType() const161 UniverseObjectType Fleet::ObjectType() const
162 { return OBJ_FLEET; }
163
Dump(unsigned short ntabs) const164 std::string Fleet::Dump(unsigned short ntabs) const {
165 std::stringstream os;
166 os << UniverseObject::Dump(ntabs);
167 os << ( m_aggressive ? " agressive" : " passive")
168 << " cur system: " << SystemID()
169 << " moving to: " << FinalDestinationID()
170 << " prev system: " << m_prev_system
171 << " next system: " << m_next_system
172 << " arrival lane: " << m_arrival_starlane
173 << " ships: ";
174 for (auto it = m_ships.begin(); it != m_ships.end();) {
175 int ship_id = *it;
176 ++it;
177 os << ship_id << (it == m_ships.end() ? "" : ", ");
178 }
179 return os.str();
180 }
181
ContainerObjectID() const182 int Fleet::ContainerObjectID() const
183 { return this->SystemID(); }
184
ContainedObjectIDs() const185 const std::set<int>& Fleet::ContainedObjectIDs() const
186 { return m_ships; }
187
Contains(int object_id) const188 bool Fleet::Contains(int object_id) const
189 { return object_id != INVALID_OBJECT_ID && m_ships.count(object_id); }
190
ContainedBy(int object_id) const191 bool Fleet::ContainedBy(int object_id) const
192 { return object_id != INVALID_OBJECT_ID && this->SystemID() == object_id; }
193
PublicName(int empire_id) const194 const std::string& Fleet::PublicName(int empire_id) const {
195 // Disclose real fleet name only to fleet owners.
196 if (GetUniverse().AllObjectsVisible() || empire_id == ALL_EMPIRES || OwnedBy(empire_id))
197 return Name();
198 else if (!Unowned())
199 return UserString("FW_FOREIGN_FLEET");
200 else if (Unowned() && HasMonsters())
201 return UserString("MONSTERS");
202 else if (Unowned() && GetVisibility(empire_id) > VIS_NO_VISIBILITY)
203 return UserString("FW_ROGUE_FLEET");
204 else
205 return UserString("OBJ_FLEET");
206 }
207
MaxShipAgeInTurns() const208 int Fleet::MaxShipAgeInTurns() const {
209 if (m_ships.empty())
210 return INVALID_OBJECT_AGE;
211
212 bool fleet_is_scrapped = true;
213 int retval = 0;
214 for (const auto& ship : Objects().find<Ship>(m_ships)) {
215 if (!ship || ship->OrderedScrapped())
216 continue;
217 if (ship->AgeInTurns() > retval)
218 retval = ship->AgeInTurns();
219 fleet_is_scrapped = false;
220 }
221
222 if (fleet_is_scrapped)
223 retval = 0;
224
225 return retval;
226 }
227
TravelRoute() const228 const std::list<int>& Fleet::TravelRoute() const
229 { return m_travel_route; }
230
MovePath(bool flag_blockades) const231 std::list<MovePathNode> Fleet::MovePath(bool flag_blockades /*= false*/) const
232 { return MovePath(TravelRoute(), flag_blockades); }
233
MovePath(const std::list<int> & route,bool flag_blockades) const234 std::list<MovePathNode> Fleet::MovePath(const std::list<int>& route, bool flag_blockades /*= false*/) const {
235 std::list<MovePathNode> retval;
236
237 if (route.empty())
238 return retval; // nowhere to go => empty path
239 // if (route.size() == 1) do nothing special. this fleet is probably on the starlane leading to
240 // its final destination. normal looping to read destination should work fine
241 if (route.size() == 2 && route.front() == route.back())
242 return retval; // nowhere to go => empty path
243 if (this->Speed() < FLEET_MOVEMENT_EPSILON) {
244 retval.emplace_back(this->X(), this->Y(), true, ETA_NEVER,
245 this->SystemID(),
246 INVALID_OBJECT_ID,
247 INVALID_OBJECT_ID,
248 false);
249 return retval; // can't move => path is just this system with explanatory ETA
250 }
251
252 float fuel = Fuel();
253 float max_fuel = MaxFuel();
254
255 auto RouteNums = [route]() {
256 std::stringstream ss;
257 for (int waypoint : route)
258 ss << waypoint << " ";
259 return ss.str();
260 };
261 TraceLogger() << "Fleet::MovePath for Fleet " << this->Name() << " (" << this->ID()
262 << ") fuel: " << fuel << " at sys id: " << this->SystemID() << " route: "
263 << RouteNums();
264
265 // determine all systems where fleet(s) can be resupplied if fuel runs out
266 const Empire* empire = GetEmpire(this->Owner());
267 auto fleet_supplied_systems = GetSupplyManager().FleetSupplyableSystemIDs(this->Owner(), ALLOW_ALLIED_SUPPLY);
268 auto& unobstructed_systems = empire ? empire->SupplyUnobstructedSystems() : EMPTY_SET;
269
270 // determine if, given fuel available and supplyable systems, fleet will ever be able to move
271 if (fuel < 1.0f &&
272 this->SystemID() != INVALID_OBJECT_ID &&
273 !fleet_supplied_systems.count(this->SystemID()))
274 {
275 retval.emplace_back(this->X(), this->Y(), true, ETA_OUT_OF_RANGE,
276 this->SystemID(),
277 INVALID_OBJECT_ID,
278 INVALID_OBJECT_ID);
279 return retval; // can't move => path is just this system with explanatory ETA
280 }
281
282
283 // get iterator pointing to std::shared_ptr<System> on route that is the first after where this fleet is currently.
284 // if this fleet is in a system, the iterator will point to the system after the current in the route
285 // if this fleet is not in a system, the iterator will point to the first system in the route
286 auto route_it = route.begin();
287 if (*route_it == SystemID())
288 ++route_it; // first system in route is current system of this fleet. skip to the next system
289 if (route_it == route.end())
290 return retval; // current system of this fleet is the *only* system in the route. path is empty.
291
292
293 // get current, previous and next systems of fleet
294 auto cur_system = Objects().get<System>(this->SystemID()); // may be 0
295 auto prev_system = Objects().get<System>(this->PreviousSystemID()); // may be 0 if this fleet is not moving or ordered to move
296 auto next_system = Objects().get<System>(*route_it); // can't use this->NextSystemID() because this fleet may not be moving and may not have a next system. this might occur when a fleet is in a system, not ordered to move or ordered to move to a system, but a projected fleet move line is being calculated to a different system
297 if (!next_system) {
298 ErrorLogger() << "Fleet::MovePath couldn't get next system with id " << *route_it << " for fleet " << this->Name() << "(" << this->ID() << ")";
299 return retval;
300 }
301
302 TraceLogger() << "Initial cur system: " << (cur_system ? cur_system->Name() : "(none)") << "(" << (cur_system ? cur_system->ID() : -1) << ")"
303 << " prev system: " << (prev_system ? prev_system->Name() : "(none)") << "(" << (prev_system ? prev_system->ID() : -1) << ")"
304 << " next system: " << (next_system ? next_system->Name() : "(none)") << "(" << (next_system ? next_system->ID() : -1) << ")";
305
306
307 bool is_post_blockade = false;
308 if (cur_system) {
309 //DebugLogger() << "Fleet::MovePath starting in system "<< SystemID();
310 if (flag_blockades) {
311 if (BlockadedAtSystem(cur_system->ID(), next_system->ID())) {
312 // blockade debug logging
313 TraceLogger() << "Fleet::MovePath checking blockade from "<< cur_system->ID() << " to "<< next_system->ID();
314 TraceLogger() << "Fleet::MovePath finds system " << cur_system->Name() << " (" <<cur_system->ID()
315 << ") blockaded for fleet " << this->Name();
316 is_post_blockade = true;
317 } else {
318 // blockade debug logging, but only for the more complex situations
319 if (next_system->ID() != m_arrival_starlane && !unobstructed_systems.count(cur_system->ID())) {
320 TraceLogger() << "Fleet::MovePath checking blockade from "<< cur_system->ID() << " to " << next_system->ID();
321 TraceLogger() << "Fleet::MovePath finds system " << cur_system->Name() << " (" << cur_system->ID()
322 << ") NOT blockaded for fleet " << this->Name();
323 }
324 }
325 }
326 }
327 // place initial position MovePathNode
328 retval.emplace_back(this->X(), this->Y(), false, 0,
329 (cur_system ? cur_system->ID() : INVALID_OBJECT_ID),
330 (prev_system ? prev_system->ID() : INVALID_OBJECT_ID),
331 (next_system ? next_system->ID() : INVALID_OBJECT_ID),
332 false);
333
334
335 const int TOO_LONG = 100; // limit on turns to simulate. 99 turns max keeps ETA to two digits, making UI work better
336 int turns_taken = 1;
337 double turn_dist_remaining = this->Speed(); // additional distance that can be travelled in current turn of fleet movement being simulated
338 double cur_x = this->X();
339 double cur_y = this->Y();
340 double next_x = next_system->X();
341 double next_y = next_system->Y();
342
343 // simulate fleet movement given known speed, starting position, fuel limit and systems on route
344 // need to populate retval with MovePathNodes that indicate the correct position, whether this
345 // fleet will end a turn at the node, the turns it will take to reach the node, and (when applicable)
346 // the current (if at a system), previous and next system IDs at which the fleet will be. the
347 // previous and next system ids are needed to know what starlane a given node is located on, if any.
348 // nodes at systems don't need previous system ids to be valid, but should have next system ids
349 // valid so that when rendering starlanes using the returned move path, lines departing a system
350 // can be drawn on the correct side of the system icon
351
352 while (turns_taken < TOO_LONG) {
353 // each loop iteration moves the current position to the next location of interest along the move
354 // path, and then adds a node at that position.
355
356 if (cur_system)
357 TraceLogger() << "Starting iteration at system " << cur_system->Name() << " (" << cur_system->ID() << ")";
358 else
359 TraceLogger() << "Starting iteration at (" << cur_x << ", " << cur_y << ")";
360
361 // Make sure that there actually still is a starlane between the two systems
362 // we are between
363 std::shared_ptr<const System> prev_or_cur;
364 if (cur_system) {
365 prev_or_cur = cur_system;
366 } else if (prev_system) {
367 prev_or_cur = prev_system;
368 } else {
369 ErrorLogger() << "Fleet::MovePath: No previous or current system!?";
370 }
371 if (prev_or_cur && !prev_or_cur->HasStarlaneTo(next_system->ID())) {
372 DebugLogger() << "Fleet::MovePath for Fleet " << this->Name() << " (" << this->ID()
373 << ") No starlane connection between systems " << prev_or_cur->Name() << "(" << prev_or_cur->ID()
374 << ") and " << next_system->Name() << "(" << next_system->ID()
375 << "). Abandoning the rest of the route. Route was: " << RouteNums();
376 return retval;
377 }
378
379
380 // check if fuel limits movement or current system refuels passing fleet
381 if (cur_system) {
382 // check if current system has fuel supply available
383 if (fleet_supplied_systems.count(cur_system->ID())) {
384 // current system has fuel supply. replenish fleet's supply and don't restrict movement
385 fuel = max_fuel;
386 //DebugLogger() << " ... at system with fuel supply. replenishing and continuing movement";
387
388 } else {
389 // current system has no fuel supply. require fuel to proceed
390 if (fuel >= 1.0) {
391 //DebugLogger() << " ... at system without fuel supply. consuming unit of fuel to proceed";
392 fuel -= 1.0;
393
394 } else {
395 //DebugLogger() << " ... at system without fuel supply. have insufficient fuel to continue moving";
396 turns_taken = ETA_OUT_OF_RANGE;
397 break;
398 }
399 }
400 }
401
402 // find distance to next system along path from current position
403 double dist_to_next_system = std::sqrt((next_x - cur_x)*(next_x - cur_x) + (next_y - cur_y)*(next_y - cur_y));
404 //DebugLogger() << " ... dist to next system: " << dist_to_next_system;
405
406
407 // move ship as far as it can go this turn, or to next system, whichever is closer, and deduct
408 // distance travelled from distance travellable this turn
409 if (turn_dist_remaining >= FLEET_MOVEMENT_EPSILON) {
410 double dist_travelled_this_step = std::min(turn_dist_remaining, dist_to_next_system);
411
412 //DebugLogger() << " ... fleet moving " << dist_travelled_this_step << " this iteration. dist to next system: " << dist_to_next_system << " and turn_dist_remaining: " << turn_dist_remaining;
413
414 double x_dist = next_x - cur_x;
415 double y_dist = next_y - cur_y;
416 // dist_to_next_system = std::sqrt(x_dist * x_dist + y_dist * y_dist); // should already equal this distance, so don't need to recalculate
417 double unit_vec_x = x_dist / dist_to_next_system;
418 double unit_vec_y = y_dist / dist_to_next_system;
419
420 cur_x += unit_vec_x*dist_travelled_this_step;
421 cur_y += unit_vec_y*dist_travelled_this_step;
422
423 turn_dist_remaining -= dist_travelled_this_step;
424 dist_to_next_system -= dist_travelled_this_step;
425
426 // if moved away any distance from a system, are no longer in that system
427 if (cur_system && dist_travelled_this_step >= FLEET_MOVEMENT_EPSILON) {
428 prev_system = cur_system;
429 cur_system = nullptr;
430 }
431 }
432
433 bool end_turn_at_cur_position = false;
434
435 // check if fleet can move any further this turn
436 if (turn_dist_remaining < FLEET_MOVEMENT_EPSILON) {
437 //DebugLogger() << " ... fleet can't move further this turn.";
438 turn_dist_remaining = 0.0; // to prevent any possible precision-related errors
439 end_turn_at_cur_position = true;
440 }
441
442 // check if current position is close enough to next system on route to qualify as at that system.
443 if (dist_to_next_system < FLEET_MOVEMENT_EPSILON) {
444 // close enough to be consider to be at next system.
445 // set current position to be exactly at next system to avoid rounding issues
446 cur_system = next_system;
447 if (!cur_system) {
448 ErrorLogger() << "Fleet::MovePath got null next system!";
449 break;
450 }
451 cur_x = cur_system->X(); // update positions to ensure no round-off-errors
452 cur_y = cur_system->Y();
453
454 TraceLogger() << " ... arrived at system: " << cur_system->Name();
455
456 bool clear_exit = cur_system->ID() == m_arrival_starlane; //just part of the test for the moment
457 // attempt to get next system on route, to update next system. if new current
458 // system is the end of the route, abort.
459 ++route_it;
460 if (route_it != route.end()) {
461 // update next system on route and distance to it from current position
462 next_system = EmpireKnownObjects(this->Owner()).get<System>(*route_it);
463 if (next_system) {
464 TraceLogger() << "Fleet::MovePath checking unrestriced lane travel from Sys("
465 << cur_system->ID() << ") to Sys(" << (next_system && next_system->ID()) << ")";
466 clear_exit = clear_exit || next_system->ID() == m_arrival_starlane ||
467 (empire && empire->PreservedLaneTravel(cur_system->ID(), next_system->ID()));
468 }
469 }
470 if (flag_blockades && !clear_exit) {
471 TraceLogger() << "Fleet::MovePath checking blockades at system " << cur_system->Name()
472 << " (" << cur_system->ID() << ") for fleet " << this->Name()
473 << " travelling to system " << (*route_it);
474 if (cur_system && next_system && BlockadedAtSystem(cur_system->ID(), next_system->ID())) {
475 // blockade debug logging
476 TraceLogger() << "Fleet::MovePath finds system " << cur_system->Name() << " (" << cur_system->ID()
477 << ") blockaded for fleet " << this->Name();
478 is_post_blockade = true;
479 } else {
480 TraceLogger() << "Fleet::MovePath finds system " << cur_system->Name() << " (" << cur_system->ID()
481 << ") NOT blockaded for fleet " << this->Name();
482 }
483 }
484
485 if (route_it == route.end() || !cur_system)
486 break;
487
488 if (!next_system) {
489 ErrorLogger() << "Fleet::MovePath couldn't get system with id " << *route_it;
490 break;
491 }
492 next_x = next_system->X();
493 next_y = next_system->Y();
494 }
495
496 // if new position is an obstructed system, must end turn here
497 // on client side, if have stale info on cur_system it may appear blockaded even if not actually obstructed,
498 // and so will force a stop in that situation
499 if (cur_system && !unobstructed_systems.count(cur_system->ID())) {
500 turn_dist_remaining = 0.0;
501 end_turn_at_cur_position = true;
502 }
503
504 // if turn done and turns taken is enough, abort simulation
505 if (end_turn_at_cur_position && (turns_taken + 1 >= TOO_LONG)) {
506 // exit loop before placing current node to simplify post-loop processing: now all cases require a post-loop node to be added
507 ++turns_taken;
508 break;
509 }
510
511 // blockade debug logging
512 TraceLogger() << "Fleet::MovePath for fleet " << this->Name() << " id " << this->ID() << " adding node at sysID "
513 << (cur_system ? cur_system->ID() : INVALID_OBJECT_ID) << " with post blockade status "
514 << is_post_blockade << " and ETA " << turns_taken;
515
516 // add MovePathNode for current position (end of turn position and/or system location)
517 retval.emplace_back(cur_x, cur_y, end_turn_at_cur_position, turns_taken,
518 (cur_system ? cur_system->ID() : INVALID_OBJECT_ID),
519 (prev_system ? prev_system->ID() : INVALID_OBJECT_ID),
520 (next_system ? next_system->ID() : INVALID_OBJECT_ID),
521 is_post_blockade);
522
523
524 // if the turn ended at this position, increment the turns taken and
525 // reset the distance remaining to be travelled during the current (now
526 // next) turn for the next loop iteration
527 if (end_turn_at_cur_position) {
528 //DebugLogger() << " ... end of simulated turn " << turns_taken;
529 ++turns_taken;
530 turn_dist_remaining = this->Speed();
531 }
532 }
533
534
535 // done looping. may have exited due to reaching end of path, lack of fuel, or turns taken getting too big
536 if (turns_taken == TOO_LONG)
537 turns_taken = ETA_NEVER;
538 // blockade debug logging
539 TraceLogger() << "Fleet::MovePath for fleet " << this->Name()<<" id "<<this->ID()<<" adding node at sysID "
540 << (cur_system ? cur_system->ID() : INVALID_OBJECT_ID) << " with post blockade status "
541 << is_post_blockade << " and ETA " << turns_taken;
542
543 retval.emplace_back(cur_x, cur_y, true, turns_taken,
544 (cur_system ? cur_system->ID() : INVALID_OBJECT_ID),
545 (prev_system ? prev_system->ID() : INVALID_OBJECT_ID),
546 (next_system ? next_system->ID() : INVALID_OBJECT_ID),
547 is_post_blockade);
548 TraceLogger() << "Fleet::MovePath for fleet " << this->Name() << "(" << this->ID() << ") is complete";
549
550 return retval;
551 }
552
ETA() const553 std::pair<int, int> Fleet::ETA() const
554 { return ETA(MovePath()); }
555
ETA(const std::list<MovePathNode> & move_path) const556 std::pair<int, int> Fleet::ETA(const std::list<MovePathNode>& move_path) const {
557 // check that path exists. if empty, there was no valid route or some other problem prevented pathing
558 if (move_path.empty())
559 return {ETA_UNKNOWN, ETA_UNKNOWN};
560
561 // check for single node in path. return the single node's eta as both .first and .second (likely indicates that fleet couldn't move)
562 if (move_path.size() == 1) {
563 const MovePathNode& node = *move_path.begin();
564 return {node.eta, node.eta};
565 }
566
567 // general case: there is a multi-node path. return the ETA of the first object node, and the ETA of the last node
568 int last_stop_eta = move_path.rbegin()->eta;
569 int first_stop_eta = last_stop_eta;
570 for (auto it = ++(move_path.begin()); it != move_path.end(); ++it) {
571 const MovePathNode& node = *it;
572 if (node.object_id != INVALID_OBJECT_ID) {
573 first_stop_eta = node.eta;
574 break;
575 }
576 }
577
578 return {last_stop_eta, first_stop_eta};
579 }
580
Fuel() const581 float Fleet::Fuel() const {
582 if (NumShips() < 1)
583 return 0.0f;
584
585 // determine fuel available to fleet (fuel of the ship that has the least fuel in the fleet)
586 float fuel = Meter::LARGE_VALUE;
587 bool is_fleet_scrapped = true;
588
589 for (auto& ship : Objects().find<const Ship>(m_ships)) {
590 const Meter* meter = ship->UniverseObject::GetMeter(METER_FUEL);
591 if (!meter) {
592 ErrorLogger() << "Fleet::Fuel skipping ship with no fuel meter";
593 continue;
594 }
595 if (!ship->OrderedScrapped()) {
596 fuel = std::min(fuel, meter->Current());
597 is_fleet_scrapped = false;
598 }
599 }
600 if (is_fleet_scrapped) {
601 fuel = 0.0f;
602 }
603 return fuel;
604 }
605
MaxFuel() const606 float Fleet::MaxFuel() const {
607 if (NumShips() < 1)
608 return 0.0f;
609
610 // determine the maximum amount of fuel that can be stored by the ship in the fleet that
611 // can store the least amount of fuel
612 float max_fuel = Meter::LARGE_VALUE;
613 bool is_fleet_scrapped = true;
614
615 for (auto& ship : Objects().find<const Ship>(m_ships)) {
616 const Meter* meter = ship->UniverseObject::GetMeter(METER_MAX_FUEL);
617 if (!meter) {
618 ErrorLogger() << "Fleet::MaxFuel skipping ship with no max fuel meter";
619 continue;
620 }
621 if (!ship->OrderedScrapped()) {
622 max_fuel = std::min(max_fuel, meter->Current());
623 is_fleet_scrapped = false;
624 }
625 }
626 if (is_fleet_scrapped) {
627 max_fuel = 0.0f;
628 }
629 return max_fuel;
630 }
631
FinalDestinationID() const632 int Fleet::FinalDestinationID() const {
633 if (m_travel_route.empty()) {
634 return INVALID_OBJECT_ID;
635 } else {
636 return m_travel_route.back();
637 }
638 }
639
640 namespace {
HasXShips(const std::function<bool (const std::shared_ptr<const Ship> &)> & pred,const std::set<int> & ship_ids)641 bool HasXShips(const std::function<bool(const std::shared_ptr<const Ship>&)>& pred,
642 const std::set<int>& ship_ids)
643 {
644 // Searching for each Ship one at a time is faster than
645 // find(ship_ids), because an early exit avoids searching the
646 // remaining ids.
647 return std::any_of(
648 ship_ids.begin(), ship_ids.end(),
649 [&pred](const int ship_id) {
650 const auto& ship = Objects().get<const Ship>(ship_id);
651 if (!ship) {
652 WarnLogger() << "Object map is missing ship with expected id " << ship_id;
653 return false;
654 }
655 return pred(ship);
656 });
657 }
658 }
659
HasMonsters() const660 bool Fleet::HasMonsters() const {
661 auto isX = [](const std::shared_ptr<const Ship>& ship){ return ship->IsMonster(); };
662 return HasXShips(isX, m_ships);
663 }
664
HasArmedShips() const665 bool Fleet::HasArmedShips() const {
666 auto isX = [](const std::shared_ptr<const Ship>& ship){ return ship->IsArmed(); };
667 return HasXShips(isX, m_ships);
668 }
669
HasFighterShips() const670 bool Fleet::HasFighterShips() const {
671 auto isX = [](const std::shared_ptr<const Ship>& ship){ return ship->HasFighters(); };
672 return HasXShips(isX, m_ships);
673 }
674
HasColonyShips() const675 bool Fleet::HasColonyShips() const {
676 auto isX = [](const std::shared_ptr<const Ship>& ship) {
677 if (ship->CanColonize())
678 if (const auto design = ship->Design())
679 if (design->ColonyCapacity() > 0.0f)
680 return true;
681 return false;
682 };
683 return HasXShips(isX, m_ships);
684 }
685
HasOutpostShips() const686 bool Fleet::HasOutpostShips() const {
687 auto isX = [](const std::shared_ptr<const Ship>& ship) {
688 if (ship->CanColonize())
689 if (const auto design = ship->Design())
690 if (design->ColonyCapacity() == 0.0f)
691 return true;
692 return false;
693 };
694 return HasXShips(isX, m_ships);
695 }
696
HasTroopShips() const697 bool Fleet::HasTroopShips() const {
698 auto isX = [](const std::shared_ptr<const Ship>& ship){ return ship->HasTroops(); };
699 return HasXShips(isX, m_ships);
700 }
701
HasShipsOrderedScrapped() const702 bool Fleet::HasShipsOrderedScrapped() const {
703 auto isX = [](const std::shared_ptr<const Ship>& ship){ return ship->OrderedScrapped(); };
704 return HasXShips(isX, m_ships);
705 }
706
HasShipsWithoutScrapOrders() const707 bool Fleet::HasShipsWithoutScrapOrders() const {
708 auto isX = [](const std::shared_ptr<const Ship>& ship){ return !ship->OrderedScrapped(); };
709 return HasXShips(isX, m_ships);
710 }
711
ResourceOutput(ResourceType type) const712 float Fleet::ResourceOutput(ResourceType type) const {
713 float output = 0.0f;
714 if (NumShips() < 1)
715 return output;
716 MeterType meter_type = ResourceToMeter(type);
717 if (meter_type == INVALID_METER_TYPE)
718 return output;
719
720 // determine resource output of each ship in this fleet
721 for (auto& ship : Objects().find<const Ship>(m_ships))
722 output += ship->GetMeter(meter_type)->Current();
723
724 return output;
725 }
726
UnknownRoute() const727 bool Fleet::UnknownRoute() const
728 { return m_travel_route.size() == 1 && m_travel_route.front() == INVALID_OBJECT_ID; }
729
Accept(const UniverseObjectVisitor & visitor) const730 std::shared_ptr<UniverseObject> Fleet::Accept(const UniverseObjectVisitor& visitor) const
731 { return visitor.Visit(std::const_pointer_cast<Fleet>(std::static_pointer_cast<const Fleet>(shared_from_this()))); }
732
SetRoute(const std::list<int> & route)733 void Fleet::SetRoute(const std::list<int>& route) {
734 if (UnknownRoute())
735 throw std::invalid_argument("Fleet::SetRoute() : Attempted to set an unknown route.");
736
737 if (m_prev_system != SystemID() && m_prev_system == route.front() && !CanChangeDirectionEnRoute())
738 throw std::invalid_argument("Fleet::SetRoute() : Illegally attempted to change a fleet's direction while it was in transit.");
739
740 m_travel_route = route;
741
742 TraceLogger() << "Fleet::SetRoute: " << this->Name() << " (" << this->ID() << ") input: " << [&]() {
743 std::stringstream ss;
744 for (int id : m_travel_route)
745 if (const auto obj = Objects().get<UniverseObject>(id))
746 ss << obj->Name() << " (" << id << ") ";
747 return ss.str();
748 }();
749
750 if (m_travel_route.size() == 1 && this->SystemID() == m_travel_route.front()) {
751 // Fleet was ordered to move to the system where it is currently,
752 // without an intermediate stop
753 m_travel_route.clear();
754 m_prev_system = m_next_system = INVALID_OBJECT_ID;
755
756 } else if (!m_travel_route.empty()) {
757 // Fleet was given a route to follow...
758 if (m_prev_system != SystemID() && m_prev_system == m_travel_route.front()) {
759 // Fleet was ordered to return to its previous system directly
760 m_prev_system = m_next_system;
761 } else if (SystemID() == route.front()) {
762 // Fleet was ordered to follow a route that starts at its current system
763 m_prev_system = SystemID();
764 }
765
766 auto it = m_travel_route.begin();
767 if (m_prev_system == SystemID() && m_travel_route.size() > 1) {
768 m_next_system = *++it;
769 } else {
770 m_next_system = *it;
771 }
772
773 } else if (m_travel_route.empty() && this->SystemID() != INVALID_OBJECT_ID) {
774 // route is empty and fleet is in a system. no movement needed.
775 // ensure next and previous systems are reset
776 m_prev_system = m_next_system = INVALID_OBJECT_ID;
777
778 } else { // m_travel_route.empty() && this->SystemID() == INVALID_OBJECT_ID
779 if (m_next_system != INVALID_OBJECT_ID) {
780 ErrorLogger() << "Fleet::SetRoute fleet " << this->Name() << " has empty route but fleet is not in a system. Resetting route to end at next system: " << m_next_system;
781 m_travel_route.push_back(m_next_system);
782 } else {
783 ErrorLogger() << "Fleet::SetRoute fleet " << this->Name() << " has empty route but fleet is not in a system, and has no next system set.";
784 }
785 }
786
787 TraceLogger() << "Fleet::SetRoute: " << this->Name() << " (" << this->ID() << ") final: " << [&]() {
788 std::stringstream ss;
789 for (int id : m_travel_route)
790 if (const auto obj = Objects().get<UniverseObject>(id))
791 ss << obj->Name() << " (" << id << ") ";
792 return ss.str();
793 }();
794
795 StateChangedSignal();
796 }
797
SetAggressive(bool aggressive)798 void Fleet::SetAggressive(bool aggressive/* = true*/) {
799 if (aggressive == m_aggressive)
800 return;
801 m_aggressive = aggressive;
802 StateChangedSignal();
803 }
804
AddShips(const std::vector<int> & ship_ids)805 void Fleet::AddShips(const std::vector<int>& ship_ids) {
806 size_t old_ships_size = m_ships.size();
807 std::copy(ship_ids.begin(), ship_ids.end(), std::inserter(m_ships, m_ships.end()));
808 if (old_ships_size != m_ships.size())
809 StateChangedSignal();
810 }
811
RemoveShips(const std::vector<int> & ship_ids)812 void Fleet::RemoveShips(const std::vector<int>& ship_ids) {
813 size_t old_ships_size = m_ships.size();
814 for (int ship_id : ship_ids)
815 m_ships.erase(ship_id);
816 if (old_ships_size != m_ships.size())
817 StateChangedSignal();
818 }
819
SetNextAndPreviousSystems(int next,int prev)820 void Fleet::SetNextAndPreviousSystems(int next, int prev) {
821 m_prev_system = prev;
822 m_next_system = next;
823 m_arrival_starlane = prev; // see comment for ArrivalStarlane()
824 }
825
MovementPhase()826 void Fleet::MovementPhase() {
827 Empire* empire = GetEmpire(Owner());
828 std::set<int> supply_unobstructed_systems;
829 if (empire)
830 supply_unobstructed_systems.insert(empire->SupplyUnobstructedSystems().begin(),
831 empire->SupplyUnobstructedSystems().end());
832
833 auto ships = Objects().find<Ship>(m_ships);
834
835 // if owner of fleet can resupply ships at the location of this fleet, then
836 // resupply all ships in this fleet
837 if (GetSupplyManager().SystemHasFleetSupply(SystemID(), Owner(), ALLOW_ALLIED_SUPPLY)) {
838 for (auto& ship : ships)
839 ship->Resupply();
840 }
841
842 auto current_system = Objects().get<System>(SystemID());
843 auto initial_system = current_system;
844 auto move_path = MovePath();
845
846 if (!move_path.empty()) {
847 DebugLogger() << "Fleet::MovementPhase " << this->Name() << " (" << this->ID()
848 << ") route:" << [&]() {
849 std::stringstream ss;
850 for (auto sys_id : this->TravelRoute()) {
851 auto sys = Objects().get<System>(sys_id);
852 if (sys)
853 ss << " " << sys->Name() << " (" << sys_id << ")";
854 else
855 ss << " (??\?) (" << sys_id << ")";
856 }
857 return ss.str();
858 }()
859 << " move path:" << [&]() {
860 std::stringstream ss;
861 for (auto node : move_path) {
862 auto sys = Objects().get<System>(node.object_id);
863 if (sys)
864 ss << " " << sys->Name() << " (" << node.object_id << ")";
865 else
866 ss << " (-)";
867 }
868 return ss.str();
869 }();
870 } else {
871 // enforce m_next_system and m_prev_system being INVALID_OBJECT_ID when
872 // move path is empty. bug was reported where m_next_system was somehow
873 // left with a system ID in it, which was never reset, and lead to
874 // supply propagation issues
875 m_next_system = m_prev_system = INVALID_OBJECT_ID;
876 }
877
878 // If the move path cannot lead to the destination,
879 // make the route go as far as it can
880 if (!move_path.empty() && !m_travel_route.empty() &&
881 move_path.back().object_id != m_travel_route.back())
882 {
883 auto shortened_route = TruncateRouteToEndAtSystem(m_travel_route, Owner(), move_path.back().object_id);
884 try {
885 SetRoute(shortened_route);
886 } catch (const std::exception& e) {
887 ErrorLogger() << "Caught exception in Fleet MovementPhase shorentning route: " << e.what();
888 }
889 move_path = MovePath();
890 }
891
892 auto it = move_path.begin();
893 auto next_it = it;
894 if (next_it != move_path.end())
895 ++next_it;
896
897
898 // is the fleet stuck in a system for a whole turn?
899 if (current_system) {
900 // update m_arrival_starlane if no blockade, if needed
901 if (supply_unobstructed_systems.count(SystemID()))
902 m_arrival_starlane = SystemID();// allows departure via any starlane
903
904 // in a system. if either:
905 // - there is no system after the current one in the path
906 // - the current and next nodes are the same and are system IDs or actual systems (not empty space)
907 // - this fleet is blockaded from its intended path
908 // then this fleet won't be moving further this turn
909 bool stopped = false;
910
911 if (next_it == move_path.end()) {
912 // at end of path
913 stopped = true;
914
915 } else if (it->object_id != INVALID_OBJECT_ID && it->object_id == next_it->object_id) {
916 // arriving at system
917 stopped = true;
918
919 } else if (m_arrival_starlane != SystemID()) {
920 // blockaded
921 int next_sys_id;
922 if (next_it->object_id != INVALID_OBJECT_ID) {
923 next_sys_id = next_it->object_id;
924 } else {
925 next_sys_id = next_it->lane_end_id;
926 }
927 stopped = BlockadedAtSystem(SystemID(), next_sys_id);
928 }
929
930 if (stopped)
931 return;
932
933 // record previous system on fleet's path, and the starlane along
934 // which it will arrive at the next system (for blockading purposes)
935 m_arrival_starlane = SystemID();
936 m_prev_system = SystemID();
937
938 // remove fleet and ships from system they are departing
939 current_system->Remove(ID());
940 SetSystem(INVALID_OBJECT_ID);
941 for (auto& ship : ships) {
942 current_system->Remove(ship->ID());
943 ship->SetSystem(INVALID_OBJECT_ID);
944 }
945 }
946
947
948 // if fleet not moving, nothing more to do.
949 if (move_path.empty()) {
950 m_next_system = m_prev_system = INVALID_OBJECT_ID;
951 return;
952 }
953
954
955 // move fleet in sequence to MovePathNodes it can reach this turn
956 float fuel_consumed = 0.0f;
957 for (it = move_path.begin(); it != move_path.end(); ++it) {
958 next_it = it; ++next_it;
959
960 auto system = Objects().get<System>(it->object_id);
961
962 // is this system the last node reached this turn? either it's an end of turn node,
963 // or there are no more nodes after this one on path
964 bool node_is_next_stop = (it->turn_end || next_it == move_path.end());
965
966
967 if (system) {
968 // node is a system. explore system for all owners of this fleet
969 if (empire) {
970 empire->AddExploredSystem(it->object_id);
971 empire->RecordPendingLaneUpdate(it->object_id, m_prev_system); // specifies the lane from it->object_id back to m_prev_system is available
972 }
973
974 m_prev_system = system->ID(); // passing a system, so update previous system of this fleet
975
976 // reached a system, so remove it from the route
977 if (m_travel_route.front() == system->ID())
978 m_travel_route.erase(m_travel_route.begin());
979
980 bool resupply_here = GetSupplyManager().SystemHasFleetSupply(system->ID(), this->Owner(), ALLOW_ALLIED_SUPPLY);
981
982 // if this system can provide supplies, reset consumed fuel and refuel ships
983 if (resupply_here) {
984 //DebugLogger() << " ... node has fuel supply. consumed fuel for movement reset to 0 and fleet resupplied";
985 fuel_consumed = 0.0f;
986 for (auto& ship : ships) {
987 ship->Resupply();
988 }
989 }
990
991
992 // is system the last node reached this turn?
993 if (node_is_next_stop) {
994 // fleet ends turn at this node. insert fleet and ships into system
995 InsertFleetWithShips(*this, system);
996
997 current_system = system;
998
999 if (supply_unobstructed_systems.count(SystemID()))
1000 m_arrival_starlane = SystemID();//allows departure via any starlane
1001
1002 // Add current system to the start of any existing route for next turn
1003 if (!m_travel_route.empty() && m_travel_route.front() != SystemID())
1004 m_travel_route.push_front(SystemID());
1005
1006 break;
1007
1008 } else {
1009 // fleet will continue past this system this turn.
1010 m_arrival_starlane = m_prev_system;
1011 if (!resupply_here) {
1012 fuel_consumed += 1.0f;
1013 }
1014 }
1015
1016 } else {
1017 // node is not a system.
1018 m_arrival_starlane = m_prev_system;
1019 if (node_is_next_stop) { // node is not a system, but is it the last node reached this turn?
1020 MoveFleetWithShips(*this, it->x, it->y);
1021 break;
1022 }
1023 }
1024 }
1025
1026
1027 // update next system
1028 if (!m_travel_route.empty() && next_it != move_path.end() && it != move_path.end()) {
1029 // there is another system later on the path to aim for. find it
1030 for (; next_it != move_path.end(); ++next_it) {
1031 if (Objects().get<System>(next_it->object_id)) {
1032 //DebugLogger() << "___ setting m_next_system to " << next_it->object_id;
1033 m_next_system = next_it->object_id;
1034 break;
1035 }
1036 }
1037
1038 } else {
1039 // no more systems on path
1040 m_arrived_this_turn = current_system != initial_system;
1041 m_next_system = m_prev_system = INVALID_OBJECT_ID;
1042 }
1043
1044
1045 // consume fuel from ships in fleet
1046 if (fuel_consumed > 0.0f) {
1047 for (auto& ship : ships) {
1048 if (Meter* meter = ship->UniverseObject::GetMeter(METER_FUEL)) {
1049 meter->AddToCurrent(-fuel_consumed);
1050 meter->BackPropagate();
1051 }
1052 }
1053 }
1054 }
1055
ResetTargetMaxUnpairedMeters()1056 void Fleet::ResetTargetMaxUnpairedMeters() {
1057 UniverseObject::ResetTargetMaxUnpairedMeters();
1058
1059 // give fleets base stealth very high, so that they can (almost?) never be
1060 // seen by empires that don't own them, unless their ships are seen and
1061 // that visibility is propagated to the fleet that contains the ships
1062 if (Meter* stealth = GetMeter(METER_STEALTH)) {
1063 stealth->ResetCurrent();
1064 stealth->AddToCurrent(2000.0f);
1065 }
1066 }
1067
CalculateRouteTo(int target_system_id)1068 void Fleet::CalculateRouteTo(int target_system_id) {
1069 std::list<int> route;
1070
1071 //DebugLogger() << "Fleet::CalculateRoute";
1072 if (target_system_id == INVALID_OBJECT_ID) {
1073 try {
1074 SetRoute(route);
1075 } catch (const std::exception& e) {
1076 ErrorLogger() << "Caught exception in Fleet CalculateRouteTo: " << e.what();
1077 }
1078 return;
1079 }
1080
1081 if (m_prev_system != INVALID_OBJECT_ID && SystemID() == m_prev_system) {
1082 // if we haven't actually left yet, we have to move from whichever system we are at now
1083
1084 if (!Objects().get<System>(target_system_id)) {
1085 // destination system doesn't exist or doesn't exist in known universe, so can't move to it. leave route empty.
1086 try {
1087 SetRoute(route);
1088 } catch (const std::exception& e) {
1089 ErrorLogger() << "Caught exception in Fleet CalculateRouteTo: " << e.what();
1090 }
1091 return;
1092 }
1093
1094 std::pair<std::list<int>, double> path;
1095 try {
1096 path = GetPathfinder()->ShortestPath(m_prev_system, target_system_id, this->Owner());
1097 } catch (...) {
1098 DebugLogger() << "Fleet::CalculateRoute couldn't find route to system(s):"
1099 << " fleet's previous: " << m_prev_system << " or moving to: " << target_system_id;
1100 }
1101 try {
1102 SetRoute(path.first);
1103 } catch (const std::exception& e) {
1104 ErrorLogger() << "Caught exception in Fleet CalculateRouteTo: " << e.what();
1105 }
1106 return;
1107 }
1108
1109 int dest_system_id = target_system_id;
1110
1111 // if we're between systems, the shortest route may be through either one
1112 if (this->CanChangeDirectionEnRoute()) {
1113 std::pair<std::list<int>, double> path1;
1114 try {
1115 path1 = GetPathfinder()->ShortestPath(m_next_system, dest_system_id, this->Owner());
1116 } catch (...) {
1117 DebugLogger() << "Fleet::CalculateRoute couldn't find route to system(s):"
1118 << " fleet's next: " << m_next_system << " or destination: " << dest_system_id;
1119 }
1120 auto& sys_list1 = path1.first;
1121 if (sys_list1.empty()) {
1122 ErrorLogger() << "Fleet::CalculateRoute got empty route from ShortestPath";
1123 return;
1124 }
1125 auto obj = Objects().get(sys_list1.front());
1126 if (!obj) {
1127 ErrorLogger() << "Fleet::CalculateRoute couldn't get path start object with id " << path1.first.front();
1128 return;
1129 }
1130 double dist_x = obj->X() - this->X();
1131 double dist_y = obj->Y() - this->Y();
1132 double dist1 = std::sqrt(dist_x*dist_x + dist_y*dist_y);
1133
1134 std::pair<std::list<int>, double> path2;
1135 try {
1136 path2 = GetPathfinder()->ShortestPath(m_prev_system, dest_system_id, this->Owner());
1137 } catch (...) {
1138 DebugLogger() << "Fleet::CalculateRoute couldn't find route to system(s):"
1139 << " fleet's previous: " << m_prev_system << " or destination: " << dest_system_id;
1140 }
1141 auto& sys_list2 = path2.first;
1142 if (sys_list2.empty()) {
1143 ErrorLogger() << "Fleet::CalculateRoute got empty route from ShortestPath";
1144 return;
1145 }
1146 obj = Objects().get(sys_list2.front());
1147 if (!obj) {
1148 ErrorLogger() << "Fleet::CalculateRoute couldn't get path start object with id " << path2.first.front();
1149 return;
1150 }
1151 dist_x = obj->X() - this->X();
1152 dist_y = obj->Y() - this->Y();
1153 double dist2 = std::sqrt(dist_x*dist_x + dist_y*dist_y);
1154
1155 try {
1156 // pick whichever path is quicker
1157 if (dist1 + path1.second < dist2 + path2.second) {
1158 SetRoute(path1.first);
1159 } else {
1160 SetRoute(path2.first);
1161 }
1162 } catch (const std::exception& e) {
1163 ErrorLogger() << "Caught exception in Fleet CalculateRouteTo: " << e.what();
1164 }
1165
1166 } else {
1167 // Cannot change direction. Must go to the end of the current starlane
1168 std::pair<std::list<int>, double> path;
1169 try {
1170 path = GetPathfinder()->ShortestPath(m_next_system, dest_system_id, this->Owner());
1171 } catch (...) {
1172 DebugLogger() << "Fleet::CalculateRoute couldn't find route to system(s):"
1173 << " fleet's next: " << m_next_system << " or destination: " << dest_system_id;
1174 }
1175 try {
1176 SetRoute(path.first);
1177 } catch (const std::exception& e) {
1178 ErrorLogger() << "Caught exception in Fleet CalculateRouteTo: " << e.what();
1179 }
1180 }
1181 }
1182
Blockaded() const1183 bool Fleet::Blockaded() const {
1184 auto system = Objects().get<System>(this->SystemID());
1185
1186 if (!system)
1187 return false;
1188
1189 if (m_next_system != INVALID_OBJECT_ID)
1190 return BlockadedAtSystem(SystemID(), m_next_system);
1191
1192 for (const auto& target_system : system->StarlanesWormholes()) {
1193 if (BlockadedAtSystem(this->SystemID(), target_system.first))
1194 return true;
1195 }
1196
1197 return false;
1198 }
1199
BlockadedAtSystem(int start_system_id,int dest_system_id) const1200 bool Fleet::BlockadedAtSystem(int start_system_id, int dest_system_id) const {
1201 /** If a newly arrived fleet joins a non-blockaded fleet of the same empire
1202 * (perhaps should include allies?) already at the system, the newly
1203 * arrived fleet will not be blockaded. Additionally, since fleets are
1204 * blockade-checked at movement phase and also postcombat, the following
1205 * tests mean that post-compbat, this fleet will be blockaded iff it was
1206 * blockaded pre-combat AND there are armed aggressive enemies surviving in
1207 * system post-combat which can detect this fleet. Fleets arriving at the
1208 * same time do not blockade each other. Unrestricted lane access (i.e,
1209 * (fleet->ArrivalStarlane() == system->ID()) ) is used as a proxy for
1210 * order of arrival -- if an enemy has unrestricted lane access and you
1211 * don't, they must have arrived before you, or be in cahoots with someone
1212 * who did. */
1213
1214 if (m_arrival_starlane == start_system_id) {
1215 //DebugLogger() << "Fleet::BlockadedAtSystem fleet " << ID() << " has cleared blockade flag for system (" << start_system_id << ")";
1216 return false;
1217 }
1218 bool not_yet_in_system = SystemID() != start_system_id;
1219
1220 if (!not_yet_in_system && m_arrival_starlane == dest_system_id)
1221 return false;
1222
1223 // find which empires have blockading aggressive armed ships in system;
1224 // fleets that just arrived do not blockade by themselves, but may
1225 // reinforce a preexisting blockade, and may possibly contribute to detection
1226 auto current_system = Objects().get<System>(start_system_id);
1227 if (!current_system) {
1228 DebugLogger() << "Fleet::BlockadedAtSystem fleet " << ID() << " considering system (" << start_system_id << ") but can't retrieve system copy";
1229 return false;
1230 }
1231
1232 auto empire = GetEmpire(this->Owner());
1233 if (empire) {
1234 auto unobstructed_systems = empire->SupplyUnobstructedSystems();
1235 if (unobstructed_systems.count(start_system_id))
1236 return false;
1237 if (empire->PreservedLaneTravel(start_system_id, dest_system_id)) {
1238 return false;
1239 } else {
1240 TraceLogger() << "Fleet::BlockadedAtSystem fleet " << ID() << " considering travel from system (" << start_system_id << ") to system (" << dest_system_id << ")";
1241 }
1242 }
1243
1244 float lowest_ship_stealth = 99999.9f; // arbitrary large number. actual stealth of ships should be less than this...
1245 for (auto& ship : Objects().find<const Ship>(this->ShipIDs())) {
1246 float ship_stealth = ship->GetMeter(METER_STEALTH)->Current();
1247 if (lowest_ship_stealth > ship_stealth)
1248 lowest_ship_stealth = ship_stealth;
1249 }
1250
1251 float monster_detection = 0.0f;
1252 auto fleets = Objects().find<const Fleet>(current_system->FleetIDs());
1253 for (auto& fleet : fleets) {
1254 if (!fleet->Unowned())
1255 continue;
1256
1257 for (auto& ship : Objects().find<const Ship>(fleet->ShipIDs())) {
1258 float cur_detection = ship->GetMeter(METER_DETECTION)->Current();
1259 if (cur_detection >= monster_detection)
1260 monster_detection = cur_detection;
1261 }
1262 }
1263
1264 bool can_be_blockaded = false;
1265 for (auto& fleet : fleets) {
1266 if (fleet->NextSystemID() != INVALID_OBJECT_ID) //fleets trying to leave this turn can't blockade pre-combat.
1267 continue;
1268 bool unrestricted = (fleet->m_arrival_starlane == start_system_id);
1269 if (fleet->Owner() == this->Owner()) {
1270 if (unrestricted) // perhaps should consider allies
1271 return false;
1272 continue;
1273 }
1274 if (!unrestricted && !not_yet_in_system)
1275 continue;
1276
1277 bool can_see;
1278 if (!fleet->Unowned())
1279 can_see = (GetEmpire(fleet->Owner())->GetMeter("METER_DETECTION_STRENGTH")->Current() >= lowest_ship_stealth);
1280 else
1281 can_see = (monster_detection >= lowest_ship_stealth);
1282 if (!can_see)
1283 continue;
1284
1285 bool at_war = Unowned() || fleet->Unowned() ||
1286 Empires().GetDiplomaticStatus(this->Owner(), fleet->Owner()) == DIPLO_WAR;
1287 if (!at_war)
1288 continue;
1289 bool aggressive = (fleet->Aggressive() || fleet->Unowned());
1290 if (!aggressive)
1291 continue;
1292 // Newly created ships/monsters are not allowed to block other fleet movement since they have not even
1293 // potentially gone through a combat round at the present location. Potential sources for such new ships are
1294 // monsters created via Effect and Ships/fleets newly constructed by empires. We check ship ages not fleet
1295 // ageas since fleets can be created/destroyed as purely organizational matters. Since these checks are
1296 // pertinent just during those stages of turn processing immediately following turn number advancement,
1297 // whereas the new ships were created just prior to turn advamcenemt, we require age greater than 1.
1298 if (fleet->MaxShipAgeInTurns() <= 1)
1299 continue;
1300 // These are the most costly checks. Do them last
1301 if (!fleet->HasArmedShips())
1302 continue;
1303
1304 // don't exit early here, because blockade may yet be thwarted by ownership & presence check above
1305 can_be_blockaded = true;
1306
1307 }
1308
1309 return can_be_blockaded;
1310 }
1311
Speed() const1312 float Fleet::Speed() const {
1313 if (m_ships.empty())
1314 return 0.0f;
1315
1316 bool fleet_is_scrapped = true;
1317 float retval = MAX_SHIP_SPEED; // max speed no ship can go faster than
1318 for (const auto& ship : Objects().find<Ship>(m_ships)) {
1319 if (!ship || ship->OrderedScrapped())
1320 continue;
1321 if (ship->Speed() < retval)
1322 retval = ship->Speed();
1323 fleet_is_scrapped = false;
1324 }
1325
1326 if (fleet_is_scrapped)
1327 retval = 0.0f;
1328
1329 return retval;
1330 }
1331
Damage() const1332 float Fleet::Damage() const {
1333 if (m_ships.empty())
1334 return 0.0f;
1335
1336 bool fleet_is_scrapped = true;
1337 float retval = 0.0f;
1338 for (const auto& ship : Objects().find<Ship>(m_ships)) {
1339 if (!ship || ship->OrderedScrapped())
1340 continue;
1341 if (const auto design = ship->Design())
1342 retval += design->Attack();
1343 fleet_is_scrapped = false;
1344 }
1345
1346 if (fleet_is_scrapped)
1347 retval = 0.0f;
1348
1349 return retval;
1350 }
1351
Structure() const1352 float Fleet::Structure() const {
1353 if (m_ships.empty())
1354 return 0.0f;
1355
1356 bool fleet_is_scrapped = true;
1357 float retval = 0.0f;
1358 for (const auto& ship : Objects().find<Ship>(m_ships)) {
1359 if (!ship || ship->OrderedScrapped())
1360 continue;
1361 retval += ship->GetMeter(METER_STRUCTURE)->Current();
1362 fleet_is_scrapped = false;
1363 }
1364
1365 if (fleet_is_scrapped)
1366 retval = 0.0f;
1367
1368 return retval;
1369 }
1370
Shields() const1371 float Fleet::Shields() const {
1372 if (m_ships.empty())
1373 return 0.0f;
1374
1375 bool fleet_is_scrapped = true;
1376 float retval = 0.0f;
1377 for (const auto& ship : Objects().find<Ship>(m_ships)) {
1378 if (!ship || ship->OrderedScrapped())
1379 continue;
1380 retval += ship->GetMeter(METER_SHIELD)->Current();
1381 fleet_is_scrapped = false;
1382 }
1383
1384 if (fleet_is_scrapped)
1385 retval = 0.0f;
1386
1387 return retval;
1388 }
1389
1390 namespace {
IsCombatShip(const Ship & ship)1391 bool IsCombatShip(const Ship& ship)
1392 { return ship.IsArmed() || ship.HasFighters() || ship.CanHaveTroops() || ship.CanBombard(); }
1393 }
1394
GenerateFleetName()1395 std::string Fleet::GenerateFleetName() {
1396 // TODO: Change returned name based on passed ship designs. eg. return "colony fleet" if
1397 // ships are colony ships, or "battle fleet" if ships are armed.
1398 if (ID() == INVALID_OBJECT_ID)
1399 return UserString("NEW_FLEET_NAME_NO_NUMBER");
1400
1401 std::vector<std::shared_ptr<const Ship>> ships;
1402 for (const auto& ship : Objects().find<Ship>(m_ships)) {
1403 if (!ship)
1404 continue;
1405 ships.push_back(ship);
1406 }
1407
1408 std::string fleet_name_key = UserStringNop("NEW_FLEET_NAME");
1409
1410 if (boost::algorithm::all_of(ships, [](const auto& ship){ return ship->IsMonster(); }))
1411 fleet_name_key = UserStringNop("NEW_MONSTER_FLEET_NAME");
1412 else if (boost::algorithm::all_of(ships, [](const auto& ship){ return ship->CanColonize(); }))
1413 fleet_name_key = UserStringNop("NEW_COLONY_FLEET_NAME");
1414 else if (boost::algorithm::all_of(ships, [](const auto& ship){ return !IsCombatShip(*ship); }))
1415 fleet_name_key = UserStringNop("NEW_RECON_FLEET_NAME");
1416 else if (boost::algorithm::all_of(ships, [](const auto& ship){ return ship->CanHaveTroops(); }))
1417 fleet_name_key = UserStringNop("NEW_TROOP_FLEET_NAME");
1418 else if (boost::algorithm::all_of(ships, [](const auto& ship){ return ship->CanBombard(); }))
1419 fleet_name_key = UserStringNop("NEW_BOMBARD_FLEET_NAME");
1420 else if (boost::algorithm::all_of(ships, [](const auto& ship){ return IsCombatShip(*ship); }))
1421 fleet_name_key = UserStringNop("NEW_BATTLE_FLEET_NAME");
1422
1423 return boost::io::str(FlexibleFormat(UserString(fleet_name_key)) % ID());
1424 }
1425
SetGiveToEmpire(int empire_id)1426 void Fleet::SetGiveToEmpire(int empire_id) {
1427 if (empire_id == m_ordered_given_to_empire_id) return;
1428 m_ordered_given_to_empire_id = empire_id;
1429 StateChangedSignal();
1430 }
1431
ClearGiveToEmpire()1432 void Fleet::ClearGiveToEmpire()
1433 { SetGiveToEmpire(ALL_EMPIRES); }
1434
1435