1 /* Fleet.cpp
2 Copyright (c) 2014 by Michael Zahniser
3
4 Endless Sky is free software: you can redistribute it and/or modify it under the
5 terms of the GNU General Public License as published by the Free Software
6 Foundation, either version 3 of the License, or (at your option) any later version.
7
8 Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
9 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10 PARTICULAR PURPOSE. See the GNU General Public License for more details.
11 */
12
13 #include "Fleet.h"
14
15 #include "DataNode.h"
16 #include "Files.h"
17 #include "GameData.h"
18 #include "Government.h"
19 #include "Phrase.h"
20 #include "pi.h"
21 #include "Planet.h"
22 #include "Random.h"
23 #include "Ship.h"
24 #include "StellarObject.h"
25 #include "System.h"
26
27 #include <algorithm>
28 #include <cmath>
29 #include <iterator>
30
31 using namespace std;
32
33 namespace {
34 // Generate an offset magnitude that will sample from an annulus (planets)
35 // or a circle (systems without inhabited planets).
OffsetFrom(pair<Point,double> & center)36 double OffsetFrom(pair<Point, double> ¢er)
37 {
38 // If the center has a radius, then position ships further away.
39 double minimumOffset = center.second ? 1. : 0.;
40 // Since it is sensible that ships would be nearer to the object of
41 // interest on average, do not apply the sqrt(rand) correction.
42 return (Random::Real() + minimumOffset) * 400. + 2. * center.second;
43 }
44
45 // Construct a list of all outfits for sale in this system and its linked neighbors.
GetOutfitsForSale(const System * here)46 Sale<Outfit> GetOutfitsForSale(const System *here)
47 {
48 auto outfits = Sale<Outfit>();
49 if(here)
50 {
51 for(const StellarObject &object : here->Objects())
52 {
53 const Planet *planet = object.GetPlanet();
54 if(planet && planet->IsValid() && planet->HasOutfitter())
55 outfits.Add(planet->Outfitter());
56 }
57 }
58 return outfits;
59 }
60
61 // Construct a list of varying numbers of outfits that were either specified for
62 // this fleet directly, or are sold in this system or its linked neighbors.
OutfitChoices(const set<const Sale<Outfit> * > & outfitters,const System * hub,int maxSize)63 vector<const Outfit *> OutfitChoices(const set<const Sale<Outfit> *> &outfitters, const System *hub, int maxSize)
64 {
65 auto outfits = vector<const Outfit *>();
66 if(maxSize > 0)
67 {
68 auto choices = Sale<Outfit>();
69 // If no outfits were directly specified, choose from those sold nearby.
70 if(outfitters.empty() && hub)
71 {
72 choices = GetOutfitsForSale(hub);
73 for(const System *other : hub->Links())
74 choices.Add(GetOutfitsForSale(other));
75 }
76 else
77 for(const auto outfitter : outfitters)
78 choices.Add(*outfitter);
79
80 if(!choices.empty())
81 {
82 for(const auto outfit : choices)
83 {
84 double mass = outfit->Mass();
85 // Avoid free outfits, massless outfits, and those too large to fit.
86 if(mass > 0. && mass < maxSize && outfit->Cost() > 0)
87 {
88 // Also avoid outfits that add space (such as Outfits / Cargo Expansions)
89 // or modify bunks.
90 // TODO: Specify rejection criteria in datafiles as ConditionSets or similar.
91 const auto &attributes = outfit->Attributes();
92 if(attributes.Get("outfit space") > 0.
93 || attributes.Get("cargo space") > 0.
94 || attributes.Get("bunks"))
95 continue;
96
97 outfits.push_back(outfit);
98 }
99 }
100 }
101 }
102 // Sort this list of choices ascending by mass, so it can be easily trimmed to just
103 // the outfits that fit as the ship's free space decreases.
104 sort(outfits.begin(), outfits.end(), [](const Outfit *a, const Outfit *b)
105 { return a->Mass() < b->Mass(); });
106 return outfits;
107 }
108
109 // Add a random commodity from the list to the ship's cargo.
AddRandomCommodity(Ship & ship,int freeSpace,const vector<string> & commodities)110 void AddRandomCommodity(Ship &ship, int freeSpace, const vector<string> &commodities)
111 {
112 int index = Random::Int(GameData::Commodities().size());
113 if(!commodities.empty())
114 {
115 // If a list of possible commodities was given, pick one of them at
116 // random and then double-check that it's a valid commodity name.
117 const string &name = commodities[Random::Int(commodities.size())];
118 for(const auto &it : GameData::Commodities())
119 if(it.name == name)
120 {
121 index = &it - &GameData::Commodities().front();
122 break;
123 }
124 }
125
126 const Trade::Commodity &commodity = GameData::Commodities()[index];
127 int amount = Random::Int(freeSpace) + 1;
128 ship.Cargo().Add(commodity.name, amount);
129 }
130
131 // Add a random outfit from the list to the ship's cargo.
AddRandomOutfit(Ship & ship,int freeSpace,const vector<const Outfit * > & outfits)132 void AddRandomOutfit(Ship &ship, int freeSpace, const vector<const Outfit *> &outfits)
133 {
134 if(outfits.empty())
135 return;
136 int index = Random::Int(outfits.size());
137 const Outfit *picked = outfits[index];
138 int maxQuantity = floor(static_cast<double>(freeSpace) / picked->Mass());
139 int amount = Random::Int(maxQuantity) + 1;
140 ship.Cargo().Add(picked, amount);
141 }
142 }
143
144
145
146 // Construct and Load() at the same time.
Fleet(const DataNode & node)147 Fleet::Fleet(const DataNode &node)
148 {
149 Load(node);
150 }
151
152
153
Load(const DataNode & node)154 void Fleet::Load(const DataNode &node)
155 {
156 if(node.Size() >= 2)
157 fleetName = node.Token(1);
158
159 // If Load() has already been called once on this fleet, any subsequent
160 // calls will replace the variants instead of adding to them.
161 bool resetVariants = !variants.empty();
162
163 for(const DataNode &child : node)
164 {
165 // The "add" and "remove" keywords should never be alone on a line, and
166 // are only valid with "variant" or "personality" definitions.
167 bool add = (child.Token(0) == "add");
168 bool remove = (child.Token(0) == "remove");
169 bool hasValue = (child.Size() >= 2);
170 if((add || remove) && (!hasValue || (child.Token(1) != "variant" && child.Token(1) != "personality")))
171 {
172 child.PrintTrace("Skipping invalid \"" + child.Token(0) + "\" tag:");
173 continue;
174 }
175
176 // If this line is an add or remove, the key is the token at index 1.
177 const string &key = child.Token(add || remove);
178
179 if(key == "government" && hasValue)
180 government = GameData::Governments().Get(child.Token(1));
181 else if(key == "names" && hasValue)
182 names = GameData::Phrases().Get(child.Token(1));
183 else if(key == "fighters" && hasValue)
184 fighterNames = GameData::Phrases().Get(child.Token(1));
185 else if(key == "cargo" && hasValue)
186 cargo = static_cast<int>(child.Value(1));
187 else if(key == "commodities" && hasValue)
188 {
189 commodities.clear();
190 for(int i = 1; i < child.Size(); ++i)
191 commodities.push_back(child.Token(i));
192 }
193 else if(key == "outfitters" && hasValue)
194 {
195 outfitters.clear();
196 for(int i = 1; i < child.Size(); ++i)
197 outfitters.insert(GameData::Outfitters().Get(child.Token(i)));
198 }
199 else if(key == "personality")
200 personality.Load(child);
201 else if(key == "variant" && !remove)
202 {
203 if(resetVariants && !add)
204 {
205 resetVariants = false;
206 variants.clear();
207 total = 0;
208 }
209 variants.emplace_back(child);
210 total += variants.back().weight;
211 }
212 else if(key == "variant")
213 {
214 // If given a full ship definition of one of this fleet's variant members, remove the variant.
215 bool didRemove = false;
216 Variant toRemove(child);
217 for(auto it = variants.begin(); it != variants.end(); ++it)
218 if(toRemove.ships.size() == it->ships.size() &&
219 is_permutation(it->ships.begin(), it->ships.end(), toRemove.ships.begin()))
220 {
221 total -= it->weight;
222 variants.erase(it);
223 didRemove = true;
224 break;
225 }
226
227 if(!didRemove)
228 child.PrintTrace("Did not find matching variant for specified operation:");
229 }
230 else
231 child.PrintTrace("Skipping unrecognized attribute:");
232 }
233
234 if(variants.empty())
235 node.PrintTrace("Warning: " + (fleetName.empty() ? "unnamed fleet" : "Fleet \"" + fleetName + "\"") + " contains no variants:");
236 }
237
238
239
IsValid(bool requireGovernment) const240 bool Fleet::IsValid(bool requireGovernment) const
241 {
242 // Generally, a government is required for a fleet to be valid.
243 if(requireGovernment && !government)
244 return false;
245
246 if(names && names->IsEmpty())
247 return false;
248
249 if(fighterNames && fighterNames->IsEmpty())
250 return false;
251
252 // A fleet's variants should reference at least one valid ship.
253 for(auto &&v : variants)
254 if(none_of(v.ships.begin(), v.ships.end(),
255 [](const Ship *const s) noexcept -> bool { return s->IsValid(); }))
256 return false;
257
258 return true;
259 }
260
261
262
RemoveInvalidVariants()263 void Fleet::RemoveInvalidVariants()
264 {
265 auto IsInvalidVariant = [](const Variant &v) noexcept -> bool
266 {
267 return v.ships.empty() || none_of(v.ships.begin(), v.ships.end(),
268 [](const Ship *const s) noexcept -> bool { return s->IsValid(); });
269 };
270 auto firstInvalid = find_if(variants.begin(), variants.end(), IsInvalidVariant);
271 if(firstInvalid == variants.end())
272 return;
273
274 // Ensure the class invariant can be maintained.
275 // (This must be done first as we cannot do anything but `erase` elements filtered by `remove_if`.)
276 int removedWeight = 0;
277 for(auto it = firstInvalid; it != variants.end(); ++it)
278 if(IsInvalidVariant(*it))
279 removedWeight += it->weight;
280
281 auto removeIt = remove_if(firstInvalid, variants.end(), IsInvalidVariant);
282 int count = distance(removeIt, variants.end());
283 Files::LogError("Warning: " + (fleetName.empty() ? "unnamed fleet" : "fleet \"" + fleetName + "\"")
284 + ": Removing " + to_string(count) + " invalid " + (count > 1 ? "variants" : "variant")
285 + " (" + to_string(removedWeight) + " of " + to_string(total) + " weight)");
286
287 total -= removedWeight;
288 variants.erase(removeIt, variants.end());
289 }
290
291
292
293 // Get the government of this fleet.
GetGovernment() const294 const Government *Fleet::GetGovernment() const
295 {
296 return government;
297 }
298
299
300 // Choose a fleet to be created during flight, and have it enter the system via jump or planetary departure.
Enter(const System & system,list<shared_ptr<Ship>> & ships,const Planet * planet) const301 void Fleet::Enter(const System &system, list<shared_ptr<Ship>> &ships, const Planet *planet) const
302 {
303 if(!total || variants.empty())
304 return;
305
306 // Pick a fleet variant to instantiate.
307 const Variant &variant = ChooseVariant();
308 if(variant.ships.empty())
309 return;
310
311 // Figure out what system the fleet is starting in, where it is going, and
312 // what position it should start from in the system.
313 const System *source = &system;
314 const System *target = &system;
315 Point position;
316 double radius = 1000.;
317
318 // Only pick a random entry point for this fleet if a source planet was not specified.
319 if(!planet)
320 {
321 // Where this fleet can come from depends on whether it is friendly to any
322 // planets in this system and whether it has jump drives.
323 vector<const System *> linkVector;
324 // Find out what the "best" jump method the fleet has is. Assume that if the
325 // others don't have that jump method, they are being carried as fighters.
326 // That is, content creators should avoid creating fleets with a mix of jump
327 // drives and hyperdrives.
328 bool hasJump = false;
329 bool hasHyper = false;
330 double jumpDistance = System::DEFAULT_NEIGHBOR_DISTANCE;
331 for(const Ship *ship : variant.ships)
332 {
333 if(ship->Attributes().Get("jump drive"))
334 {
335 hasJump = true;
336 jumpDistance = ship->JumpRange();
337 break;
338 }
339 if(ship->Attributes().Get("hyperdrive"))
340 hasHyper = true;
341 }
342 // Don't try to make a fleet "enter" from another system if none of the
343 // ships have jump drives.
344 if(hasJump || hasHyper)
345 {
346 bool isWelcomeHere = !system.GetGovernment()->IsEnemy(government);
347 for(const System *neighbor : (hasJump ? system.JumpNeighbors(jumpDistance) : system.Links()))
348 {
349 // If this ship is not "welcome" in the current system, prefer to have
350 // it enter from a system that is friendly to it. (This is for realism,
351 // so attack fleets don't come from what ought to be a safe direction.)
352 if(isWelcomeHere || neighbor->GetGovernment()->IsEnemy(government))
353 linkVector.push_back(neighbor);
354 else
355 linkVector.insert(linkVector.end(), 8, neighbor);
356 }
357 }
358
359 // Find all the inhabited planets this fleet could take off from.
360 vector<const Planet *> planetVector;
361 if(!personality.IsSurveillance())
362 for(const StellarObject &object : system.Objects())
363 if(object.HasValidPlanet() && object.GetPlanet()->HasSpaceport()
364 && !object.GetPlanet()->GetGovernment()->IsEnemy(government))
365 planetVector.push_back(object.GetPlanet());
366
367 // If there is nowhere for this fleet to come from, don't create it.
368 size_t options = linkVector.size() + planetVector.size();
369 if(!options)
370 {
371 // Prefer to launch from inhabited planets, but launch from
372 // uninhabited ones if there is no other option.
373 for(const StellarObject &object : system.Objects())
374 if(object.HasValidPlanet() && !object.GetPlanet()->GetGovernment()->IsEnemy(government))
375 planetVector.push_back(object.GetPlanet());
376 options = planetVector.size();
377 if(!options)
378 return;
379 }
380
381 // Choose a random planet or star system to come from.
382 size_t choice = Random::Int(options);
383
384 // If a planet is chosen, also pick a system to travel to after taking off.
385 if(choice >= linkVector.size())
386 {
387 planet = planetVector[choice - linkVector.size()];
388 if(!linkVector.empty())
389 target = linkVector[Random::Int(linkVector.size())];
390 }
391 // We are entering this system via hyperspace, not taking off from a planet.
392 else
393 source = linkVector[choice];
394 }
395
396 auto placed = Instantiate(variant);
397 // Carry all ships that can be carried, as they don't need to be positioned
398 // or checked to see if they can access a particular planet.
399 for(auto &ship : placed)
400 PlaceFighter(ship, placed);
401
402 // Find the stellar object for this planet, and place the ships there.
403 if(planet)
404 {
405 const StellarObject *object = system.FindStellar(planet);
406 if(!object)
407 {
408 // Log this error.
409 Files::LogError("Fleet::Enter: Unable to find valid stellar object for planet \""
410 + planet->TrueName() + "\" in system \"" + system.Name() + "\"");
411 return;
412 }
413 // To take off from the planet, all non-carried ships must be able to access it.
414 else if(planet->IsUnrestricted() || all_of(placed.cbegin(), placed.cend(), [&](const shared_ptr<Ship> &ship)
415 { return ship->GetParent() || planet->IsAccessible(ship.get()); }))
416 {
417 position = object->Position();
418 radius = object->Radius();
419 }
420 // The chosen planet could not be departed from by all ships in the variant.
421 else
422 {
423 // If there are no departure paths, then there are no arrival paths either.
424 if(source == target)
425 return;
426 // Otherwise, have the fleet arrive here from the target system.
427 std::swap(source, target);
428 planet = nullptr;
429 }
430 }
431
432 // Place all the ships in the chosen fleet variant.
433 shared_ptr<Ship> flagship;
434 for(shared_ptr<Ship> &ship : placed)
435 {
436 // If this is a carried fighter, no need to position it.
437 if(ship->GetParent())
438 continue;
439
440 Angle angle = Angle::Random(360.);
441 Point pos = position + angle.Unit() * (Random::Real() * radius);
442
443 ships.push_front(ship);
444 ship->SetSystem(source);
445 ship->SetPlanet(planet);
446 if(source == &system)
447 ship->Place(pos, angle.Unit(), angle);
448 else
449 {
450 // Place the ship stationary and pointed in the right direction.
451 angle = Angle(system.Position() - source->Position());
452 ship->Place(pos, Point(), angle);
453 }
454 if(target != source)
455 ship->SetTargetSystem(target);
456
457 if(flagship)
458 ship->SetParent(flagship);
459 else
460 flagship = ship;
461
462 SetCargo(&*ship);
463 }
464 }
465
466
467
468 // Place one of the variants in the given system, already "in action." If the carried flag is set,
469 // only uncarried ships will be added to the list (as any carriables will be stored in bays).
Place(const System & system,list<shared_ptr<Ship>> & ships,bool carried) const470 void Fleet::Place(const System &system, list<shared_ptr<Ship>> &ships, bool carried) const
471 {
472 if(!total || variants.empty())
473 return;
474
475 // Pick a fleet variant to instantiate.
476 const Variant &variant = ChooseVariant();
477 if(variant.ships.empty())
478 return;
479
480 // Determine where the fleet is going to or coming from.
481 auto center = ChooseCenter(system);
482
483 // Place all the ships in the chosen fleet variant.
484 shared_ptr<Ship> flagship;
485 vector<shared_ptr<Ship>> placed = Instantiate(variant);
486 for(shared_ptr<Ship> &ship : placed)
487 {
488 // If this is a fighter and someone can carry it, no need to position it.
489 if(carried && PlaceFighter(ship, placed))
490 continue;
491
492 Angle angle = Angle::Random();
493 Point pos = center.first + Angle::Random().Unit() * OffsetFrom(center);
494 double velocity = Random::Real() * ship->MaxVelocity();
495
496 ships.push_front(ship);
497 ship->SetSystem(&system);
498 ship->Place(pos, velocity * angle.Unit(), angle);
499
500 if(flagship)
501 ship->SetParent(flagship);
502 else
503 flagship = ship;
504
505 SetCargo(&*ship);
506 }
507 }
508
509
510
511 // Do the randomization to make a ship enter or be in the given system.
Enter(const System & system,Ship & ship,const System * source)512 const System *Fleet::Enter(const System &system, Ship &ship, const System *source)
513 {
514 if(system.Links().empty() || (source && !system.Links().count(source)))
515 {
516 Place(system, ship);
517 return &system;
518 }
519
520 // Choose which system this ship is coming from.
521 if(!source)
522 {
523 auto it = system.Links().cbegin();
524 advance(it, Random::Int(system.Links().size()));
525 source = *it;
526 }
527
528 Angle angle = Angle::Random();
529 Point pos = angle.Unit() * Random::Real() * 1000.;
530
531 ship.Place(pos, angle.Unit(), angle);
532 ship.SetSystem(source);
533 ship.SetTargetSystem(&system);
534
535 return source;
536 }
537
538
539
Place(const System & system,Ship & ship)540 void Fleet::Place(const System &system, Ship &ship)
541 {
542 // Choose a random inhabited object in the system to spawn around.
543 auto center = ChooseCenter(system);
544 Point pos = center.first + Angle::Random().Unit() * OffsetFrom(center);
545
546 double velocity = ship.IsDisabled() ? 0. : Random::Real() * ship.MaxVelocity();
547
548 ship.SetSystem(&system);
549 Angle angle = Angle::Random();
550 ship.Place(pos, velocity * angle.Unit(), angle);
551 }
552
553
554
Strength() const555 int64_t Fleet::Strength() const
556 {
557 if(!total || variants.empty())
558 return 0;
559
560 int64_t sum = 0;
561 for(const Variant &variant : variants)
562 {
563 int64_t thisSum = 0;
564 for(const Ship *ship : variant.ships)
565 thisSum += ship->Cost();
566 sum += thisSum * variant.weight;
567 }
568 return sum / total;
569 }
570
571
572
Variant(const DataNode & node)573 Fleet::Variant::Variant(const DataNode &node)
574 {
575 weight = 1;
576 if(node.Token(0) == "variant" && node.Size() >= 2)
577 weight = node.Value(1);
578 else if(node.Token(0) == "add" && node.Size() >= 3)
579 weight = node.Value(2);
580
581 for(const DataNode &child : node)
582 {
583 int n = 1;
584 if(child.Size() >= 2 && child.Value(1) >= 1.)
585 n = child.Value(1);
586 ships.insert(ships.end(), n, GameData::Ships().Get(child.Token(0)));
587 }
588 }
589
590
591
ChooseVariant() const592 const Fleet::Variant &Fleet::ChooseVariant() const
593 {
594 // Pick a random variant based on the weights.
595 unsigned index = 0;
596 for(int choice = Random::Int(total); choice >= variants[index].weight; ++index)
597 choice -= variants[index].weight;
598
599 return variants[index];
600 }
601
602
603
604 // Obtain a positional reference and the radius of the object at that position (e.g. a planet).
605 // Spaceport status can be modified during normal gameplay, so this information is not cached.
ChooseCenter(const System & system)606 pair<Point, double> Fleet::ChooseCenter(const System &system)
607 {
608 auto centers = vector<pair<Point, double>>();
609 for(const StellarObject &object : system.Objects())
610 if(object.HasValidPlanet() && object.GetPlanet()->HasSpaceport())
611 centers.emplace_back(object.Position(), object.Radius());
612
613 if(centers.empty())
614 return {Point(), 0.};
615 return centers[Random::Int(centers.size())];
616 }
617
618
619
Instantiate(const Variant & variant) const620 vector<shared_ptr<Ship>> Fleet::Instantiate(const Variant &variant) const
621 {
622 vector<shared_ptr<Ship>> placed;
623 for(const Ship *model : variant.ships)
624 {
625 // At least one of this variant's ships is valid, but we should avoid spawning any that are not defined.
626 if(!model->IsValid())
627 {
628 Files::LogError("Skipping invalid ship model \"" + model->ModelName() + "\" in fleet \"" + fleetName + "\".");
629 continue;
630 }
631
632 auto ship = make_shared<Ship>(*model);
633
634 const Phrase *phrase = ((ship->CanBeCarried() && fighterNames) ? fighterNames : names);
635 if(phrase)
636 ship->SetName(phrase->Get());
637 ship->SetGovernment(government);
638 ship->SetPersonality(personality);
639
640 placed.push_back(ship);
641 }
642 return placed;
643 }
644
645
646
PlaceFighter(shared_ptr<Ship> fighter,vector<shared_ptr<Ship>> & placed) const647 bool Fleet::PlaceFighter(shared_ptr<Ship> fighter, vector<shared_ptr<Ship>> &placed) const
648 {
649 if(!fighter->CanBeCarried())
650 return false;
651
652 for(const shared_ptr<Ship> &parent : placed)
653 if(parent->Carry(fighter))
654 return true;
655
656 return false;
657 }
658
659
660
661 // Choose the cargo associated with this ship in the fleet.
662 // If outfits were specified, but not commodities, do not pick commodities.
663 // If commodities were specified, but not outfits, do not pick outfits.
664 // If neither or both were specified, choose commodities more often..
SetCargo(Ship * ship) const665 void Fleet::SetCargo(Ship *ship) const
666 {
667 const bool canChooseOutfits = commodities.empty() || !outfitters.empty();
668 const bool canChooseCommodities = outfitters.empty() || !commodities.empty();
669 // Populate the possible outfits that may be chosen.
670 int free = ship->Cargo().Free();
671 auto outfits = OutfitChoices(outfitters, ship->GetSystem(), free);
672
673 // Choose random outfits or commodities to transport.
674 for(int i = 0; i < cargo; ++i)
675 {
676 if(free <= 0)
677 break;
678 // Remove any outfits that do not fit into remaining cargo.
679 if(canChooseOutfits && !outfits.empty())
680 outfits.erase(remove_if(outfits.begin(), outfits.end(),
681 [&free](const Outfit *a) { return a->Mass() > free; }),
682 outfits.end());
683
684 if(canChooseCommodities && canChooseOutfits)
685 {
686 if(Random::Real() < .8)
687 AddRandomCommodity(*ship, free, commodities);
688 else
689 AddRandomOutfit(*ship, free, outfits);
690 }
691 else if(canChooseCommodities)
692 AddRandomCommodity(*ship, free, commodities);
693 else
694 AddRandomOutfit(*ship, free, outfits);
695
696 free = ship->Cargo().Free();
697 }
698 int extraCrew = ship->Attributes().Get("bunks") - ship->RequiredCrew();
699 if(extraCrew > 0)
700 ship->AddCrew(Random::Int(extraCrew + 1));
701 }
702