- scriptified the remaining parts of the conversationmenu.

- do not resolve the backdrop texture to a texture ID at load time. This will allow custom menu classes to use this info differently.
- added a new ZSDF userstring property to dialog pages to give mods more means for customization.
- allow overriding the conversation menu class both globally through MAPINFO and per conversation in ZSDF.
This commit is contained in:
Christoph Oelckers 2017-02-19 14:21:49 +01:00
parent d85b9cdd71
commit e05242e44d
10 changed files with 197 additions and 191 deletions

View File

@ -38,6 +38,7 @@ conversation
page
{
drop = <string>;
userstring = <string>; New field which can be used to pass data to custom conversation menu classes.
ifitem
{
item = <string>;
@ -63,10 +64,6 @@ either refuse loading dialogues with the 'ZDoom' namespace or if it does not
outright abort on incompatible namespaces fail with a type mismatch error on
one of the specified propeties.
In addition ZDoom defines one new field in the top level of a conversation block:
id = <integer>; Assigns a conversation ID for use in Thing_SetConversation or in UDMF's 'conversation' actor property.
ZDoom-format dialogues need to start with the line:
namespace = "ZDoom";
@ -86,6 +83,7 @@ conversation // Starts a dialog.
// the standard conversation ID ('actor' property) is used instead
// for this purpose but since 'ZDoom' namespace requires the actor
// to be a class name it needs a separate field for this.
class = <string>; //Override the default conversation menu class for this conversation.
page
{

View File

@ -362,6 +362,7 @@ void FMapInfoParser::ParseGameInfo()
GAMEINFOKEY_INT(TextScreenX, "textscreenx")
GAMEINFOKEY_INT(TextScreenY, "textscreeny")
GAMEINFOKEY_STRING(DefaultEndSequence, "defaultendsequence")
GAMEINFOKEY_STRING(DefaultConversationMenuClass, "defaultconversationmenuclass")
GAMEINFOKEY_FONT(mStatscreenMapNameFont, "statscreen_mapnamefont")
GAMEINFOKEY_FONT(mStatscreenFinishedFont, "statscreen_finishedfont")
GAMEINFOKEY_FONT(mStatscreenEnteringFont, "statscreen_enteringfont")

View File

@ -172,6 +172,7 @@ struct gameinfo_t
double gibfactor;
int TextScreenX;
int TextScreenY;
FName DefaultConversationMenuClass;
FName DefaultEndSequence;
FString mMapArrow, mCheatMapArrow;
FString mEasyKey, mCheatKey;

View File

@ -653,6 +653,7 @@ xx(Link)
xx(Goodbye)
xx(Require)
xx(Exclude)
xx(Userstring)
// Special menus
xx(Mainmenu)

View File

@ -61,6 +61,7 @@
#include "p_local.h"
#include "menu/menu.h"
#include "g_levellocals.h"
#include "virtual.h"
// The conversations as they exist inside a SCRIPTxx lump.
struct Response
@ -124,9 +125,6 @@ static void TerminalResponse (const char *str);
static FStrifeDialogueNode *PrevNode;
#define NUM_RANDOM_LINES 10
#define NUM_RANDOM_GOODBYES 3
//============================================================================
//
// GetStrifeType
@ -351,7 +349,7 @@ static FStrifeDialogueNode *ReadRetailNode (FileReader *lump, DWORD &prevSpeaker
// The speaker's portrait, if any.
speech.Dialogue[0] = 0; //speech.Backdrop[8] = 0;
node->Backdrop = TexMan.CheckForTexture (speech.Backdrop, FTexture::TEX_MiscPatch);
node->Backdrop = speech.Backdrop;
// The speaker's voice for this node, if any.
speech.Backdrop[0] = 0; //speech.Sound[8] = 0;
@ -425,7 +423,7 @@ static FStrifeDialogueNode *ReadTeaserNode (FileReader *lump, DWORD &prevSpeaker
node->Dialogue = speech.Dialogue;
// The Teaser version doesn't have portraits.
node->Backdrop.SetInvalid();
node->Backdrop = "";
// The speaker's voice for this node, if any.
if (speech.VoiceNumber != 0)
@ -743,153 +741,6 @@ public:
}
};
//============================================================================
//
// The conversation menu
//
//============================================================================
class DConversationMenu : public DMenu
{
DECLARE_CLASS(DConversationMenu, DMenu)
public:
FString mSpeaker;
DBrokenLines *mDialogueLines;
TArray<FString> mResponseLines;
TArray<unsigned int> mResponses;
bool mShowGold;
FStrifeDialogueNode *mCurNode;
int mYpos;
player_t *mPlayer;
int mSelection;
//=============================================================================
//
//
//
//=============================================================================
DConversationMenu(FStrifeDialogueNode *CurNode, player_t *player, int activereply)
{
mCurNode = CurNode;
mPlayer = player;
mDialogueLines = NULL;
mShowGold = false;
// Format the speaker's message.
const char * toSay = CurNode->Dialogue;
if (strncmp (toSay, "RANDOM_", 7) == 0)
{
FString dlgtext;
dlgtext.Format("TXT_%s_%02d", toSay, 1+(pr_randomspeech() % NUM_RANDOM_LINES));
toSay = GStrings[dlgtext];
if (toSay == NULL)
{
toSay = GStrings["TXT_GOAWAY"]; // Ok, it's lame - but it doesn't look like an error to the player. ;)
}
}
else
{
// handle string table replacement
if (toSay[0] == '$')
{
toSay = GStrings(toSay + 1);
}
}
if (toSay == NULL)
{
toSay = ".";
}
unsigned int count;
auto bl = V_BreakLines (SmallFont, screen->GetWidth()/CleanXfac - 24*2, toSay, true, &count);
mDialogueLines = new DBrokenLines(bl, count);
mSelection = -1;
FStrifeDialogueReply *reply;
int r = -1;
int i,j;
for (reply = CurNode->Children, i = 1; reply != NULL; reply = reply->Next)
{
r++;
if (ShouldSkipReply(reply, mPlayer))
{
continue;
}
if (activereply == r) mSelection = i - 1;
mShowGold |= reply->NeedsGold;
const char *ReplyText = reply->Reply;
if (ReplyText[0] == '$')
{
ReplyText = GStrings(ReplyText + 1);
}
FString ReplyString = ReplyText;
if (reply->NeedsGold) ReplyString.AppendFormat(" for %u", reply->PrintAmount);
FBrokenLines *ReplyLines = V_BreakLines (SmallFont, 320-50-10, ReplyString);
mResponses.Push(mResponseLines.Size());
for (j = 0; ReplyLines[j].Width >= 0; ++j)
{
mResponseLines.Push(ReplyLines[j].Text);
}
++i;
V_FreeBrokenLines (ReplyLines);
}
if (mSelection == -1)
{
mSelection = r < activereply ? r + 1 : 0;
}
const char *goodbyestr = CurNode->Goodbye;
if (*goodbyestr == 0)
{
char goodbye[25];
mysnprintf(goodbye, countof(goodbye), "TXT_RANDOMGOODBYE_%d", 1 + (pr_randomspeech() % NUM_RANDOM_GOODBYES));
goodbyestr = GStrings[goodbye];
}
else if (strncmp(goodbyestr, "RANDOM_", 7) == 0)
{
FString byetext;
byetext.Format("TXT_%s_%02d", goodbyestr, 1 + (pr_randomspeech() % NUM_RANDOM_LINES));
goodbyestr = GStrings[byetext];
}
else if (goodbyestr[0] == '$')
{
goodbyestr = GStrings(goodbyestr + 1);
}
if (goodbyestr == nullptr) goodbyestr = "Bye.";
mResponses.Push(mResponseLines.Size());
mResponseLines.Push(FString(goodbyestr));
// Determine where the top of the reply list should be positioned.
mYpos = MIN<int> (140, 192 - mResponseLines.Size() * OptionSettings.mLinespacing);
i = 44 + count * (OptionSettings.mLinespacing + 2);
if (mYpos - 100 < i - screen->GetHeight() / CleanYfac / 2)
{
mYpos = i - screen->GetHeight() / CleanYfac / 2 + 100;
}
ConversationMenuY = mYpos;
//ConversationMenu.indent = 50;
// Because replies can be selectively hidden mResponses.Size() won't be consistent.
// So make sure mSelection doesn't exceed mResponses.Size(). [FishyClockwork]
if (mSelection >= (int)mResponses.Size())
{
mSelection = mResponses.Size() - 1;
}
}
};
IMPLEMENT_CLASS(DConversationMenu, true, false)
//============================================================================
//
// P_FreeStrifeConversations
@ -909,7 +760,7 @@ void P_FreeStrifeConversations ()
ClassRoots.Clear();
PrevNode = NULL;
if (CurrentMenu != NULL && CurrentMenu->IsKindOf(RUNTIME_CLASS(DConversationMenu)))
if (CurrentMenu != NULL && CurrentMenu->IsKindOf("ConversationMenu"))
{
CurrentMenu->Close();
}
@ -1007,8 +858,21 @@ void P_StartConversation (AActor *npc, AActor *pc, bool facetalker, bool saveang
S_Sound (npc, CHAN_VOICE|CHAN_NOPAUSE, CurNode->SpeakerVoice, 1, ATTN_NORM);
}
DConversationMenu *cmenu = new DConversationMenu(CurNode, pc->player, StaticLastReply);
// Create the menu. This may be a user-defined class so check if it is good to use.
FName cls = CurNode->MenuClassName;
if (cls == NAME_None) cls = gameinfo.DefaultConversationMenuClass;
if (cls == NAME_None) cls = "ConversationMenu";
auto mcls = PClass::FindClass(cls);
if (mcls == nullptr || !mcls->IsDescendantOf("ConversationMenu")) mcls = PClass::FindClass("ConversationMenu");
assert(mcls);
auto cmenu = mcls->CreateNew();
IFVIRTUALPTRNAME(cmenu, "ConversationMenu", Init)
{
VMValue params[] = { cmenu, CurNode, pc->player, StaticLastReply };
VMReturn ret(&ConversationMenuY);
GlobalVMStack.Call(func, params, countof(params), &ret, 1);
}
if (CurNode != PrevNode)
{ // Only reset the selection if showing a different menu.
@ -1018,7 +882,7 @@ void P_StartConversation (AActor *npc, AActor *pc, bool facetalker, bool saveang
// And open the menu
M_StartControlPanel (false);
M_ActivateMenu(cmenu);
M_ActivateMenu((DMenu*)cmenu);
menuactive = MENU_OnNoPause;
}
}
@ -1255,8 +1119,7 @@ void P_ConversationCommand (int netcode, int pnum, BYTE **stream)
// The conversation menus are normally closed by the menu code, but that
// doesn't happen during demo playback, so we need to do it here.
if (demoplayback && CurrentMenu != NULL &&
CurrentMenu->IsKindOf(RUNTIME_CLASS(DConversationMenu)))
if (demoplayback && CurrentMenu != NULL && CurrentMenu->IsKindOf("ConversationMenu"))
{
CurrentMenu->Close();
}
@ -1332,6 +1195,8 @@ DEFINE_FIELD(FStrifeDialogueNode, Backdrop);
DEFINE_FIELD(FStrifeDialogueNode, Dialogue);
DEFINE_FIELD(FStrifeDialogueNode, Goodbye);
DEFINE_FIELD(FStrifeDialogueNode, Children);
DEFINE_FIELD(FStrifeDialogueNode, MenuClassName);
DEFINE_FIELD(FStrifeDialogueNode, UserData);
DEFINE_FIELD(FStrifeDialogueReply, Next);
DEFINE_FIELD(FStrifeDialogueReply, GiveType);
@ -1345,14 +1210,3 @@ DEFINE_FIELD(FStrifeDialogueReply, LogString);
DEFINE_FIELD(FStrifeDialogueReply, NextNode);
DEFINE_FIELD(FStrifeDialogueReply, LogNumber);
DEFINE_FIELD(FStrifeDialogueReply, NeedsGold);
DEFINE_FIELD(DConversationMenu, mSpeaker);
DEFINE_FIELD(DConversationMenu, mDialogueLines);
DEFINE_FIELD(DConversationMenu, mResponseLines);
DEFINE_FIELD(DConversationMenu, mResponses);
DEFINE_FIELD(DConversationMenu, mShowGold);
DEFINE_FIELD(DConversationMenu, mCurNode);
DEFINE_FIELD(DConversationMenu, mYpos);
DEFINE_FIELD(DConversationMenu, mPlayer);
DEFINE_FIELD(DConversationMenu, mSelection);

View File

@ -28,11 +28,13 @@ struct FStrifeDialogueNode
PClassActor *SpeakerType;
FString SpeakerName;
FSoundID SpeakerVoice;
FTextureID Backdrop;
FString Backdrop;
FString Dialogue;
FString Goodbye; // must init to null for binary scripts to work as intended
FStrifeDialogueReply *Children;
FName MenuClassName;
FString UserData;
};
// FStrifeDialogueReply holds responses the player can give to the NPC

View File

@ -316,7 +316,14 @@ class USDFParser : public UDMFParserBase
break;
case NAME_Panel:
node->Backdrop = TexMan.CheckForTexture (CheckString(key), FTexture::TEX_MiscPatch);
node->Backdrop = CheckString(key);
break;
case NAME_Userstring:
if (namespace_bits == Zd)
{
node->UserData = CheckString(key);
}
break;
case NAME_Voice:
@ -391,6 +398,7 @@ class USDFParser : public UDMFParserBase
{
PClassActor *type = NULL;
int dlgid = -1;
FName clsid;
unsigned int startpos = StrifeDialogues.Size();
while (!sc.CheckToken('}'))
@ -415,6 +423,13 @@ class USDFParser : public UDMFParserBase
dlgid = CheckInt(key);
}
break;
case NAME_Class:
if (namespace_bits == Zd)
{
clsid = CheckString(key);
}
break;
}
}
else
@ -440,6 +455,7 @@ class USDFParser : public UDMFParserBase
for(;startpos < StrifeDialogues.Size(); startpos++)
{
StrifeDialogues[startpos]->SpeakerType = type;
StrifeDialogues[startpos]->MenuClassName = clsid;
}
return true;
}

