1 /* MissionAction.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 "MissionAction.h"
14
15 #include "CargoHold.h"
16 #include "ConversationPanel.h"
17 #include "DataNode.h"
18 #include "DataWriter.h"
19 #include "Dialog.h"
20 #include "text/Format.h"
21 #include "GameData.h"
22 #include "GameEvent.h"
23 #include "Messages.h"
24 #include "Outfit.h"
25 #include "PlayerInfo.h"
26 #include "Random.h"
27 #include "Ship.h"
28 #include "UI.h"
29
30 #include <cstdlib>
31
32 using namespace std;
33
34 namespace {
DoGift(PlayerInfo & player,const Ship * model,const string & name)35 void DoGift(PlayerInfo &player, const Ship *model, const string &name)
36 {
37 if(model->ModelName().empty())
38 return;
39
40 player.BuyShip(model, name, true);
41 Messages::Add("The " + model->ModelName() + " \"" + name + "\" was added to your fleet.");
42 }
43
DoGift(PlayerInfo & player,const Outfit * outfit,int count,UI * ui)44 void DoGift(PlayerInfo &player, const Outfit *outfit, int count, UI *ui)
45 {
46 Ship *flagship = player.Flagship();
47 bool isSingle = (abs(count) == 1);
48 string nameWas = (isSingle ? outfit->Name() : outfit->PluralName());
49 if(!flagship || !count || nameWas.empty())
50 return;
51
52 nameWas += (isSingle ? " was" : " were");
53 string message;
54 if(isSingle)
55 {
56 char c = tolower(nameWas.front());
57 bool isVowel = (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u');
58 message = (isVowel ? "An " : "A ");
59 }
60 else
61 message = to_string(abs(count)) + " ";
62
63 message += nameWas;
64 if(count > 0)
65 message += " added to your ";
66 else
67 message += " removed from your ";
68
69 bool didCargo = false;
70 bool didShip = false;
71 // If not landed, transfers must be done into the flagship's CargoHold.
72 CargoHold &cargo = (player.GetPlanet() ? player.Cargo() : flagship->Cargo());
73 int cargoCount = cargo.Get(outfit);
74 if(count < 0 && cargoCount)
75 {
76 int moved = min(cargoCount, -count);
77 count += moved;
78 cargo.Remove(outfit, moved);
79 didCargo = true;
80 }
81 while(count)
82 {
83 int moved = (count > 0) ? 1 : -1;
84 if(flagship->Attributes().CanAdd(*outfit, moved))
85 {
86 flagship->AddOutfit(outfit, moved);
87 didShip = true;
88 }
89 else
90 break;
91 count -= moved;
92 }
93 if(count > 0)
94 {
95 // Ignore cargo size limits.
96 int size = cargo.Size();
97 cargo.SetSize(-1);
98 cargo.Add(outfit, count);
99 cargo.SetSize(size);
100 didCargo = true;
101 if(ui)
102 {
103 string special = "The " + nameWas;
104 special += " put in your cargo hold because there is not enough space to install ";
105 special += (isSingle ? "it" : "them");
106 special += " in your ship.";
107 ui->Push(new Dialog(special));
108 }
109 }
110 if(didCargo && didShip)
111 message += "cargo hold and your flagship.";
112 else if(didCargo)
113 message += "cargo hold.";
114 else
115 message += "flagship.";
116 Messages::Add(message);
117 }
118
CountInCargo(const Outfit * outfit,const PlayerInfo & player)119 int CountInCargo(const Outfit *outfit, const PlayerInfo &player)
120 {
121 int available = 0;
122 // If landed, all cargo from available ships is pooled together.
123 if(player.GetPlanet())
124 available += player.Cargo().Get(outfit);
125 // Otherwise only count outfits in the cargo holds of in-system ships.
126 else
127 {
128 const System *here = player.GetSystem();
129 for(const auto &ship : player.Ships())
130 {
131 if(ship->IsDisabled() || ship->IsParked())
132 continue;
133 if(ship->GetSystem() == here || (ship->CanBeCarried()
134 && !ship->GetSystem() && ship->GetParent()->GetSystem() == here))
135 available += ship->Cargo().Get(outfit);
136 }
137 }
138 return available;
139 }
140 }
141
142
143
144 // Construct and Load() at the same time.
MissionAction(const DataNode & node,const string & missionName)145 MissionAction::MissionAction(const DataNode &node, const string &missionName)
146 {
147 Load(node, missionName);
148 }
149
150
151
Load(const DataNode & node,const string & missionName)152 void MissionAction::Load(const DataNode &node, const string &missionName)
153 {
154 if(node.Size() >= 2)
155 trigger = node.Token(1);
156 if(node.Size() >= 3)
157 system = node.Token(2);
158
159 for(const DataNode &child : node)
160 {
161 const string &key = child.Token(0);
162 bool hasValue = (child.Size() >= 2);
163
164 if(key == "log")
165 {
166 bool isSpecial = (child.Size() >= 3);
167 string &text = (isSpecial ?
168 specialLogText[child.Token(1)][child.Token(2)] : logText);
169 Dialog::ParseTextNode(child, isSpecial ? 3 : 1, text);
170 }
171 else if(key == "dialog")
172 {
173 // Dialog text may be supplied from a stock named phrase, a
174 // private unnamed phrase, or directly specified.
175 if(hasValue && child.Token(1) == "phrase")
176 {
177 if(!child.HasChildren() && child.Size() == 3)
178 stockDialogPhrase = GameData::Phrases().Get(child.Token(2));
179 else
180 child.PrintTrace("Skipping unsupported dialog phrase syntax:");
181 }
182 else if(!hasValue && child.HasChildren() && (*child.begin()).Token(0) == "phrase")
183 {
184 const DataNode &firstGrand = (*child.begin());
185 if(firstGrand.Size() == 1 && firstGrand.HasChildren())
186 dialogPhrase.Load(firstGrand);
187 else
188 firstGrand.PrintTrace("Skipping unsupported dialog phrase syntax:");
189 }
190 else
191 Dialog::ParseTextNode(child, 1, dialogText);
192 }
193 else if(key == "conversation" && child.HasChildren())
194 conversation.Load(child);
195 else if(key == "conversation" && hasValue)
196 stockConversation = GameData::Conversations().Get(child.Token(1));
197 else if(key == "give" && hasValue)
198 {
199 if(child.Token(1) == "ship" && child.Size() >= 3)
200 giftShips.emplace_back(GameData::Ships().Get(child.Token(2)), child.Size() >= 4 ? child.Token(3) : "");
201 else
202 child.PrintTrace("Skipping unsupported \"give\" syntax:");
203 }
204 else if(key == "outfit" && hasValue)
205 {
206 int count = (child.Size() < 3 ? 1 : static_cast<int>(child.Value(2)));
207 if(count)
208 giftOutfits[GameData::Outfits().Get(child.Token(1))] = count;
209 else
210 {
211 // outfit <outfit> 0 means the player must have this outfit.
212 child.PrintTrace("Warning: deprecated use of \"outfit\" with count of 0. Use \"require <outfit>\" instead:");
213 requiredOutfits[GameData::Outfits().Get(child.Token(1))] = 1;
214 }
215 }
216 else if(key == "require" && hasValue)
217 {
218 int count = (child.Size() < 3 ? 1 : static_cast<int>(child.Value(2)));
219 if(count >= 0)
220 requiredOutfits[GameData::Outfits().Get(child.Token(1))] = count;
221 else
222 child.PrintTrace("Skipping invalid \"require\" amount:");
223 }
224 else if(key == "payment")
225 {
226 if(child.Size() == 1)
227 paymentMultiplier += 150;
228 if(child.Size() >= 2)
229 payment += child.Value(1);
230 if(child.Size() >= 3)
231 paymentMultiplier += child.Value(2);
232 }
233 else if(key == "event" && hasValue)
234 {
235 int minDays = (child.Size() >= 3 ? child.Value(2) : 0);
236 int maxDays = (child.Size() >= 4 ? child.Value(3) : minDays);
237 if(maxDays < minDays)
238 swap(minDays, maxDays);
239 events[GameData::Events().Get(child.Token(1))] = make_pair(minDays, maxDays);
240 }
241 else if(key == "fail")
242 {
243 string toFail = child.Size() >= 2 ? child.Token(1) : missionName;
244 fail.insert(toFail);
245 // Create a GameData reference to this mission name.
246 GameData::Missions().Get(toFail);
247 }
248 else if(key == "system")
249 {
250 if(system.empty() && child.HasChildren())
251 systemFilter.Load(child);
252 else
253 child.PrintTrace("Unsupported use of \"system\" LocationFilter:");
254 }
255 else
256 conditions.Add(child);
257 }
258 }
259
260
261
262 // Note: the Save() function can assume this is an instantiated mission, not
263 // a template, so it only has to save a subset of the data.
Save(DataWriter & out) const264 void MissionAction::Save(DataWriter &out) const
265 {
266 if(system.empty())
267 out.Write("on", trigger);
268 else
269 out.Write("on", trigger, system);
270 out.BeginChild();
271 {
272 if(!systemFilter.IsEmpty())
273 {
274 out.Write("system");
275 // LocationFilter indentation is handled by its Save method.
276 systemFilter.Save(out);
277 }
278 if(!logText.empty())
279 {
280 out.Write("log");
281 out.BeginChild();
282 {
283 // Break the text up into paragraphs.
284 for(const string &line : Format::Split(logText, "\n\t"))
285 out.Write(line);
286 }
287 out.EndChild();
288 }
289 for(const auto &it : specialLogText)
290 for(const auto &eit : it.second)
291 {
292 out.Write("log", it.first, eit.first);
293 out.BeginChild();
294 {
295 // Break the text up into paragraphs.
296 for(const string &line : Format::Split(eit.second, "\n\t"))
297 out.Write(line);
298 }
299 out.EndChild();
300 }
301 if(!dialogText.empty())
302 {
303 out.Write("dialog");
304 out.BeginChild();
305 {
306 // Break the text up into paragraphs.
307 for(const string &line : Format::Split(dialogText, "\n\t"))
308 out.Write(line);
309 }
310 out.EndChild();
311 }
312 if(!conversation.IsEmpty())
313 conversation.Save(out);
314
315 for(const auto &it : giftShips)
316 out.Write("give", "ship", it.first->VariantName(), it.second);
317 for(const auto &it : giftOutfits)
318 out.Write("outfit", it.first->Name(), it.second);
319 for(const auto &it : requiredOutfits)
320 out.Write("require", it.first->Name(), it.second);
321 if(payment)
322 out.Write("payment", payment);
323 for(const auto &it : events)
324 {
325 if(it.second.first == it.second.second)
326 out.Write("event", it.first->Name(), it.second.first);
327 else
328 out.Write("event", it.first->Name(), it.second.first, it.second.second);
329 }
330 for(auto &&missionName : fail)
331 out.Write("fail", missionName);
332
333 conditions.Save(out);
334 }
335 out.EndChild();
336 }
337
338
339
340 // Check this template or instantiated MissionAction to see if any used content
341 // is not fully defined (e.g. plugin removal, typos in names, etc.).
Validate() const342 string MissionAction::Validate() const
343 {
344 // Any filter used to control where this action triggers must be valid.
345 if(!systemFilter.IsValid())
346 return "system location filter";
347
348 // Stock phrases that generate text must be defined.
349 if(stockDialogPhrase && stockDialogPhrase->IsEmpty())
350 return "stock phrase";
351
352 // Stock conversations must be defined.
353 if(stockConversation && stockConversation->IsEmpty())
354 return "stock conversation";
355
356 // Events which get activated by this action must be valid.
357 for(auto &&event : events)
358 if(!event.first->IsValid())
359 return "event \"" + event.first->Name() + "\"";
360
361 // Gifted or required content must be defined & valid.
362 for(auto &&it : giftShips)
363 if(!it.first->IsValid())
364 return "gift ship model \"" + it.first->VariantName() + "\"";
365 for(auto &&outfit : giftOutfits)
366 if(!outfit.first->IsDefined())
367 return "gift outfit \"" + outfit.first->Name() + "\"";
368 for(auto &&outfit : requiredOutfits)
369 if(!outfit.first->IsDefined())
370 return "required outfit \"" + outfit.first->Name() + "\"";
371
372 // It is OK for this action to try to fail a mission that does not exist.
373 // (E.g. a plugin may be designed for interoperability with other plugins.)
374
375 return "";
376 }
377
378
379
Payment() const380 int MissionAction::Payment() const
381 {
382 return payment;
383 }
384
385
386
DialogText() const387 const string &MissionAction::DialogText() const
388 {
389 return dialogText;
390 }
391
392
393
394 // Check if this action can be completed right now. It cannot be completed
395 // if it takes away money or outfits that the player does not have.
CanBeDone(const PlayerInfo & player,const shared_ptr<Ship> & boardingShip) const396 bool MissionAction::CanBeDone(const PlayerInfo &player, const shared_ptr<Ship> &boardingShip) const
397 {
398 if(player.Accounts().Credits() < -payment)
399 return false;
400
401 const Ship *flagship = player.Flagship();
402 for(const auto &it : giftOutfits)
403 {
404 // If this outfit is being given, the player doesn't need to have it.
405 if(it.second > 0)
406 continue;
407
408 // Outfits may always be taken from the flagship. If landed, they may also be taken from
409 // the collective cargohold of any in-system, non-disabled escorts (player.Cargo()). If
410 // boarding, consider only the flagship's cargo hold. If in-flight, show mission status
411 // by checking the cargo holds of ships that would contribute to player.Cargo if landed.
412 int available = flagship ? flagship->OutfitCount(it.first) : 0;
413 available += boardingShip ? flagship->Cargo().Get(it.first)
414 : CountInCargo(it.first, player);
415
416 if(available < -it.second)
417 return false;
418 }
419
420 for(const auto &it : requiredOutfits)
421 {
422 int available = 0;
423 // Requiring the player to have 0 of this outfit means all ships and all cargo holds
424 // must be checked, even if the ship is disabled, parked, or out-of-system.
425 bool checkAll = !it.second;
426 if(checkAll)
427 {
428 for(const auto &ship : player.Ships())
429 if(!ship->IsDestroyed())
430 {
431 available += ship->Cargo().Get(it.first);
432 available += ship->OutfitCount(it.first);
433 }
434 }
435 else
436 {
437 // Required outfits must be present on able ships in the
438 // player's location (or the respective cargo hold).
439 available += flagship ? flagship->OutfitCount(it.first) : 0;
440 available += boardingShip ? flagship->Cargo().Get(it.first)
441 : CountInCargo(it.first, player);
442 }
443
444 if(available < it.second)
445 return false;
446
447 // If the required count is 0, the player must not have any of the outfit.
448 if(checkAll && available)
449 return false;
450 }
451
452 // An `on enter` MissionAction may have defined a LocationFilter that
453 // specifies the systems in which it can occur.
454 if(!systemFilter.IsEmpty() && !systemFilter.Matches(player.GetSystem()))
455 return false;
456 return true;
457 }
458
459
460
Do(PlayerInfo & player,UI * ui,const System * destination,const shared_ptr<Ship> & ship,const bool isUnique) const461 void MissionAction::Do(PlayerInfo &player, UI *ui, const System *destination, const shared_ptr<Ship> &ship, const bool isUnique) const
462 {
463 bool isOffer = (trigger == "offer");
464 if(!conversation.IsEmpty() && ui)
465 {
466 // Conversations offered while boarding or assisting reference a ship,
467 // which may be destroyed depending on the player's choices.
468 ConversationPanel *panel = new ConversationPanel(player, conversation, destination, ship);
469 if(isOffer)
470 panel->SetCallback(&player, &PlayerInfo::MissionCallback);
471 // Use a basic callback to handle forced departure outside of `on offer`
472 // conversations.
473 else
474 panel->SetCallback(&player, &PlayerInfo::BasicCallback);
475 ui->Push(panel);
476 }
477 else if(!dialogText.empty() && ui)
478 {
479 map<string, string> subs;
480 subs["<first>"] = player.FirstName();
481 subs["<last>"] = player.LastName();
482 if(player.Flagship())
483 subs["<ship>"] = player.Flagship()->Name();
484 string text = Format::Replace(dialogText, subs);
485
486 // Don't push the dialog text if this is a visit action on a nonunique
487 // mission; on visit, nonunique dialogs are handled by PlayerInfo as to
488 // avoid the player being spammed by dialogs if they have multiple
489 // missions active with the same destination (e.g. in the case of
490 // stacking bounty jobs).
491 if(isOffer)
492 ui->Push(new Dialog(text, player, destination));
493 else if(isUnique || trigger != "visit")
494 ui->Push(new Dialog(text));
495 }
496 else if(isOffer && ui)
497 player.MissionCallback(Conversation::ACCEPT);
498
499 if(!logText.empty())
500 player.AddLogEntry(logText);
501 for(const auto &it : specialLogText)
502 for(const auto &eit : it.second)
503 player.AddSpecialLog(it.first, eit.first, eit.second);
504
505 for(const auto &it : giftShips)
506 DoGift(player, it.first, it.second);
507 // If multiple outfits are being transferred, first remove them before
508 // adding any new ones.
509 for(const auto &it : giftOutfits)
510 if(it.second < 0)
511 DoGift(player, it.first, it.second, ui);
512 for(const auto &it : giftOutfits)
513 if(it.second > 0)
514 DoGift(player, it.first, it.second, ui);
515
516 if(payment)
517 player.Accounts().AddCredits(payment);
518
519 for(const auto &it : events)
520 player.AddEvent(*it.first, player.GetDate() + it.second.first);
521
522 if(!fail.empty())
523 {
524 // If this action causes this or any other mission to fail, mark that
525 // mission as failed. It will not be removed from the player's mission
526 // list until it is safe to do so.
527 for(const Mission &mission : player.Missions())
528 if(fail.count(mission.Identifier()))
529 player.FailMission(mission);
530 }
531
532 // Check if applying the conditions changes the player's reputations.
533 player.SetReputationConditions();
534 conditions.Apply(player.Conditions());
535 player.CheckReputationConditions();
536 }
537
538
539
540 // Convert this validated template into a populated action.
Instantiate(map<string,string> & subs,const System * origin,int jumps,int64_t payload) const541 MissionAction MissionAction::Instantiate(map<string, string> &subs, const System *origin, int jumps, int64_t payload) const
542 {
543 MissionAction result;
544 result.trigger = trigger;
545 result.system = system;
546 // Convert any "distance" specifiers into "near <system>" specifiers.
547 result.systemFilter = systemFilter.SetOrigin(origin);
548
549 // All contained events are valid, else we would not be calling Instantiate. For these
550 // valid events, pick a date within the specified range on which the event will occur.
551 for(const auto &it : events)
552 {
553 int day = it.second.first + Random::Int(it.second.second - it.second.first + 1);
554 result.events[it.first] = make_pair(day, day);
555 }
556 for(const auto &it : giftShips)
557 result.giftShips.emplace_back(it.first, !it.second.empty() ? it.second : GameData::Phrases().Get("civilian")->Get());
558 result.giftOutfits = giftOutfits;
559 result.requiredOutfits = requiredOutfits;
560 result.payment = payment + (jumps + 1) * payload * paymentMultiplier;
561 // Fill in the payment amount if this is the "complete" action.
562 string previousPayment = subs["<payment>"];
563 if(result.payment)
564 subs["<payment>"] = Format::Credits(abs(result.payment))
565 + (result.payment == 1 ? " credit" : " credits");
566
567 if(!logText.empty())
568 result.logText = Format::Replace(logText, subs);
569 for(const auto &it : specialLogText)
570 for(const auto &eit : it.second)
571 result.specialLogText[it.first][eit.first] = Format::Replace(eit.second, subs);
572
573 // Create any associated dialog text from phrases, or use the directly specified text.
574 string dialogText = stockDialogPhrase ? stockDialogPhrase->Get()
575 : (!dialogPhrase.Name().empty() ? dialogPhrase.Get()
576 : this->dialogText);
577 if(!dialogText.empty())
578 result.dialogText = Format::Replace(dialogText, subs);
579
580 if(stockConversation)
581 result.conversation = stockConversation->Substitute(subs);
582 else if(!conversation.IsEmpty())
583 result.conversation = conversation.Substitute(subs);
584
585 result.fail = fail;
586
587 result.conditions = conditions;
588
589 // Restore the "<payment>" value from the "on complete" condition, for use
590 // in other parts of this mission.
591 if(result.payment && trigger != "complete")
592 subs["<payment>"] = previousPayment;
593
594 return result;
595 }
596