2006-02-24 04:48:15 +00:00
|
|
|
#include <assert.h>
|
|
|
|
|
|
|
|
#include "actor.h"
|
|
|
|
#include "r_data.h"
|
|
|
|
#include "p_conversation.h"
|
|
|
|
#include "w_wad.h"
|
|
|
|
#include "cmdlib.h"
|
|
|
|
#include "s_sound.h"
|
|
|
|
#include "m_menu.h"
|
|
|
|
#include "v_text.h"
|
|
|
|
#include "v_video.h"
|
|
|
|
#include "m_random.h"
|
|
|
|
#include "gi.h"
|
|
|
|
#include "templates.h"
|
|
|
|
#include "a_strifeglobal.h"
|
|
|
|
#include "a_keys.h"
|
|
|
|
#include "p_enemy.h"
|
2006-05-11 12:11:59 +00:00
|
|
|
#include "gstrings.h"
|
2006-04-14 12:58:52 +00:00
|
|
|
#include "sound/i_music.h"
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
// The conversations as they exist inside a SCRIPTxx lump.
|
|
|
|
struct Response
|
|
|
|
{
|
|
|
|
SDWORD GiveType;
|
|
|
|
SDWORD Item[3];
|
|
|
|
SDWORD Count[3];
|
|
|
|
char Reply[32];
|
|
|
|
char Yes[80];
|
|
|
|
SDWORD Link;
|
|
|
|
DWORD Log;
|
|
|
|
char No[80];
|
|
|
|
};
|
|
|
|
|
|
|
|
struct Speech
|
|
|
|
{
|
|
|
|
DWORD SpeakerType;
|
|
|
|
SDWORD DropType;
|
|
|
|
SDWORD ItemCheck[3];
|
|
|
|
SDWORD Link;
|
|
|
|
char Name[16];
|
|
|
|
char Sound[8];
|
|
|
|
char Backdrop[8];
|
|
|
|
char Dialogue[320];
|
|
|
|
Response Responses[5];
|
|
|
|
};
|
|
|
|
|
|
|
|
// The Teaser version of the game uses an older version of the structure
|
|
|
|
struct TeaserSpeech
|
|
|
|
{
|
|
|
|
DWORD SpeakerType;
|
|
|
|
SDWORD DropType;
|
|
|
|
DWORD VoiceNumber;
|
|
|
|
char Name[16];
|
|
|
|
char Dialogue[320];
|
|
|
|
Response Responses[5];
|
|
|
|
};
|
|
|
|
|
|
|
|
static FRandom pr_randomspeech("RandomSpeech");
|
|
|
|
|
2006-05-10 02:40:43 +00:00
|
|
|
void GiveSpawner (player_t *player, const PClass *type);
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
TArray<FStrifeDialogueNode *> StrifeDialogues;
|
|
|
|
|
|
|
|
// There were 344 types in Strife, and Strife conversations refer
|
|
|
|
// to their index in the mobjinfo table. This table indexes all
|
|
|
|
// the Strife actor types in the order Strife had them and is
|
|
|
|
// initialized as part of the actor's setup in infodefaults.cpp.
|
2006-05-19 21:31:25 +00:00
|
|
|
const PClass *StrifeTypes[999];
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
static menu_t ConversationMenu;
|
|
|
|
static TArray<menuitem_t> ConversationItems;
|
|
|
|
static int ConversationPauseTic;
|
|
|
|
static bool ShowGold;
|
|
|
|
|
|
|
|
static void LoadScriptFile (const char *name);
|
|
|
|
static FStrifeDialogueNode *ReadRetailNode (FWadLump *lump, DWORD &prevSpeakerType);
|
|
|
|
static FStrifeDialogueNode *ReadTeaserNode (FWadLump *lump, DWORD &prevSpeakerType);
|
|
|
|
static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses);
|
|
|
|
static void DrawConversationMenu ();
|
|
|
|
static void PickConversationReply ();
|
|
|
|
static void CleanupConversationMenu ();
|
|
|
|
|
|
|
|
static FStrifeDialogueNode *CurNode, *PrevNode;
|
|
|
|
static brokenlines_t *DialogueLines;
|
|
|
|
static AActor *ConversationNPC, *ConversationPC;
|
|
|
|
static angle_t ConversationNPCAngle;
|
2006-06-03 12:30:11 +00:00
|
|
|
static bool ConversationFaceTalker;
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
#define NUM_RANDOM_LINES 10
|
|
|
|
#define NUM_RANDOM_GOODBYES 3
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// GetStrifeType
|
|
|
|
//
|
2006-05-10 02:40:43 +00:00
|
|
|
// Given an item type number, returns the corresponding PClass.
|
2006-02-24 04:48:15 +00:00
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
2006-05-10 02:40:43 +00:00
|
|
|
static const PClass *GetStrifeType (int typenum)
|
2006-02-24 04:48:15 +00:00
|
|
|
{
|
|
|
|
if (typenum > 0 && typenum < 344)
|
|
|
|
{
|
|
|
|
return StrifeTypes[typenum];
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// P_LoadStrifeConversations
|
|
|
|
//
|
|
|
|
// Loads the SCRIPT00 and SCRIPTxx files for a corresponding map.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
void P_LoadStrifeConversations (const char *mapname)
|
|
|
|
{
|
|
|
|
if (strnicmp (mapname, "MAP", 3) != 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
char scriptname[9] = { 'S','C','R','I','P','T',mapname[3],mapname[4],0 };
|
|
|
|
|
|
|
|
LoadScriptFile ("SCRIPT00");
|
|
|
|
LoadScriptFile (scriptname);
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// P_FreeStrifeConversations
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
void P_FreeStrifeConversations ()
|
|
|
|
{
|
|
|
|
FStrifeDialogueNode *node;
|
|
|
|
|
|
|
|
while (StrifeDialogues.Pop (node))
|
|
|
|
{
|
|
|
|
delete node;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < 344; ++i)
|
|
|
|
{
|
|
|
|
if (StrifeTypes[i] != NULL)
|
|
|
|
{
|
2006-05-11 12:11:59 +00:00
|
|
|
AActor * ac = GetDefaultByType (StrifeTypes[i]);
|
|
|
|
if (ac != NULL) ac->Conversation = NULL;
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
CurNode = NULL;
|
|
|
|
PrevNode = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// ncopystring
|
|
|
|
//
|
|
|
|
// If the string has no content, returns NULL. Otherwise, returns a copy.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static char *ncopystring (const char *string)
|
|
|
|
{
|
|
|
|
if (string == NULL || string[0] == 0)
|
|
|
|
{
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
return copystring (string);
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// LoadScriptFile
|
|
|
|
//
|
|
|
|
// Loads a SCRIPTxx file and converts it into a more useful internal format.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static void LoadScriptFile (const char *name)
|
|
|
|
{
|
|
|
|
int lumpnum = Wads.CheckNumForName (name);
|
|
|
|
int numnodes, i;
|
|
|
|
DWORD prevSpeakerType;
|
|
|
|
FStrifeDialogueNode *node;
|
|
|
|
FWadLump *lump;
|
|
|
|
|
|
|
|
if (lumpnum < 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
numnodes = Wads.LumpLength (lumpnum);
|
|
|
|
if (!(gameinfo.flags & GI_SHAREWARE))
|
|
|
|
{
|
|
|
|
// Strife scripts are always a multiple of 1516 bytes because each entry
|
|
|
|
// is exactly 1516 bytes long.
|
|
|
|
if (numnodes % 1516 != 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
numnodes /= 1516;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// And the teaser version has 1488-byte entries.
|
|
|
|
if (numnodes % 1488 != 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
numnodes /= 1488;
|
|
|
|
}
|
|
|
|
|
|
|
|
lump = Wads.ReopenLumpNum (lumpnum);
|
|
|
|
prevSpeakerType = 0;
|
|
|
|
|
|
|
|
for (i = 0; i < numnodes; ++i)
|
|
|
|
{
|
|
|
|
if (!(gameinfo.flags & GI_SHAREWARE))
|
|
|
|
{
|
|
|
|
node = ReadRetailNode (lump, prevSpeakerType);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
node = ReadTeaserNode (lump, prevSpeakerType);
|
|
|
|
}
|
|
|
|
StrifeDialogues.Push (node);
|
|
|
|
}
|
|
|
|
delete lump;
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// ReadRetailNode
|
|
|
|
//
|
|
|
|
// Converts a single dialogue node from the Retail version of Strife.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static FStrifeDialogueNode *ReadRetailNode (FWadLump *lump, DWORD &prevSpeakerType)
|
|
|
|
{
|
|
|
|
FStrifeDialogueNode *node;
|
|
|
|
Speech speech;
|
|
|
|
char fullsound[16];
|
2006-05-10 02:40:43 +00:00
|
|
|
const PClass *type;
|
2006-02-24 04:48:15 +00:00
|
|
|
int j;
|
|
|
|
|
|
|
|
node = new FStrifeDialogueNode;
|
|
|
|
|
|
|
|
lump->Read (&speech, sizeof(speech));
|
|
|
|
|
|
|
|
// Byte swap all the ints in the original data
|
|
|
|
speech.SpeakerType = LittleLong(speech.SpeakerType);
|
|
|
|
speech.DropType = LittleLong(speech.DropType);
|
|
|
|
speech.Link = LittleLong(speech.Link);
|
|
|
|
|
|
|
|
// Assign the first instance of a conversation as the default for its
|
|
|
|
// actor, so newly spawned actors will use this conversation by default.
|
|
|
|
type = GetStrifeType (speech.SpeakerType);
|
|
|
|
node->SpeakerType = type;
|
|
|
|
if (prevSpeakerType != speech.SpeakerType)
|
|
|
|
{
|
|
|
|
if (type != NULL)
|
|
|
|
{
|
|
|
|
GetDefaultByType (type)->Conversation = node;
|
|
|
|
}
|
|
|
|
prevSpeakerType = speech.SpeakerType;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert the rest of the data to our own internal format.
|
|
|
|
node->Dialogue = ncopystring (speech.Dialogue);
|
|
|
|
|
|
|
|
// The speaker's portrait, if any.
|
|
|
|
speech.Backdrop[8] = 0;
|
|
|
|
node->Backdrop = TexMan.AddPatch (speech.Backdrop);
|
|
|
|
|
|
|
|
// The speaker's voice for this node, if any.
|
|
|
|
speech.Sound[8] = 0;
|
|
|
|
sprintf (fullsound, "svox/%s", speech.Sound);
|
|
|
|
node->SpeakerVoice = S_FindSound (fullsound);
|
|
|
|
|
|
|
|
// The speaker's name, if any.
|
|
|
|
speech.Name[16] = 0;
|
|
|
|
node->SpeakerName = ncopystring (speech.Name);
|
|
|
|
|
|
|
|
// The item the speaker should drop when killed.
|
|
|
|
node->DropType = GetStrifeType (speech.DropType);
|
|
|
|
|
|
|
|
// Items you need to have to make the speaker use a different node.
|
|
|
|
for (j = 0; j < 3; ++j)
|
|
|
|
{
|
|
|
|
node->ItemCheck[j] = GetStrifeType (speech.ItemCheck[j]);
|
|
|
|
}
|
|
|
|
node->ItemCheckNode = speech.Link;
|
|
|
|
node->Children = NULL;
|
|
|
|
|
|
|
|
ParseReplies (&node->Children, &speech.Responses[0]);
|
|
|
|
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// ReadTeaserNode
|
|
|
|
//
|
|
|
|
// Converts a single dialogue node from the Teaser version of Strife.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static FStrifeDialogueNode *ReadTeaserNode (FWadLump *lump, DWORD &prevSpeakerType)
|
|
|
|
{
|
|
|
|
FStrifeDialogueNode *node;
|
|
|
|
TeaserSpeech speech;
|
|
|
|
char fullsound[16];
|
2006-05-10 02:40:43 +00:00
|
|
|
const PClass *type;
|
2006-02-24 04:48:15 +00:00
|
|
|
int j;
|
|
|
|
|
|
|
|
node = new FStrifeDialogueNode;
|
|
|
|
|
|
|
|
lump->Read (&speech, sizeof(speech));
|
|
|
|
|
|
|
|
// Byte swap all the ints in the original data
|
|
|
|
speech.SpeakerType = LittleLong(speech.SpeakerType);
|
|
|
|
speech.DropType = LittleLong(speech.DropType);
|
|
|
|
|
|
|
|
// Assign the first instance of a conversation as the default for its
|
|
|
|
// actor, so newly spawned actors will use this conversation by default.
|
|
|
|
type = GetStrifeType (speech.SpeakerType);
|
|
|
|
node->SpeakerType = type;
|
|
|
|
if (prevSpeakerType != speech.SpeakerType)
|
|
|
|
{
|
|
|
|
if (type != NULL)
|
|
|
|
{
|
|
|
|
GetDefaultByType (type)->Conversation = node;
|
|
|
|
}
|
|
|
|
prevSpeakerType = speech.SpeakerType;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert the rest of the data to our own internal format.
|
|
|
|
node->Dialogue = ncopystring (speech.Dialogue);
|
|
|
|
|
|
|
|
// The Teaser version doesn't have portraits.
|
|
|
|
node->Backdrop = -1;
|
|
|
|
|
|
|
|
// The speaker's voice for this node, if any.
|
|
|
|
if (speech.VoiceNumber != 0)
|
|
|
|
{
|
|
|
|
sprintf (fullsound, "svox/voc%lu", speech.VoiceNumber);
|
|
|
|
node->SpeakerVoice = S_FindSound (fullsound);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
node->SpeakerVoice = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The speaker's name, if any.
|
|
|
|
speech.Name[16] = 0;
|
|
|
|
node->SpeakerName = ncopystring (speech.Name);
|
|
|
|
|
|
|
|
// The item the speaker should drop when killed.
|
|
|
|
node->DropType = GetStrifeType (speech.DropType);
|
|
|
|
|
|
|
|
// Items you need to have to make the speaker use a different node.
|
|
|
|
for (j = 0; j < 3; ++j)
|
|
|
|
{
|
|
|
|
node->ItemCheck[j] = NULL;
|
|
|
|
}
|
|
|
|
node->ItemCheckNode = 0;
|
|
|
|
node->Children = NULL;
|
|
|
|
|
|
|
|
ParseReplies (&node->Children, &speech.Responses[0]);
|
|
|
|
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// ParseReplies
|
|
|
|
//
|
|
|
|
// Convert PC responses. Rather than being stored inside the main node, they
|
|
|
|
// hang off it as a singly-linked list, so no space is wasted on replies that
|
|
|
|
// don't even matter.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses)
|
|
|
|
{
|
|
|
|
FStrifeDialogueReply *reply;
|
|
|
|
int j, k;
|
|
|
|
|
|
|
|
// Byte swap first.
|
|
|
|
for (j = 0; j < 5; ++j)
|
|
|
|
{
|
|
|
|
responses[j].GiveType = LittleLong(responses[j].GiveType);
|
|
|
|
responses[j].Link = LittleLong(responses[j].Link);
|
|
|
|
responses[j].Log = LittleLong(responses[j].Log);
|
|
|
|
for (k = 0; k < 3; ++k)
|
|
|
|
{
|
|
|
|
responses[j].Item[k] = LittleLong(responses[j].Item[k]);
|
|
|
|
responses[j].Count[k] = LittleLong(responses[j].Count[k]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (j = 0; j < 5; ++j)
|
|
|
|
{
|
|
|
|
Response *rsp = &responses[j];
|
|
|
|
|
|
|
|
// If the reply has no text and goes nowhere, then it doesn't
|
|
|
|
// need to be remembered.
|
|
|
|
if (rsp->Reply[0] == 0 && rsp->Link == 0)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
reply = new FStrifeDialogueReply;
|
|
|
|
|
|
|
|
// The next node to use when this reply is chosen.
|
|
|
|
reply->NextNode = rsp->Link;
|
|
|
|
|
|
|
|
// The message to record in the log for this reply.
|
|
|
|
reply->LogNumber = rsp->Log;
|
|
|
|
|
|
|
|
// The item to receive when this reply is used.
|
|
|
|
reply->GiveType = GetStrifeType (rsp->GiveType);
|
|
|
|
|
|
|
|
// Do you need anything special for this reply to succeed?
|
|
|
|
for (k = 0; k < 3; ++k)
|
|
|
|
{
|
|
|
|
reply->ItemCheck[k] = GetStrifeType (rsp->Item[k]);
|
|
|
|
reply->ItemCheckAmount[k] = rsp->Count[k];
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReplyLines is calculated when the menu is shown. It is just Reply
|
|
|
|
// with word wrap turned on.
|
|
|
|
reply->ReplyLines = NULL;
|
|
|
|
|
|
|
|
// If the first item check has a positive amount required, then
|
|
|
|
// add that to the reply string. Otherwise, use the reply as-is.
|
|
|
|
if (rsp->Count[0] > 0)
|
|
|
|
{
|
|
|
|
char moneystr[128];
|
|
|
|
|
|
|
|
sprintf (moneystr, "%s for %lu", rsp->Reply, rsp->Count[0]);
|
|
|
|
reply->Reply = copystring (moneystr);
|
|
|
|
reply->NeedsGold = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
reply->Reply = copystring (rsp->Reply);
|
|
|
|
reply->NeedsGold = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// QuickYes messages are shown when you meet the item checks.
|
|
|
|
// QuickNo messages are shown when you don't.
|
|
|
|
if (rsp->Yes[0] == '_' && rsp->Yes[1] == 0)
|
|
|
|
{
|
|
|
|
reply->QuickYes = NULL;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
reply->QuickYes = ncopystring (rsp->Yes);
|
|
|
|
}
|
|
|
|
if (reply->ItemCheck[0] != 0)
|
|
|
|
{
|
|
|
|
reply->QuickNo = ncopystring (rsp->No);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
reply->QuickNo = NULL;
|
|
|
|
}
|
|
|
|
reply->Next = *replyptr;
|
|
|
|
*replyptr = reply;
|
|
|
|
replyptr = &reply->Next;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// FStrifeDialogueNode :: ~FStrifeDialogueNode
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
FStrifeDialogueNode::~FStrifeDialogueNode ()
|
|
|
|
{
|
|
|
|
if (SpeakerName != NULL) delete[] SpeakerName;
|
|
|
|
if (Dialogue != NULL) delete[] Dialogue;
|
|
|
|
FStrifeDialogueReply *tokill = Children;
|
|
|
|
while (tokill != NULL)
|
|
|
|
{
|
|
|
|
FStrifeDialogueReply *next = tokill->Next;
|
|
|
|
delete tokill;
|
|
|
|
tokill = next;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// FStrifeDialogueReply :: ~FStrifeDialogueReply
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
FStrifeDialogueReply::~FStrifeDialogueReply ()
|
|
|
|
{
|
|
|
|
if (Reply != NULL) delete[] Reply;
|
|
|
|
if (QuickYes != NULL) delete[] QuickYes;
|
|
|
|
if (QuickNo != NULL) delete[] QuickNo;
|
|
|
|
if (ReplyLines != NULL) V_FreeBrokenLines (ReplyLines);
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// FindNode
|
|
|
|
//
|
|
|
|
// Returns the index that matches the given conversation node.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static int FindNode (const FStrifeDialogueNode *node)
|
|
|
|
{
|
|
|
|
int rootnode = 0;
|
|
|
|
|
|
|
|
while (StrifeDialogues[rootnode] != node)
|
|
|
|
{
|
|
|
|
rootnode++;
|
|
|
|
}
|
|
|
|
return rootnode;
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// CheckStrifeItem
|
|
|
|
//
|
|
|
|
// Checks if you have an item. A NULL itemtype is always considered to be
|
|
|
|
// present.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
2006-05-10 02:40:43 +00:00
|
|
|
static bool CheckStrifeItem (const PClass *itemtype, int amount=-1)
|
2006-02-24 04:48:15 +00:00
|
|
|
{
|
|
|
|
AInventory *item;
|
|
|
|
|
|
|
|
if (itemtype == NULL || amount == 0)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
item = ConversationPC->FindInventory (itemtype);
|
|
|
|
if (item == NULL)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return amount < 0 || item->Amount >= amount;
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// TakeStrifeItem
|
|
|
|
//
|
|
|
|
// Takes away some of an item, unless that item is special and should not
|
|
|
|
// be removed.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
2006-05-10 02:40:43 +00:00
|
|
|
static void TakeStrifeItem (const PClass *itemtype, int amount)
|
2006-02-24 04:48:15 +00:00
|
|
|
{
|
|
|
|
if (itemtype == NULL || amount == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Don't take quest items.
|
2006-05-10 02:40:43 +00:00
|
|
|
if (itemtype->IsDescendantOf (PClass::FindClass(NAME_QuestItem)))
|
2006-02-24 04:48:15 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
// Don't take keys
|
|
|
|
if (itemtype->IsDescendantOf (RUNTIME_CLASS(AKey)))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Don't take the sigil
|
|
|
|
if (itemtype == RUNTIME_CLASS(ASigil))
|
|
|
|
return;
|
|
|
|
|
|
|
|
AInventory *item = ConversationPC->FindInventory (itemtype);
|
|
|
|
if (item != NULL)
|
|
|
|
{
|
|
|
|
item->Amount -= amount;
|
|
|
|
if (item->Amount <= 0)
|
|
|
|
{
|
|
|
|
item->Destroy ();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2006-04-14 12:58:52 +00:00
|
|
|
CUSTOM_CVAR(Float, dlg_musicvolume, 1.0f, CVAR_ARCHIVE)
|
|
|
|
{
|
|
|
|
if (self < 0.f) self = 0.f;
|
|
|
|
else if (self > 1.f) self = 1.f;
|
|
|
|
}
|
|
|
|
|
2006-02-24 04:48:15 +00:00
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// P_StartConversation
|
|
|
|
//
|
|
|
|
// Begins a conversation between a PC and NPC.
|
|
|
|
// FIXME: Make this work in multiplayer.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
2006-06-03 12:30:11 +00:00
|
|
|
void P_StartConversation (AActor *npc, AActor *pc, bool facetalker)
|
2006-02-24 04:48:15 +00:00
|
|
|
{
|
|
|
|
FStrifeDialogueReply *reply;
|
|
|
|
menuitem_t item;
|
|
|
|
const char *toSay;
|
|
|
|
int i, j;
|
|
|
|
|
|
|
|
pc->momx = pc->momy = 0; // Stop moving
|
|
|
|
pc->player->momx = pc->player->momy = 0;
|
|
|
|
|
|
|
|
if (pc->player - players != consoleplayer)
|
|
|
|
return;
|
|
|
|
|
|
|
|
ConversationPC = pc;
|
|
|
|
ConversationNPC = npc;
|
|
|
|
|
|
|
|
CurNode = npc->Conversation;
|
|
|
|
|
|
|
|
if (pc->player == &players[consoleplayer])
|
|
|
|
{
|
|
|
|
S_Sound (CHAN_VOICE, "misc/chat", 1, ATTN_NONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
npc->reactiontime = 2;
|
|
|
|
if (!(npc->flags & MF_FRIENDLY) && !(npc->flags4 & MF4_NOHATEPLAYERS))
|
|
|
|
{
|
|
|
|
npc->target = pc;
|
|
|
|
}
|
2006-06-03 12:30:11 +00:00
|
|
|
ConversationFaceTalker = facetalker;
|
2006-02-24 04:48:15 +00:00
|
|
|
ConversationNPCAngle = npc->angle;
|
2006-06-03 12:30:11 +00:00
|
|
|
if (facetalker)
|
|
|
|
{
|
|
|
|
A_FaceTarget (npc);
|
|
|
|
pc->angle = R_PointToAngle2 (pc->x, pc->y, npc->x, npc->y);
|
|
|
|
}
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
// Check if we should jump to another node
|
|
|
|
while (CurNode->ItemCheck[0] != NULL)
|
|
|
|
{
|
|
|
|
if (CheckStrifeItem (CurNode->ItemCheck[0]) &&
|
|
|
|
CheckStrifeItem (CurNode->ItemCheck[1]) &&
|
|
|
|
CheckStrifeItem (CurNode->ItemCheck[2]))
|
|
|
|
{
|
|
|
|
int root = FindNode (ConversationNPC->GetDefault()->Conversation);
|
|
|
|
CurNode = StrifeDialogues[root + CurNode->ItemCheckNode - 1];
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (CurNode->SpeakerVoice != 0)
|
|
|
|
{
|
2006-04-14 12:58:52 +00:00
|
|
|
I_SetMusicVolume(dlg_musicvolume);
|
2006-05-23 04:25:49 +00:00
|
|
|
S_SoundID (npc, CHAN_VOICE|CHAN_NOPAUSE, CurNode->SpeakerVoice, 1, ATTN_NORM);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set up the menu
|
|
|
|
ConversationMenu.PreDraw = DrawConversationMenu;
|
|
|
|
ConversationMenu.EscapeHandler = CleanupConversationMenu;
|
|
|
|
|
|
|
|
// Format the speaker's message.
|
|
|
|
toSay = CurNode->Dialogue;
|
|
|
|
if (strncmp (toSay, "RANDOM_", 7) == 0)
|
|
|
|
{
|
2006-05-11 12:11:59 +00:00
|
|
|
FString dlgtext;
|
|
|
|
|
|
|
|
dlgtext.Format("TXT_%s_%02d", toSay, 1+(pr_randomspeech() % NUM_RANDOM_LINES));
|
|
|
|
toSay = GStrings[dlgtext.GetChars()];
|
|
|
|
if (toSay==NULL) toSay = "Go away!"; // Ok, it's lame - but it doesn't look like an error to the player. ;)
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
DialogueLines = V_BreakLines (screen->GetWidth()/CleanXfac-24*2, toSay);
|
|
|
|
|
|
|
|
// Fill out the possible choices
|
|
|
|
ShowGold = false;
|
|
|
|
item.type = numberedmore;
|
|
|
|
item.e.mfunc = PickConversationReply;
|
|
|
|
for (reply = CurNode->Children, i = 1; reply != NULL; reply = reply->Next)
|
|
|
|
{
|
|
|
|
if (reply->Reply == NULL)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
ShowGold |= reply->NeedsGold;
|
|
|
|
reply->ReplyLines = V_BreakLines (320-50-10, reply->Reply);
|
|
|
|
for (j = 0; reply->ReplyLines[j].width != -1; ++j)
|
|
|
|
{
|
|
|
|
item.label = reply->ReplyLines[j].string;
|
|
|
|
item.b.position = j == 0 ? i : 0;
|
|
|
|
item.c.extra = reply;
|
|
|
|
ConversationItems.Push (item);
|
|
|
|
}
|
|
|
|
++i;
|
|
|
|
}
|
2006-05-11 12:11:59 +00:00
|
|
|
char goodbye[25];
|
|
|
|
sprintf(goodbye, "TXT_RANDOMGOODBYE_%d", 1+(pr_randomspeech() % NUM_RANDOM_GOODBYES));
|
|
|
|
item.label = (char*)GStrings[goodbye];
|
|
|
|
if (item.label == NULL) item.label = "Bye.";
|
2006-02-24 04:48:15 +00:00
|
|
|
item.b.position = i;
|
|
|
|
item.c.extra = NULL;
|
|
|
|
ConversationItems.Push (item);
|
|
|
|
|
|
|
|
// Determine where the top of the reply list should be positioned.
|
|
|
|
i = (gameinfo.gametype & GAME_Raven) ? 9 : 8;
|
|
|
|
ConversationMenu.y = MIN<int> (140, 192 - ConversationItems.Size() * i);
|
|
|
|
for (i = 0; DialogueLines[i].width != -1; ++i)
|
|
|
|
{ }
|
|
|
|
i = 44 + i * 10;
|
|
|
|
if (ConversationMenu.y - 100 < i - screen->GetHeight() / CleanYfac / 2)
|
|
|
|
{
|
|
|
|
ConversationMenu.y = i - screen->GetHeight() / CleanYfac / 2 + 100;
|
|
|
|
}
|
|
|
|
ConversationMenu.indent = 50;
|
|
|
|
|
|
|
|
// Finish setting up the menu
|
|
|
|
ConversationMenu.items = &ConversationItems[0];
|
|
|
|
ConversationMenu.numitems = ConversationItems.Size();
|
|
|
|
if (CurNode != PrevNode)
|
|
|
|
{ // Only reset the selection if showing a different menu.
|
|
|
|
ConversationMenu.lastOn = 0;
|
|
|
|
PrevNode = CurNode;
|
|
|
|
}
|
|
|
|
ConversationMenu.DontDim = true;
|
|
|
|
|
|
|
|
// And open the menu
|
|
|
|
M_StartControlPanel (false);
|
|
|
|
OptionsActive = true;
|
|
|
|
menuactive = MENU_OnNoPause;
|
|
|
|
ConversationPauseTic = gametic + 20;
|
|
|
|
M_SwitchMenu (&ConversationMenu);
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// P_ResumeConversation
|
|
|
|
//
|
|
|
|
// Resumes a conversation that was interrupted by a slideshow.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
void P_ResumeConversation ()
|
|
|
|
{
|
|
|
|
if (ConversationPC != NULL && ConversationNPC != NULL)
|
|
|
|
{
|
2006-06-03 12:30:11 +00:00
|
|
|
P_StartConversation (ConversationNPC, ConversationPC, ConversationFaceTalker);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// DrawConversationMenu
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static void DrawConversationMenu ()
|
|
|
|
{
|
|
|
|
const char *speakerName;
|
|
|
|
int i, x, y, linesize;
|
|
|
|
|
|
|
|
assert (DialogueLines != NULL);
|
|
|
|
assert (CurNode != NULL);
|
|
|
|
|
|
|
|
if (CurNode == NULL)
|
|
|
|
{
|
|
|
|
M_ClearMenus ();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ConversationPauseTic < gametic)
|
|
|
|
{
|
|
|
|
menuactive = MENU_On;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (CurNode->Backdrop >= 0)
|
|
|
|
{
|
|
|
|
screen->DrawTexture (TexMan(CurNode->Backdrop), 0, 0, DTA_320x200, true, TAG_DONE);
|
|
|
|
}
|
|
|
|
x = 16 * screen->GetWidth() / 320;
|
|
|
|
y = 16 * screen->GetHeight() / 200;
|
|
|
|
linesize = 10 * CleanYfac;
|
|
|
|
|
|
|
|
// Who is talking to you?
|
|
|
|
if (CurNode->SpeakerName != NULL)
|
|
|
|
{
|
|
|
|
speakerName = CurNode->SpeakerName;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
speakerName = ConversationNPC->GetClass()->Meta.GetMetaString (AMETA_StrifeName);
|
|
|
|
if (speakerName == NULL)
|
|
|
|
{
|
|
|
|
speakerName = "Person";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dim the screen behind the dialogue (but only if there is no backdrop).
|
|
|
|
if (CurNode->Backdrop <= 0)
|
|
|
|
{
|
|
|
|
for (i = 0; DialogueLines[i].width != -1; ++i)
|
|
|
|
{ }
|
|
|
|
screen->Dim (0, 0.45f, 14 * screen->GetWidth() / 320, 13 * screen->GetHeight() / 200,
|
|
|
|
308 * screen->GetWidth() / 320 - 14 * screen->GetWidth () / 320,
|
|
|
|
speakerName == NULL ? linesize * i + 6 * CleanYfac
|
|
|
|
: linesize * i + 6 * CleanYfac + linesize * 3/2);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dim the screen behind the PC's choices.
|
|
|
|
screen->Dim (0, 0.45f, (24-160) * CleanXfac + screen->GetWidth()/2,
|
|
|
|
(ConversationMenu.y - 2 - 100) * CleanYfac + screen->GetHeight()/2,
|
|
|
|
272 * CleanXfac,
|
|
|
|
MIN(ConversationMenu.numitems * (gameinfo.gametype & GAME_Raven ? 9 : 8) + 4,
|
|
|
|
200 - ConversationMenu.y) * CleanYfac);
|
|
|
|
|
|
|
|
if (speakerName != NULL)
|
|
|
|
{
|
|
|
|
screen->DrawText (CR_WHITE, x, y, speakerName,
|
|
|
|
DTA_CleanNoMove, true, TAG_DONE);
|
|
|
|
y += linesize * 3 / 2;
|
|
|
|
}
|
|
|
|
x = 24 * screen->GetWidth() / 320;
|
|
|
|
for (i = 0; DialogueLines[i].width >= 0; ++i)
|
|
|
|
{
|
|
|
|
screen->DrawText (CR_UNTRANSLATED, x, y, DialogueLines[i].string,
|
|
|
|
DTA_CleanNoMove, true, TAG_DONE);
|
|
|
|
y += linesize;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ShowGold)
|
|
|
|
{
|
|
|
|
AInventory *coin = ConversationPC->FindInventory (RUNTIME_CLASS(ACoin));
|
|
|
|
char goldstr[32];
|
|
|
|
|
|
|
|
sprintf (goldstr, "%d", coin != NULL ? coin->Amount : 0);
|
|
|
|
screen->DrawText (CR_GRAY, 21, 191, goldstr, DTA_320x200, true,
|
|
|
|
DTA_FillColor, 0, DTA_Alpha, HR_SHADOW, TAG_DONE);
|
|
|
|
screen->DrawTexture (TexMan(((AInventory *)GetDefaultByType (RUNTIME_CLASS(ACoin)))->Icon),
|
|
|
|
3, 190, DTA_320x200, true,
|
|
|
|
DTA_FillColor, 0, DTA_Alpha, HR_SHADOW, TAG_DONE);
|
|
|
|
screen->DrawText (CR_GRAY, 20, 190, goldstr, DTA_320x200, true, TAG_DONE);
|
|
|
|
screen->DrawTexture (TexMan(((AInventory *)GetDefaultByType (RUNTIME_CLASS(ACoin)))->Icon),
|
|
|
|
2, 189, DTA_320x200, true, TAG_DONE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// PickConversationReply
|
|
|
|
//
|
|
|
|
// FIXME: Make this work in multiplayer
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
static void PickConversationReply ()
|
|
|
|
{
|
|
|
|
const char *replyText = NULL;
|
|
|
|
FStrifeDialogueReply *reply = (FStrifeDialogueReply *)ConversationItems[ConversationMenu.lastOn].c.extra;
|
|
|
|
bool takestuff;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
M_ClearMenus ();
|
|
|
|
CleanupConversationMenu ();
|
|
|
|
if (reply == NULL)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if you have the requisite items for this choice
|
|
|
|
for (i = 0; i < 3; ++i)
|
|
|
|
{
|
|
|
|
if (!CheckStrifeItem (reply->ItemCheck[i], reply->ItemCheckAmount[i]))
|
|
|
|
{
|
|
|
|
// No, you don't. Say so and let the NPC animate negatively.
|
|
|
|
if (reply->QuickNo)
|
|
|
|
{
|
|
|
|
Printf ("%s\n", reply->QuickNo);
|
|
|
|
}
|
|
|
|
ConversationNPC->ConversationAnimation (2);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Yay, you do! Let the NPC animate affirmatively.
|
|
|
|
ConversationNPC->ConversationAnimation (1);
|
|
|
|
|
|
|
|
// If this reply gives you something, then try to receive it.
|
|
|
|
takestuff = true;
|
|
|
|
if (reply->GiveType != NULL)
|
|
|
|
{
|
2006-04-13 16:52:24 +00:00
|
|
|
if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(AInventory)))
|
2006-02-24 04:48:15 +00:00
|
|
|
{
|
2006-04-13 16:52:24 +00:00
|
|
|
if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(AWeapon)))
|
2006-04-11 16:27:41 +00:00
|
|
|
{
|
2006-04-13 16:52:24 +00:00
|
|
|
if (players[consoleplayer].mo->FindInventory(reply->GiveType) != NULL)
|
|
|
|
{
|
|
|
|
takestuff = false;
|
|
|
|
}
|
2006-04-11 16:27:41 +00:00
|
|
|
}
|
2006-04-13 16:52:24 +00:00
|
|
|
|
|
|
|
if (takestuff)
|
2006-04-11 16:27:41 +00:00
|
|
|
{
|
2006-04-13 16:52:24 +00:00
|
|
|
AInventory *item = static_cast<AInventory *> (Spawn (reply->GiveType, 0, 0, 0));
|
|
|
|
// Items given here should not count as items!
|
|
|
|
if (item->flags & MF_COUNTITEM)
|
|
|
|
{
|
|
|
|
level.total_items--;
|
|
|
|
item->flags &= ~MF_COUNTITEM;
|
|
|
|
}
|
2006-04-18 22:15:05 +00:00
|
|
|
if (item->IsA(RUNTIME_CLASS(AFlameThrower)))
|
|
|
|
{
|
|
|
|
// The flame thrower gives less ammo when given in a dialog
|
|
|
|
static_cast<AWeapon*>(item)->AmmoGive1 = 40;
|
|
|
|
}
|
2006-04-13 16:52:24 +00:00
|
|
|
item->flags |= MF_DROPPED;
|
|
|
|
if (!item->TryPickup (players[consoleplayer].mo))
|
|
|
|
{
|
|
|
|
item->Destroy ();
|
|
|
|
takestuff = false;
|
|
|
|
}
|
2006-04-11 16:27:41 +00:00
|
|
|
}
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
2006-04-13 16:52:24 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
// Trying to give a non-inventory item.
|
|
|
|
takestuff = false;
|
2006-05-10 02:40:43 +00:00
|
|
|
Printf("Attempting to give non-inventory item %s\n", reply->GiveType->TypeName.GetChars());
|
2006-04-13 16:52:24 +00:00
|
|
|
}
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Take away required items if the give was successful or none was needed.
|
|
|
|
if (takestuff)
|
|
|
|
{
|
|
|
|
for (i = 0; i < 3; ++i)
|
|
|
|
{
|
|
|
|
TakeStrifeItem (reply->ItemCheck[i], reply->ItemCheckAmount[i]);
|
|
|
|
}
|
|
|
|
replyText = reply->QuickYes;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
replyText = "You seem to have enough!";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the quest log, if needed.
|
|
|
|
if (reply->LogNumber != 0)
|
|
|
|
{
|
|
|
|
players[consoleplayer].SetLogNumber (reply->LogNumber);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (replyText != NULL)
|
|
|
|
{
|
|
|
|
Printf ("%s\n", replyText);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Does this reply alter the speaker's conversation node? If NextNode is positive,
|
|
|
|
// the next time they talk, the will show the new node. If it is negative, then they
|
|
|
|
// will show the new node right away without terminating the dialogue.
|
|
|
|
if (reply->NextNode != 0)
|
|
|
|
{
|
|
|
|
int rootnode = FindNode (ConversationNPC->GetDefault()->Conversation);
|
|
|
|
if (reply->NextNode < 0)
|
|
|
|
{
|
|
|
|
ConversationNPC->Conversation = StrifeDialogues[rootnode - reply->NextNode - 1];
|
|
|
|
if (gameaction != ga_slideshow)
|
|
|
|
{
|
2006-06-03 12:30:11 +00:00
|
|
|
P_StartConversation (ConversationNPC, players[consoleplayer].mo, ConversationFaceTalker);
|
2006-02-24 04:48:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
S_StopSound (ConversationNPC, CHAN_VOICE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ConversationNPC->Conversation = StrifeDialogues[rootnode + reply->NextNode - 1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ConversationNPC->angle = ConversationNPCAngle;
|
2006-04-14 12:58:52 +00:00
|
|
|
I_SetMusicVolume(1.f);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
//
|
|
|
|
// CleanupConversationMenu
|
|
|
|
//
|
|
|
|
// Release the resources used to create the most recent conversation menu.
|
|
|
|
//
|
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
void CleanupConversationMenu ()
|
|
|
|
{
|
|
|
|
FStrifeDialogueReply *reply;
|
|
|
|
|
|
|
|
if (CurNode != NULL)
|
|
|
|
{
|
|
|
|
for (reply = CurNode->Children; reply != NULL; reply = reply->Next)
|
|
|
|
{
|
|
|
|
if (reply->ReplyLines != NULL)
|
|
|
|
{
|
|
|
|
V_FreeBrokenLines (reply->ReplyLines);
|
|
|
|
reply->ReplyLines = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
CurNode = NULL;
|
|
|
|
}
|
|
|
|
if (DialogueLines != NULL)
|
|
|
|
{
|
|
|
|
V_FreeBrokenLines (DialogueLines);
|
|
|
|
DialogueLines = NULL;
|
|
|
|
}
|
|
|
|
ConversationItems.Clear ();
|
2006-04-14 12:58:52 +00:00
|
|
|
I_SetMusicVolume(1.f);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|