From e05242e44dc8119dbb839b0688edd324c4f95c78 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sun, 19 Feb 2017 14:21:49 +0100 Subject: [PATCH] - 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. --- specs/usdf_zdoom.txt | 6 +- src/gi.cpp | 1 + src/gi.h | 1 + src/namedef.h | 1 + src/p_conversation.cpp | 190 ++---------------- src/p_conversation.h | 4 +- src/p_usdf.cpp | 18 +- src/scripting/thingdef_data.cpp | 8 + wadsrc/static/zscript/base.txt | 1 + .../static/zscript/menu/conversationmenu.txt | 158 +++++++++++++-- 10 files changed, 197 insertions(+), 191 deletions(-) diff --git a/specs/usdf_zdoom.txt b/specs/usdf_zdoom.txt index f800b5ae7..cd4393351 100644 --- a/specs/usdf_zdoom.txt +++ b/specs/usdf_zdoom.txt @@ -38,6 +38,7 @@ conversation page { drop = ; + userstring = ; New field which can be used to pass data to custom conversation menu classes. ifitem { item = ; @@ -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 = ; 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 = ; //Override the default conversation menu class for this conversation. page { diff --git a/src/gi.cpp b/src/gi.cpp index 247036ea7..c497bf176 100644 --- a/src/gi.cpp +++ b/src/gi.cpp @@ -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") diff --git a/src/gi.h b/src/gi.h index 1bab4e9a7..24053b002 100644 --- a/src/gi.h +++ b/src/gi.h @@ -172,6 +172,7 @@ struct gameinfo_t double gibfactor; int TextScreenX; int TextScreenY; + FName DefaultConversationMenuClass; FName DefaultEndSequence; FString mMapArrow, mCheatMapArrow; FString mEasyKey, mCheatKey; diff --git a/src/namedef.h b/src/namedef.h index ebd9f3e8e..efc70273f 100644 --- a/src/namedef.h +++ b/src/namedef.h @@ -653,6 +653,7 @@ xx(Link) xx(Goodbye) xx(Require) xx(Exclude) +xx(Userstring) // Special menus xx(Mainmenu) diff --git a/src/p_conversation.cpp b/src/p_conversation.cpp index 10562428b..449f9e286 100644 --- a/src/p_conversation.cpp +++ b/src/p_conversation.cpp @@ -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 mResponseLines; - TArray 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 (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); diff --git a/src/p_conversation.h b/src/p_conversation.h index c10c4c697..302203bd2 100644 --- a/src/p_conversation.h +++ b/src/p_conversation.h @@ -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 diff --git a/src/p_usdf.cpp b/src/p_usdf.cpp index b8e290deb..d2a1bb25e 100644 --- a/src/p_usdf.cpp +++ b/src/p_usdf.cpp @@ -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; } diff --git a/src/scripting/thingdef_data.cpp b/src/scripting/thingdef_data.cpp index b527832fd..1abd0bc4c 100644 --- a/src/scripting/thingdef_data.cpp +++ b/src/scripting/thingdef_data.cpp @@ -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); diff --git a/wadsrc/static/zscript/base.txt b/wadsrc/static/zscript/base.txt index b2eff2601..500fe3abc 100644 --- a/wadsrc/static/zscript/base.txt +++ b/wadsrc/static/zscript/base.txt @@ -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); diff --git a/wadsrc/static/zscript/menu/conversationmenu.txt b/wadsrc/static/zscript/menu/conversationmenu.txt index efe843c51..dba162ad7 100644 --- a/wadsrc/static/zscript/menu/conversationmenu.txt +++ b/wadsrc/static/zscript/menu/conversationmenu.txt @@ -41,7 +41,7 @@ struct StrifeDialogueNode native native Class SpeakerType; native String SpeakerName; native Sound SpeakerVoice; - native TextureID Backdrop; + native String Backdrop; native String Dialogue; native String Goodbye; @@ -68,23 +68,147 @@ struct StrifeDialogueReply native } -class ConversationMenu : Menu native +class ConversationMenu : Menu { - native String mSpeaker; - native BrokenLines mDialogueLines; - native Array mResponseLines; - native Array mResponses; - native bool mShowGold; - native StrifeDialogueNode mCurNode; - native int mYpos; - native PlayerInfo mPlayer; - native int mSelection; + String mSpeaker; + BrokenLines mDialogueLines; + Array mResponseLines; + Array 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; }