1 /*
2 ** p_converstation.cpp
3 ** Implements Strife style conversation dialogs
4 **
5 **---------------------------------------------------------------------------
6 ** Copyright 2004-2008 Randy Heit
7 ** All rights reserved.
8 **
9 ** Redistribution and use in source and binary forms, with or without
10 ** modification, are permitted provided that the following conditions
11 ** are met:
12 **
13 ** 1. Redistributions of source code must retain the above copyright
14 **    notice, this list of conditions and the following disclaimer.
15 ** 2. Redistributions in binary form must reproduce the above copyright
16 **    notice, this list of conditions and the following disclaimer in the
17 **    documentation and/or other materials provided with the distribution.
18 ** 3. The name of the author may not be used to endorse or promote products
19 **    derived from this software without specific prior written permission.
20 **
21 ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22 ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23 ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24 ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25 ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26 ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30 ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 **---------------------------------------------------------------------------
32 **
33 */
34 
35 #include <assert.h>
36 
37 #include "actor.h"
38 #include "p_conversation.h"
39 #include "w_wad.h"
40 #include "cmdlib.h"
41 #include "s_sound.h"
42 #include "v_text.h"
43 #include "v_video.h"
44 #include "m_random.h"
45 #include "gi.h"
46 #include "templates.h"
47 #include "a_strifeglobal.h"
48 #include "a_keys.h"
49 #include "p_enemy.h"
50 #include "gstrings.h"
51 #include "sound/i_music.h"
52 #include "p_setup.h"
53 #include "d_net.h"
54 #include "g_level.h"
55 #include "d_event.h"
56 #include "d_gui.h"
57 #include "doomstat.h"
58 #include "c_console.h"
59 #include "sbar.h"
60 #include "farchive.h"
61 #include "p_lnspec.h"
62 #include "r_utility.h"
63 #include "p_local.h"
64 #include "menu/menu.h"
65 
66 // The conversations as they exist inside a SCRIPTxx lump.
67 struct Response
68 {
69 	SDWORD GiveType;
70 	SDWORD Item[3];
71 	SDWORD Count[3];
72 	char Reply[32];
73 	char Yes[80];
74 	SDWORD Link;
75 	DWORD Log;
76 	char No[80];
77 };
78 
79 struct Speech
80 {
81 	DWORD SpeakerType;
82 	SDWORD DropType;
83 	SDWORD ItemCheck[3];
84 	SDWORD Link;
85 	char Name[16];
86 	char Sound[8];
87 	char Backdrop[8];
88 	char Dialogue[320];
89 	Response Responses[5];
90 };
91 
92 // The Teaser version of the game uses an older version of the structure
93 struct TeaserSpeech
94 {
95 	DWORD SpeakerType;
96 	SDWORD DropType;
97 	DWORD VoiceNumber;
98 	char Name[16];
99 	char Dialogue[320];
100 	Response Responses[5];
101 };
102 
103 static FRandom pr_randomspeech("RandomSpeech");
104 
105 void GiveSpawner (player_t *player, const PClass *type);
106 
107 TArray<FStrifeDialogueNode *> StrifeDialogues;
108 
109 typedef TMap<int, int> FDialogueIDMap;				// maps dialogue IDs to dialogue array index (for ACS)
110 typedef TMap<FName, int> FDialogueMap;				// maps actor class names to dialogue array index
111 
112 FClassMap StrifeTypes;
113 static FDialogueIDMap DialogueRoots;
114 static FDialogueMap ClassRoots;
115 static int ConversationMenuY;
116 
117 static int ConversationPauseTic;
118 static bool ShowGold;
119 
120 static bool LoadScriptFile(int lumpnum, FileReader *lump, int numnodes, bool include, int type);
121 static FStrifeDialogueNode *ReadRetailNode (FileReader *lump, DWORD &prevSpeakerType);
122 static FStrifeDialogueNode *ReadTeaserNode (FileReader *lump, DWORD &prevSpeakerType);
123 static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses);
124 static bool DrawConversationMenu ();
125 static void PickConversationReply (int replyindex);
126 static void TerminalResponse (const char *str);
127 
128 static FStrifeDialogueNode *PrevNode;
129 
130 #define NUM_RANDOM_LINES 10
131 #define NUM_RANDOM_GOODBYES 3
132 
133 //============================================================================
134 //
135 // GetStrifeType
136 //
137 // Given an item type number, returns the corresponding PClass.
138 //
139 //============================================================================
140 
SetStrifeType(int convid,const PClass * Class)141 void SetStrifeType(int convid, const PClass *Class)
142 {
143 	StrifeTypes[convid] = Class;
144 }
145 
ClearStrifeTypes()146 void ClearStrifeTypes()
147 {
148 	StrifeTypes.Clear();
149 }
150 
SetConversation(int convid,const PClass * Class,int dlgindex)151 void SetConversation(int convid, const PClass *Class, int dlgindex)
152 {
153 	if (convid != -1)
154 	{
155 		DialogueRoots[convid] = dlgindex;
156 	}
157 	if (Class != NULL)
158 	{
159 		ClassRoots[Class->TypeName] = dlgindex;
160 	}
161 }
162 
GetStrifeType(int typenum)163 const PClass *GetStrifeType (int typenum)
164 {
165 	const PClass **ptype = StrifeTypes.CheckKey(typenum);
166 	if (ptype == NULL) return NULL;
167 	else return *ptype;
168 }
169 
GetConversation(int conv_id)170 int GetConversation(int conv_id)
171 {
172 	int *pindex = DialogueRoots.CheckKey(conv_id);
173 	if (pindex == NULL) return -1;
174 	else return *pindex;
175 }
176 
GetConversation(FName classname)177 int GetConversation(FName classname)
178 {
179 	int *pindex = ClassRoots.CheckKey(classname);
180 	if (pindex == NULL) return -1;
181 	else return *pindex;
182 }
183 
184 //============================================================================
185 //
186 // P_LoadStrifeConversations
187 //
188 // Loads the SCRIPT00 and SCRIPTxx files for a corresponding map.
189 //
190 //============================================================================
191 
P_LoadStrifeConversations(MapData * map,const char * mapname)192 void P_LoadStrifeConversations (MapData *map, const char *mapname)
193 {
194 	P_FreeStrifeConversations ();
195 	if (map->Size(ML_CONVERSATION) > 0)
196 	{
197 		map->Seek(ML_CONVERSATION);
198 		LoadScriptFile (map->lumpnum, map->file, map->Size(ML_CONVERSATION), false, 0);
199 	}
200 	else
201 	{
202 		if (strnicmp (mapname, "MAP", 3) != 0)
203 		{
204 			return;
205 		}
206 		char scriptname_b[9] = { 'S','C','R','I','P','T',mapname[3],mapname[4],0 };
207 		char scriptname_t[9] = { 'D','I','A','L','O','G',mapname[3],mapname[4],0 };
208 
209 		if (!LoadScriptFile(scriptname_t, false, 2))
210 		{
211 			if (!LoadScriptFile (scriptname_b, false, 1))
212 			{
213 				LoadScriptFile ("SCRIPT00", false, 1);
214 			}
215 		}
216 	}
217 }
218 
219 //============================================================================
220 //
221 // LoadScriptFile
222 //
223 // Loads a SCRIPTxx file and converts it into a more useful internal format.
224 //
225 //============================================================================
226 
LoadScriptFile(const char * name,bool include,int type)227 bool LoadScriptFile (const char *name, bool include, int type)
228 {
229 	int lumpnum = Wads.CheckNumForName (name);
230 	FileReader *lump;
231 
232 	if (lumpnum < 0)
233 	{
234 		return false;
235 	}
236 	lump = Wads.ReopenLumpNum (lumpnum);
237 
238 	bool res = LoadScriptFile(lumpnum, lump, Wads.LumpLength(lumpnum), include, type);
239 	delete lump;
240 	return res;
241 }
242 
LoadScriptFile(int lumpnum,FileReader * lump,int numnodes,bool include,int type)243 static bool LoadScriptFile(int lumpnum, FileReader *lump, int numnodes, bool include, int type)
244 {
245 	int i;
246 	DWORD prevSpeakerType;
247 	FStrifeDialogueNode *node;
248 	char buffer[4];
249 
250 	lump->Read(buffer, 4);
251 	lump->Seek(-4, SEEK_CUR);
252 
253 	// The binary format is so primitive that this check is enough to detect it.
254 	bool isbinary = (buffer[0] == 0 || buffer[1] == 0 || buffer[2] == 0 || buffer[3] == 0);
255 
256 	if ((type == 1 && !isbinary) || (type == 2 && isbinary))
257 	{
258 		DPrintf("Incorrect data format for %s.", Wads.GetLumpFullName(lumpnum));
259 		return false;
260 	}
261 
262 	if (!isbinary)
263 	{
264 		P_ParseUSDF(lumpnum, lump, numnodes);
265 	}
266 	else
267 	{
268 		if (!include)
269 		{
270 			LoadScriptFile("SCRIPT00", true, 1);
271 		}
272 		if (!(gameinfo.flags & GI_SHAREWARE))
273 		{
274 			// Strife scripts are always a multiple of 1516 bytes because each entry
275 			// is exactly 1516 bytes long.
276 			if (numnodes % 1516 != 0)
277 			{
278 				DPrintf("Incorrect data format for %s.", Wads.GetLumpFullName(lumpnum));
279 				return false;
280 			}
281 			numnodes /= 1516;
282 		}
283 		else
284 		{
285 			// And the teaser version has 1488-byte entries.
286 			if (numnodes % 1488 != 0)
287 			{
288 				DPrintf("Incorrect data format for %s.", Wads.GetLumpFullName(lumpnum));
289 				return false;
290 			}
291 			numnodes /= 1488;
292 		}
293 
294 		prevSpeakerType = 0;
295 
296 		for (i = 0; i < numnodes; ++i)
297 		{
298 			if (!(gameinfo.flags & GI_SHAREWARE))
299 			{
300 				node = ReadRetailNode (lump, prevSpeakerType);
301 			}
302 			else
303 			{
304 				node = ReadTeaserNode (lump, prevSpeakerType);
305 			}
306 			node->ThisNodeNum = StrifeDialogues.Push(node);
307 		}
308 	}
309 	return true;
310 }
311 
312 //============================================================================
313 //
314 // ReadRetailNode
315 //
316 // Converts a single dialogue node from the Retail version of Strife.
317 //
318 //============================================================================
319 
ReadRetailNode(FileReader * lump,DWORD & prevSpeakerType)320 static FStrifeDialogueNode *ReadRetailNode (FileReader *lump, DWORD &prevSpeakerType)
321 {
322 	FStrifeDialogueNode *node;
323 	Speech speech;
324 	char fullsound[16];
325 	const PClass *type;
326 	int j;
327 
328 	node = new FStrifeDialogueNode;
329 
330 	lump->Read (&speech, sizeof(speech));
331 
332 	// Byte swap all the ints in the original data
333 	speech.SpeakerType = LittleLong(speech.SpeakerType);
334 	speech.DropType = LittleLong(speech.DropType);
335 	speech.Link = LittleLong(speech.Link);
336 
337 	// Assign the first instance of a conversation as the default for its
338 	// actor, so newly spawned actors will use this conversation by default.
339 	type = GetStrifeType (speech.SpeakerType);
340 	node->SpeakerType = type;
341 
342 	if ((signed)(speech.SpeakerType) >= 0 && prevSpeakerType != speech.SpeakerType)
343 	{
344 		if (type != NULL)
345 		{
346 			ClassRoots[type->TypeName] = StrifeDialogues.Size();
347 		}
348 		DialogueRoots[speech.SpeakerType] = StrifeDialogues.Size();
349 		prevSpeakerType = speech.SpeakerType;
350 	}
351 
352 	// Convert the rest of the data to our own internal format.
353 	node->Dialogue = ncopystring (speech.Dialogue);
354 
355 	// The speaker's portrait, if any.
356 	speech.Dialogue[0] = 0; 	//speech.Backdrop[8] = 0;
357 	node->Backdrop = TexMan.CheckForTexture (speech.Backdrop, FTexture::TEX_MiscPatch);
358 
359 	// The speaker's voice for this node, if any.
360 	speech.Backdrop[0] = 0; 	//speech.Sound[8] = 0;
361 	mysnprintf (fullsound, countof(fullsound), "svox/%s", speech.Sound);
362 	node->SpeakerVoice = fullsound;
363 
364 	// The speaker's name, if any.
365 	speech.Sound[0] = 0; 		//speech.Name[16] = 0;
366 	node->SpeakerName = ncopystring (speech.Name);
367 
368 	// The item the speaker should drop when killed.
369 	node->DropType = GetStrifeType (speech.DropType);
370 
371 	// Items you need to have to make the speaker use a different node.
372 	node->ItemCheck.Resize(3);
373 	for (j = 0; j < 3; ++j)
374 	{
375 		node->ItemCheck[j].Item = GetStrifeType (speech.ItemCheck[j]);
376 		node->ItemCheck[j].Amount = -1;
377 	}
378 	node->ItemCheckNode = speech.Link;
379 	node->Children = NULL;
380 
381 	ParseReplies (&node->Children, &speech.Responses[0]);
382 
383 	return node;
384 }
385 
386 //============================================================================
387 //
388 // ReadTeaserNode
389 //
390 // Converts a single dialogue node from the Teaser version of Strife.
391 //
392 //============================================================================
393 
ReadTeaserNode(FileReader * lump,DWORD & prevSpeakerType)394 static FStrifeDialogueNode *ReadTeaserNode (FileReader *lump, DWORD &prevSpeakerType)
395 {
396 	FStrifeDialogueNode *node;
397 	TeaserSpeech speech;
398 	char fullsound[16];
399 	const PClass *type;
400 	int j;
401 
402 	node = new FStrifeDialogueNode;
403 
404 	lump->Read (&speech, sizeof(speech));
405 
406 	// Byte swap all the ints in the original data
407 	speech.SpeakerType = LittleLong(speech.SpeakerType);
408 	speech.DropType = LittleLong(speech.DropType);
409 
410 	// Assign the first instance of a conversation as the default for its
411 	// actor, so newly spawned actors will use this conversation by default.
412 	type = GetStrifeType (speech.SpeakerType);
413 	node->SpeakerType = type;
414 
415 	if ((signed)speech.SpeakerType >= 0 && prevSpeakerType != speech.SpeakerType)
416 	{
417 		if (type != NULL)
418 		{
419 			ClassRoots[type->TypeName] = StrifeDialogues.Size();
420 		}
421 		DialogueRoots[speech.SpeakerType] = StrifeDialogues.Size();
422 		prevSpeakerType = speech.SpeakerType;
423 	}
424 
425 	// Convert the rest of the data to our own internal format.
426 	node->Dialogue = ncopystring (speech.Dialogue);
427 
428 	// The Teaser version doesn't have portraits.
429 	node->Backdrop.SetInvalid();
430 
431 	// The speaker's voice for this node, if any.
432 	if (speech.VoiceNumber != 0)
433 	{
434 		mysnprintf (fullsound, countof(fullsound), "svox/voc%u", speech.VoiceNumber);
435 		node->SpeakerVoice = fullsound;
436 	}
437 	else
438 	{
439 		node->SpeakerVoice = 0;
440 	}
441 
442 	// The speaker's name, if any.
443 	speech.Dialogue[0] = 0; 	//speech.Name[16] = 0;
444 	node->SpeakerName = ncopystring (speech.Name);
445 
446 	// The item the speaker should drop when killed.
447 	node->DropType = GetStrifeType (speech.DropType);
448 
449 	// Items you need to have to make the speaker use a different node.
450 	node->ItemCheck.Resize(3);
451 	for (j = 0; j < 3; ++j)
452 	{
453 		node->ItemCheck[j].Item = NULL;
454 		node->ItemCheck[j].Amount = -1;
455 	}
456 	node->ItemCheckNode = 0;
457 	node->Children = NULL;
458 
459 	ParseReplies (&node->Children, &speech.Responses[0]);
460 
461 	return node;
462 }
463 
464 //============================================================================
465 //
466 // ParseReplies
467 //
468 // Convert PC responses. Rather than being stored inside the main node, they
469 // hang off it as a singly-linked list, so no space is wasted on replies that
470 // don't even matter.
471 //
472 //============================================================================
473 
ParseReplies(FStrifeDialogueReply ** replyptr,Response * responses)474 static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses)
475 {
476 	FStrifeDialogueReply *reply;
477 	int j, k;
478 
479 	// Byte swap first.
480 	for (j = 0; j < 5; ++j)
481 	{
482 		responses[j].GiveType = LittleLong(responses[j].GiveType);
483 		responses[j].Link = LittleLong(responses[j].Link);
484 		responses[j].Log = LittleLong(responses[j].Log);
485 		for (k = 0; k < 3; ++k)
486 		{
487 			responses[j].Item[k] = LittleLong(responses[j].Item[k]);
488 			responses[j].Count[k] = LittleLong(responses[j].Count[k]);
489 		}
490 	}
491 
492 	for (j = 0; j < 5; ++j)
493 	{
494 		Response *rsp = &responses[j];
495 
496 		// If the reply has no text and goes nowhere, then it doesn't
497 		// need to be remembered.
498 		if (rsp->Reply[0] == 0 && rsp->Link == 0)
499 		{
500 			continue;
501 		}
502 		reply = new FStrifeDialogueReply;
503 
504 		// The next node to use when this reply is chosen.
505 		reply->NextNode = rsp->Link;
506 
507 		// The message to record in the log for this reply.
508 		reply->LogNumber = rsp->Log;
509 		reply->LogString = NULL;
510 
511 		// The item to receive when this reply is used.
512 		reply->GiveType = GetStrifeType (rsp->GiveType);
513 		reply->ActionSpecial = 0;
514 
515 		// Do you need anything special for this reply to succeed?
516 		reply->ItemCheck.Resize(3);
517 		for (k = 0; k < 3; ++k)
518 		{
519 			reply->ItemCheck[k].Item = GetStrifeType (rsp->Item[k]);
520 			reply->ItemCheck[k].Amount = rsp->Count[k];
521 		}
522 
523 		// If the first item check has a positive amount required, then
524 		// add that to the reply string. Otherwise, use the reply as-is.
525 		reply->Reply = copystring (rsp->Reply);
526 		reply->NeedsGold = (rsp->Count[0] > 0);
527 
528 		// QuickYes messages are shown when you meet the item checks.
529 		// QuickNo messages are shown when you don't.
530 		if (rsp->Yes[0] == '_' && rsp->Yes[1] == 0)
531 		{
532 			reply->QuickYes = NULL;
533 		}
534 		else
535 		{
536 			reply->QuickYes = ncopystring (rsp->Yes);
537 		}
538 		if (reply->ItemCheck[0].Item != 0)
539 		{
540 			reply->QuickNo = ncopystring (rsp->No);
541 		}
542 		else
543 		{
544 			reply->QuickNo = NULL;
545 		}
546 		reply->Next = *replyptr;
547 		*replyptr = reply;
548 		replyptr = &reply->Next;
549 	}
550 }
551 
552 //============================================================================
553 //
554 // FStrifeDialogueNode :: ~FStrifeDialogueNode
555 //
556 //============================================================================
557 
~FStrifeDialogueNode()558 FStrifeDialogueNode::~FStrifeDialogueNode ()
559 {
560 	if (SpeakerName != NULL) delete[] SpeakerName;
561 	if (Dialogue != NULL) delete[] Dialogue;
562 	FStrifeDialogueReply *tokill = Children;
563 	while (tokill != NULL)
564 	{
565 		FStrifeDialogueReply *next = tokill->Next;
566 		delete tokill;
567 		tokill = next;
568 	}
569 }
570 
571 //============================================================================
572 //
573 // FStrifeDialogueReply :: ~FStrifeDialogueReply
574 //
575 //============================================================================
576 
~FStrifeDialogueReply()577 FStrifeDialogueReply::~FStrifeDialogueReply ()
578 {
579 	if (Reply != NULL) delete[] Reply;
580 	if (QuickYes != NULL) delete[] QuickYes;
581 	if (QuickNo != NULL) delete[] QuickNo;
582 }
583 
584 //============================================================================
585 //
586 // FindNode
587 //
588 // Returns the index that matches the given conversation node.
589 //
590 //============================================================================
591 
FindNode(const FStrifeDialogueNode * node)592 static int FindNode (const FStrifeDialogueNode *node)
593 {
594 	int rootnode = 0;
595 
596 	while (StrifeDialogues[rootnode] != node)
597 	{
598 		rootnode++;
599 	}
600 	return rootnode;
601 }
602 
603 //============================================================================
604 //
605 // CheckStrifeItem
606 //
607 // Checks if you have an item. A NULL itemtype is always considered to be
608 // present.
609 //
610 //============================================================================
611 
CheckStrifeItem(player_t * player,const PClass * itemtype,int amount=-1)612 static bool CheckStrifeItem (player_t *player, const PClass *itemtype, int amount=-1)
613 {
614 	AInventory *item;
615 
616 	if (itemtype == NULL || amount == 0)
617 		return true;
618 
619 	item = player->ConversationPC->FindInventory (itemtype);
620 	if (item == NULL)
621 		return false;
622 
623 	return amount < 0 || item->Amount >= amount;
624 }
625 
626 //============================================================================
627 //
628 // TakeStrifeItem
629 //
630 // Takes away some of an item, unless that item is special and should not
631 // be removed.
632 //
633 //============================================================================
634 
TakeStrifeItem(player_t * player,const PClass * itemtype,int amount)635 static void TakeStrifeItem (player_t *player, const PClass *itemtype, int amount)
636 {
637 	if (itemtype == NULL || amount == 0)
638 		return;
639 
640 	// Don't take quest items.
641 	if (itemtype->IsDescendantOf (PClass::FindClass(NAME_QuestItem)))
642 		return;
643 
644 	// Don't take keys.
645 	if (itemtype->IsDescendantOf (RUNTIME_CLASS(AKey)))
646 		return;
647 
648 	// Don't take the sigil.
649 	if (itemtype == RUNTIME_CLASS(ASigil))
650 		return;
651 
652 	player->mo->TakeInventory(itemtype, amount);
653 }
654 
655 CUSTOM_CVAR(Float, dlg_musicvolume, 1.0f, CVAR_ARCHIVE)
656 {
657 	if (self < 0.f) self = 0.f;
658 	else if (self > 1.f) self = 1.f;
659 }
660 
661 //============================================================================
662 //
663 // The conversation menu
664 //
665 //============================================================================
666 
667 class DConversationMenu : public DMenu
668 {
669 	DECLARE_CLASS(DConversationMenu, DMenu)
670 
671 	FString mSpeaker;
672 	FBrokenLines *mDialogueLines;
673 	TArray<FString> mResponseLines;
674 	TArray<unsigned int> mResponses;
675 	bool mShowGold;
676 	FStrifeDialogueNode *mCurNode;
677 	int mYpos;
678 
679 public:
680 	static int mSelection;
681 
682 	//=============================================================================
683 	//
684 	//
685 	//
686 	//=============================================================================
687 
DConversationMenu(FStrifeDialogueNode * CurNode)688 	DConversationMenu(FStrifeDialogueNode *CurNode)
689 	{
690 		mCurNode = CurNode;
691 		mDialogueLines = NULL;
692 		mShowGold = false;
693 
694 		// Format the speaker's message.
695 		const char * toSay = CurNode->Dialogue;
696 		if (strncmp (toSay, "RANDOM_", 7) == 0)
697 		{
698 			FString dlgtext;
699 
700 			dlgtext.Format("TXT_%s_%02d", toSay, 1+(pr_randomspeech() % NUM_RANDOM_LINES));
701 			toSay = GStrings[dlgtext];
702 			if (toSay == NULL)
703 			{
704 				toSay = GStrings["TXT_GOAWAY"];	// Ok, it's lame - but it doesn't look like an error to the player. ;)
705 			}
706 		}
707 		else
708 		{
709 			// handle string table replacement
710 			if (toSay[0] == '$')
711 			{
712 				toSay = GStrings(toSay + 1);
713 			}
714 		}
715 		if (toSay == NULL)
716 		{
717 			toSay = ".";
718 		}
719 		mDialogueLines = V_BreakLines (SmallFont, screen->GetWidth()/CleanXfac - 24*2, toSay);
720 
721 		FStrifeDialogueReply *reply;
722 		int i,j;
723 		for (reply = CurNode->Children, i = 1; reply != NULL; reply = reply->Next)
724 		{
725 			if (reply->Reply == NULL)
726 			{
727 				continue;
728 			}
729 			mShowGold |= reply->NeedsGold;
730 
731 			const char *ReplyText = reply->Reply;
732 			if (ReplyText[0] == '$')
733 			{
734 				ReplyText = GStrings(ReplyText + 1);
735 			}
736 			FString ReplyString = ReplyText;
737 			if (reply->NeedsGold) ReplyString.AppendFormat(" for %u", reply->ItemCheck[0].Amount);
738 
739 			FBrokenLines *ReplyLines = V_BreakLines (SmallFont, 320-50-10, ReplyString);
740 
741 			mResponses.Push(mResponseLines.Size());
742 			for (j = 0; ReplyLines[j].Width >= 0; ++j)
743 			{
744 				mResponseLines.Push(ReplyLines[j].Text);
745 			}
746 			++i;
747 			V_FreeBrokenLines (ReplyLines);
748 		}
749 		char goodbye[25];
750 		mysnprintf(goodbye, countof(goodbye), "TXT_RANDOMGOODBYE_%d", 1+(pr_randomspeech() % NUM_RANDOM_GOODBYES));
751 		const char *goodbyestr = GStrings[goodbye];
752 		if (goodbyestr == NULL) goodbyestr = "Bye.";
753 		mResponses.Push(mResponseLines.Size());
754 		mResponseLines.Push(FString(goodbyestr));
755 
756 		// Determine where the top of the reply list should be positioned.
757 		i = OptionSettings.mLinespacing;
758 		mYpos = MIN<int> (140, 192 - mResponseLines.Size() * i);
759 		for (i = 0; mDialogueLines[i].Width >= 0; ++i)
760 		{ }
761 		i = 44 + i * 10;
762 		if (mYpos - 100 < i - screen->GetHeight() / CleanYfac / 2)
763 		{
764 			mYpos = i - screen->GetHeight() / CleanYfac / 2 + 100;
765 		}
766 		ConversationMenuY = mYpos;
767 		//ConversationMenu.indent = 50;
768 	}
769 
770 	//=============================================================================
771 	//
772 	//
773 	//
774 	//=============================================================================
775 
Destroy()776 	void Destroy()
777 	{
778 		V_FreeBrokenLines(mDialogueLines);
779 		mDialogueLines = NULL;
780 		I_SetMusicVolume (1.f);
781 	}
782 
DimAllowed()783 	bool DimAllowed()
784 	{
785 		return false;
786 	}
787 
788 	//=============================================================================
789 	//
790 	//
791 	//
792 	//=============================================================================
793 
MenuEvent(int mkey,bool fromcontroller)794 	bool MenuEvent(int mkey, bool fromcontroller)
795 	{
796 		if (demoplayback)
797 		{ // During demo playback, don't let the user do anything besides close this menu.
798 			if (mkey == MKEY_Back)
799 			{
800 				Close();
801 				return true;
802 			}
803 			return false;
804 		}
805 		if (mkey == MKEY_Up)
806 		{
807 			if (--mSelection < 0) mSelection = mResponses.Size() - 1;
808 			return true;
809 		}
810 		else if (mkey == MKEY_Down)
811 		{
812 			if (++mSelection >= (int)mResponses.Size()) mSelection = 0;
813 			return true;
814 		}
815 		else if (mkey == MKEY_Back)
816 		{
817 			Net_WriteByte (DEM_CONVNULL);
818 			Close();
819 			return true;
820 		}
821 		else if (mkey == MKEY_Enter)
822 		{
823 			if ((unsigned)mSelection >= mResponses.Size())
824 			{
825 				Net_WriteByte(DEM_CONVCLOSE);
826 			}
827 			else
828 			{
829 				// Send dialogue and reply numbers across the wire.
830 				assert((unsigned)mCurNode->ThisNodeNum < StrifeDialogues.Size());
831 				assert(StrifeDialogues[mCurNode->ThisNodeNum] == mCurNode);
832 				Net_WriteByte(DEM_CONVREPLY);
833 				Net_WriteWord(mCurNode->ThisNodeNum);
834 				Net_WriteByte(mSelection);
835 			}
836 			Close();
837 			return true;
838 		}
839 		return false;
840 	}
841 
842 	//=============================================================================
843 	//
844 	//
845 	//
846 	//=============================================================================
847 
MouseEvent(int type,int x,int y)848 	bool MouseEvent(int type, int x, int y)
849 	{
850 		int sel = -1;
851 		int fh = OptionSettings.mLinespacing;
852 
853 		// convert x/y from screen to virtual coordinates, according to CleanX/Yfac use in DrawTexture
854 		x = ((x - (screen->GetWidth() / 2)) / CleanXfac) + 160;
855 		y = ((y - (screen->GetHeight() / 2)) / CleanYfac) + 100;
856 
857 		if (x >= 24 && x <= 320-24 && y >= mYpos && y < mYpos + fh * (int)mResponseLines.Size())
858 		{
859 			sel = (y - mYpos) / fh;
860 			for(unsigned i=0;i<mResponses.Size(); i++)
861 			{
862 				if ((int)mResponses[i] > sel)
863 				{
864 					sel = i-1;
865 					break;
866 				}
867 			}
868 		}
869 		if (sel != -1 && sel != mSelection)
870 		{
871 			//S_Sound (CHAN_VOICE | CHAN_UI, "menu/cursor", snd_menuvolume, ATTN_NONE);
872 		}
873 		mSelection = sel;
874 		if (type == MOUSE_Release)
875 		{
876 			return MenuEvent(MKEY_Enter, true);
877 		}
878 		return true;
879 	}
880 
881 
882 	//=============================================================================
883 	//
884 	//
885 	//
886 	//=============================================================================
887 
Responder(event_t * ev)888 	bool Responder(event_t *ev)
889 	{
890 		if (demoplayback)
891 		{ // No interaction during demo playback
892 			return false;
893 		}
894 		if (ev->type == EV_GUI_Event && ev->subtype == EV_GUI_Char && ev->data1 >= '0' && ev->data1 <= '9')
895 		{ // Activate an item of type numberedmore (dialogue only)
896 			mSelection = ev->data1 == '0' ? 9 : ev->data1 - '1';
897 			return MenuEvent(MKEY_Enter, false);
898 		}
899 		return Super::Responder(ev);
900 	}
901 
902 	//============================================================================
903 	//
904 	// DrawConversationMenu
905 	//
906 	//============================================================================
907 
Drawer()908 	void Drawer()
909 	{
910 		const char *speakerName;
911 		int x, y, linesize;
912 		int width, fontheight;
913 
914 		player_t *cp = &players[consoleplayer];
915 
916 		assert (mDialogueLines != NULL);
917 		assert (mCurNode != NULL);
918 
919 		FStrifeDialogueNode *CurNode = mCurNode;
920 
921 		if (CurNode == NULL)
922 		{
923 			Close ();
924 			return;
925 		}
926 
927 		// [CW] Freeze the game depending on MAPINFO options.
928 		if (ConversationPauseTic < gametic && !multiplayer && !(level.flags2 & LEVEL2_CONV_SINGLE_UNFREEZE))
929 		{
930 			menuactive = MENU_On;
931 		}
932 
933 		if (CurNode->Backdrop.isValid())
934 		{
935 			screen->DrawTexture (TexMan(CurNode->Backdrop), 0, 0, DTA_320x200, true, TAG_DONE);
936 		}
937 		x = 16 * screen->GetWidth() / 320;
938 		y = 16 * screen->GetHeight() / 200;
939 		linesize = 10 * CleanYfac;
940 
941 		// Who is talking to you?
942 		if (CurNode->SpeakerName != NULL)
943 		{
944 			speakerName = CurNode->SpeakerName;
945 			if (speakerName[0] == '$') speakerName = GStrings(speakerName+1);
946 		}
947 		else
948 		{
949 			speakerName = cp->ConversationNPC->GetTag("Person");
950 		}
951 
952 		// Dim the screen behind the dialogue (but only if there is no backdrop).
953 		if (!CurNode->Backdrop.isValid())
954 		{
955 			int i;
956 			for (i = 0; mDialogueLines[i].Width >= 0; ++i)
957 			{ }
958 			screen->Dim (0, 0.45f, 14 * screen->GetWidth() / 320, 13 * screen->GetHeight() / 200,
959 				308 * screen->GetWidth() / 320 - 14 * screen->GetWidth () / 320,
960 				speakerName == NULL ? linesize * i + 6 * CleanYfac
961 				: linesize * i + 6 * CleanYfac + linesize * 3/2);
962 		}
963 
964 		// Dim the screen behind the PC's choices.
965 
966 		screen->Dim (0, 0.45f, (24-160) * CleanXfac + screen->GetWidth()/2,
967 			(mYpos - 2 - 100) * CleanYfac + screen->GetHeight()/2,
968 			272 * CleanXfac,
969 			MIN<int>(mResponseLines.Size() * OptionSettings.mLinespacing + 4, 200 - mYpos) * CleanYfac);
970 
971 		if (speakerName != NULL)
972 		{
973 			screen->DrawText (SmallFont, CR_WHITE, x, y, speakerName,
974 				DTA_CleanNoMove, true, TAG_DONE);
975 			y += linesize * 3 / 2;
976 		}
977 		x = 24 * screen->GetWidth() / 320;
978 		for (int i = 0; mDialogueLines[i].Width >= 0; ++i)
979 		{
980 			screen->DrawText (SmallFont, CR_UNTRANSLATED, x, y, mDialogueLines[i].Text,
981 				DTA_CleanNoMove, true, TAG_DONE);
982 			y += linesize;
983 		}
984 
985 		if (ShowGold)
986 		{
987 			AInventory *coin = cp->ConversationPC->FindInventory (RUNTIME_CLASS(ACoin));
988 			char goldstr[32];
989 
990 			mysnprintf (goldstr, countof(goldstr), "%d", coin != NULL ? coin->Amount : 0);
991 			screen->DrawText (SmallFont, CR_GRAY, 21, 191, goldstr, DTA_320x200, true,
992 				DTA_FillColor, 0, DTA_Alpha, HR_SHADOW, TAG_DONE);
993 			screen->DrawTexture (TexMan(((AInventory *)GetDefaultByType (RUNTIME_CLASS(ACoin)))->Icon),
994 				3, 190, DTA_320x200, true,
995 				DTA_FillColor, 0, DTA_Alpha, HR_SHADOW, TAG_DONE);
996 			screen->DrawText (SmallFont, CR_GRAY, 20, 190, goldstr, DTA_320x200, true, TAG_DONE);
997 			screen->DrawTexture (TexMan(((AInventory *)GetDefaultByType (RUNTIME_CLASS(ACoin)))->Icon),
998 				2, 189, DTA_320x200, true, TAG_DONE);
999 		}
1000 
1001 		y = mYpos;
1002 		fontheight = OptionSettings.mLinespacing;
1003 
1004 		int response = 0;
1005 		for (unsigned i = 0; i < mResponseLines.Size(); i++, y += fontheight)
1006 		{
1007 			width = SmallFont->StringWidth(mResponseLines[i]);
1008 			x = 64;
1009 
1010 			screen->DrawText (SmallFont, CR_GREEN, x, y, mResponseLines[i], DTA_Clean, true, TAG_DONE);
1011 
1012 			if (i == mResponses[response])
1013 			{
1014 				char tbuf[16];
1015 
1016 				response++;
1017 				mysnprintf (tbuf, countof(tbuf), "%d.", response);
1018 				x = 50 - SmallFont->StringWidth (tbuf);
1019 				screen->DrawText (SmallFont, CR_GREY, x, y, tbuf, DTA_Clean, true, TAG_DONE);
1020 
1021 				if (response == mSelection+1)
1022 				{
1023 					int color = ((DMenu::MenuTime%8) < 4) || DMenu::CurrentMenu != this ? CR_RED:CR_GREY;
1024 
1025 					x = (50 + 3 - 160) * CleanXfac + screen->GetWidth() / 2;
1026 					int yy = (y + fontheight/2 - 5 - 100) * CleanYfac + screen->GetHeight() / 2;
1027 					screen->DrawText (ConFont, color, x, yy, "\xd",
1028 						DTA_CellX, 8 * CleanXfac,
1029 						DTA_CellY, 8 * CleanYfac,
1030 						TAG_DONE);
1031 				}
1032 			}
1033 		}
1034 	}
1035 
1036 };
1037 
1038 IMPLEMENT_ABSTRACT_CLASS(DConversationMenu)
1039 int DConversationMenu::mSelection;	// needs to be preserved if the same dialogue is restarted
1040 
1041 
1042 //============================================================================
1043 //
1044 // P_FreeStrifeConversations
1045 //
1046 //============================================================================
1047 
P_FreeStrifeConversations()1048 void P_FreeStrifeConversations ()
1049 {
1050 	FStrifeDialogueNode *node;
1051 
1052 	while (StrifeDialogues.Pop (node))
1053 	{
1054 		delete node;
1055 	}
1056 
1057 	DialogueRoots.Clear();
1058 	ClassRoots.Clear();
1059 
1060 	PrevNode = NULL;
1061 	if (DMenu::CurrentMenu != NULL && DMenu::CurrentMenu->IsKindOf(RUNTIME_CLASS(DConversationMenu)))
1062 	{
1063 		DMenu::CurrentMenu->Close();
1064 	}
1065 }
1066 
1067 //============================================================================
1068 //
1069 // P_StartConversation
1070 //
1071 // Begins a conversation between a PC and NPC.
1072 //
1073 //============================================================================
1074 
P_StartConversation(AActor * npc,AActor * pc,bool facetalker,bool saveangle)1075 void P_StartConversation (AActor *npc, AActor *pc, bool facetalker, bool saveangle)
1076 {
1077 	AActor *oldtarget;
1078 	int i;
1079 
1080 	// Make sure this is actually a player.
1081 	if (pc->player == NULL) return;
1082 
1083 	// [CW] If an NPC is talking to a PC already, then don't let
1084 	// anyone else talk to the NPC.
1085 	for (i = 0; i < MAXPLAYERS; i++)
1086 	{
1087 		if (!playeringame[i] || pc->player == &players[i])
1088 			continue;
1089 
1090 		if (npc == players[i].ConversationNPC)
1091 			return;
1092 	}
1093 
1094 	pc->velx = pc->vely = 0;	// Stop moving
1095 	pc->player->velx = pc->player->vely = 0;
1096 	static_cast<APlayerPawn*>(pc)->PlayIdle ();
1097 
1098 	pc->player->ConversationPC = pc;
1099 	pc->player->ConversationNPC = npc;
1100 	npc->flags5 |= MF5_INCONVERSATION;
1101 
1102 	FStrifeDialogueNode *CurNode = npc->Conversation;
1103 
1104 	if (pc->player == &players[consoleplayer])
1105 	{
1106 		S_Sound (CHAN_VOICE | CHAN_UI, gameinfo.chatSound, 1, ATTN_NONE);
1107 	}
1108 
1109 	npc->reactiontime = 2;
1110 	pc->player->ConversationFaceTalker = facetalker;
1111 	if (saveangle)
1112 	{
1113 		pc->player->ConversationNPCAngle = npc->angle;
1114 	}
1115 	oldtarget = npc->target;
1116 	npc->target = pc;
1117 	if (facetalker)
1118 	{
1119 		A_FaceTarget (npc);
1120 		pc->angle = pc->AngleTo(npc);
1121 	}
1122 	if ((npc->flags & MF_FRIENDLY) || (npc->flags4 & MF4_NOHATEPLAYERS))
1123 	{
1124 		npc->target = oldtarget;
1125 	}
1126 
1127 	// Check if we should jump to another node
1128 	while (CurNode->ItemCheck.Size() > 0 && CurNode->ItemCheck[0].Item != NULL)
1129 	{
1130 		bool jump = true;
1131 		for (i = 0; i < (int)CurNode->ItemCheck.Size(); ++i)
1132 		{
1133 			if(!CheckStrifeItem (pc->player, CurNode->ItemCheck[i].Item, CurNode->ItemCheck[i].Amount))
1134 			{
1135 				jump = false;
1136 				break;
1137 			}
1138 		}
1139 		if (jump && CurNode->ItemCheckNode > 0)
1140 		{
1141 			int root = pc->player->ConversationNPC->ConversationRoot;
1142 			CurNode = StrifeDialogues[root + CurNode->ItemCheckNode - 1];
1143 		}
1144 		else
1145 		{
1146 			break;
1147 		}
1148 	}
1149 
1150 	// The rest is only done when the conversation is actually displayed.
1151 	if (pc->player == &players[consoleplayer])
1152 	{
1153 		if (CurNode->SpeakerVoice != 0)
1154 		{
1155 			I_SetMusicVolume (dlg_musicvolume);
1156 			S_Sound (npc, CHAN_VOICE|CHAN_NOPAUSE, CurNode->SpeakerVoice, 1, ATTN_NORM);
1157 		}
1158 
1159 		DConversationMenu *cmenu = new DConversationMenu(CurNode);
1160 
1161 
1162 		if (CurNode != PrevNode)
1163 		{ // Only reset the selection if showing a different menu.
1164 			DConversationMenu::mSelection = 0;
1165 			PrevNode = CurNode;
1166 		}
1167 
1168 		// And open the menu
1169 		M_StartControlPanel (false);
1170 		M_ActivateMenu(cmenu);
1171 		ConversationPauseTic = gametic + 20;
1172 		menuactive = MENU_OnNoPause;
1173 	}
1174 }
1175 
1176 //============================================================================
1177 //
1178 // P_ResumeConversation
1179 //
1180 // Resumes a conversation that was interrupted by a slideshow.
1181 //
1182 //============================================================================
1183 
P_ResumeConversation()1184 void P_ResumeConversation ()
1185 {
1186 	for (int i = 0; i < MAXPLAYERS; i++)
1187 	{
1188 		if (!playeringame[i])
1189 			continue;
1190 
1191 		player_t *p = &players[i];
1192 
1193 		if (p->ConversationPC != NULL && p->ConversationNPC != NULL)
1194 		{
1195 			P_StartConversation (p->ConversationNPC, p->ConversationPC, p->ConversationFaceTalker, false);
1196 		}
1197 	}
1198 }
1199 
1200 //============================================================================
1201 //
1202 // HandleReply
1203 //
1204 // Run by the netcode on all machines.
1205 //
1206 //============================================================================
1207 
HandleReply(player_t * player,bool isconsole,int nodenum,int replynum)1208 static void HandleReply(player_t *player, bool isconsole, int nodenum, int replynum)
1209 {
1210 	const char *replyText = NULL;
1211 	FStrifeDialogueReply *reply;
1212 	FStrifeDialogueNode *node;
1213 	AActor *npc;
1214 	bool takestuff;
1215 	int i;
1216 
1217 	if (player->ConversationNPC == NULL || (unsigned)nodenum >= StrifeDialogues.Size())
1218 	{
1219 		return;
1220 	}
1221 
1222 	// Find the reply.
1223 	node = StrifeDialogues[nodenum];
1224 	for (i = 0, reply = node->Children; reply != NULL && i != replynum; ++i, reply = reply->Next)
1225 	{ }
1226 	npc = player->ConversationNPC;
1227 	if (reply == NULL)
1228 	{
1229 		// The default reply was selected
1230 		npc->angle = player->ConversationNPCAngle;
1231 		npc->flags5 &= ~MF5_INCONVERSATION;
1232 		return;
1233 	}
1234 
1235 	// Check if you have the requisite items for this choice
1236 	for (i = 0; i < (int)reply->ItemCheck.Size(); ++i)
1237 	{
1238 		if (!CheckStrifeItem(player, reply->ItemCheck[i].Item, reply->ItemCheck[i].Amount))
1239 		{
1240 			// No, you don't. Say so and let the NPC animate negatively.
1241 			if (reply->QuickNo && isconsole)
1242 			{
1243 				TerminalResponse(reply->QuickNo);
1244 			}
1245 			npc->ConversationAnimation(2);
1246 			npc->angle = player->ConversationNPCAngle;
1247 			npc->flags5 &= ~MF5_INCONVERSATION;
1248 			return;
1249 		}
1250 	}
1251 
1252 	// Yay, you do! Let the NPC animate affirmatively.
1253 	npc->ConversationAnimation(1);
1254 
1255 	// If this reply gives you something, then try to receive it.
1256 	takestuff = true;
1257 	if (reply->GiveType != NULL)
1258 	{
1259 		if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(AInventory)))
1260 		{
1261 			if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(AWeapon)))
1262 			{
1263 				if (player->mo->FindInventory(reply->GiveType) != NULL)
1264 				{
1265 					takestuff = false;
1266 				}
1267 			}
1268 
1269 			if (takestuff)
1270 			{
1271 				AInventory *item = static_cast<AInventory *>(Spawn(reply->GiveType, 0, 0, 0, NO_REPLACE));
1272 				// Items given here should not count as items!
1273 				item->ClearCounters();
1274 				if (item->GetClass()->TypeName == NAME_FlameThrower)
1275 				{
1276 					// The flame thrower gives less ammo when given in a dialog
1277 					static_cast<AWeapon*>(item)->AmmoGive1 = 40;
1278 				}
1279 				item->flags |= MF_DROPPED;
1280 				if (!item->CallTryPickup(player->mo))
1281 				{
1282 					item->Destroy();
1283 					takestuff = false;
1284 				}
1285 			}
1286 
1287 			if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(ASlideshowStarter)))
1288 				gameaction = ga_slideshow;
1289 		}
1290 		else
1291 		{
1292 			// Trying to give a non-inventory item.
1293 			takestuff = false;
1294 			if (isconsole)
1295 			{
1296 				Printf("Attempting to give non-inventory item %s\n", reply->GiveType->TypeName.GetChars());
1297 			}
1298 		}
1299 	}
1300 
1301 	if (reply->ActionSpecial != 0)
1302 	{
1303 		takestuff |= !!P_ExecuteSpecial(reply->ActionSpecial, NULL, player->mo, false,
1304 			reply->Args[0], reply->Args[1], reply->Args[2], reply->Args[3], reply->Args[4]);
1305 	}
1306 
1307 	// Take away required items if the give was successful or none was needed.
1308 	if (takestuff)
1309 	{
1310 		for (i = 0; i < (int)reply->ItemCheck.Size(); ++i)
1311 		{
1312 			TakeStrifeItem (player, reply->ItemCheck[i].Item, reply->ItemCheck[i].Amount);
1313 		}
1314 		replyText = reply->QuickYes;
1315 	}
1316 	else
1317 	{
1318 		replyText = "$txt_haveenough";
1319 	}
1320 
1321 	// Update the quest log, if needed.
1322 	if (reply->LogString != NULL)
1323 	{
1324 		const char *log = reply->LogString;
1325 		if (log[0] == '$')
1326 		{
1327 			log = GStrings(log + 1);
1328 		}
1329 
1330 		player->SetLogText(log);
1331 	}
1332 	else if (reply->LogNumber != 0)
1333 	{
1334 		player->SetLogNumber(reply->LogNumber);
1335 	}
1336 
1337 	if (replyText != NULL && isconsole)
1338 	{
1339 		TerminalResponse(replyText);
1340 	}
1341 
1342 	// Does this reply alter the speaker's conversation node? If NextNode is positive,
1343 	// the next time they talk, they will show the new node. If it is negative, then they
1344 	// will show the new node right away without terminating the dialogue.
1345 	if (reply->NextNode != 0)
1346 	{
1347 		int rootnode = npc->ConversationRoot;
1348 		const bool isNegative = reply->NextNode < 0;
1349 		const unsigned next = (unsigned)(rootnode + (isNegative ? -1 : 1) * reply->NextNode - 1);
1350 
1351 		if (next < StrifeDialogues.Size())
1352 		{
1353 			npc->Conversation = StrifeDialogues[next];
1354 
1355 			if (isNegative)
1356 			{
1357 				if (gameaction != ga_slideshow)
1358 				{
1359 					P_StartConversation (npc, player->mo, player->ConversationFaceTalker, false);
1360 					return;
1361 				}
1362 				else
1363 				{
1364 					S_StopSound (npc, CHAN_VOICE);
1365 				}
1366 			}
1367 		}
1368 		else
1369 		{
1370 			Printf ("Next node %u is invalid, no such dialog page\n", next);
1371 		}
1372 	}
1373 
1374 	npc->angle = player->ConversationNPCAngle;
1375 
1376 	// [CW] Set these to NULL because we're not using to them
1377 	// anymore. However, this can interfere with slideshows
1378 	// so we don't set them to NULL in that case.
1379 	if (gameaction != ga_slideshow)
1380 	{
1381 		npc->flags5 &= ~MF5_INCONVERSATION;
1382 		player->ConversationFaceTalker = false;
1383 		player->ConversationNPC = NULL;
1384 		player->ConversationPC = NULL;
1385 		player->ConversationNPCAngle = 0;
1386 	}
1387 
1388 	if (isconsole)
1389 	{
1390 		I_SetMusicVolume (1.f);
1391 	}
1392 }
1393 
1394 //============================================================================
1395 //
1396 // P_ConversationCommand
1397 //
1398 // Complete a conversation command.
1399 //
1400 //============================================================================
1401 
P_ConversationCommand(int netcode,int pnum,BYTE ** stream)1402 void P_ConversationCommand (int netcode, int pnum, BYTE **stream)
1403 {
1404 	player_t *player = &players[pnum];
1405 
1406 	// The conversation menus are normally closed by the menu code, but that
1407 	// doesn't happen during demo playback, so we need to do it here.
1408 	if (demoplayback && DMenu::CurrentMenu != NULL &&
1409 		DMenu::CurrentMenu->IsKindOf(RUNTIME_CLASS(DConversationMenu)))
1410 	{
1411 		DMenu::CurrentMenu->Close();
1412 	}
1413 	if (netcode == DEM_CONVREPLY)
1414 	{
1415 		int nodenum = ReadWord(stream);
1416 		int replynum = ReadByte(stream);
1417 		HandleReply(player, pnum == consoleplayer, nodenum, replynum);
1418 	}
1419 	else
1420 	{
1421 		assert(netcode == DEM_CONVNULL || netcode == DEM_CONVCLOSE);
1422 		if (player->ConversationNPC != NULL)
1423 		{
1424 			player->ConversationNPC->angle = player->ConversationNPCAngle;
1425 			player->ConversationNPC->flags5 &= ~MF5_INCONVERSATION;
1426 		}
1427 		if (netcode == DEM_CONVNULL)
1428 		{
1429 			player->ConversationFaceTalker = false;
1430 			player->ConversationNPC = NULL;
1431 			player->ConversationPC = NULL;
1432 			player->ConversationNPCAngle = 0;
1433 		}
1434 	}
1435 }
1436 
1437 //============================================================================
1438 //
1439 // TerminalResponse
1440 //
1441 // Similar to C_MidPrint, but lower and colored and sized to match the
1442 // rest of the dialogue text.
1443 //
1444 //============================================================================
1445 
TerminalResponse(const char * str)1446 static void TerminalResponse (const char *str)
1447 {
1448 	if (str != NULL)
1449 	{
1450 		// handle string table replacement
1451 		if (str[0] == '$')
1452 		{
1453 			str = GStrings(str + 1);
1454 		}
1455 
1456 		if (StatusBar != NULL)
1457 		{
1458 			AddToConsole(-1, str);
1459 			AddToConsole(-1, "\n");
1460 			// The message is positioned a bit above the menu choices, because
1461 			// merchants can tell you something like this but continue to show
1462 			// their dialogue screen. I think most other conversations use this
1463 			// only as a response for terminating the dialogue.
1464 			StatusBar->AttachMessage(new DHUDMessageFadeOut(SmallFont, str,
1465 				float(CleanWidth/2) + 0.4f, float(ConversationMenuY - 110 + CleanHeight/2), CleanWidth, -CleanHeight,
1466 				CR_UNTRANSLATED, 3, 1), MAKE_ID('T','A','L','K'));
1467 		}
1468 		else
1469 		{
1470 			Printf("%s\n", str);
1471 		}
1472 	}
1473 }
1474 
1475 
operator <<(FArchive & arc,FStrifeDialogueNode * & node)1476 template<> FArchive &operator<< (FArchive &arc, FStrifeDialogueNode *&node)
1477 {
1478 	DWORD convnum;
1479 	if (arc.IsStoring())
1480 	{
1481 		arc.WriteCount (node == NULL? ~0u : node->ThisNodeNum);
1482 	}
1483 	else
1484 	{
1485 		convnum = arc.ReadCount();
1486 		if (convnum >= StrifeDialogues.Size())
1487 		{
1488 			node = NULL;
1489 		}
1490 		else
1491 		{
1492 			node = StrifeDialogues[convnum];
1493 		}
1494 	}
1495 	return arc;
1496 }
1497