From 8c06a00ee6f5a761dc79efe649771b2956902a8e Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Fri, 22 Feb 2019 19:07:58 +0100 Subject: [PATCH] - moved all dialogue loading code into the map loader. --- src/CMakeLists.txt | 3 +- src/maploader/maploader.cpp | 2 +- src/maploader/maploader.h | 14 + src/maploader/strifedialogue.cpp | 560 +++++++++++++++++++++++++ src/maploader/udmf.cpp | 2 +- src/{p_udmf.h => maploader/udmf.h} | 0 src/{p_usdf.cpp => maploader/usdf.cpp} | 13 +- src/p_conversation.cpp | 510 ---------------------- src/p_conversation.h | 4 - 9 files changed, 585 insertions(+), 523 deletions(-) create mode 100644 src/maploader/strifedialogue.cpp rename src/{p_udmf.h => maploader/udmf.h} (100%) rename src/{p_usdf.cpp => maploader/usdf.cpp} (97%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 007e3ea12..bfc20d71e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -952,7 +952,6 @@ set (PCH_SOURCES p_states.cpp p_things.cpp p_tick.cpp - p_usdf.cpp p_user.cpp r_utility.cpp r_sky.cpp @@ -1075,6 +1074,8 @@ set (PCH_SOURCES maploader/slopes.cpp maploader/glnodes.cpp maploader/udmf.cpp + maploader/usdf.cpp + maploader/strifedialogue.cpp maploader/polyobjects.cpp maploader/renderinfo.cpp maploader/compatibility.cpp diff --git a/src/maploader/maploader.cpp b/src/maploader/maploader.cpp index 5e3dc9669..81da37591 100644 --- a/src/maploader/maploader.cpp +++ b/src/maploader/maploader.cpp @@ -3016,7 +3016,7 @@ void MapLoader::LoadLevel(MapData *map, const char *lumpname, int position) LoadMapinfoACSLump(); - P_LoadStrifeConversations(Level, map, lumpname); + LoadStrifeConversations(map, lumpname); FMissingTextureTracker missingtex; diff --git a/src/maploader/maploader.h b/src/maploader/maploader.h index 37d372edb..67c78c36b 100644 --- a/src/maploader/maploader.h +++ b/src/maploader/maploader.h @@ -4,6 +4,9 @@ #include "g_levellocals.h" class FileReader; +struct FStrifeDialogueNode; +struct FStrifeDialogueReply; +struct Response; struct EDMapthing { @@ -98,6 +101,7 @@ struct MapData; class MapLoader { friend class UDMFParser; + friend class USDFParser; void *level; // this is to hide the global variable and produce an error for referencing it. public: FLevelLocals *Level; @@ -176,6 +180,16 @@ private: void ReportUnpairedMinisegs(); void CalcIndices(); + // Strife dialogue + void LoadStrifeConversations (MapData *map, const char *mapname); + bool LoadScriptFile (const char *name, bool include, int type); + bool LoadScriptFile(const char *name, int lumpnum, FileReader &lump, int numnodes, bool include, int type); + FStrifeDialogueNode *ReadRetailNode (const char *name, FileReader &lump, uint32_t &prevSpeakerType); + FStrifeDialogueNode *ReadTeaserNode (const char *name, FileReader &lump, uint32_t &prevSpeakerType); + void ParseReplies (const char *name, int pos, FStrifeDialogueReply **replyptr, Response *responses); + + bool ParseUSDF(int lumpnum, FileReader &lump, int lumplen); + // Specials void SpawnSpecials(); void InitSectorSpecial(sector_t *sector, int special); diff --git a/src/maploader/strifedialogue.cpp b/src/maploader/strifedialogue.cpp new file mode 100644 index 000000000..ea018b6d5 --- /dev/null +++ b/src/maploader/strifedialogue.cpp @@ -0,0 +1,560 @@ +/* +** strifedialogue.cpp +** loads Strife style conversation dialogs +** +**--------------------------------------------------------------------------- +** Copyright 2004-2008 Randy Heit +** Copyright 2006-2019 Christoph Oelckers +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#include + +#include "actor.h" +#include "p_conversation.h" +#include "w_wad.h" +#include "cmdlib.h" +#include "v_text.h" +#include "gi.h" +#include "a_keys.h" +#include "p_enemy.h" +#include "gstrings.h" +#include "p_setup.h" +#include "d_net.h" +#include "d_event.h" +#include "doomstat.h" +#include "c_console.h" +#include "g_levellocals.h" +#include "maploader.h" + +// The conversations as they exist inside a SCRIPTxx lump. +struct Response +{ + int32_t GiveType; + int32_t Item[3]; + int32_t Count[3]; + char Reply[32]; + char Yes[80]; + int32_t Link; + uint32_t Log; + char No[80]; +}; + +struct Speech +{ + uint32_t SpeakerType; + int32_t DropType; + int32_t ItemCheck[3]; + int32_t 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 +{ + uint32_t SpeakerType; + int32_t DropType; + uint32_t VoiceNumber; + char Name[16]; + char Dialogue[320]; + Response Responses[5]; +}; + +//============================================================================ +// +// P_LoadStrifeConversations +// +// Loads the SCRIPT00 and SCRIPTxx files for a corresponding map. +// +//============================================================================ + +void MapLoader::LoadStrifeConversations (MapData *map, const char *mapname) +{ + if (map->Size(ML_CONVERSATION) > 0) + { + LoadScriptFile (nullptr, map->lumpnum, map->Reader(ML_CONVERSATION), map->Size(ML_CONVERSATION), false, 0); + } + else + { + if (strnicmp (mapname, "MAP", 3) == 0) + { + char scriptname_b[9] = { 'S','C','R','I','P','T',mapname[3],mapname[4],0 }; + char scriptname_t[9] = { 'D','I','A','L','O','G',mapname[3],mapname[4],0 }; + + if ( LoadScriptFile(scriptname_t, false, 2) + || LoadScriptFile(scriptname_b, false, 1)) + { + return; + } + } + + if (gameinfo.Dialogue.IsNotEmpty()) + { + if (LoadScriptFile(gameinfo.Dialogue, false, 0)) + { + return; + } + } + + LoadScriptFile("SCRIPT00", false, 1); + } +} + +//============================================================================ +// +// LoadScriptFile +// +// Loads a SCRIPTxx file and converts it into a more useful internal format. +// +//============================================================================ + +bool MapLoader::LoadScriptFile (const char *name, bool include, int type) +{ + int lumpnum = Wads.CheckNumForName (name); + const bool found = lumpnum >= 0 + || (lumpnum = Wads.CheckNumForFullName (name)) >= 0; + + if (!found) + { + if (type == 0) + { + Printf(TEXTCOLOR_RED "Could not find dialog file %s\n", name); + } + + return false; + } + FileReader lump = Wads.ReopenLumpReader (lumpnum); + + auto fn = Wads.GetLumpFile(lumpnum); + auto wadname = Wads.GetWadName(fn); + if (stricmp(wadname, "STRIFE0.WAD") && stricmp(wadname, "STRIFE1.WAD") && stricmp(wadname, "SVE.WAD")) name = nullptr; // Only localize IWAD content. + if (name && !stricmp(name, "SCRIPT00")) name = nullptr; // This only contains random string references which already use the string table. + + bool res = LoadScriptFile(name, lumpnum, lump, Wads.LumpLength(lumpnum), include, type); + return res; +} + +bool MapLoader::LoadScriptFile(const char *name, int lumpnum, FileReader &lump, int numnodes, bool include, int type) +{ + int i; + uint32_t prevSpeakerType; + FStrifeDialogueNode *node; + char buffer[4]; + + lump.Read(buffer, 4); + lump.Seek(-4, FileReader::SeekCur); + + // The binary format is so primitive that this check is enough to detect it. + bool isbinary = (buffer[0] == 0 || buffer[1] == 0 || buffer[2] == 0 || buffer[3] == 0); + + if ((type == 1 && !isbinary) || (type == 2 && isbinary)) + { + DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum)); + return false; + } + + if (!isbinary) + { + ParseUSDF(lumpnum, lump, numnodes); + } + else + { + if (!include) + { + LoadScriptFile("SCRIPT00", true, 1); + } + 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) + { + DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum)); + return false; + } + numnodes /= 1516; + } + else + { + // And the teaser version has 1488-byte entries. + if (numnodes % 1488 != 0) + { + DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum)); + return false; + } + numnodes /= 1488; + } + + prevSpeakerType = 0; + + for (i = 0; i < numnodes; ++i) + { + if (!(gameinfo.flags & GI_SHAREWARE)) + { + node = ReadRetailNode (name, lump, prevSpeakerType); + } + else + { + node = ReadTeaserNode (name, lump, prevSpeakerType); + } + node->ThisNodeNum = Level->StrifeDialogues.Push(node); + } + } + return true; +} + +//============================================================================ +// +// ReadRetailNode +// +// Converts a single dialogue node from the Retail version of Strife. +// +//============================================================================ + +static FString TokenFromString(const char *speech) +{ + FString token = speech; + token.ToUpper(); + token.ReplaceChars(".,-+!?'", ' '); + token.Substitute(" ", ""); + token.Truncate(5); + return token; +} + +FStrifeDialogueNode *MapLoader::ReadRetailNode (const char *name, FileReader &lump, uint32_t &prevSpeakerType) +{ + FStrifeDialogueNode *node; + Speech speech; + char fullsound[16]; + PClassActor *type; + int j; + + node = new FStrifeDialogueNode; + + auto pos = lump.Tell(); + 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 ((signed)(speech.SpeakerType) >= 0 && prevSpeakerType != speech.SpeakerType) + { + if (type != NULL) + { + Level->ClassRoots[type->TypeName] = Level->StrifeDialogues.Size(); + } + Level->DialogueRoots[speech.SpeakerType] = Level->StrifeDialogues.Size(); + prevSpeakerType = speech.SpeakerType; + } + + // Convert the rest of the data to our own internal format. + + if (name) + { + FStringf label("$TXT_DLG_%s_d%d_%s", name, int(pos), TokenFromString(speech.Dialogue).GetChars()); + node->Dialogue = GStrings.exists(label.GetChars()+1)? label : FString(speech.Dialogue); + } + else + { + node->Dialogue = speech.Dialogue; + } + + // The speaker's portrait, if any. + speech.Dialogue[0] = 0; //speech.Backdrop[8] = 0; + node->Backdrop = speech.Backdrop; + + // The speaker's voice for this node, if any. + speech.Backdrop[0] = 0; //speech.Sound[8] = 0; + mysnprintf (fullsound, countof(fullsound), "svox/%s", speech.Sound); + node->SpeakerVoice = fullsound; + + // The speaker's name, if any. + speech.Sound[0] = 0; //speech.Name[16] = 0; + if (name && speech.Name[0]) + { + FString label = speech.Name; + label.ReplaceChars(' ', '_'); + label.ReplaceChars('\'', '_'); + node->SpeakerName.Format("$TXT_SPEAKER_%s", label.GetChars()); + if (!GStrings.exists(node->SpeakerName.GetChars() + 1)) node->SpeakerName = speech.Name; + + } + else + { + node->SpeakerName = 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. + node->ItemCheck.Resize(3); + for (j = 0; j < 3; ++j) + { + auto inv = GetStrifeType(speech.ItemCheck[j]); + if (!inv->IsDescendantOf(NAME_Inventory)) inv = nullptr; + node->ItemCheck[j].Item = inv; + node->ItemCheck[j].Amount = -1; + } + node->ItemCheckNode = speech.Link; + node->Children = NULL; + + ParseReplies (name, int(pos), &node->Children, &speech.Responses[0]); + + return node; +} + +//============================================================================ +// +// ReadTeaserNode +// +// Converts a single dialogue node from the Teaser version of Strife. +// +//============================================================================ + +FStrifeDialogueNode *MapLoader::ReadTeaserNode (const char *name, FileReader &lump, uint32_t &prevSpeakerType) +{ + FStrifeDialogueNode *node; + TeaserSpeech speech; + char fullsound[16]; + PClassActor *type; + int j; + + node = new FStrifeDialogueNode; + + auto pos = lump.Tell() * 1516 / 1488; + 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 ((signed)speech.SpeakerType >= 0 && prevSpeakerType != speech.SpeakerType) + { + if (type != NULL) + { + Level->ClassRoots[type->TypeName] = Level->StrifeDialogues.Size(); + } + Level->DialogueRoots[speech.SpeakerType] = Level->StrifeDialogues.Size(); + prevSpeakerType = speech.SpeakerType; + } + + // Convert the rest of the data to our own internal format. + if (name) + { + FStringf label("$TXT_DLG_%s_d%d_%s", name, pos, TokenFromString(speech.Dialogue).GetChars()); + node->Dialogue = GStrings.exists(label.GetChars() + 1)? label : FString(speech.Dialogue); + } + else + { + node->Dialogue = speech.Dialogue; + } + + // The Teaser version doesn't have portraits. + node->Backdrop = ""; + + // The speaker's voice for this node, if any. + if (speech.VoiceNumber != 0) + { + mysnprintf (fullsound, countof(fullsound), "svox/voc%u", speech.VoiceNumber); + node->SpeakerVoice = fullsound; + } + else + { + node->SpeakerVoice = 0; + } + + // The speaker's name, if any. + speech.Dialogue[0] = 0; //speech.Name[16] = 0; + if (name && speech.Name[0]) + { + FString label = speech.Name; + label.ReplaceChars(' ', '_'); + label.ReplaceChars('\'', '_'); + node->SpeakerName.Format("$TXT_SPEAKER_%s", label.GetChars()); + if (!GStrings.exists(node->SpeakerName.GetChars() + 1)) node->SpeakerName = speech.Name; + } + else + { + node->SpeakerName = 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. + node->ItemCheck.Resize(3); + for (j = 0; j < 3; ++j) + { + node->ItemCheck[j].Item = NULL; + node->ItemCheck[j].Amount = -1; + } + node->ItemCheckNode = 0; + node->Children = NULL; + + ParseReplies (name, int(pos), &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. +// +//============================================================================ + +void MapLoader::ParseReplies (const char *name, int pos, 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; + if (reply->NextNode < 0) + { + reply->NextNode *= -1; + reply->CloseDialog = false; + } + + // The message to record in the log for this reply. + reply->LogNumber = rsp->Log; + reply->LogString = ""; + + // The item to receive when this reply is used. + reply->GiveType = GetStrifeType (rsp->GiveType); + reply->ActionSpecial = 0; + + // Do you need anything special for this reply to succeed? + reply->ItemCheck.Resize(3); + for (k = 0; k < 3; ++k) + { + auto inv = GetStrifeType(rsp->Item[k]); + if (!inv->IsDescendantOf(NAME_Inventory)) inv = nullptr; + reply->ItemCheck[k].Item = inv; + reply->ItemCheck[k].Amount = rsp->Count[k]; + } + reply->PrintAmount = reply->ItemCheck[0].Amount; + reply->ItemCheckRequire.Clear(); + reply->ItemCheckExclude.Clear(); + + if (name) + { + FStringf label("$TXT_RPLY%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->Reply).GetChars()); + reply->Reply = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->Reply); + + reply->Reply = label; + } + else + { + reply->Reply = rsp->Reply; + } + + + // If the first item check has a positive amount required, then + // add that to the reply string. Otherwise, use the reply as-is. + reply->NeedsGold = (rsp->Count[0] > 0); + + // QuickYes messages are shown when you meet the item checks. + // QuickNo messages are shown when you don't. + // Note that empty nodes contain a '_' in retail Strife, a '.' in the teasers and an empty string in SVE. + if (((rsp->Yes[0] == '_' || rsp->Yes[0] == '.') && rsp->Yes[1] == 0) || rsp->Yes[0] == 0) + { + reply->QuickYes = ""; + } + else + { + if (name) + { + FStringf label("$TXT_RYES%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->Yes).GetChars()); + reply->QuickYes = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->Yes); + } + else + { + reply->QuickYes = rsp->Yes; + } + } + if (reply->ItemCheck[0].Item != 0) + { + FStringf label("$TXT_RNO%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->No).GetChars()); + reply->QuickNo = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->No); + } + else + { + reply->QuickNo = ""; + } + reply->Next = *replyptr; + *replyptr = reply; + replyptr = &reply->Next; + } +} + diff --git a/src/maploader/udmf.cpp b/src/maploader/udmf.cpp index 3a86bfcdf..b8eac3744 100644 --- a/src/maploader/udmf.cpp +++ b/src/maploader/udmf.cpp @@ -40,7 +40,7 @@ #include "gi.h" #include "r_sky.h" #include "g_level.h" -#include "p_udmf.h" +#include "udmf.h" #include "r_state.h" #include "w_wad.h" #include "p_tags.h" diff --git a/src/p_udmf.h b/src/maploader/udmf.h similarity index 100% rename from src/p_udmf.h rename to src/maploader/udmf.h diff --git a/src/p_usdf.cpp b/src/maploader/usdf.cpp similarity index 97% rename from src/p_usdf.cpp rename to src/maploader/usdf.cpp index e88bbeda8..d6098ab31 100644 --- a/src/p_usdf.cpp +++ b/src/maploader/usdf.cpp @@ -35,12 +35,13 @@ #include "p_setup.h" #include "p_lnspec.h" #include "p_conversation.h" -#include "p_udmf.h" +#include "udmf.h" #include "doomerrors.h" #include "actor.h" #include "a_pickups.h" #include "w_wad.h" #include "g_levellocals.h" +#include "maploader.h" #define Zd 1 #define St 2 @@ -496,9 +497,9 @@ class USDFParser : public UDMFParserBase //=========================================================================== public: - bool Parse(FLevelLocals *l, int lumpnum, FileReader &lump, int lumplen) + bool Parse(MapLoader *loader,int lumpnum, FileReader &lump, int lumplen) { - Level = l; + Level = loader->Level; sc.OpenMem(Wads.GetLumpFullName(lumpnum), lump.Read(lumplen)); sc.SetCMode(true); // Namespace must be the first field because everything else depends on it. @@ -541,7 +542,7 @@ public: { sc.MustGetToken('='); sc.MustGetToken(TK_StringConst); - LoadScriptFile(Level, sc.String, true); + loader->LoadScriptFile(sc.String, true, 0); sc.MustGetToken(';'); } else @@ -607,13 +608,13 @@ public: -bool P_ParseUSDF(FLevelLocals *l, int lumpnum, FileReader &lump, int lumplen) +bool MapLoader::ParseUSDF(int lumpnum, FileReader &lump, int lumplen) { USDFParser parse; try { - if (!parse.Parse(l, lumpnum, lump, lumplen)) + if (!parse.Parse(this, lumpnum, lump, lumplen)) { // clean up the incomplete dialogue structures here return false; diff --git a/src/p_conversation.cpp b/src/p_conversation.cpp index c536611ae..b94d19535 100644 --- a/src/p_conversation.cpp +++ b/src/p_conversation.cpp @@ -58,43 +58,6 @@ #include "v_video.h" #include "actorinlines.h" -// The conversations as they exist inside a SCRIPTxx lump. -struct Response -{ - int32_t GiveType; - int32_t Item[3]; - int32_t Count[3]; - char Reply[32]; - char Yes[80]; - int32_t Link; - uint32_t Log; - char No[80]; -}; - -struct Speech -{ - uint32_t SpeakerType; - int32_t DropType; - int32_t ItemCheck[3]; - int32_t 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 -{ - uint32_t SpeakerType; - int32_t DropType; - uint32_t VoiceNumber; - char Name[16]; - char Dialogue[320]; - Response Responses[5]; -}; - static FRandom pr_randomspeech("RandomSpeech"); static int ConversationMenuY; @@ -103,10 +66,6 @@ static int ConversationMenuY; static FStrifeDialogueNode *PrevNode; static int StaticLastReply; -static bool LoadScriptFile(FLevelLocals *Level, const char *name, int lumpnum, FileReader &lump, int numnodes, bool include, int type); -static FStrifeDialogueNode *ReadRetailNode (FLevelLocals *Level, const char *name, FileReader &lump, uint32_t &prevSpeakerType); -static FStrifeDialogueNode *ReadTeaserNode (FLevelLocals *Level, const char *name, FileReader &lump, uint32_t &prevSpeakerType); -static void ParseReplies (const char *name, int pos, FStrifeDialogueReply **replyptr, Response *responses); static bool DrawConversationMenu (); static void PickConversationReply (int replyindex); static void TerminalResponse (const char *str); @@ -153,475 +112,6 @@ int FLevelLocals::GetConversation(FName classname) else return *pindex; } -//============================================================================ -// -// P_LoadStrifeConversations -// -// Loads the SCRIPT00 and SCRIPTxx files for a corresponding map. -// -//============================================================================ - -void P_LoadStrifeConversations (FLevelLocals *Level, MapData *map, const char *mapname) -{ - if (map->Size(ML_CONVERSATION) > 0) - { - LoadScriptFile (Level, nullptr, map->lumpnum, map->Reader(ML_CONVERSATION), map->Size(ML_CONVERSATION), false, 0); - } - else - { - if (strnicmp (mapname, "MAP", 3) == 0) - { - char scriptname_b[9] = { 'S','C','R','I','P','T',mapname[3],mapname[4],0 }; - char scriptname_t[9] = { 'D','I','A','L','O','G',mapname[3],mapname[4],0 }; - - if ( LoadScriptFile(Level, scriptname_t, false, 2) - || LoadScriptFile(Level, scriptname_b, false, 1)) - { - return; - } - } - - if (gameinfo.Dialogue.IsNotEmpty()) - { - if (LoadScriptFile(Level, gameinfo.Dialogue, false, 0)) - { - return; - } - } - - LoadScriptFile(Level, "SCRIPT00", false, 1); - } -} - -//============================================================================ -// -// LoadScriptFile -// -// Loads a SCRIPTxx file and converts it into a more useful internal format. -// -//============================================================================ - -bool LoadScriptFile (FLevelLocals *Level, const char *name, bool include, int type) -{ - int lumpnum = Wads.CheckNumForName (name); - const bool found = lumpnum >= 0 - || (lumpnum = Wads.CheckNumForFullName (name)) >= 0; - - if (!found) - { - if (type == 0) - { - Printf(TEXTCOLOR_RED "Could not find dialog file %s\n", name); - } - - return false; - } - FileReader lump = Wads.ReopenLumpReader (lumpnum); - - auto fn = Wads.GetLumpFile(lumpnum); - auto wadname = Wads.GetWadName(fn); - if (stricmp(wadname, "STRIFE0.WAD") && stricmp(wadname, "STRIFE1.WAD") && stricmp(wadname, "SVE.WAD")) name = nullptr; // Only localize IWAD content. - if (name && !stricmp(name, "SCRIPT00")) name = nullptr; // This only contains random string references which already use the string table. - - bool res = LoadScriptFile(Level, name, lumpnum, lump, Wads.LumpLength(lumpnum), include, type); - return res; -} - -static bool LoadScriptFile(FLevelLocals *Level, const char *name, int lumpnum, FileReader &lump, int numnodes, bool include, int type) -{ - int i; - uint32_t prevSpeakerType; - FStrifeDialogueNode *node; - char buffer[4]; - - lump.Read(buffer, 4); - lump.Seek(-4, FileReader::SeekCur); - - // The binary format is so primitive that this check is enough to detect it. - bool isbinary = (buffer[0] == 0 || buffer[1] == 0 || buffer[2] == 0 || buffer[3] == 0); - - if ((type == 1 && !isbinary) || (type == 2 && isbinary)) - { - DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum)); - return false; - } - - if (!isbinary) - { - P_ParseUSDF(Level, lumpnum, lump, numnodes); - } - else - { - if (!include) - { - LoadScriptFile(Level, "SCRIPT00", true, 1); - } - 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) - { - DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum)); - return false; - } - numnodes /= 1516; - } - else - { - // And the teaser version has 1488-byte entries. - if (numnodes % 1488 != 0) - { - DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum)); - return false; - } - numnodes /= 1488; - } - - prevSpeakerType = 0; - - for (i = 0; i < numnodes; ++i) - { - if (!(gameinfo.flags & GI_SHAREWARE)) - { - node = ReadRetailNode (Level, name, lump, prevSpeakerType); - } - else - { - node = ReadTeaserNode (Level, name, lump, prevSpeakerType); - } - node->ThisNodeNum = Level->StrifeDialogues.Push(node); - } - } - return true; -} - -//============================================================================ -// -// ReadRetailNode -// -// Converts a single dialogue node from the Retail version of Strife. -// -//============================================================================ - -static FString TokenFromString(const char *speech) -{ - FString token = speech; - token.ToUpper(); - token.ReplaceChars(".,-+!?'", ' '); - token.Substitute(" ", ""); - token.Truncate(5); - return token; -} - -static FStrifeDialogueNode *ReadRetailNode (FLevelLocals *Level, const char *name, FileReader &lump, uint32_t &prevSpeakerType) -{ - FStrifeDialogueNode *node; - Speech speech; - char fullsound[16]; - PClassActor *type; - int j; - - node = new FStrifeDialogueNode; - - auto pos = lump.Tell(); - 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 ((signed)(speech.SpeakerType) >= 0 && prevSpeakerType != speech.SpeakerType) - { - if (type != NULL) - { - Level->ClassRoots[type->TypeName] = Level->StrifeDialogues.Size(); - } - Level->DialogueRoots[speech.SpeakerType] = Level->StrifeDialogues.Size(); - prevSpeakerType = speech.SpeakerType; - } - - // Convert the rest of the data to our own internal format. - - if (name) - { - FStringf label("$TXT_DLG_%s_d%d_%s", name, int(pos), TokenFromString(speech.Dialogue).GetChars()); - node->Dialogue = GStrings.exists(label.GetChars()+1)? label : FString(speech.Dialogue); - } - else - { - node->Dialogue = speech.Dialogue; - } - - // The speaker's portrait, if any. - speech.Dialogue[0] = 0; //speech.Backdrop[8] = 0; - node->Backdrop = speech.Backdrop; - - // The speaker's voice for this node, if any. - speech.Backdrop[0] = 0; //speech.Sound[8] = 0; - mysnprintf (fullsound, countof(fullsound), "svox/%s", speech.Sound); - node->SpeakerVoice = fullsound; - - // The speaker's name, if any. - speech.Sound[0] = 0; //speech.Name[16] = 0; - if (name && speech.Name[0]) - { - FString label = speech.Name; - label.ReplaceChars(' ', '_'); - label.ReplaceChars('\'', '_'); - node->SpeakerName.Format("$TXT_SPEAKER_%s", label.GetChars()); - if (!GStrings.exists(node->SpeakerName.GetChars() + 1)) node->SpeakerName = speech.Name; - - } - else - { - node->SpeakerName = 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. - node->ItemCheck.Resize(3); - for (j = 0; j < 3; ++j) - { - auto inv = GetStrifeType(speech.ItemCheck[j]); - if (!inv->IsDescendantOf(NAME_Inventory)) inv = nullptr; - node->ItemCheck[j].Item = inv; - node->ItemCheck[j].Amount = -1; - } - node->ItemCheckNode = speech.Link; - node->Children = NULL; - - ParseReplies (name, int(pos), &node->Children, &speech.Responses[0]); - - return node; -} - -//============================================================================ -// -// ReadTeaserNode -// -// Converts a single dialogue node from the Teaser version of Strife. -// -//============================================================================ - -static FStrifeDialogueNode *ReadTeaserNode (FLevelLocals *Level, const char *name, FileReader &lump, uint32_t &prevSpeakerType) -{ - FStrifeDialogueNode *node; - TeaserSpeech speech; - char fullsound[16]; - PClassActor *type; - int j; - - node = new FStrifeDialogueNode; - - auto pos = lump.Tell() * 1516 / 1488; - 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 ((signed)speech.SpeakerType >= 0 && prevSpeakerType != speech.SpeakerType) - { - if (type != NULL) - { - Level->ClassRoots[type->TypeName] = Level->StrifeDialogues.Size(); - } - Level->DialogueRoots[speech.SpeakerType] = Level->StrifeDialogues.Size(); - prevSpeakerType = speech.SpeakerType; - } - - // Convert the rest of the data to our own internal format. - if (name) - { - FStringf label("$TXT_DLG_%s_d%d_%s", name, pos, TokenFromString(speech.Dialogue).GetChars()); - node->Dialogue = GStrings.exists(label.GetChars() + 1)? label : FString(speech.Dialogue); - } - else - { - node->Dialogue = speech.Dialogue; - } - - // The Teaser version doesn't have portraits. - node->Backdrop = ""; - - // The speaker's voice for this node, if any. - if (speech.VoiceNumber != 0) - { - mysnprintf (fullsound, countof(fullsound), "svox/voc%u", speech.VoiceNumber); - node->SpeakerVoice = fullsound; - } - else - { - node->SpeakerVoice = 0; - } - - // The speaker's name, if any. - speech.Dialogue[0] = 0; //speech.Name[16] = 0; - if (name && speech.Name[0]) - { - FString label = speech.Name; - label.ReplaceChars(' ', '_'); - label.ReplaceChars('\'', '_'); - node->SpeakerName.Format("$TXT_SPEAKER_%s", label.GetChars()); - if (!GStrings.exists(node->SpeakerName.GetChars() + 1)) node->SpeakerName = speech.Name; - } - else - { - node->SpeakerName = 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. - node->ItemCheck.Resize(3); - for (j = 0; j < 3; ++j) - { - node->ItemCheck[j].Item = NULL; - node->ItemCheck[j].Amount = -1; - } - node->ItemCheckNode = 0; - node->Children = NULL; - - ParseReplies (name, int(pos), &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 (const char *name, int pos, 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; - if (reply->NextNode < 0) - { - reply->NextNode *= -1; - reply->CloseDialog = false; - } - - // The message to record in the log for this reply. - reply->LogNumber = rsp->Log; - reply->LogString = ""; - - // The item to receive when this reply is used. - reply->GiveType = GetStrifeType (rsp->GiveType); - reply->ActionSpecial = 0; - - // Do you need anything special for this reply to succeed? - reply->ItemCheck.Resize(3); - for (k = 0; k < 3; ++k) - { - auto inv = GetStrifeType(rsp->Item[k]); - if (!inv->IsDescendantOf(NAME_Inventory)) inv = nullptr; - reply->ItemCheck[k].Item = inv; - reply->ItemCheck[k].Amount = rsp->Count[k]; - } - reply->PrintAmount = reply->ItemCheck[0].Amount; - reply->ItemCheckRequire.Clear(); - reply->ItemCheckExclude.Clear(); - - if (name) - { - FStringf label("$TXT_RPLY%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->Reply).GetChars()); - reply->Reply = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->Reply); - - reply->Reply = label; - } - else - { - reply->Reply = rsp->Reply; - } - - - // If the first item check has a positive amount required, then - // add that to the reply string. Otherwise, use the reply as-is. - reply->NeedsGold = (rsp->Count[0] > 0); - - // QuickYes messages are shown when you meet the item checks. - // QuickNo messages are shown when you don't. - // Note that empty nodes contain a '_' in retail Strife, a '.' in the teasers and an empty string in SVE. - if (((rsp->Yes[0] == '_' || rsp->Yes[0] == '.') && rsp->Yes[1] == 0) || rsp->Yes[0] == 0) - { - reply->QuickYes = ""; - } - else - { - if (name) - { - FStringf label("$TXT_RYES%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->Yes).GetChars()); - reply->QuickYes = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->Yes); - } - else - { - reply->QuickYes = rsp->Yes; - } - } - if (reply->ItemCheck[0].Item != 0) - { - FStringf label("$TXT_RNO%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->No).GetChars()); - reply->QuickNo = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->No); - } - else - { - reply->QuickNo = ""; - } - reply->Next = *replyptr; - *replyptr = reply; - replyptr = &reply->Next; - } -} - //============================================================================ // // FStrifeDialogueNode :: ~FStrifeDialogueNode diff --git a/src/p_conversation.h b/src/p_conversation.h index d102e59f7..b9f18a24c 100644 --- a/src/p_conversation.h +++ b/src/p_conversation.h @@ -65,9 +65,6 @@ struct MapData; PClassActor *GetStrifeType (int typenum); -bool LoadScriptFile (FLevelLocals *Level, const char *name, bool include, int type = 0); - -void P_LoadStrifeConversations (FLevelLocals *Level, MapData *map, const char *mapname); void P_FreeStrifeConversations (); void P_StartConversation (AActor *npc, AActor *pc, bool facetalker, bool saveangle); @@ -76,7 +73,6 @@ void P_ResumeConversation (); void P_ConversationCommand (int netcode, int player, uint8_t **stream); class FileReader; -bool P_ParseUSDF(FLevelLocals *Level, int lumpnum, FileReader &lump, int lumplen); #endif