1 /* PlayerInfo.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 "PlayerInfo.h"
14
15 #include "Audio.h"
16 #include "ConversationPanel.h"
17 #include "DataFile.h"
18 #include "DataWriter.h"
19 #include "Dialog.h"
20 #include "Files.h"
21 #include "text/Format.h"
22 #include "GameData.h"
23 #include "Government.h"
24 #include "Hardpoint.h"
25 #include "Messages.h"
26 #include "Mission.h"
27 #include "Outfit.h"
28 #include "Person.h"
29 #include "Planet.h"
30 #include "Politics.h"
31 #include "Preferences.h"
32 #include "Random.h"
33 #include "SavedGame.h"
34 #include "Ship.h"
35 #include "ShipEvent.h"
36 #include "StartConditions.h"
37 #include "StellarObject.h"
38 #include "System.h"
39 #include "UI.h"
40
41 #include <algorithm>
42 #include <cmath>
43 #include <ctime>
44 #include <iterator>
45 #include <limits>
46 #include <sstream>
47 #include <stdexcept>
48
49 using namespace std;
50
51
52
53 // Completely clear all loaded information, to prepare for loading a file or
54 // creating a new pilot.
Clear()55 void PlayerInfo::Clear()
56 {
57 *this = PlayerInfo();
58
59 Random::Seed(time(nullptr));
60 GameData::Revert();
61 Messages::Reset();
62 }
63
64
65
66 // Check if a player has been loaded.
IsLoaded() const67 bool PlayerInfo::IsLoaded() const
68 {
69 return !firstName.empty();
70 }
71
72
73
74 // Make a new player.
New(const StartConditions & start)75 void PlayerInfo::New(const StartConditions &start)
76 {
77 // Clear any previously loaded data.
78 Clear();
79
80 // Copy the core information from the full starting scenario.
81 startData = start;
82 // Copy any ships in the start conditions.
83 for(const Ship &ship : start.Ships())
84 {
85 ships.emplace_back(new Ship(ship));
86 ships.back()->SetSystem(&start.GetSystem());
87 ships.back()->SetPlanet(&start.GetPlanet());
88 ships.back()->SetIsSpecial();
89 ships.back()->SetIsYours();
90 ships.back()->SetGovernment(GameData::PlayerGovernment());
91 }
92 // Load starting conditions from a "start" item in the data files. If no
93 // such item exists, StartConditions defines default values.
94 date = start.GetDate();
95 GameData::SetDate(date);
96 // Make sure the fleet depreciation object knows it is tracking the player's
97 // fleet, not the planet's stock.
98 depreciation.Init(ships, date.DaysSinceEpoch());
99
100 SetSystem(start.GetSystem());
101 SetPlanet(&start.GetPlanet());
102 accounts = start.GetAccounts();
103 start.GetConditions().Apply(conditions);
104 UpdateAutoConditions();
105
106 // Generate missions that will be available on the first day.
107 CreateMissions();
108
109 // Add to the list of events that should happen on certain days.
110 for(const auto &it : GameData::Events())
111 if(it.second.GetDate())
112 AddEvent(it.second, it.second.GetDate());
113 }
114
115
116
117 // Load player information from a saved game file.
Load(const string & path)118 void PlayerInfo::Load(const string &path)
119 {
120 // Make sure any previously loaded data is cleared.
121 Clear();
122
123 filePath = path;
124 // Strip anything after the "~" from snapshots, so that the file we save
125 // will be the auto-save, not the snapshot.
126 size_t pos = filePath.find('~');
127 size_t namePos = filePath.length() - Files::Name(filePath).length();
128 if(pos != string::npos && pos > namePos)
129 filePath = filePath.substr(0, pos) + ".txt";
130
131 // The player may have bribed their current planet in the last session. Ensure
132 // we provide the same access to services in this session, too.
133 bool hasFullClearance = false;
134
135 DataFile file(path);
136 for(const DataNode &child : file)
137 {
138 // Basic player information and persistent UI settings:
139 if(child.Token(0) == "pilot" && child.Size() >= 3)
140 {
141 firstName = child.Token(1);
142 lastName = child.Token(2);
143 }
144 else if(child.Token(0) == "date" && child.Size() >= 4)
145 date = Date(child.Value(1), child.Value(2), child.Value(3));
146 else if(child.Token(0) == "system" && child.Size() >= 2)
147 system = GameData::Systems().Get(child.Token(1));
148 else if(child.Token(0) == "planet" && child.Size() >= 2)
149 planet = GameData::Planets().Get(child.Token(1));
150 else if(child.Token(0) == "clearance")
151 hasFullClearance = true;
152 else if(child.Token(0) == "launching")
153 shouldLaunch = true;
154 else if(child.Token(0) == "playtime" && child.Size() >= 2)
155 playTime = child.Value(1);
156 else if(child.Token(0) == "travel" && child.Size() >= 2)
157 travelPlan.push_back(GameData::Systems().Get(child.Token(1)));
158 else if(child.Token(0) == "travel destination" && child.Size() >= 2)
159 travelDestination = GameData::Planets().Get(child.Token(1));
160 else if(child.Token(0) == "map coloring" && child.Size() >= 2)
161 mapColoring = child.Value(1);
162 else if(child.Token(0) == "map zoom" && child.Size() >= 2)
163 mapZoom = child.Value(1);
164 else if(child.Token(0) == "collapsed" && child.Size() >= 2)
165 {
166 for(const DataNode &grand : child)
167 collapsed[child.Token(1)].insert(grand.Token(0));
168 }
169 else if(child.Token(0) == "reputation with")
170 {
171 for(const DataNode &grand : child)
172 if(grand.Size() >= 2)
173 reputationChanges.emplace_back(
174 GameData::Governments().Get(grand.Token(0)), grand.Value(1));
175 }
176
177 // Records of things you own:
178 else if(child.Token(0) == "ship")
179 {
180 // Ships owned by the player have various special characteristics:
181 ships.push_back(make_shared<Ship>(child));
182 ships.back()->SetIsSpecial();
183 ships.back()->SetIsYours();
184 // Defer finalizing this ship until we have processed all changes to game state.
185 }
186 else if(child.Token(0) == "groups" && child.Size() >= 2 && !ships.empty())
187 groups[ships.back().get()] = child.Value(1);
188 else if(child.Token(0) == "storage")
189 {
190 for(const DataNode &grand : child)
191 if(grand.Size() >= 2 && grand.Token(0) == "planet")
192 for(const DataNode &grandGrand : grand)
193 if(grandGrand.Token(0) == "cargo")
194 {
195 CargoHold &storage = planetaryStorage[GameData::Planets().Get(grand.Token(1))];
196 storage.Load(grandGrand);
197 }
198 }
199 else if(child.Token(0) == "account")
200 accounts.Load(child, true);
201 else if(child.Token(0) == "cargo")
202 cargo.Load(child);
203 else if(child.Token(0) == "basis")
204 {
205 for(const DataNode &grand : child)
206 if(grand.Size() >= 2)
207 costBasis[grand.Token(0)] += grand.Value(1);
208 }
209 else if(child.Token(0) == "stock")
210 {
211 for(const DataNode &grand : child)
212 if(grand.Size() >= 2)
213 stock[GameData::Outfits().Get(grand.Token(0))] += grand.Value(1);
214 }
215 else if(child.Token(0) == "fleet depreciation")
216 depreciation.Load(child);
217 else if(child.Token(0) == "stock depreciation")
218 stockDepreciation.Load(child);
219
220 // Records of things you have done or are doing, or have happened to you:
221 else if(child.Token(0) == "mission")
222 {
223 missions.emplace_back(child);
224 cargo.AddMissionCargo(&missions.back());
225 }
226 else if(child.Token(0) == "available job")
227 availableJobs.emplace_back(child);
228 else if(child.Token(0) == "available mission")
229 availableMissions.emplace_back(child);
230 else if(child.Token(0) == "conditions")
231 {
232 for(const DataNode &grand : child)
233 conditions[grand.Token(0)] = (grand.Size() >= 2) ? grand.Value(1) : 1;
234 }
235 else if(child.Token(0) == "event")
236 gameEvents.emplace_back(child);
237 else if(child.Token(0) == "changes")
238 {
239 for(const DataNode &grand : child)
240 dataChanges.push_back(grand);
241 }
242 else if(child.Token(0) == "economy")
243 economy = child;
244 else if(child.Token(0) == "destroyed" && child.Size() >= 2)
245 destroyedPersons.push_back(child.Token(1));
246
247 // Records of things you have discovered:
248 else if(child.Token(0) == "visited" && child.Size() >= 2)
249 Visit(*GameData::Systems().Get(child.Token(1)));
250 else if(child.Token(0) == "visited planet" && child.Size() >= 2)
251 Visit(*GameData::Planets().Get(child.Token(1)));
252 else if(child.Token(0) == "harvested")
253 {
254 for(const DataNode &grand : child)
255 if(grand.Size() >= 2)
256 harvested.emplace(
257 GameData::Systems().Get(grand.Token(0)),
258 GameData::Outfits().Get(grand.Token(1)));
259 }
260 else if(child.Token(0) == "logbook")
261 {
262 for(const DataNode &grand : child)
263 {
264 if(grand.Size() >= 3)
265 {
266 Date date(grand.Value(0), grand.Value(1), grand.Value(2));
267 string text;
268 for(const DataNode &great : grand)
269 {
270 if(!text.empty())
271 text += "\n\t";
272 text += great.Token(0);
273 }
274 logbook.emplace(date, text);
275 }
276 else if(grand.Size() >= 2)
277 {
278 string &text = specialLogs[grand.Token(0)][grand.Token(1)];
279 for(const DataNode &great : grand)
280 {
281 if(!text.empty())
282 text += "\n\t";
283 text += great.Token(0);
284 }
285 }
286 }
287 }
288 else if(child.Token(0) == "start")
289 startData.Load(child);
290 }
291 // Modify the game data with any changes that were loaded from this file.
292 ApplyChanges();
293 // Ensure the player is in a valid state after loading & applying changes.
294 ValidateLoad();
295
296 // Restore access to services, if it was granted previously.
297 if(planet && hasFullClearance)
298 planet->Bribe();
299
300 // Based on the ships that were loaded, calculate the player's capacity for
301 // cargo and passengers.
302 UpdateCargoCapacities();
303
304 // If no depreciation record was loaded, every item in the player's fleet
305 // will count as non-depreciated.
306 if(!depreciation.IsLoaded())
307 depreciation.Init(ships, date.DaysSinceEpoch());
308 }
309
310
311
312 // Load the most recently saved player (if any). Returns false when no save was loaded.
LoadRecent()313 bool PlayerInfo::LoadRecent()
314 {
315 string recentPath = Files::Read(Files::Config() + "recent.txt");
316 // Trim trailing whitespace (including newlines) from the path.
317 while(!recentPath.empty() && recentPath.back() <= ' ')
318 recentPath.pop_back();
319
320 if(recentPath.empty() || !Files::Exists(recentPath))
321 {
322 Clear();
323 return false;
324 }
325
326 Load(recentPath);
327 return true;
328 }
329
330
331
332 // Save this player. The file name is based on the player's name.
Save() const333 void PlayerInfo::Save() const
334 {
335 // Don't save dead players or players that are not fully created.
336 if(!CanBeSaved())
337 return;
338
339 // Remember that this was the most recently saved player.
340 Files::Write(Files::Config() + "recent.txt", filePath + '\n');
341
342 if(filePath.rfind(".txt") == filePath.length() - 4)
343 {
344 // Only update the backups if this save will have a newer date.
345 SavedGame saved(filePath);
346 if(saved.GetDate() != date.ToString())
347 {
348 string root = filePath.substr(0, filePath.length() - 4);
349 string files[4] = {
350 root + "~~previous-3.txt",
351 root + "~~previous-2.txt",
352 root + "~~previous-1.txt",
353 filePath
354 };
355 for(int i = 0; i < 3; ++i)
356 if(Files::Exists(files[i + 1]))
357 Files::Move(files[i + 1], files[i]);
358 }
359 }
360
361 Save(filePath);
362 }
363
364
365
366 // Get the base file name for the player, without the ".txt" extension. This
367 // will usually be "<first> <last>", but may be different if multiple players
368 // exist with the same name, in which case a number is appended.
Identifier() const369 string PlayerInfo::Identifier() const
370 {
371 string name = Files::Name(filePath);
372 return (name.length() < 4) ? "" : name.substr(0, name.length() - 4);
373 }
374
375
376
377 // Apply the given set of changes to the game data.
AddChanges(list<DataNode> & changes)378 void PlayerInfo::AddChanges(list<DataNode> &changes)
379 {
380 bool changedSystems = false;
381 for(const DataNode &change : changes)
382 {
383 changedSystems |= (change.Token(0) == "system");
384 changedSystems |= (change.Token(0) == "link");
385 changedSystems |= (change.Token(0) == "unlink");
386 GameData::Change(change);
387 }
388 if(changedSystems)
389 {
390 // Recalculate what systems have been seen.
391 GameData::UpdateSystems();
392 seen.clear();
393 for(const System *system : visitedSystems)
394 {
395 seen.insert(system);
396 for(const System *neighbor : system->VisibleNeighbors())
397 if(!neighbor->Hidden() || system->Links().count(neighbor))
398 seen.insert(neighbor);
399 }
400 }
401
402 // Only move the changes into my list if they are not already there.
403 if(&changes != &dataChanges)
404 dataChanges.splice(dataChanges.end(), changes);
405 }
406
407
408
409 // Add an event that will happen at the given date.
AddEvent(const GameEvent & event,const Date & date)410 void PlayerInfo::AddEvent(const GameEvent &event, const Date &date)
411 {
412 gameEvents.push_back(event);
413 gameEvents.back().SetDate(date);
414 }
415
416
417
418 // Mark this player as dead, and handle the changes to the player's fleet.
Die(int response,const shared_ptr<Ship> & capturer)419 void PlayerInfo::Die(int response, const shared_ptr<Ship> &capturer)
420 {
421 isDead = true;
422 // The player loses access to all their ships if they die on a planet.
423 if(GetPlanet() || !flagship)
424 ships.clear();
425 // If the flagship should explode due to choices made in a mission's
426 // conversation, it should still appear in the player's ship list (but
427 // will be red, because it is dead). The player's escorts will scatter
428 // automatically, as they have a now-dead parent.
429 else if(response == Conversation::EXPLODE)
430 flagship->Destroy();
431 // If it died in open combat, it is already marked destroyed.
432 else if(!flagship->IsDestroyed())
433 {
434 // The player died due to the failed capture of an NPC or a
435 // "mutiny". The flagship is either captured or changes government.
436 if(!flagship->IsYours())
437 {
438 // The flagship was already captured, via BoardingPanel,
439 // and its parent-escort relationships were updated in
440 // Ship::WasCaptured().
441 }
442 // The referenced ship may not be boarded by the player, so before
443 // letting it capture the flagship it must be near the flagship.
444 else if(capturer && capturer->Position().Distance(flagship->Position()) <= 1.)
445 flagship->WasCaptured(capturer);
446 else
447 {
448 // A "mutiny" occurred.
449 flagship->SetIsYours(false);
450 // TODO: perhaps allow missions to set the new government.
451 flagship->SetGovernment(GameData::Governments().Get("Independent"));
452 // Your escorts do not follow it, nor does it wait for them.
453 for(const shared_ptr<Ship> &ship : ships)
454 ship->SetParent(nullptr);
455 }
456 // Remove the flagship from the player's ship list.
457 auto it = find(ships.begin(), ships.end(), flagship);
458 if(it != ships.end())
459 ships.erase(it);
460 }
461 }
462
463
464
465 // Query whether this player is dead.
IsDead() const466 bool PlayerInfo::IsDead() const
467 {
468 return isDead;
469 }
470
471
472
473 // Get the player's first name.
FirstName() const474 const string &PlayerInfo::FirstName() const
475 {
476 return firstName;
477 }
478
479
480
481 // Get the player's last name.
LastName() const482 const string &PlayerInfo::LastName() const
483 {
484 return lastName;
485 }
486
487
488
489 // Set the player's name. This will also set the saved game file name.
SetName(const string & first,const string & last)490 void PlayerInfo::SetName(const string &first, const string &last)
491 {
492 firstName = first;
493 lastName = last;
494
495 string fileName = first + " " + last;
496
497 // If there are multiple pilots with the same name, append a number to the
498 // pilot name to generate a unique file name.
499 filePath = Files::Saves() + fileName;
500 int index = 0;
501 while(true)
502 {
503 string path = filePath;
504 if(index++)
505 path += " " + to_string(index);
506 path += ".txt";
507
508 if(!Files::Exists(path))
509 {
510 filePath.swap(path);
511 break;
512 }
513 }
514 }
515
516
517
518 // Get the current date (game world, not real world).
GetDate() const519 const Date &PlayerInfo::GetDate() const
520 {
521 return date;
522 }
523
524
525
526 // Set the date to the next day, and perform all daily actions.
IncrementDate()527 void PlayerInfo::IncrementDate()
528 {
529 ++date;
530 conditions["day"] = date.Day();
531 conditions["month"] = date.Month();
532 conditions["year"] = date.Year();
533
534 // Check if any special events should happen today.
535 auto it = gameEvents.begin();
536 while(it != gameEvents.end())
537 {
538 if(date < it->GetDate())
539 ++it;
540 else
541 {
542 it->Apply(*this);
543 it = gameEvents.erase(it);
544 }
545 }
546
547 // Check if any missions have failed because of deadlines.
548 for(Mission &mission : missions)
549 if(mission.CheckDeadline(date) && mission.IsVisible())
550 Messages::Add("You failed to meet the deadline for the mission \"" + mission.Name() + "\".");
551
552 // Check what salaries and tribute the player receives.
553 int64_t total[2] = {0, 0};
554 static const string prefix[2] = {"salary: ", "tribute: "};
555 for(int i = 0; i < 2; ++i)
556 {
557 auto it = conditions.lower_bound(prefix[i]);
558 for( ; it != conditions.end() && !it->first.compare(0, prefix[i].length(), prefix[i]); ++it)
559 total[i] += it->second;
560 }
561 if(total[0] || total[1])
562 {
563 string message = "You receive ";
564 if(total[0])
565 message += Format::Credits(total[0]) + " credits salary";
566 if(total[0] && total[1])
567 message += " and ";
568 if(total[1])
569 message += Format::Credits(total[1]) + " credits in tribute";
570 message += ".";
571 Messages::Add(message);
572 accounts.AddCredits(total[0] + total[1]);
573 }
574
575 // For accounting, keep track of the player's net worth. This is for
576 // calculation of yearly income to determine maximum mortgage amounts.
577 int64_t assets = depreciation.Value(ships, date.DaysSinceEpoch());
578 for(const shared_ptr<Ship> &ship : ships)
579 assets += ship->Cargo().Value(system);
580
581 // Have the player pay salaries, mortgages, etc. and print a message that
582 // summarizes the payments that were made.
583 string message = accounts.Step(assets, Salaries(), Maintenance());
584 if(!message.empty())
585 Messages::Add(message);
586
587 // Reset the reload counters for all your ships.
588 for(const shared_ptr<Ship> &ship : ships)
589 ship->GetArmament().ReloadAll();
590
591 // Re-calculate all automatic conditions
592 UpdateAutoConditions();
593 }
594
595
596
StartData() const597 const CoreStartData &PlayerInfo::StartData() const noexcept
598 {
599 return startData;
600 }
601
602
603
604 // Set the player's current start system, and mark that system as visited.
SetSystem(const System & system)605 void PlayerInfo::SetSystem(const System &system)
606 {
607 this->system = &system;
608 Visit(system);
609 }
610
611
612
613 // Get the player's current star system.
GetSystem() const614 const System *PlayerInfo::GetSystem() const
615 {
616 return system;
617 }
618
619
620
621 // Set the planet the player is landed on.
SetPlanet(const Planet * planet)622 void PlayerInfo::SetPlanet(const Planet *planet)
623 {
624 this->planet = planet;
625 }
626
627
628
629 // Get the planet the player is landed on.
GetPlanet() const630 const Planet *PlayerInfo::GetPlanet() const
631 {
632 return planet;
633 }
634
635
636
637 // If the player is landed, return the stellar object they are on. Some planets
638 // (e.g. ringworlds) may include multiple stellar objects in the same system.
GetStellarObject() const639 const StellarObject *PlayerInfo::GetStellarObject() const
640 {
641 if(!system || !planet)
642 return nullptr;
643
644 double closestDistance = numeric_limits<double>::infinity();
645 const StellarObject *closestObject = nullptr;
646 for(const StellarObject &object : system->Objects())
647 if(object.GetPlanet() == planet)
648 {
649 if(!Flagship())
650 return &object;
651
652 double distance = Flagship()->Position().Distance(object.Position());
653 if(distance < closestDistance)
654 {
655 closestDistance = distance;
656 closestObject = &object;
657 }
658 }
659 return closestObject;
660 }
661
662
663
664 // Check if the player must take off immediately.
ShouldLaunch() const665 bool PlayerInfo::ShouldLaunch() const
666 {
667 return shouldLaunch;
668 }
669
670
671
672 // Access the player's account information.
Accounts() const673 const Account &PlayerInfo::Accounts() const
674 {
675 return accounts;
676 }
677
678
679
680 // Access the player's account information (and allow modifying it).
Accounts()681 Account &PlayerInfo::Accounts()
682 {
683 return accounts;
684 }
685
686
687
688 // Calculate how much the player pays in daily salaries.
Salaries() const689 int64_t PlayerInfo::Salaries() const
690 {
691 // Don't count extra crew on anything but the flagship.
692 int64_t crew = 0;
693 const Ship *flagship = Flagship();
694 if(flagship)
695 crew = flagship->Crew() - flagship->RequiredCrew();
696
697 // A ship that is "parked" remains on a planet and requires no salaries.
698 for(const shared_ptr<Ship> &ship : ships)
699 if(!ship->IsParked() && !ship->IsDestroyed())
700 crew += ship->RequiredCrew();
701 if(!crew)
702 return 0;
703
704 // Every crew member except the player receives 100 credits per day.
705 return 100 * (crew - 1);
706 }
707
708
709
710 // Calculate the daily maintenance cost for all ships and in cargo outfits.
Maintenance() const711 int64_t PlayerInfo::Maintenance() const
712 {
713 int64_t maintenance = 0;
714 // If the player is landed, then cargo will be in the player's
715 // pooled cargo. Check there so that the bank panel can display the
716 // correct total maintenance costs. When launched all cargo will be
717 // in the player's ships instead of in the pooled cargo, so no outfit
718 // will be counted twice.
719 for(const auto &outfit : Cargo().Outfits())
720 maintenance += max<int64_t>(0, outfit.first->Get("maintenance costs")) * outfit.second;
721 for(const shared_ptr<Ship> &ship : ships)
722 if(!ship->IsDestroyed())
723 {
724 maintenance += max<int64_t>(0, ship->Attributes().Get("maintenance costs"));
725 for(const auto &outfit : ship->Cargo().Outfits())
726 maintenance += max<int64_t>(0, outfit.first->Get("maintenance costs")) * outfit.second;
727 if(!ship->IsParked())
728 maintenance += max<int64_t>(0, ship->Attributes().Get("operating costs"));
729 }
730 return maintenance;
731 }
732
733
734
735 // Get a pointer to the ship that the player controls. This is always the first
736 // ship in the list.
Flagship() const737 const Ship *PlayerInfo::Flagship() const
738 {
739 return const_cast<PlayerInfo *>(this)->FlagshipPtr().get();
740 }
741
742
743
744 // Get a pointer to the ship that the player controls. This is always the first
745 // ship in the list.
Flagship()746 Ship *PlayerInfo::Flagship()
747 {
748 return FlagshipPtr().get();
749 }
750
751
752
753 // Determine which ship is the flagship and return the shared pointer to it.
FlagshipPtr()754 const shared_ptr<Ship> &PlayerInfo::FlagshipPtr()
755 {
756 if(!flagship)
757 {
758 for(const shared_ptr<Ship> &it : ships)
759 if(!it->IsParked() && it->GetSystem() == system && it->CanBeFlagship())
760 {
761 flagship = it;
762 break;
763 }
764 }
765
766 static const shared_ptr<Ship> empty;
767 return (flagship && flagship->IsYours()) ? flagship : empty;
768 }
769
770
771
772 // Access the full list of ships that the player owns.
Ships() const773 const vector<shared_ptr<Ship>> &PlayerInfo::Ships() const
774 {
775 return ships;
776 }
777
778
779
780 // Inspect the flightworthiness of the player's active fleet, individually and
781 // as a whole, to determine which ships cannot travel with the group.
782 // Returns a mapping of ships to the reason their flight check failed.
FlightCheck() const783 map<const shared_ptr<Ship>, vector<string>> PlayerInfo::FlightCheck() const
784 {
785 // Count of all bay types in the active fleet.
786 auto bayCount = map<string, size_t>{};
787 // Classification of the present ships by category. Parked ships are ignored.
788 auto categoryCount = map<string, vector<shared_ptr<Ship>>>{};
789
790 auto flightChecks = map<const shared_ptr<Ship>, vector<string>>{};
791 for(const auto &ship : ships)
792 if(ship->GetSystem() == system && !ship->IsDisabled() && !ship->IsParked())
793 {
794 auto checks = ship->FlightCheck();
795 if(!checks.empty())
796 flightChecks.emplace(ship, checks);
797
798 categoryCount[ship->Attributes().Category()].emplace_back(ship);
799 if(ship->CanBeCarried() || !ship->HasBays())
800 continue;
801
802 for(auto &bay : ship->Bays())
803 {
804 ++bayCount[bay.category];
805 // The bays should always be empty. But if not, count that ship too.
806 if(bay.ship)
807 {
808 Files::LogError("Expected bay to be empty for " + ship->ModelName() + ": " + ship->Name());
809 categoryCount[bay.ship->Attributes().Category()].emplace_back(bay.ship);
810 }
811 }
812 }
813
814 // Identify transportable ships that cannot jump and have no bay to be carried in.
815 for(auto &bayType : bayCount)
816 {
817 const auto &shipsOfType = categoryCount[bayType.first];
818 if(shipsOfType.empty())
819 continue;
820 for(const auto &carriable : shipsOfType)
821 {
822 if(carriable->JumpsRemaining() != 0)
823 {
824 // This ship can travel between systems and does not require a bay.
825 }
826 // This ship requires a bay to travel between systems.
827 else if(bayType.second > 0)
828 --bayType.second;
829 else
830 {
831 // Include the lack of bay availability amongst any other
832 // warnings for this carriable ship.
833 auto it = flightChecks.find(carriable);
834 string warning = "no bays?";
835 if(it != flightChecks.end())
836 it->second.emplace_back(warning);
837 else
838 flightChecks.emplace(carriable, vector<string>{warning});
839 }
840 }
841 }
842 return flightChecks;
843 }
844
845
846
847 // Add a captured ship to your fleet.
AddShip(const shared_ptr<Ship> & ship)848 void PlayerInfo::AddShip(const shared_ptr<Ship> &ship)
849 {
850 ships.push_back(ship);
851 ship->SetIsSpecial();
852 ship->SetIsYours();
853 }
854
855
856
857 // Adds a ship of the given model with the given name to the player's fleet.
858 // If this ship is being gifted, it costs nothing and starts fully depreciated.
BuyShip(const Ship * model,const string & name,bool isGift)859 void PlayerInfo::BuyShip(const Ship *model, const string &name, bool isGift)
860 {
861 if(!model)
862 return;
863
864 int day = date.DaysSinceEpoch();
865 int64_t cost = isGift ? 0 : stockDepreciation.Value(*model, day);
866 if(accounts.Credits() >= cost)
867 {
868 ships.push_back(make_shared<Ship>(*model));
869 ships.back()->SetName(name);
870 ships.back()->SetSystem(system);
871 ships.back()->SetPlanet(planet);
872 ships.back()->SetIsSpecial();
873 ships.back()->SetIsYours();
874 ships.back()->SetGovernment(GameData::PlayerGovernment());
875
876 accounts.AddCredits(-cost);
877 flagship.reset();
878
879 // Record the transfer of this ship in the depreciation and stock info.
880 if(!isGift)
881 {
882 depreciation.Buy(*model, day, &stockDepreciation);
883 for(const auto &it : model->Outfits())
884 stock[it.first] -= it.second;
885 }
886 }
887 }
888
889
890
891 // Sell the given ship (if it belongs to the player).
SellShip(const Ship * selected)892 void PlayerInfo::SellShip(const Ship *selected)
893 {
894 for(auto it = ships.begin(); it != ships.end(); ++it)
895 if(it->get() == selected)
896 {
897 int day = date.DaysSinceEpoch();
898 int64_t cost = depreciation.Value(*selected, day);
899
900 // Record the transfer of this ship in the depreciation and stock info.
901 stockDepreciation.Buy(*selected, day, &depreciation);
902 for(const auto &it : selected->Outfits())
903 stock[it.first] += it.second;
904
905 accounts.AddCredits(cost);
906 ships.erase(it);
907 flagship.reset();
908 return;
909 }
910 }
911
912
913
DisownShip(const Ship * selected)914 void PlayerInfo::DisownShip(const Ship *selected)
915 {
916 for(auto it = ships.begin(); it != ships.end(); ++it)
917 if(it->get() == selected)
918 {
919 ships.erase(it);
920 flagship.reset();
921 return;
922 }
923 }
924
925
926
927 // Park or unpark the given ship. A parked ship remains on a planet instead of
928 // flying with the player, and requires no daily crew payments.
ParkShip(const Ship * selected,bool isParked)929 void PlayerInfo::ParkShip(const Ship *selected, bool isParked)
930 {
931 for(auto it = ships.begin(); it != ships.end(); ++it)
932 if(it->get() == selected)
933 {
934 isParked &= !(*it)->IsDisabled();
935 (*it)->SetIsParked(isParked);
936 UpdateCargoCapacities();
937 flagship.reset();
938 return;
939 }
940 }
941
942
943
944 // Rename the given ship.
RenameShip(const Ship * selected,const string & name)945 void PlayerInfo::RenameShip(const Ship *selected, const string &name)
946 {
947 for(auto it = ships.begin(); it != ships.end(); ++it)
948 if(it->get() == selected)
949 {
950 (*it)->SetName(name);
951 return;
952 }
953 }
954
955
956
957 // Change the order of the given ship in the list.
ReorderShip(int fromIndex,int toIndex)958 void PlayerInfo::ReorderShip(int fromIndex, int toIndex)
959 {
960 // Make sure the indices are valid.
961 if(fromIndex == toIndex)
962 return;
963 if(static_cast<unsigned>(fromIndex) >= ships.size())
964 return;
965 if(static_cast<unsigned>(toIndex) >= ships.size())
966 return;
967
968 // Reorder the list.
969 shared_ptr<Ship> ship = ships[fromIndex];
970 ships.erase(ships.begin() + fromIndex);
971 ships.insert(ships.begin() + toIndex, ship);
972 flagship.reset();
973 }
974
975
976
ReorderShips(const set<int> & fromIndices,int toIndex)977 int PlayerInfo::ReorderShips(const set<int> &fromIndices, int toIndex)
978 {
979 if(fromIndices.empty() || static_cast<unsigned>(toIndex) >= ships.size())
980 return -1;
981
982 // When shifting ships up in the list, move to the desired index. If
983 // moving down, move after the selected index.
984 int direction = (*fromIndices.begin() < toIndex) ? 1 : 0;
985
986 // Remove the ships from last to first, so that each removal leaves all the
987 // remaining indices in the set still valid.
988 vector<shared_ptr<Ship>> removed;
989 for(set<int>::const_iterator it = fromIndices.end(); it != fromIndices.begin(); )
990 {
991 // The "it" pointer doesn't point to the beginning of the list, so it is
992 // safe to decrement it here.
993 --it;
994
995 // Bail out if any invalid indices are encountered.
996 if(static_cast<unsigned>(*it) >= ships.size())
997 return -1;
998
999 removed.insert(removed.begin(), ships[*it]);
1000 ships.erase(ships.begin() + *it);
1001 // If this index is before the insertion point, removing it causes the
1002 // insertion point to shift back one space.
1003 if(*it < toIndex)
1004 --toIndex;
1005 }
1006 // Make sure the insertion index is within the list.
1007 toIndex = min<int>(toIndex + direction, ships.size());
1008 ships.insert(ships.begin() + toIndex, removed.begin(), removed.end());
1009 flagship.reset();
1010
1011 return toIndex;
1012 }
1013
1014
1015
1016 // Find out how attractive the player's fleet is to pirates. Aside from a
1017 // heavy freighter, no single ship should attract extra pirate attention.
RaidFleetFactors() const1018 pair<double, double> PlayerInfo::RaidFleetFactors() const
1019 {
1020 double attraction = 0.;
1021 double deterrence = 0.;
1022 for(const shared_ptr<Ship> &ship : Ships())
1023 {
1024 if(ship->IsParked() || ship->IsDestroyed())
1025 continue;
1026
1027 attraction += max(0., .4 * sqrt(ship->Attributes().Get("cargo space")) - 1.8);
1028 for(const Hardpoint &hardpoint : ship->Weapons())
1029 if(hardpoint.GetOutfit())
1030 {
1031 const Outfit *weapon = hardpoint.GetOutfit();
1032 if(weapon->Ammo() && !ship->OutfitCount(weapon->Ammo()))
1033 continue;
1034 double damage = weapon->ShieldDamage() + weapon->HullDamage()
1035 + (weapon->RelativeShieldDamage() * ship->Attributes().Get("shields"))
1036 + (weapon->RelativeHullDamage() * ship->Attributes().Get("hull"));
1037 deterrence += .12 * damage / weapon->Reload();
1038 }
1039 }
1040
1041 return make_pair(attraction, deterrence);
1042 }
1043
1044
1045
1046 // Get cargo information.
Cargo()1047 CargoHold &PlayerInfo::Cargo()
1048 {
1049 return cargo;
1050 }
1051
1052
1053
1054 // Get cargo information.
Cargo() const1055 const CargoHold &PlayerInfo::Cargo() const
1056 {
1057 return cargo;
1058 }
1059
1060
1061
1062 // Get planetary storage information for current planet. Returns a pointer,
1063 // since we might not be on a planet, or since the storage might be empty.
Storage(bool forceCreate)1064 CargoHold *PlayerInfo::Storage(bool forceCreate)
1065 {
1066 if(planet && (forceCreate || planetaryStorage.count(planet)))
1067 return &(planetaryStorage[planet]);
1068
1069 // Nullptr can be returned when forceCreate is true if there is no
1070 // planet; nullptr is the best we can offer in such cases.
1071 return nullptr;
1072 }
1073
1074
1075
1076 // Get planetary storage information for all planets (for map and overviews).
PlanetaryStorage() const1077 const std::map<const Planet *, CargoHold> &PlayerInfo::PlanetaryStorage() const
1078 {
1079 return planetaryStorage;
1080 }
1081
1082
1083
1084 // Adjust the cost basis for the given commodity.
AdjustBasis(const string & commodity,int64_t adjustment)1085 void PlayerInfo::AdjustBasis(const string &commodity, int64_t adjustment)
1086 {
1087 costBasis[commodity] += adjustment;
1088 }
1089
1090
1091
1092 // Get the cost basis for some number of tons of the given commodity. Each ton
1093 // of the commodity that you own is assumed to have the same basis.
GetBasis(const string & commodity,int tons) const1094 int64_t PlayerInfo::GetBasis(const string &commodity, int tons) const
1095 {
1096 // Handle cost basis even when not landed on a planet.
1097 int total = cargo.Get(commodity);
1098 for(const auto &ship : ships)
1099 total += ship->Cargo().Get(commodity);
1100 if(!total)
1101 return 0;
1102
1103 auto it = costBasis.find(commodity);
1104 int64_t basis = (it == costBasis.end()) ? 0 : it->second;
1105 return (basis * tons) / total;
1106 }
1107
1108
1109
1110 // Switch cargo from being stored in ships to being stored here. Also recharge
1111 // ships, check for mission completion, and apply fines for contraband.
Land(UI * ui)1112 void PlayerInfo::Land(UI *ui)
1113 {
1114 // This can only be done while landed.
1115 if(!system || !planet)
1116 return;
1117
1118 Audio::Play(Audio::Get("landing"));
1119 Audio::PlayMusic(planet->MusicName());
1120
1121 // Mark this planet as visited.
1122 Visit(*planet);
1123 if(planet == travelDestination)
1124 travelDestination = nullptr;
1125
1126 // Remove any ships that have been destroyed or captured.
1127 map<string, int> lostCargo;
1128 vector<shared_ptr<Ship>>::iterator it = ships.begin();
1129 while(it != ships.end())
1130 {
1131 if((*it)->IsDestroyed() || !(*it)->IsYours())
1132 {
1133 // If any of your ships are destroyed, your cargo "cost basis" should
1134 // be adjusted based on what you lost.
1135 for(const auto &cargo : (*it)->Cargo().Commodities())
1136 if(cargo.second)
1137 lostCargo[cargo.first] += cargo.second;
1138 // Also, the ship and everything in it should be removed from your
1139 // depreciation records. Transfer it to a throw-away record:
1140 Depreciation().Buy(**it, date.DaysSinceEpoch(), &depreciation);
1141
1142 it = ships.erase(it);
1143 }
1144 else
1145 ++it;
1146 }
1147
1148 // "Unload" all fighters, so they will get recharged, etc.
1149 for(const shared_ptr<Ship> &ship : ships)
1150 ship->UnloadBays();
1151
1152 // Ships that are landed with you on the planet should fully recharge
1153 // and pool all their cargo together. Those in remote systems restore
1154 // what they can without landing.
1155 bool hasSpaceport = planet->HasSpaceport() && planet->CanUseServices();
1156 UpdateCargoCapacities();
1157 for(const shared_ptr<Ship> &ship : ships)
1158 if(!ship->IsParked() && !ship->IsDisabled())
1159 {
1160 if(ship->GetSystem() == system)
1161 {
1162 ship->Recharge(hasSpaceport);
1163 ship->Cargo().TransferAll(cargo);
1164 if(!ship->GetPlanet())
1165 ship->SetPlanet(planet);
1166 }
1167 else
1168 ship->Recharge(false);
1169 }
1170 // Adjust cargo cost basis for any cargo lost due to a ship being destroyed.
1171 for(const auto &it : lostCargo)
1172 AdjustBasis(it.first, -(costBasis[it.first] * it.second) / (cargo.Get(it.first) + it.second));
1173
1174 // Bring auto conditions up-to-date for missions to check your current status.
1175 UpdateAutoConditions();
1176
1177 // Evaluate changes to NPC spawning criteria.
1178 if(!freshlyLoaded)
1179 UpdateMissionNPCs();
1180
1181 // Update missions that are completed, or should be failed.
1182 StepMissions(ui);
1183 UpdateCargoCapacities();
1184
1185 // If the player is actually landing (rather than simply loading the game),
1186 // new missions are created and new fines may be levied.
1187 if(!freshlyLoaded)
1188 {
1189 // Create whatever missions this planet has to offer.
1190 CreateMissions();
1191 // Check if the player is doing anything illegal.
1192 Fine(ui);
1193 }
1194 // Upon loading the game, prompt the player about any paused missions, but if there are many
1195 // do not name them all (since this would overflow the screen).
1196 else if(ui && !inactiveMissions.empty())
1197 {
1198 string message = "These active missions or jobs were deactivated due to a missing definition - perhaps you recently removed a plugin?\n";
1199 auto mit = inactiveMissions.rbegin();
1200 int named = 0;
1201 while(mit != inactiveMissions.rend() && (++named < 10))
1202 {
1203 message += "\t\"" + mit->Name() + "\"\n";
1204 ++mit;
1205 }
1206 if(mit != inactiveMissions.rend())
1207 message += " and " + to_string(distance(mit, inactiveMissions.rend())) + " more.\n";
1208 message += "They will be reactivated when the necessary plugin is reinstalled.";
1209 ui->Push(new Dialog(message));
1210 }
1211
1212 // Hire extra crew back if any were lost in-flight (i.e. boarding) or
1213 // some bunks were freed up upon landing (i.e. completed missions).
1214 if(Preferences::Has("Rehire extra crew when lost") && hasSpaceport && flagship)
1215 {
1216 int added = desiredCrew - flagship->Crew();
1217 if(added > 0)
1218 {
1219 flagship->AddCrew(added);
1220 Messages::Add("You hire " + to_string(added) + (added == 1
1221 ? " extra crew member to fill your now-empty bunk."
1222 : " extra crew members to fill your now-empty bunks."));
1223 }
1224 }
1225
1226 freshlyLoaded = false;
1227 flagship.reset();
1228 }
1229
1230
1231
1232 // Load the cargo back into your ships. This may require selling excess, in
1233 // which case a message will be returned.
TakeOff(UI * ui)1234 bool PlayerInfo::TakeOff(UI *ui)
1235 {
1236 // This can only be done while landed.
1237 if(!system || !planet)
1238 return false;
1239
1240 if(flagship)
1241 flagship->AllowCarried(true);
1242 flagship.reset();
1243 flagship = FlagshipPtr();
1244 if(!flagship)
1245 return false;
1246 flagship->AllowCarried(false);
1247
1248 shouldLaunch = false;
1249 Audio::Play(Audio::Get("takeoff"));
1250
1251 // Jobs are only available when you are landed.
1252 availableJobs.clear();
1253 availableMissions.clear();
1254 doneMissions.clear();
1255 stock.clear();
1256
1257 // Special persons who appeared last time you left the planet, can appear again.
1258 GameData::ResetPersons();
1259
1260 // Store the total cargo counts in case we need to adjust cost bases below.
1261 map<string, int> originalTotals = cargo.Commodities();
1262
1263 // Move the flagship to the start of your list of ships. It does not make
1264 // sense that the flagship would change if you are reunited with a different
1265 // ship that was higher up the list.
1266 auto it = find(ships.begin(), ships.end(), flagship);
1267 if(it != ships.begin() && it != ships.end())
1268 {
1269 ships.erase(it);
1270 ships.insert(ships.begin(), flagship);
1271 }
1272 // Make sure your jump-capable ships all know who the flagship is.
1273 for(const shared_ptr<Ship> &ship : ships)
1274 {
1275 bool shouldHaveParent = (ship != flagship && !ship->IsParked() && (!ship->CanBeCarried() || ship->JumpFuel()));
1276 ship->SetParent(shouldHaveParent ? flagship : shared_ptr<Ship>());
1277 }
1278 // Make sure your flagship is not included in the escort selection.
1279 for(auto it = selectedShips.begin(); it != selectedShips.end(); )
1280 {
1281 shared_ptr<Ship> ship = it->lock();
1282 if(!ship || ship == flagship)
1283 it = selectedShips.erase(it);
1284 else
1285 ++it;
1286 }
1287
1288 // Recharge any ships that can be recharged, and load available cargo.
1289 bool hasSpaceport = planet->HasSpaceport() && planet->CanUseServices();
1290 for(const shared_ptr<Ship> &ship : ships)
1291 if(!ship->IsParked() && !ship->IsDisabled())
1292 {
1293 if(ship->GetSystem() != system)
1294 {
1295 ship->Recharge(false);
1296 continue;
1297 }
1298 else
1299 ship->Recharge(hasSpaceport);
1300
1301 if(ship != flagship)
1302 {
1303 ship->Cargo().SetBunks(ship->Attributes().Get("bunks") - ship->RequiredCrew());
1304 cargo.TransferAll(ship->Cargo());
1305 }
1306 else
1307 {
1308 // Your flagship takes first priority for passengers but last for cargo.
1309 desiredCrew = ship->Crew();
1310 ship->Cargo().SetBunks(ship->Attributes().Get("bunks") - desiredCrew);
1311 for(const auto &it : cargo.PassengerList())
1312 cargo.TransferPassengers(it.first, it.second, ship->Cargo());
1313 }
1314 }
1315 // Load up your flagship last, so that it will have space free for any
1316 // plunder that you happen to acquire.
1317 cargo.TransferAll(flagship->Cargo());
1318
1319 if(cargo.Passengers())
1320 {
1321 int extra = min(cargo.Passengers(), flagship->Crew() - flagship->RequiredCrew());
1322 if(extra)
1323 {
1324 flagship->AddCrew(-extra);
1325 Messages::Add("You fired " + to_string(extra) + " crew members to free up bunks for passengers.");
1326 flagship->Cargo().SetBunks(flagship->Attributes().Get("bunks") - flagship->Crew());
1327 cargo.TransferAll(flagship->Cargo());
1328 }
1329 }
1330
1331 int extra = flagship->Crew() + flagship->Cargo().Passengers() - flagship->Attributes().Get("bunks");
1332 if(extra > 0)
1333 {
1334 flagship->AddCrew(-extra);
1335 Messages::Add("You fired " + to_string(extra) + " crew members because you have no bunks for them.");
1336 flagship->Cargo().SetBunks(flagship->Attributes().Get("bunks") - flagship->Crew());
1337 }
1338
1339 // For each active, carriable ship you own, try to find an active ship that has a bay for it.
1340 auto carriers = vector<Ship *>{};
1341 auto toLoad = vector<shared_ptr<Ship>>{};
1342 for(auto &ship : ships)
1343 if(!ship->IsParked() && !ship->IsDisabled())
1344 {
1345 if(ship->CanBeCarried())
1346 toLoad.emplace_back(ship);
1347 else if(ship->HasBays())
1348 carriers.emplace_back(ship.get());
1349 }
1350 if(!toLoad.empty())
1351 {
1352 size_t uncarried = toLoad.size();
1353 if(!carriers.empty())
1354 {
1355 // Order carried ships such that those requiring bays are loaded first. For
1356 // jump-capable carried ships, prefer loading those with a shorter range.
1357 stable_sort(toLoad.begin(), toLoad.end(),
1358 [](const shared_ptr<Ship> &a, const shared_ptr<Ship> &b)
1359 {
1360 return a->JumpsRemaining() < b->JumpsRemaining();
1361 });
1362 // We are guaranteed that each carried `ship` is not parked and not disabled, and that
1363 // all possible parents are also not parked, not disabled, and not `ship`.
1364 for(auto &ship : toLoad)
1365 for(auto &parent : carriers)
1366 if(parent->GetSystem() == ship->GetSystem() && parent->Carry(ship))
1367 {
1368 --uncarried;
1369 break;
1370 }
1371 }
1372
1373 if(uncarried)
1374 {
1375 // The remaining uncarried ships are launched alongside the player.
1376 string message = (uncarried > 1) ? "Some escorts were" : "One escort was";
1377 Messages::Add(message + " unable to dock with a carrier.");
1378 }
1379 }
1380
1381 // By now, all cargo should have been divvied up among your ships. So, any
1382 // mission cargo or passengers left behind cannot be carried, and those
1383 // missions have failed.
1384 vector<const Mission *> missionsToRemove;
1385 for(const auto &it : cargo.MissionCargo())
1386 if(it.second)
1387 {
1388 if(it.first->IsVisible())
1389 Messages::Add("Mission \"" + it.first->Name()
1390 + "\" failed because you do not have space for the cargo.");
1391 missionsToRemove.push_back(it.first);
1392 }
1393 for(const auto &it : cargo.PassengerList())
1394 if(it.second)
1395 {
1396 if(it.first->IsVisible())
1397 Messages::Add("Mission \"" + it.first->Name()
1398 + "\" failed because you do not have enough passenger bunks free.");
1399 missionsToRemove.push_back(it.first);
1400
1401 }
1402 for(const Mission *mission : missionsToRemove)
1403 RemoveMission(Mission::FAIL, *mission, ui);
1404
1405 // Any ordinary cargo left behind can be sold.
1406 int64_t income = 0;
1407 int day = date.DaysSinceEpoch();
1408 int64_t sold = cargo.Used();
1409 int64_t totalBasis = 0;
1410 if(sold)
1411 {
1412 for(const auto &commodity : cargo.Commodities())
1413 {
1414 if(!commodity.second)
1415 continue;
1416
1417 // Figure out how much income you get for selling this cargo.
1418 int64_t value = commodity.second * static_cast<int64_t>(system->Trade(commodity.first));
1419 income += value;
1420
1421 int original = originalTotals[commodity.first];
1422 auto it = costBasis.find(commodity.first);
1423 if(!original || it == costBasis.end() || !it->second)
1424 continue;
1425
1426 // Now, figure out how much of that income is profit by calculating
1427 // the cost basis for this cargo (which is just the total cost basis
1428 // multiplied by the percent of the cargo you are selling).
1429 int64_t basis = it->second * commodity.second / original;
1430 it->second -= basis;
1431 totalBasis += basis;
1432 }
1433 for(const auto &outfit : cargo.Outfits())
1434 {
1435 // Compute the total value for each type of excess outfit.
1436 if(!outfit.second)
1437 continue;
1438 int64_t cost = depreciation.Value(outfit.first, day, outfit.second);
1439 for(int i = 0; i < outfit.second; ++i)
1440 stockDepreciation.Buy(outfit.first, day, &depreciation);
1441 income += cost;
1442 }
1443 }
1444 accounts.AddCredits(income);
1445 cargo.Clear();
1446 stockDepreciation = Depreciation();
1447 if(sold)
1448 {
1449 // Report how much excess cargo was sold, and what profit you earned.
1450 ostringstream out;
1451 out << "You sold " << sold << " tons of excess cargo for " << Format::Credits(income) << " credits";
1452 if(totalBasis && totalBasis != income)
1453 out << " (for a profit of " << (income - totalBasis) << " credits).";
1454 else
1455 out << ".";
1456 Messages::Add(out.str());
1457 }
1458
1459 return true;
1460 }
1461
1462
1463
AddPlayTime(chrono::nanoseconds timeVal)1464 void PlayerInfo::AddPlayTime(chrono::nanoseconds timeVal)
1465 {
1466 playTime += timeVal.count() * .000000001;
1467 }
1468
1469
1470
GetPlayTime() const1471 double PlayerInfo::GetPlayTime() const noexcept
1472 {
1473 return playTime;
1474 }
1475
1476
1477
1478 // Get the player's logbook.
Logbook() const1479 const multimap<Date, string> &PlayerInfo::Logbook() const
1480 {
1481 return logbook;
1482 }
1483
1484
1485
AddLogEntry(const string & text)1486 void PlayerInfo::AddLogEntry(const string &text)
1487 {
1488 logbook.emplace(date, text);
1489 }
1490
1491
1492
SpecialLogs() const1493 const map<string, map<string, string>> &PlayerInfo::SpecialLogs() const
1494 {
1495 return specialLogs;
1496 }
1497
1498
1499
AddSpecialLog(const string & type,const string & name,const string & text)1500 void PlayerInfo::AddSpecialLog(const string &type, const string &name, const string &text)
1501 {
1502 string &entry = specialLogs[type][name];
1503 if(!entry.empty())
1504 entry += "\n\t";
1505 entry += text;
1506 }
1507
1508
1509
HasLogs() const1510 bool PlayerInfo::HasLogs() const
1511 {
1512 return !logbook.empty() || !specialLogs.empty();
1513 }
1514
1515
1516
1517 // Call this after missions update, or if leaving the outfitter, shipyard, or
1518 // hiring panel. Updates the information on how much space is available.
UpdateCargoCapacities()1519 void PlayerInfo::UpdateCargoCapacities()
1520 {
1521 int size = 0;
1522 int bunks = 0;
1523 flagship = FlagshipPtr();
1524 for(const shared_ptr<Ship> &ship : ships)
1525 if(ship->GetSystem() == system && !ship->IsParked() && !ship->IsDisabled())
1526 {
1527 size += ship->Attributes().Get("cargo space");
1528 int crew = (ship == flagship ? ship->Crew() : ship->RequiredCrew());
1529 bunks += ship->Attributes().Get("bunks") - crew;
1530 }
1531 cargo.SetSize(size);
1532 cargo.SetBunks(bunks);
1533 }
1534
1535
1536
1537 // Get the list of active missions.
Missions() const1538 const list<Mission> &PlayerInfo::Missions() const
1539 {
1540 return missions;
1541 }
1542
1543
1544
1545 // Get the list of ordinary jobs that are available on the job board.
AvailableJobs() const1546 const list<Mission> &PlayerInfo::AvailableJobs() const
1547 {
1548 return availableJobs;
1549 }
1550
1551
1552
1553 // Return a pointer to the mission that was most recently accepted while in-flight.
ActiveBoardingMission() const1554 const Mission *PlayerInfo::ActiveBoardingMission() const
1555 {
1556 return activeBoardingMission;
1557 }
1558
1559
1560
1561 // Update mission NPCs with the player's current conditions.
UpdateMissionNPCs()1562 void PlayerInfo::UpdateMissionNPCs()
1563 {
1564 for(Mission &mission : missions)
1565 mission.UpdateNPCs(*this);
1566 }
1567
1568
1569
1570 // Accept the given job.
AcceptJob(const Mission & mission,UI * ui)1571 void PlayerInfo::AcceptJob(const Mission &mission, UI *ui)
1572 {
1573 for(auto it = availableJobs.begin(); it != availableJobs.end(); ++it)
1574 if(&*it == &mission)
1575 {
1576 cargo.AddMissionCargo(&mission);
1577 it->Do(Mission::OFFER, *this);
1578 it->Do(Mission::ACCEPT, *this, ui);
1579 auto spliceIt = it->IsUnique() ? missions.begin() : missions.end();
1580 missions.splice(spliceIt, availableJobs, it);
1581 break;
1582 }
1583 }
1584
1585
1586
1587 // Look at the list of available missions and see if any of them can be offered
1588 // right now, in the given location (landing or spaceport). If there are no
1589 // missions that can be accepted, return a null pointer.
MissionToOffer(Mission::Location location)1590 Mission *PlayerInfo::MissionToOffer(Mission::Location location)
1591 {
1592 if(ships.empty())
1593 return nullptr;
1594
1595 // If a mission can be offered right now, move it to the start of the list
1596 // so we know what mission the callback is referring to, and return it.
1597 for(auto it = availableMissions.begin(); it != availableMissions.end(); ++it)
1598 if(it->IsAtLocation(location) && it->CanOffer(*this) && it->HasSpace(*this))
1599 {
1600 availableMissions.splice(availableMissions.begin(), availableMissions, it);
1601 return &availableMissions.front();
1602 }
1603 return nullptr;
1604 }
1605
1606
1607
1608 // Check if any of the game's missions can be offered from this ship, given its
1609 // relationship with the player. If none offer, return nullptr.
BoardingMission(const shared_ptr<Ship> & ship)1610 Mission *PlayerInfo::BoardingMission(const shared_ptr<Ship> &ship)
1611 {
1612 // Do not create missions from existing mission NPC's, or the player's ships.
1613 if(ship->IsSpecial())
1614 return nullptr;
1615 // Ensure that boarding this NPC again does not create a mission.
1616 ship->SetIsSpecial();
1617
1618 // Update auto conditions to reflect the player's flagship's free capacity.
1619 UpdateAutoConditions(true);
1620 // "boardingMissions" is emptied by MissionCallback, but to be sure:
1621 boardingMissions.clear();
1622
1623 Mission::Location location = (ship->GetGovernment()->IsEnemy()
1624 ? Mission::BOARDING : Mission::ASSISTING);
1625
1626 // Check for available boarding or assisting missions.
1627 for(const auto &it : GameData::Missions())
1628 if(it.second.IsAtLocation(location) && it.second.CanOffer(*this, ship))
1629 {
1630 boardingMissions.push_back(it.second.Instantiate(*this, ship));
1631 if(boardingMissions.back().HasFailed(*this))
1632 boardingMissions.pop_back();
1633 else
1634 return &boardingMissions.back();
1635 }
1636
1637 return nullptr;
1638 }
1639
1640
1641
1642 // Engine calls this after placing the boarding mission's NPCs.
ClearActiveBoardingMission()1643 void PlayerInfo::ClearActiveBoardingMission()
1644 {
1645 activeBoardingMission = nullptr;
1646 }
1647
1648
1649
1650 // If one of your missions cannot be offered because you do not have enough
1651 // space for it, and it specifies a message to be shown in that situation,
1652 // show that message.
HandleBlockedMissions(Mission::Location location,UI * ui)1653 void PlayerInfo::HandleBlockedMissions(Mission::Location location, UI *ui)
1654 {
1655 list<Mission> &missionList = availableMissions.empty() ? boardingMissions : availableMissions;
1656 if(ships.empty() || missionList.empty())
1657 return;
1658
1659 for(auto it = missionList.begin(); it != missionList.end(); ++it)
1660 if(it->IsAtLocation(location) && it->CanOffer(*this) && !it->HasSpace(*this))
1661 {
1662 string message = it->BlockedMessage(*this);
1663 if(!message.empty())
1664 {
1665 ui->Push(new Dialog(message));
1666 return;
1667 }
1668 }
1669 }
1670
1671
1672
1673 // Callback for accepting or declining whatever mission has been offered.
1674 // Responses which would kill the player are handled before the on offer
1675 // conversation ended.
MissionCallback(int response)1676 void PlayerInfo::MissionCallback(int response)
1677 {
1678 list<Mission> &missionList = availableMissions.empty() ? boardingMissions : availableMissions;
1679 if(missionList.empty())
1680 return;
1681
1682 Mission &mission = missionList.front();
1683
1684 // If landed, this conversation may require the player to immediately depart.
1685 shouldLaunch |= (GetPlanet() && Conversation::RequiresLaunch(response));
1686 if(response == Conversation::ACCEPT || response == Conversation::LAUNCH)
1687 {
1688 bool shouldAutosave = mission.RecommendsAutosave();
1689 if(planet)
1690 {
1691 cargo.AddMissionCargo(&mission);
1692 UpdateCargoCapacities();
1693 }
1694 else if(Flagship())
1695 flagship->Cargo().AddMissionCargo(&mission);
1696 else
1697 return;
1698
1699 // Move this mission from the offering list into the "accepted"
1700 // list, viewable on the MissionPanel. Unique missions are moved
1701 // to the front, so they appear at the top of the list if viewed.
1702 auto spliceIt = mission.IsUnique() ? missions.begin() : missions.end();
1703 missions.splice(spliceIt, missionList, missionList.begin());
1704 mission.Do(Mission::ACCEPT, *this);
1705 if(shouldAutosave)
1706 Autosave();
1707 // If this is a mission offered in-flight, expose a pointer to it
1708 // so Engine::SpawnFleets can add its ships without requiring the
1709 // player to land.
1710 if(mission.IsAtLocation(Mission::BOARDING) || mission.IsAtLocation(Mission::ASSISTING))
1711 activeBoardingMission = &*--spliceIt;
1712 }
1713 else if(response == Conversation::DECLINE || response == Conversation::FLEE)
1714 {
1715 mission.Do(Mission::DECLINE, *this);
1716 missionList.pop_front();
1717 }
1718 else if(response == Conversation::DEFER || response == Conversation::DEPART)
1719 {
1720 mission.Do(Mission::DEFER, *this);
1721 missionList.pop_front();
1722 }
1723 }
1724
1725
1726
1727 // Basic callback, allowing conversations to force the player to depart from a
1728 // planet without requiring a mission to offer.
BasicCallback(int response)1729 void PlayerInfo::BasicCallback(int response)
1730 {
1731 // If landed, this conversation may require the player to immediately depart.
1732 shouldLaunch |= (GetPlanet() && Conversation::RequiresLaunch(response));
1733 }
1734
1735
1736
1737 // Mark a mission for removal, either because it was completed, or it failed,
1738 // or because the player aborted it.
RemoveMission(Mission::Trigger trigger,const Mission & mission,UI * ui)1739 void PlayerInfo::RemoveMission(Mission::Trigger trigger, const Mission &mission, UI *ui)
1740 {
1741 for(auto it = missions.begin(); it != missions.end(); ++it)
1742 if(&*it == &mission)
1743 {
1744 // Don't delete the mission yet, because the conversation or dialog
1745 // panel may still be showing. Instead, just mark it as done. Doing
1746 // this first avoids the possibility of an infinite loop, e.g. if a
1747 // mission's "on fail" fails the mission itself.
1748 doneMissions.splice(doneMissions.end(), missions, it);
1749
1750 it->Do(trigger, *this, ui);
1751 cargo.RemoveMissionCargo(&mission);
1752 for(shared_ptr<Ship> &ship : ships)
1753 ship->Cargo().RemoveMissionCargo(&mission);
1754 return;
1755 }
1756 }
1757
1758
1759
1760 // Mark a mission as failed, but do not remove it from the mission list yet.
FailMission(const Mission & mission)1761 void PlayerInfo::FailMission(const Mission &mission)
1762 {
1763 for(auto it = missions.begin(); it != missions.end(); ++it)
1764 if(&*it == &mission)
1765 {
1766 it->Fail();
1767 return;
1768 }
1769 }
1770
1771
1772
1773 // Update mission status based on an event.
HandleEvent(const ShipEvent & event,UI * ui)1774 void PlayerInfo::HandleEvent(const ShipEvent &event, UI *ui)
1775 {
1776 // Combat rating increases when you disable an enemy ship.
1777 if(event.ActorGovernment() && event.ActorGovernment()->IsPlayer())
1778 if((event.Type() & ShipEvent::DISABLE) && event.Target() && !event.Target()->IsYours())
1779 {
1780 auto &rating = conditions["combat rating"];
1781 static const int64_t maxRating = 2000000000;
1782 rating = min(maxRating, rating + (event.Target()->Cost() + 250000) / 500000);
1783 }
1784
1785 for(Mission &mission : missions)
1786 mission.Do(event, *this, ui);
1787
1788 // If the player's flagship was destroyed, the player is dead.
1789 if((event.Type() & ShipEvent::DESTROY) && !ships.empty() && event.Target().get() == Flagship())
1790 Die();
1791 }
1792
1793
1794
1795 // Get the value of the given condition (default 0).
GetCondition(const string & name) const1796 int64_t PlayerInfo::GetCondition(const string &name) const
1797 {
1798 auto it = conditions.find(name);
1799 return (it == conditions.end()) ? 0 : it->second;
1800 }
1801
1802
1803
1804 // Get mutable access to the player's list of conditions.
Conditions()1805 map<string, int64_t> &PlayerInfo::Conditions()
1806 {
1807 return conditions;
1808 }
1809
1810
1811
1812 // Access the player's list of conditions.
Conditions() const1813 const map<string, int64_t> &PlayerInfo::Conditions() const
1814 {
1815 return conditions;
1816 }
1817
1818
1819
1820 // Set and check the reputation conditions, which missions and events can use to
1821 // modify the player's reputation with other governments.
SetReputationConditions()1822 void PlayerInfo::SetReputationConditions()
1823 {
1824 for(const auto &it : GameData::Governments())
1825 {
1826 int64_t rep = it.second.Reputation();
1827 conditions["reputation: " + it.first] = rep;
1828 }
1829 }
1830
1831
1832
CheckReputationConditions()1833 void PlayerInfo::CheckReputationConditions()
1834 {
1835 for(const auto &it : GameData::Governments())
1836 {
1837 int64_t rep = it.second.Reputation();
1838 int64_t newRep = conditions["reputation: " + it.first];
1839 if(newRep != rep)
1840 it.second.AddReputation(newRep - rep);
1841 }
1842 }
1843
1844
1845
1846 // Check if the player knows the location of the given system (whether or not
1847 // they have actually visited it).
HasSeen(const System & system) const1848 bool PlayerInfo::HasSeen(const System &system) const
1849 {
1850 if(seen.count(&system))
1851 return true;
1852
1853 auto usesSystem = [&system](const Mission &m) noexcept -> bool
1854 {
1855 if(!m.IsVisible())
1856 return false;
1857 if(m.Waypoints().count(&system))
1858 return true;
1859 for(auto &&p : m.Stopovers())
1860 if(p->IsInSystem(&system))
1861 return true;
1862 return false;
1863 };
1864 if(any_of(availableJobs.begin(), availableJobs.end(), usesSystem))
1865 return true;
1866 if(any_of(missions.begin(), missions.end(), usesSystem))
1867 return true;
1868
1869 return KnowsName(system);
1870 }
1871
1872
1873
1874 // Check if the player has visited the given system.
HasVisited(const System & system) const1875 bool PlayerInfo::HasVisited(const System &system) const
1876 {
1877 return visitedSystems.count(&system);
1878 }
1879
1880
1881
1882 // Check if the player has visited the given system.
HasVisited(const Planet & planet) const1883 bool PlayerInfo::HasVisited(const Planet &planet) const
1884 {
1885 return visitedPlanets.count(&planet);
1886 }
1887
1888
1889
1890 // Check if the player knows the name of a system, either from visiting there or
1891 // because a job or active mission includes the name of that system.
KnowsName(const System & system) const1892 bool PlayerInfo::KnowsName(const System &system) const
1893 {
1894 if(HasVisited(system))
1895 return true;
1896
1897 for(const Mission &mission : availableJobs)
1898 if(mission.Destination()->IsInSystem(&system))
1899 return true;
1900
1901 for(const Mission &mission : missions)
1902 if(mission.IsVisible() && mission.Destination()->IsInSystem(&system))
1903 return true;
1904
1905 return false;
1906 }
1907
1908
1909 // Mark the given system as visited, and mark all its neighbors as seen.
Visit(const System & system)1910 void PlayerInfo::Visit(const System &system)
1911 {
1912 visitedSystems.insert(&system);
1913 seen.insert(&system);
1914 for(const System *neighbor : system.VisibleNeighbors())
1915 if(!neighbor->Hidden() || system.Links().count(neighbor))
1916 seen.insert(neighbor);
1917 }
1918
1919
1920
1921 // Mark the given planet as visited.
Visit(const Planet & planet)1922 void PlayerInfo::Visit(const Planet &planet)
1923 {
1924 visitedPlanets.insert(&planet);
1925 }
1926
1927
1928
1929 // Mark a system as unvisited, even if visited previously.
Unvisit(const System & system)1930 void PlayerInfo::Unvisit(const System &system)
1931 {
1932 visitedSystems.erase(&system);
1933 for(const StellarObject &object : system.Objects())
1934 if(object.GetPlanet())
1935 Unvisit(*object.GetPlanet());
1936 }
1937
1938
1939
Unvisit(const Planet & planet)1940 void PlayerInfo::Unvisit(const Planet &planet)
1941 {
1942 visitedPlanets.erase(&planet);
1943 }
1944
1945
1946
1947 // Check if the player has a hyperspace route set.
HasTravelPlan() const1948 bool PlayerInfo::HasTravelPlan() const
1949 {
1950 return !travelPlan.empty();
1951 }
1952
1953
1954
1955 // Access the player's travel plan.
TravelPlan() const1956 const vector<const System *> &PlayerInfo::TravelPlan() const
1957 {
1958 return travelPlan;
1959 }
1960
1961
1962
TravelPlan()1963 vector<const System *> &PlayerInfo::TravelPlan()
1964 {
1965 return travelPlan;
1966 }
1967
1968
1969
1970 // This is called when the player enters the system that is their current
1971 // hyperspace target.
PopTravel()1972 void PlayerInfo::PopTravel()
1973 {
1974 if(!travelPlan.empty())
1975 {
1976 Visit(*travelPlan.back());
1977 travelPlan.pop_back();
1978 }
1979 }
1980
1981
1982
1983 // Get the planet to land on at the end of the travel path.
TravelDestination() const1984 const Planet *PlayerInfo::TravelDestination() const
1985 {
1986 return travelDestination;
1987 }
1988
1989
1990
1991 // Set the planet to land on at the end of the travel path.
SetTravelDestination(const Planet * planet)1992 void PlayerInfo::SetTravelDestination(const Planet *planet)
1993 {
1994 travelDestination = planet;
1995 if(planet && planet->IsInSystem(system) && Flagship())
1996 Flagship()->SetTargetStellar(system->FindStellar(planet));
1997 }
1998
1999
2000
2001 // Check which secondary weapon the player has selected.
SelectedWeapon() const2002 const Outfit *PlayerInfo::SelectedWeapon() const
2003 {
2004 return selectedWeapon;
2005 }
2006
2007
2008
2009 // Cycle through all available secondary weapons.
SelectNext()2010 void PlayerInfo::SelectNext()
2011 {
2012 if(!flagship || flagship->Outfits().empty())
2013 return;
2014
2015 // Start with the currently selected weapon, if any.
2016 auto it = flagship->Outfits().find(selectedWeapon);
2017 if(it == flagship->Outfits().end())
2018 it = flagship->Outfits().begin();
2019 else
2020 ++it;
2021
2022 // Find the next secondary weapon.
2023 for( ; it != flagship->Outfits().end(); ++it)
2024 if(it->first->Icon())
2025 {
2026 selectedWeapon = it->first;
2027 return;
2028 }
2029 selectedWeapon = nullptr;
2030 }
2031
2032
2033
2034 // Escorts currently selected for giving orders.
SelectedShips() const2035 const vector<weak_ptr<Ship>> &PlayerInfo::SelectedShips() const
2036 {
2037 return selectedShips;
2038 }
2039
2040
2041
2042 // Select any player ships in the given box or list. Return true if any were
2043 // selected, so we know not to search further for a match.
SelectShips(const Rectangle & box,bool hasShift)2044 bool PlayerInfo::SelectShips(const Rectangle &box, bool hasShift)
2045 {
2046 // If shift is not held down, replace the current selection.
2047 if(!hasShift)
2048 selectedShips.clear();
2049 // If shift is not held, the first ship in the box will also become the
2050 // player's flagship's target.
2051 bool first = !hasShift;
2052
2053 bool matched = false;
2054 for(const shared_ptr<Ship> &ship : ships)
2055 if(!ship->IsDestroyed() && !ship->IsParked() && ship->GetSystem() == system && ship.get() != Flagship()
2056 && box.Contains(ship->Position()))
2057 {
2058 matched = true;
2059 SelectShip(ship, &first);
2060 }
2061 return matched;
2062 }
2063
2064
2065
SelectShips(const vector<const Ship * > & stack,bool hasShift)2066 bool PlayerInfo::SelectShips(const vector<const Ship *> &stack, bool hasShift)
2067 {
2068 // If shift is not held down, replace the current selection.
2069 if(!hasShift)
2070 selectedShips.clear();
2071 // If shift is not held, the first ship in the stack will also become the
2072 // player's flagship's target.
2073 bool first = !hasShift;
2074
2075 // Loop through all the player's ships and check which of them are in the
2076 // given stack.
2077 bool matched = false;
2078 for(const shared_ptr<Ship> &ship : ships)
2079 {
2080 auto it = find(stack.begin(), stack.end(), ship.get());
2081 if(it != stack.end())
2082 {
2083 matched = true;
2084 SelectShip(ship, &first);
2085 }
2086 }
2087 return matched;
2088 }
2089
2090
2091
SelectShip(const Ship * ship,bool hasShift)2092 void PlayerInfo::SelectShip(const Ship *ship, bool hasShift)
2093 {
2094 // If shift is not held down, replace the current selection.
2095 if(!hasShift)
2096 selectedShips.clear();
2097
2098 bool first = !hasShift;
2099 for(const shared_ptr<Ship> &it : ships)
2100 if(it.get() == ship)
2101 SelectShip(it, &first);
2102 }
2103
2104
2105
SelectGroup(int group,bool hasShift)2106 void PlayerInfo::SelectGroup(int group, bool hasShift)
2107 {
2108 int bit = (1 << group);
2109 // If the shift key is held down and all the ships in the given group are
2110 // already selected, deselect them all. Otherwise, select them all. The easy
2111 // way to do this is first to remove all the ships that match in one pass,
2112 // then add them in a subsequent pass if any were not selected.
2113 const Ship *oldTarget = nullptr;
2114 if(Flagship() && Flagship()->GetTargetShip())
2115 {
2116 oldTarget = Flagship()->GetTargetShip().get();
2117 Flagship()->SetTargetShip(shared_ptr<Ship>());
2118 }
2119 if(hasShift)
2120 {
2121 bool allWereSelected = true;
2122 for(const shared_ptr<Ship> &ship : ships)
2123 if(groups[ship.get()] & bit)
2124 {
2125 auto it = selectedShips.begin();
2126 for( ; it != selectedShips.end(); ++it)
2127 if(it->lock() == ship)
2128 break;
2129 if(it != selectedShips.end())
2130 selectedShips.erase(it);
2131 else
2132 allWereSelected = false;
2133 }
2134 if(allWereSelected)
2135 return;
2136 }
2137 else
2138 selectedShips.clear();
2139
2140 // Now, go through and add any ships in the group to the selection. Even if
2141 // shift is held they won't be added twice, because we removed them above.
2142 for(const shared_ptr<Ship> &ship : ships)
2143 if(groups[ship.get()] & bit)
2144 {
2145 selectedShips.push_back(ship);
2146 if(ship.get() == oldTarget)
2147 Flagship()->SetTargetShip(ship);
2148 }
2149 }
2150
2151
2152
SetGroup(int group,const set<Ship * > * newShips)2153 void PlayerInfo::SetGroup(int group, const set<Ship *> *newShips)
2154 {
2155 int bit = (1 << group);
2156 int mask = ~bit;
2157 // First, remove any of your ships that are in the group.
2158 for(const shared_ptr<Ship> &ship : ships)
2159 groups[ship.get()] &= mask;
2160 // Then, add all the currently selected ships to the group.
2161 if(newShips)
2162 {
2163 for(const Ship *ship : *newShips)
2164 groups[ship] |= bit;
2165 }
2166 else
2167 {
2168 for(const weak_ptr<Ship> &ptr : selectedShips)
2169 {
2170 shared_ptr<Ship> ship = ptr.lock();
2171 if(ship)
2172 groups[ship.get()] |= bit;
2173 }
2174 }
2175 }
2176
2177
2178
GetGroup(int group)2179 set<Ship *> PlayerInfo::GetGroup(int group)
2180 {
2181 int bit = (1 << group);
2182 set<Ship *> result;
2183
2184 for(const shared_ptr<Ship> &ship : ships)
2185 {
2186 auto it = groups.find(ship.get());
2187 if(it != groups.end() && (it->second & bit))
2188 result.insert(ship.get());
2189 }
2190 return result;
2191 }
2192
2193
2194
2195 // Keep track of any outfits that you have sold since landing. These will be
2196 // available to buy back until you take off.
Stock(const Outfit * outfit) const2197 int PlayerInfo::Stock(const Outfit *outfit) const
2198 {
2199 auto it = stock.find(outfit);
2200 return (it == stock.end() ? 0 : it->second);
2201 }
2202
2203
2204
2205 // Transfer outfits from the player to the planet or vice versa.
AddStock(const Outfit * outfit,int count)2206 void PlayerInfo::AddStock(const Outfit *outfit, int count)
2207 {
2208 // If you sell an individual outfit that is not sold here and that you
2209 // acquired by buying a ship here, have it appear as "in stock" in case you
2210 // change your mind about selling it. (On the other hand, if you sell an
2211 // entire ship right after buying it, its outfits will not be "in stock.")
2212 if(count > 0 && stock[outfit] < 0)
2213 stock[outfit] = 0;
2214 stock[outfit] += count;
2215
2216 int day = date.DaysSinceEpoch();
2217 if(count > 0)
2218 {
2219 // Remember how depreciated these items are.
2220 for(int i = 0; i < count; ++i)
2221 stockDepreciation.Buy(outfit, day, &depreciation);
2222 }
2223 else
2224 {
2225 // If the count is negative, outfits are being transferred from stock
2226 // into the player's possession.
2227 for(int i = 0; i < -count; ++i)
2228 depreciation.Buy(outfit, day, &stockDepreciation);
2229 }
2230 }
2231
2232
2233
2234 // Get depreciation information.
FleetDepreciation() const2235 const Depreciation &PlayerInfo::FleetDepreciation() const
2236 {
2237 return depreciation;
2238 }
2239
2240
2241
StockDepreciation() const2242 const Depreciation &PlayerInfo::StockDepreciation() const
2243 {
2244 return stockDepreciation;
2245 }
2246
2247
2248
Harvest(const Outfit * type)2249 void PlayerInfo::Harvest(const Outfit *type)
2250 {
2251 if(type && system)
2252 harvested.insert(make_pair(system, type));
2253 }
2254
2255
2256
Harvested() const2257 const set<pair<const System *, const Outfit *>> &PlayerInfo::Harvested() const
2258 {
2259 return harvested;
2260 }
2261
2262
2263
GetEscortDestination() const2264 const pair<const System *, Point> &PlayerInfo::GetEscortDestination() const
2265 {
2266 return interstellarEscortDestination;
2267 }
2268
2269
2270
2271 // Determine if a system and nonzero position were specified.
HasEscortDestination() const2272 bool PlayerInfo::HasEscortDestination() const
2273 {
2274 return interstellarEscortDestination.first && interstellarEscortDestination.second;
2275 }
2276
2277
2278
2279 // Set (or clear) the stored escort travel destination.
SetEscortDestination(const System * system,Point pos)2280 void PlayerInfo::SetEscortDestination(const System *system, Point pos)
2281 {
2282 interstellarEscortDestination.first = system;
2283 interstellarEscortDestination.second = pos;
2284 }
2285
2286
2287
2288 // Get what coloring is currently selected in the map.
MapColoring() const2289 int PlayerInfo::MapColoring() const
2290 {
2291 return mapColoring;
2292 }
2293
2294
2295
2296 // Set what the map is being colored by.
SetMapColoring(int index)2297 void PlayerInfo::SetMapColoring(int index)
2298 {
2299 mapColoring = index;
2300 }
2301
2302
2303
2304 // Get the map zoom level.
MapZoom() const2305 int PlayerInfo::MapZoom() const
2306 {
2307 return mapZoom;
2308 }
2309
2310
2311
2312 // Set the map zoom level.
SetMapZoom(int level)2313 void PlayerInfo::SetMapZoom(int level)
2314 {
2315 mapZoom = level;
2316 }
2317
2318
2319
2320 // Get the set of collapsed categories for the named panel.
Collapsed(const string & name)2321 set<string> &PlayerInfo::Collapsed(const string &name)
2322 {
2323 return collapsed[name];
2324 }
2325
2326
2327
2328 // Apply any "changes" saved in this player info to the global game state.
ApplyChanges()2329 void PlayerInfo::ApplyChanges()
2330 {
2331 for(const auto &it : reputationChanges)
2332 it.first->SetReputation(it.second);
2333 reputationChanges.clear();
2334 AddChanges(dataChanges);
2335 GameData::ReadEconomy(economy);
2336 economy = DataNode();
2337
2338 // Make sure all stellar objects are correctly positioned. This is needed
2339 // because EnterSystem() is not called the first time through.
2340 GameData::SetDate(GetDate());
2341 // SetDate() clears any bribes from yesterday, so restore any auto-clearance.
2342 for(const Mission &mission : Missions())
2343 if(mission.ClearanceMessage() == "auto")
2344 {
2345 mission.Destination()->Bribe(mission.HasFullClearance());
2346 for(const Planet *planet : mission.Stopovers())
2347 planet->Bribe(mission.HasFullClearance());
2348 }
2349
2350 // Check if any special persons have been destroyed.
2351 GameData::DestroyPersons(destroyedPersons);
2352 destroyedPersons.clear();
2353
2354 // Check which planets you have dominated.
2355 static const string prefix = "tribute: ";
2356 for(auto it = conditions.lower_bound(prefix); it != conditions.end(); ++it)
2357 {
2358 if(it->first.compare(0, prefix.length(), prefix))
2359 break;
2360
2361 const Planet *planet = GameData::Planets().Find(it->first.substr(prefix.length()));
2362 if(planet)
2363 GameData::GetPolitics().DominatePlanet(planet);
2364 }
2365
2366 // Issue warnings for any data which has been mentioned but not actually defined, and
2367 // ensure that all "undefined" data is appropriately named.
2368 GameData::CheckReferences();
2369
2370 // Now that all outfits have names, we can finish loading the player's ships.
2371 for(auto &&ship : ships)
2372 {
2373 // Government changes may have changed the player's ship swizzles.
2374 ship->SetGovernment(GameData::PlayerGovernment());
2375 ship->FinishLoading(false);
2376 }
2377 }
2378
2379
2380
2381 // Make change's to the player's planet, system, & ship locations as needed, to ensure the player and
2382 // their ships are in valid locations, even if the player did something drastic, such as remove a mod.
ValidateLoad()2383 void PlayerInfo::ValidateLoad()
2384 {
2385 // If a system was not specified in the player data, use the flagship's system.
2386 if(!planet && !ships.empty())
2387 {
2388 string warning = "Warning: no planet specified for player";
2389 auto it = find_if(ships.begin(), ships.end(), [](const shared_ptr<Ship> &ship) noexcept -> bool
2390 { return ship->GetPlanet() && ship->GetPlanet()->IsValid() && !ship->IsParked() && ship->CanBeFlagship(); });
2391 if(it != ships.end())
2392 {
2393 planet = (*it)->GetPlanet();
2394 system = (*it)->GetSystem();
2395 warning += ". Defaulting to location of flagship \"" + (*it)->Name() + "\", " + planet->TrueName() + ".";
2396 }
2397 else
2398 warning += " (no ships could supply a valid player location).";
2399
2400 Files::LogError(warning);
2401 }
2402
2403 // As a result of external game data changes (e.g. unloading a mod) it's possible the player ended up
2404 // with an undefined system or planet. In that case, move them to the starting system to avoid crashing.
2405 if(planet && !system)
2406 {
2407 system = planet->GetSystem();
2408 Files::LogError("Warning: player system was not specified. Defaulting to the specified planet's system.");
2409 }
2410 if(!planet || !planet->IsValid() || !system || !system->IsValid())
2411 {
2412 system = &startData.GetSystem();
2413 planet = &startData.GetPlanet();
2414 Files::LogError("Warning: player system and/or planet was not valid. Defaulting to the starting location.");
2415 }
2416
2417 // Every ship ought to have specified a valid location, but if not,
2418 // move it to the player's location to avoid invalid states.
2419 for(auto &&ship : ships)
2420 {
2421 if(!ship->GetSystem() || !ship->GetSystem()->IsValid())
2422 {
2423 ship->SetSystem(system);
2424 Files::LogError("Warning: player ship \"" + ship->Name()
2425 + "\" did not specify a valid system. Defaulting to the player's system.");
2426 }
2427 // In-system ships that aren't on a valid planet should get moved to the player's planet
2428 // (but e.g. disabled ships or those that didn't have a planet should remain in space).
2429 if(ship->GetSystem() == system && ship->GetPlanet() && !ship->GetPlanet()->IsValid())
2430 {
2431 ship->SetPlanet(planet);
2432 Files::LogError("Warning: in-system player ship \"" + ship->Name()
2433 + "\" specified an invalid planet. Defaulting to the player's planet.");
2434 }
2435 // Owned ships that are not in the player's system always start in flight.
2436 }
2437
2438 // Validate the travel plan.
2439 if(travelDestination && !travelDestination->IsValid())
2440 {
2441 Files::LogError("Warning: removed invalid travel plan destination \"" + travelDestination->TrueName() + ".\"");
2442 travelDestination = nullptr;
2443 }
2444 if(!travelPlan.empty() && any_of(travelPlan.begin(), travelPlan.end(),
2445 [](const System *waypoint) noexcept -> bool { return !waypoint->IsValid(); }))
2446 {
2447 travelPlan.clear();
2448 travelDestination = nullptr;
2449 Files::LogError("Warning: reset the travel plan due to use of invalid system(s).");
2450 }
2451
2452 // For old saves, default to the first start condition (the default "Endless Sky" start).
2453 if(startData.Identifier().empty())
2454 {
2455 // It is possible that there are no start conditions defined (e.g. a bad installation or
2456 // incomplete total conversion plugin). In that case, it is not possible to continue.
2457 const auto startCount = GameData::StartOptions().size();
2458 if(startCount >= 1)
2459 {
2460 startData = GameData::StartOptions().front();
2461 // When necessary, record in the pilot file that the starting data is just an assumption.
2462 if(startCount >= 2)
2463 conditions["unverified start scenario"] = true;
2464 }
2465 else
2466 throw runtime_error("Unable to set a starting scenario for an existing pilot. (No valid \"start\" "
2467 "nodes were found in data files or loaded plugins--make sure you've installed the game properly.)");
2468 }
2469
2470 // Validate the missions that were loaded. Active-but-invalid missions are removed from
2471 // the standard mission list, effectively pausing them until necessary data is restored.
2472 missions.sort([](const Mission &lhs, const Mission &rhs) noexcept -> bool { return lhs.IsValid(); });
2473 auto isInvalidMission = [](const Mission &m) noexcept -> bool { return !m.IsValid(); };
2474 auto mit = find_if(missions.begin(), missions.end(), isInvalidMission);
2475 if(mit != missions.end())
2476 inactiveMissions.splice(inactiveMissions.end(), missions, mit, missions.end());
2477
2478 // Invalid available jobs or missions are erased (since there is no guarantee
2479 // the player will be on the correct planet when a plugin is re-added).
2480 availableJobs.remove_if(isInvalidMission);
2481 availableMissions.remove_if(isInvalidMission);
2482 }
2483
2484
2485
2486 // Update the conditions that reflect the current status of the player.
UpdateAutoConditions(bool isBoarding)2487 void PlayerInfo::UpdateAutoConditions(bool isBoarding)
2488 {
2489 // Bound financial conditions to +/- 4.6 x 10^18 credits, within the range of a 64-bit int.
2490 static constexpr int64_t limit = static_cast<int64_t>(1) << 62;
2491 conditions["net worth"] = min(limit, max(-limit, accounts.NetWorth()));
2492 conditions["credits"] = min(limit, accounts.Credits());
2493 conditions["unpaid mortgages"] = min(limit, accounts.TotalDebt("Mortgage"));
2494 conditions["unpaid fines"] = min(limit, accounts.TotalDebt("Fine"));
2495 conditions["unpaid salaries"] = min(limit, accounts.SalariesOwed());
2496 conditions["unpaid maintenance"] = min(limit, accounts.MaintenanceDue());
2497 conditions["credit score"] = accounts.CreditScore();
2498 // Serialize the current reputation with other governments.
2499 SetReputationConditions();
2500 // Helper lambda function to clear a range
2501 auto clearRange = [](map<string, int64_t> &conditionsMap, string firstStr, string lastStr)
2502 {
2503 auto first = conditionsMap.lower_bound(firstStr);
2504 auto last = conditionsMap.lower_bound(lastStr);
2505 if(first != last)
2506 conditionsMap.erase(first, last);
2507 };
2508 // Clear any existing ships: conditions. (Note: '!' = ' ' + 1.)
2509 clearRange(conditions, "ships: ", "ships:!");
2510 // Store special conditions for cargo and passenger space.
2511 conditions["cargo space"] = 0;
2512 conditions["passenger space"] = 0;
2513 for(const shared_ptr<Ship> &ship : ships)
2514 if(!ship->IsParked() && !ship->IsDisabled() && ship->GetSystem() == system)
2515 {
2516 conditions["cargo space"] += ship->Attributes().Get("cargo space");
2517 conditions["passenger space"] += ship->Attributes().Get("bunks") - ship->RequiredCrew();
2518 ++conditions["ships: " + ship->Attributes().Category()];
2519 }
2520 // If boarding a ship, missions should not consider the space available
2521 // in the player's entire fleet. The only fleet parameter offered to a
2522 // boarding mission is the fleet composition (e.g. 4 Heavy Warships).
2523 if(isBoarding && flagship)
2524 {
2525 conditions["cargo space"] = flagship->Cargo().Free();
2526 conditions["passenger space"] = flagship->Cargo().BunksFree();
2527 }
2528
2529 // Clear any existing flagship system: and planet: conditions. (Note: '!' = ' ' + 1.)
2530 clearRange(conditions, "flagship system: ", "flagship system:!");
2531 clearRange(conditions, "flagship planet: ", "flagship planet:!");
2532
2533 // Store conditions for flagship current crew, required crew, and bunks.
2534 if(flagship)
2535 {
2536 conditions["flagship crew"] = flagship->Crew();
2537 conditions["flagship required crew"] = flagship->RequiredCrew();
2538 conditions["flagship bunks"] = flagship->Attributes().Get("bunks");
2539 if(flagship->GetSystem())
2540 conditions["flagship system: " + flagship->GetSystem()->Name()] = 1;
2541 if(flagship->GetPlanet())
2542 conditions["flagship planet: " + flagship->GetPlanet()->TrueName()] = 1;
2543 }
2544 else
2545 {
2546 conditions["flagship crew"] = 0;
2547 conditions["flagship required crew"] = 0;
2548 conditions["flagship bunks"] = 0;
2549 }
2550
2551 // Conditions for your fleet's attractiveness to pirates:
2552 pair<double, double> factors = RaidFleetFactors();
2553 conditions["cargo attractiveness"] = factors.first;
2554 conditions["armament deterrence"] = factors.second;
2555 conditions["pirate attraction"] = factors.first - factors.second;
2556 }
2557
2558
2559
2560 // New missions are generated each time you land on a planet.
CreateMissions()2561 void PlayerInfo::CreateMissions()
2562 {
2563 boardingMissions.clear();
2564
2565 // Check for available missions.
2566 bool skipJobs = planet && !planet->IsInhabited();
2567 bool hasPriorityMissions = false;
2568 for(const auto &it : GameData::Missions())
2569 {
2570 if(it.second.IsAtLocation(Mission::BOARDING) || it.second.IsAtLocation(Mission::ASSISTING))
2571 continue;
2572 if(skipJobs && it.second.IsAtLocation(Mission::JOB))
2573 continue;
2574
2575 if(it.second.CanOffer(*this))
2576 {
2577 list<Mission> &missions =
2578 it.second.IsAtLocation(Mission::JOB) ? availableJobs : availableMissions;
2579
2580 missions.push_back(it.second.Instantiate(*this));
2581 if(missions.back().HasFailed(*this))
2582 missions.pop_back();
2583 else if(!it.second.IsAtLocation(Mission::JOB))
2584 hasPriorityMissions |= missions.back().HasPriority();
2585 }
2586 }
2587
2588 // If any of the available missions are "priority" missions, no other
2589 // special missions will be offered in the spaceport.
2590 if(hasPriorityMissions)
2591 {
2592 auto it = availableMissions.begin();
2593 while(it != availableMissions.end())
2594 {
2595 if(it->IsAtLocation(Mission::SPACEPORT) && !it->HasPriority())
2596 it = availableMissions.erase(it);
2597 else
2598 ++it;
2599 }
2600 }
2601 else if(availableMissions.size() > 1)
2602 {
2603 // Minor missions only get offered if no other missions (including other
2604 // minor missions) are competing with them. This is to avoid having two
2605 // or three missions pop up as soon as you enter the spaceport.
2606 auto it = availableMissions.begin();
2607 while(it != availableMissions.end())
2608 {
2609 if(it->IsMinor())
2610 {
2611 it = availableMissions.erase(it);
2612 if(availableMissions.size() <= 1)
2613 break;
2614 }
2615 else
2616 ++it;
2617 }
2618 }
2619 }
2620
2621
2622
2623 // Updates each mission upon landing, to perform landing actions (Stopover,
2624 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
StepMissions(UI * ui)2625 void PlayerInfo::StepMissions(UI *ui)
2626 {
2627 // Check for NPCs that have been destroyed without their destruction
2628 // being registered, e.g. by self-destruct:
2629 for(Mission &mission : missions)
2630 for(const NPC &npc : mission.NPCs())
2631 for(const shared_ptr<Ship> &ship : npc.Ships())
2632 if(ship->IsDestroyed())
2633 mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
2634
2635 // Check missions for status changes from landing.
2636 string visitText;
2637 int missionVisits = 0;
2638 auto substitutions = map<string, string>{
2639 {"<first>", firstName},
2640 {"<last>", lastName}
2641 };
2642 if(Flagship())
2643 substitutions["<ship>"] = Flagship()->Name();
2644
2645 auto mit = missions.begin();
2646 while(mit != missions.end())
2647 {
2648 Mission &mission = *mit;
2649 ++mit;
2650
2651 // If this is a stopover for the mission, perform the stopover action.
2652 mission.Do(Mission::STOPOVER, *this, ui);
2653
2654 if(mission.HasFailed(*this))
2655 RemoveMission(Mission::FAIL, mission, ui);
2656 else if(mission.CanComplete(*this))
2657 RemoveMission(Mission::COMPLETE, mission, ui);
2658 else if(mission.Destination() == GetPlanet() && !freshlyLoaded)
2659 {
2660 mission.Do(Mission::VISIT, *this, ui);
2661 if(mission.IsUnique() || !mission.IsVisible())
2662 continue;
2663
2664 // On visit dialogs are handled separately as to avoid a player
2665 // getting spammed by on visit dialogs if they are stacking jobs
2666 // from the same destination.
2667 if(visitText.empty())
2668 {
2669 const auto &text = mission.GetAction(Mission::VISIT).DialogText();
2670 if(!text.empty())
2671 visitText = Format::Replace(text, substitutions);
2672 }
2673 ++missionVisits;
2674 }
2675 }
2676 if(!visitText.empty())
2677 {
2678 if(missionVisits > 1)
2679 visitText += "\n\t(You have " + Format::Number(missionVisits - 1) + " other unfinished "
2680 + ((missionVisits > 2) ? "missions" : "mission") + " at this location.)";
2681 ui->Push(new Dialog(visitText));
2682 }
2683 // One mission's actions may influence another mission, so loop through one
2684 // more time to see if any mission is now completed or failed due to a change
2685 // that happened in another mission the first time through.
2686 mit = missions.begin();
2687 while(mit != missions.end())
2688 {
2689 Mission &mission = *mit;
2690 ++mit;
2691
2692 if(mission.HasFailed(*this))
2693 RemoveMission(Mission::FAIL, mission, ui);
2694 else if(mission.CanComplete(*this))
2695 RemoveMission(Mission::COMPLETE, mission, ui);
2696 }
2697
2698 // Search for any missions that have failed but for which we are still
2699 // holding on to some cargo.
2700 set<const Mission *> active;
2701 for(const Mission &it : missions)
2702 active.insert(&it);
2703
2704 vector<const Mission *> missionsToRemove;
2705 for(const auto &it : cargo.MissionCargo())
2706 if(!active.count(it.first))
2707 missionsToRemove.push_back(it.first);
2708 for(const auto &it : cargo.PassengerList())
2709 if(!active.count(it.first))
2710 missionsToRemove.push_back(it.first);
2711 for(const Mission *mission : missionsToRemove)
2712 cargo.RemoveMissionCargo(mission);
2713 }
2714
2715
2716
Autosave() const2717 void PlayerInfo::Autosave() const
2718 {
2719 if(!CanBeSaved() || filePath.length() < 4)
2720 return;
2721
2722 string path = filePath.substr(0, filePath.length() - 4) + "~autosave.txt";
2723 Save(path);
2724 }
2725
2726
2727
Save(const string & path) const2728 void PlayerInfo::Save(const string &path) const
2729 {
2730 DataWriter out(path);
2731
2732
2733 // Basic player information and persistent UI settings:
2734
2735 // Pilot information:
2736 out.Write("pilot", firstName, lastName);
2737 out.Write("date", date.Day(), date.Month(), date.Year());
2738 if(system)
2739 out.Write("system", system->Name());
2740 if(planet)
2741 out.Write("planet", planet->TrueName());
2742 if(planet && planet->CanUseServices())
2743 out.Write("clearance");
2744 out.Write("playtime", playTime);
2745 // This flag is set if the player must leave the planet immediately upon
2746 // entering their ship (i.e. because a mission forced them to take off).
2747 if(shouldLaunch)
2748 out.Write("launching");
2749 for(const System *system : travelPlan)
2750 out.Write("travel", system->Name());
2751 if(travelDestination)
2752 out.Write("travel destination", travelDestination->TrueName());
2753
2754 // Save the current setting for the map coloring;
2755 out.Write("map coloring", mapColoring);
2756 out.Write("map zoom", mapZoom);
2757 // Remember what categories are collapsed.
2758 for(const auto &it : collapsed)
2759 {
2760 // Skip panels where nothing was collapsed.
2761 if(it.second.empty())
2762 continue;
2763
2764 out.Write("collapsed", it.first);
2765 out.BeginChild();
2766 {
2767 for(const auto &cit : it.second)
2768 out.Write(cit);
2769 }
2770 out.EndChild();
2771 }
2772
2773 out.Write("reputation with");
2774 out.BeginChild();
2775 {
2776 for(const auto &it : GameData::Governments())
2777 if(!it.second.IsPlayer())
2778 out.Write(it.first, it.second.Reputation());
2779 }
2780 out.EndChild();
2781
2782
2783 // Records of things you own:
2784 out.Write();
2785 out.WriteComment("What you own:");
2786
2787 // Save all the data for all the player's ships.
2788 for(const shared_ptr<Ship> &ship : ships)
2789 {
2790 ship->Save(out);
2791 auto it = groups.find(ship.get());
2792 if(it != groups.end() && it->second)
2793 out.Write("groups", it->second);
2794 }
2795 if(!planetaryStorage.empty())
2796 {
2797 out.Write("storage");
2798 out.BeginChild();
2799 {
2800 for(const auto &it : planetaryStorage)
2801 if(!it.second.IsEmpty())
2802 {
2803 out.Write("planet", it.first->TrueName());
2804 out.BeginChild();
2805 {
2806 it.second.Save(out);
2807 }
2808 out.EndChild();
2809 }
2810 }
2811 out.EndChild();
2812 }
2813
2814 // Save accounting information, cargo, and cargo cost bases.
2815 accounts.Save(out);
2816 cargo.Save(out);
2817 if(!costBasis.empty())
2818 {
2819 out.Write("basis");
2820 out.BeginChild();
2821 {
2822 for(const auto &it : costBasis)
2823 if(it.second)
2824 out.Write(it.first, it.second);
2825 }
2826 out.EndChild();
2827 }
2828
2829 if(!stock.empty())
2830 {
2831 out.Write("stock");
2832 out.BeginChild();
2833 {
2834 using StockElement = pair<const Outfit *const, int>;
2835 WriteSorted(stock,
2836 [](const StockElement *lhs, const StockElement *rhs)
2837 { return lhs->first->Name() < rhs->first->Name(); },
2838 [&out](const StockElement &it)
2839 {
2840 if(it.second)
2841 out.Write(it.first->Name(), it.second);
2842 });
2843 }
2844 out.EndChild();
2845 }
2846 depreciation.Save(out, date.DaysSinceEpoch());
2847 stockDepreciation.Save(out, date.DaysSinceEpoch());
2848
2849
2850 // Records of things you have done or are doing, or have happened to you:
2851 out.Write();
2852 out.WriteComment("What you've done:");
2853
2854 // Save all missions (accepted, accepted-but-invalid, and available).
2855 for(const Mission &mission : missions)
2856 mission.Save(out);
2857 for(const Mission &mission : inactiveMissions)
2858 mission.Save(out);
2859 for(const Mission &mission : availableJobs)
2860 mission.Save(out, "available job");
2861 for(const Mission &mission : availableMissions)
2862 mission.Save(out, "available mission");
2863
2864 // Save any "condition" flags that are set.
2865 if(!conditions.empty())
2866 {
2867 out.Write("conditions");
2868 out.BeginChild();
2869 {
2870 for(const auto &it : conditions)
2871 {
2872 // If the condition's value is 1, don't bother writing the 1.
2873 if(it.second == 1)
2874 out.Write(it.first);
2875 else if(it.second)
2876 out.Write(it.first, it.second);
2877 }
2878 }
2879 out.EndChild();
2880 }
2881
2882 // Save pending events, and changes that have happened due to past events.
2883 for(const GameEvent &event : gameEvents)
2884 event.Save(out);
2885 if(!dataChanges.empty())
2886 {
2887 out.Write("changes");
2888 out.BeginChild();
2889 {
2890 for(const DataNode &node : dataChanges)
2891 out.Write(node);
2892 }
2893 out.EndChild();
2894 }
2895 GameData::WriteEconomy(out);
2896
2897 // Check which persons have been captured or destroyed.
2898 for(const auto &it : GameData::Persons())
2899 if(it.second.IsDestroyed())
2900 out.Write("destroyed", it.first);
2901
2902
2903 // Records of things you have discovered:
2904 out.Write();
2905 out.WriteComment("What you know:");
2906
2907 // Save a list of systems the player has visited.
2908 WriteSorted(visitedSystems,
2909 [](const System *const *lhs, const System *const *rhs)
2910 { return (*lhs)->Name() < (*rhs)->Name(); },
2911 [&out](const System *system)
2912 {
2913 out.Write("visited", system->Name());
2914 });
2915
2916 // Save a list of planets the player has visited.
2917 WriteSorted(visitedPlanets,
2918 [](const Planet *const *lhs, const Planet *const *rhs)
2919 { return (*lhs)->TrueName() < (*rhs)->TrueName(); },
2920 [&out](const Planet *planet)
2921 {
2922 out.Write("visited planet", planet->TrueName());
2923 });
2924
2925 if(!harvested.empty())
2926 {
2927 out.Write("harvested");
2928 out.BeginChild();
2929 {
2930 using HarvestLog = pair<const System *, const Outfit *>;
2931 WriteSorted(harvested,
2932 [](const HarvestLog *lhs, const HarvestLog *rhs) -> bool
2933 {
2934 // Sort by system name and then by outfit name.
2935 if(lhs->first != rhs->first)
2936 return lhs->first->Name() < rhs->first->Name();
2937 else
2938 return lhs->second->Name() < rhs->second->Name();
2939 },
2940 [&out](const HarvestLog &it)
2941 {
2942 out.Write(it.first->Name(), it.second->Name());
2943 });
2944 }
2945 out.EndChild();
2946 }
2947
2948 out.Write("logbook");
2949 out.BeginChild();
2950 {
2951 for(auto &&it : logbook)
2952 {
2953 out.Write(it.first.Day(), it.first.Month(), it.first.Year());
2954 out.BeginChild();
2955 {
2956 // Break the text up into paragraphs.
2957 for(const string &line : Format::Split(it.second, "\n\t"))
2958 out.Write(line);
2959 }
2960 out.EndChild();
2961 }
2962 for(auto &&it : specialLogs)
2963 for(auto &&eit : it.second)
2964 {
2965 out.Write(it.first, eit.first);
2966 out.BeginChild();
2967 {
2968 // Break the text up into paragraphs.
2969 for(const string &line : Format::Split(eit.second, "\n\t"))
2970 out.Write(line);
2971 }
2972 out.EndChild();
2973 }
2974 }
2975 out.EndChild();
2976
2977 out.Write();
2978 out.WriteComment("How you began:");
2979 startData.Save(out);
2980 }
2981
2982
2983
2984 // Check (and perform) any fines incurred by planetary security. If the player
2985 // has dominated the planet, or was given clearance to this planet by a mission,
2986 // planetary security is avoided. Infiltrating implies evasion of security.
Fine(UI * ui)2987 void PlayerInfo::Fine(UI *ui)
2988 {
2989 const Planet *planet = GetPlanet();
2990 // Dominated planets should never fine you.
2991 // By default, uninhabited planets should not fine the player.
2992 if(GameData::GetPolitics().HasDominated(planet)
2993 || !(planet->IsInhabited() || planet->HasCustomSecurity()))
2994 return;
2995
2996 // Planets should not fine you if you have mission clearance or are infiltrating.
2997 for(const Mission &mission : missions)
2998 if(mission.HasClearance(planet) || (!mission.HasFullClearance() &&
2999 (mission.Destination() == planet || mission.Stopovers().count(planet))))
3000 return;
3001
3002 // The planet's government must have the authority to enforce laws.
3003 const Government *gov = planet->GetGovernment();
3004 if(!gov->CanEnforce(planet))
3005 return;
3006
3007 string message = gov->Fine(*this, 0, nullptr, planet->Security());
3008 if(!message.empty())
3009 {
3010 if(message == "atrocity")
3011 {
3012 const Conversation *conversation = gov->DeathSentence();
3013 if(conversation)
3014 ui->Push(new ConversationPanel(*this, *conversation));
3015 else
3016 {
3017 message = "Before you can leave your ship, the " + gov->GetName()
3018 + " authorities show up and begin scanning it. They say, \"Captain "
3019 + LastName()
3020 + ", we detect highly illegal material on your ship.\""
3021 "\n\tYou are sentenced to lifetime imprisonment on a penal colony."
3022 " Your days of traveling the stars have come to an end.";
3023 ui->Push(new Dialog(message));
3024 }
3025 // All ships belonging to the player should be removed.
3026 Die();
3027 }
3028 else
3029 ui->Push(new Dialog(message));
3030 }
3031 }
3032
3033
3034
3035 // Helper function to update the ship selection.
SelectShip(const shared_ptr<Ship> & ship,bool * first)3036 void PlayerInfo::SelectShip(const shared_ptr<Ship> &ship, bool *first)
3037 {
3038 // Make sure this ship is not already selected.
3039 auto it = selectedShips.begin();
3040 for( ; it != selectedShips.end(); ++it)
3041 if(it->lock() == ship)
3042 break;
3043 if(it == selectedShips.end())
3044 {
3045 // This ship is not yet selected.
3046 selectedShips.push_back(ship);
3047 Ship *flagship = Flagship();
3048 if(*first && flagship && ship.get() != flagship)
3049 {
3050 flagship->SetTargetShip(ship);
3051 *first = false;
3052 }
3053 }
3054 }
3055
3056
3057
3058 // Check that this player's current state can be saved.
CanBeSaved() const3059 bool PlayerInfo::CanBeSaved() const
3060 {
3061 return (!isDead && planet && system && !firstName.empty() && !lastName.empty());
3062 }
3063