View File

@ -1219,6 +1219,14 @@ DEFINE_ACTION_FUNCTION(FStringStruct, Mid)
ACTION_RETURN_STRING(s);
}
DEFINE_ACTION_FUNCTION(FStringStruct, Left)
{
PARAM_SELF_STRUCT_PROLOGUE(FString);
PARAM_UINT(len);
FString s = self->Left(len);
ACTION_RETURN_STRING(s);
}
DEFINE_ACTION_FUNCTION(FStringStruct, Truncate)
{
PARAM_SELF_STRUCT_PROLOGUE(FString);

View File

@ -580,6 +580,7 @@ struct StringStruct native
native vararg void AppendFormat(String fmt, ...);
native void Replace(String pattern, String replacement);
native String Left(int len);
native String Mid(int pos = 0, int len = 2147483647);
native void Truncate(int newlen);
native String CharAt(int pos);

View File

@ -41,7 +41,7 @@ struct StrifeDialogueNode native
native Class<Actor> SpeakerType;
native String SpeakerName;
native Sound SpeakerVoice;
native TextureID Backdrop;
native String Backdrop;
native String Dialogue;
native String Goodbye;
@ -68,22 +68,146 @@ struct StrifeDialogueReply native
}
class ConversationMenu : Menu native
class ConversationMenu : Menu
{
native String mSpeaker;
native BrokenLines mDialogueLines;
native Array<String> mResponseLines;
native Array<uint> mResponses;
native bool mShowGold;
native StrifeDialogueNode mCurNode;
native int mYpos;
native PlayerInfo mPlayer;
native int mSelection;
String mSpeaker;
BrokenLines mDialogueLines;
Array<String> mResponseLines;
Array<uint> mResponses;
bool mShowGold;
StrifeDialogueNode mCurNode;
int mYpos;
PlayerInfo mPlayer;
int mSelection;
int ConversationPauseTic;
int SpeechWidth;
int ReplyWidth;
native static void SendConversationReply(int node, int reply);
//ConversationPauseTic = gametic + 20;
// DontDim = true;
const NUM_RANDOM_LINES = 10;
const NUM_RANDOM_GOODBYES = 3;
//=============================================================================
//
// returns the y position of the replies boy for positioning the terminal response.
//
//=============================================================================
virtual int Init(StrifeDialogueNode CurNode, PlayerInfo player, int activereply)
{
mCurNode = CurNode;
mPlayer = player;
mShowGold = false;
ConversationPauseTic = gametic + 20;
DontDim = true;
ReplyWidth = 320-50-10;
SpeechWidth = screen.GetWidth()/CleanXfac - 24*2;
FormatSpeakerMessage();
return FormatReplies(activereply);
}
//=============================================================================
//
//
//
//=============================================================================
virtual int FormatReplies(int activereply)
{
mSelection = -1;
StrifeDialogueReply reply;
int r = -1;
int i = 1,j;
for (reply = mCurNode.Children; reply != NULL; reply = reply.Next)
{
r++;
if (reply.ShouldSkipReply(mPlayer))
{
continue;
}
if (activereply == r) mSelection = i - 1;
mShowGold |= reply.NeedsGold;
let ReplyText = Stringtable.Localize(reply.Reply);
if (reply.NeedsGold) ReplyText.AppendFormat(" for %u", reply.PrintAmount);
let ReplyLines = SmallFont.BreakLines (ReplyText, ReplyWidth);
mResponses.Push(mResponseLines.Size());
for (j = 0; j < ReplyLines.Count(); ++j)
{
mResponseLines.Push(ReplyLines.StringAt(j));
}
++i;
ReplyLines.Destroy();
}
if (mSelection == -1)
{
mSelection = r < activereply ? r + 1 : 0;
}
let goodbyestr = mCurNode.Goodbye;
if (goodbyestr.Length() == 0)
{
goodbyestr = String.Format("$TXT_RANDOMGOODBYE_%d", Random[RandomSpeech](1, NUM_RANDOM_GOODBYES));
}
else if (goodbyestr.Left(7) == "RANDOM_")
{
goodbyestr = String.Format("$TXT_%s_%02d", goodbyestr, Random[RandomSpeech](1, NUM_RANDOM_LINES));
}
goodbyestr = Stringtable.Localize(goodbyestr);
if (goodbyestr.Length() == 0 || goodbyestr.CharAt(0) == "$") goodbyestr = "Bye.";
mResponses.Push(mResponseLines.Size());
mResponseLines.Push(goodbyestr);
// Determine where the top of the reply list should be positioned.
mYpos = MIN (140, 192 - mResponseLines.Size() * OptionMenuSettings.mLinespacing);
i = 44 + mResponseLines.Size() * OptionMenuSettings.mLinespacing;
if (mYpos - 100 < i - screen.GetHeight() / CleanYfac / 2)
{
mYpos = i - screen.GetHeight() / CleanYfac / 2 + 100;
}
if (mSelection >= mResponses.Size())
{
mSelection = mResponses.Size() - 1;
}
return mYpos;
}
//=============================================================================
//
//
//
//=============================================================================
virtual void FormatSpeakerMessage()
{
// Format the speaker's message.
String toSay = mCurNode.Dialogue;
if (toSay.Left(7) == "RANDOM_")
{
let dlgtext = String.Format("$TXT_%s_%02d", toSay, random[RandomSpeech](1, NUM_RANDOM_LINES));
toSay = Stringtable.Localize(dlgtext);
if (toSay.CharAt(0) == "$") toSay = Stringtable.Localize("$TXT_GOAWAY");
}
else
{
// handle string table replacement
toSay = Stringtable.Localize(toSay);
}
if (toSay.Length() == 0)
{
toSay = ".";
}
mDialogueLines = SmallFont.BreakLines(toSay, SpeechWidth);
}
//=============================================================================
//
@ -233,9 +357,10 @@ class ConversationMenu : Menu native
virtual bool DrawBackdrop()
{
if (mCurNode.Backdrop.isValid())
let tex = TexMan.CheckForTexture (mCurNode.Backdrop, TexMan.Type_MiscPatch);
if (tex.isValid())
{
screen.DrawTexture(mCurNode.Backdrop, false, 0, 0, DTA_320x200, true);
screen.DrawTexture(tex, false, 0, 0, DTA_320x200, true);
return false;
}
return true;
@ -381,9 +506,8 @@ class ConversationMenu : Menu native
override void Ticker()
{
// will be reactivated later.
// [CW] Freeze the game depending on MAPINFO options.
//if (ConversationPauseTic < gametic && !multiplayer && !level.no_dlg_freeze)
if (ConversationPauseTic < gametic && !multiplayer && !level.no_dlg_freeze)
{
menuactive = Menu.On;
}