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