// // p_usdf.cpp // // USDF dialogue parser // //--------------------------------------------------------------------------- // Copyright (c) 2010 // Braden "Blzut3" Obrzut // 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: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * 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. // * Neither the name of the nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 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 "p_setup.h" #include "p_lnspec.h" #include "templates.h" #include "i_system.h" #include "p_conversation.h" #include "p_udmf.h" #include "doomerrors.h" #include "cmdlib.h" #include "actor.h" #include "a_pickups.h" #include "w_wad.h" #define Zd 1 #define St 2 #define Gz 4 class USDFParser : public UDMFParserBase { //=========================================================================== // // Checks an actor type (different representation depending on namespace) // //=========================================================================== PClassActor *CheckActorType(const char *key) { PClassActor *type = nullptr; if (namespace_bits == St) { type = GetStrifeType(CheckInt(key)); } else if (namespace_bits & ( Zd | Gz )) { PClassActor *cls = PClass::FindActor(CheckString(key)); if (cls == nullptr) { sc.ScriptMessage("Unknown actor class '%s'", key); return nullptr; } type = cls; } return type; } PClassActor *CheckInventoryActorType(const char *key) { PClassActor* const type = CheckActorType(key); return nullptr != type && type->IsDescendantOf(NAME_Inventory) ? type : nullptr; } //=========================================================================== // // Parse a cost/require/exclude block // //=========================================================================== bool ParseCostRequireExclude(FStrifeDialogueReply *response, FName type) { FStrifeDialogueItemCheck check; check.Item = NULL; check.Amount = -1; while (!sc.CheckToken('}')) { FName key = ParseKey(); switch(key) { case NAME_Item: check.Item = CheckInventoryActorType(key); break; case NAME_Amount: check.Amount = CheckInt(key); break; } } switch (type) { case NAME_Cost: response->ItemCheck.Push(check); break; case NAME_Require: response->ItemCheckRequire.Push(check); break; case NAME_Exclude: response->ItemCheckExclude.Push(check); break; } return true; } //=========================================================================== // // Parse a choice block // //=========================================================================== bool ParseChoice(FStrifeDialogueReply **&replyptr) { FStrifeDialogueReply *reply = new FStrifeDialogueReply; reply->Next = *replyptr; *replyptr = reply; replyptr = &reply->Next; FString ReplyString; FString QuickYes; FString QuickNo; FString LogString; bool closeDialog = false; reply->NeedsGold = false; while (!sc.CheckToken('}')) { bool block = false; FName key = ParseKey(true, &block); if (!block) { switch(key) { case NAME_Text: ReplyString = CheckString(key); break; case NAME_Displaycost: reply->NeedsGold = CheckBool(key); break; case NAME_Yesmessage: QuickYes = CheckString(key); //if (!QuickYes.Compare("_")) QuickYes = ""; break; case NAME_Nomessage: QuickNo = CheckString(key); break; case NAME_Log: if (namespace_bits == St) { const char *s = CheckString(key); if(strlen(s) < 4 || strnicmp(s, "LOG", 3) != 0) { sc.ScriptMessage("Log must be in the format of LOG# to compile, ignoring."); } else { reply->LogNumber = atoi(s + 3); } } else { LogString = CheckString(key); } break; case NAME_Giveitem: reply->GiveType = CheckActorType(key); break; case NAME_Nextpage: if (namespace_bits != Gz || sc.TokenType == TK_IntConst) reply->NextNode = CheckInt(key); else reply->NextNodeName = CheckString(key); break; case NAME_Closedialog: closeDialog = CheckBool(key); break; case NAME_Special: reply->ActionSpecial = CheckInt(key); if (reply->ActionSpecial < 0) reply->ActionSpecial = 0; break; case NAME_SpecialName: if (namespace_bits & ( Zd | Gz )) reply->ActionSpecial = P_FindLineSpecial(CheckString(key)); break; case NAME_Arg0: case NAME_Arg1: case NAME_Arg2: case NAME_Arg3: case NAME_Arg4: reply->Args[int(key)-int(NAME_Arg0)] = CheckInt(key); break; } } else { switch(key) { case NAME_Cost: case NAME_Require: case NAME_Exclude: // Require and Exclude are exclusive to namespace ZDoom. [FishyClockwork] if (key == NAME_Cost || (namespace_bits & ( Zd | Gz ))) { ParseCostRequireExclude(reply, key); break; } // Intentional fall-through default: sc.UnGet(); Skip(); } } } // Todo: Finalize if (reply->ItemCheck.Size() > 0) { reply->PrintAmount = reply->ItemCheck[0].Amount; if (reply->PrintAmount <= 0) reply->NeedsGold = false; } reply->Reply = ReplyString; reply->QuickYes = QuickYes; if (reply->ItemCheck.Size() > 0 && reply->ItemCheck[0].Item != NULL) { reply->QuickNo = QuickNo; } else { reply->QuickNo = ""; } reply->LogString = LogString; if (reply->NextNode < 0) // compatibility: handle negative numbers { reply->CloseDialog = !closeDialog; reply->NextNode *= -1; } else { reply->CloseDialog = closeDialog; } return true; } //=========================================================================== // // Parse an ifitem block // //=========================================================================== bool ParseIfItem(FStrifeDialogueNode *node) { FStrifeDialogueItemCheck check; check.Item = NULL; check.Amount = -1; while (!sc.CheckToken('}')) { FName key = ParseKey(); switch(key) { case NAME_Item: check.Item = CheckInventoryActorType(key); break; case NAME_Count: // Not yet implemented in the engine. Todo later check.Amount = CheckInt(key); break; } } node->ItemCheck.Push(check); return true; } //=========================================================================== // // Parse a page block // //=========================================================================== bool ParsePage() { FStrifeDialogueNode *node = new FStrifeDialogueNode; FStrifeDialogueReply **replyptr = &node->Children; node->ThisNodeNum = StrifeDialogues.Push(node); node->ItemCheckNode = -1; FString SpeakerName; FString Dialogue; FString Goodbye; while (!sc.CheckToken('}')) { bool block = false; FName key = ParseKey(true, &block); if (!block) { switch(key) { case NAME_Pagename: if (namespace_bits != Gz) sc.ScriptMessage("'PageName' keyword only supported in the GZDoom namespace!"); else node->ThisNodeName = CheckString(key); break; case NAME_Name: SpeakerName = CheckString(key); break; case NAME_Panel: node->Backdrop = CheckString(key); break; case NAME_Userstring: if (namespace_bits & ( Zd | Gz )) { node->UserData = CheckString(key); } break; case NAME_Voice: { const char * name = CheckString(key); if (name[0] != 0) { FString soundname = "svox/"; soundname += name; node->SpeakerVoice = FSoundID(S_FindSound(soundname)); if (node->SpeakerVoice == 0 && (namespace_bits & ( Zd | Gz ))) { node->SpeakerVoice = FSoundID(S_FindSound(name)); } } } break; case NAME_Dialog: Dialogue = CheckString(key); break; case NAME_Drop: node->DropType = CheckActorType(key); break; case NAME_Link: if (namespace_bits != Gz || sc.TokenType == TK_IntConst) node->ItemCheckNode = CheckInt(key); else node->ItemCheckNodeName = CheckString(key); break; case NAME_Goodbye: // Custom goodbyes are exclusive to namespace ZDoom. [FishyClockwork] if (namespace_bits & ( Zd | Gz )) { Goodbye = CheckString(key); } break; } } else { switch(key) { case NAME_Ifitem: if (!ParseIfItem(node)) return false; break; case NAME_Choice: if (!ParseChoice(replyptr)) return false; break; default: sc.UnGet(); Skip(); } } } node->SpeakerName = SpeakerName; node->Dialogue = Dialogue; node->Goodbye = Goodbye; return true; } //=========================================================================== // // Parse a conversation block // //=========================================================================== bool ParseConversation() { PClassActor *type = nullptr; int dlgid = -1; FName clsid = NAME_None; unsigned int startpos = StrifeDialogues.Size(); while (!sc.CheckToken('}')) { bool block = false; FName key = ParseKey(true, &block); if (!block) { switch(key) { case NAME_Actor: type = CheckActorType(key); if (namespace_bits == St) { dlgid = CheckInt(key); } break; case NAME_Id: if (namespace_bits & ( Zd | Gz )) { dlgid = CheckInt(key); } break; case NAME_Class: if (namespace_bits & ( Zd | Gz )) { clsid = CheckString(key); } break; } } else { switch(key) { case NAME_Page: if (!ParsePage()) return false; break; default: sc.UnGet(); Skip(); } } } if (type == NULL && dlgid == 0) { sc.ScriptMessage("No valid actor type defined in conversation."); return false; } SetConversation(dlgid, type, startpos); auto& dialogues = StrifeDialogues; const auto numnodes = dialogues.Size(); for (auto i = startpos; i < numnodes; i++) { dialogues[i]->SpeakerType = type; dialogues[i]->MenuClassName = clsid; } if (namespace_bits == Gz) // string page name linker { TMap nameToIndex; for (auto i = startpos; i < numnodes; i++) { FString key = dialogues[i]->ThisNodeName; if (key.IsNotEmpty()) { key.ToLower(); if (nameToIndex.CheckKey(key)) Printf("Warning! Duplicate page name '%s'!\n", dialogues[i]->ThisNodeName.GetChars()); else { nameToIndex[key] = i - startpos; DPrintf(DMSG_NOTIFY, "GZSDF linker: Assigning pagename '%s' to node %i\n", key.GetChars(), i); } } } if (nameToIndex.CountUsed()) { for (auto i = startpos; i < numnodes; i++) { FString itemLinkKey = dialogues[i]->ItemCheckNodeName; if (itemLinkKey.IsNotEmpty()) { itemLinkKey.ToLower(); if (nameToIndex.CheckKey(itemLinkKey)) { dialogues[i]->ItemCheckNode = nameToIndex[itemLinkKey] + 1; DPrintf(DMSG_NOTIFY, "GZSDF linker: Item Link '%s' in node %i was index %i\n", itemLinkKey.GetChars(), i, nameToIndex[itemLinkKey]); } else Printf("Warning! Reference to non-existent item-linked dialogue page name '%s' in page %i!\n", dialogues[i]->ItemCheckNodeName.GetChars(), i); } FStrifeDialogueReply *NodeCheck = dialogues[i]->Children; while (NodeCheck) { if (NodeCheck->NextNodeName.IsNotEmpty()) { FString key = NodeCheck->NextNodeName; key.ToLower(); if (nameToIndex.CheckKey(key)) { NodeCheck->NextNode = nameToIndex[key] + 1; DPrintf(DMSG_NOTIFY, "GZSDF linker: Nextpage Link '%s' in node %i was index %i\n", key.GetChars(), i, nameToIndex[key]); } else Printf("Warning! Reference to non-existent reply-linked dialogue page name '%s' in page %i!\n", NodeCheck->NextNodeName.GetChars(), i); } NodeCheck = NodeCheck->Next; } } } } return true; } //=========================================================================== // // Parse an USDF lump // //=========================================================================== public: bool Parse(int lumpnum, FileReader &lump, int lumplen) { sc.OpenMem(Wads.GetLumpFullName(lumpnum), lump.Read(lumplen)); sc.SetCMode(true); // Namespace must be the first field because everything else depends on it. if (sc.CheckString("namespace")) { sc.MustGetToken('='); sc.MustGetToken(TK_StringConst); namespc = sc.String; switch(namespc) { case NAME_GZDoom: namespace_bits = Gz; break; case NAME_ZDoom: namespace_bits = Zd; break; case NAME_Strife: namespace_bits = St; break; default: sc.ScriptMessage("Unknown namespace %s. Ignoring dialogue lump.\n", sc.String); return false; } sc.MustGetToken(';'); } else { sc.ScriptMessage("Dialog script does not define a namespace.\n"); return false; } while (sc.GetString()) { if (sc.Compare("conversation")) { sc.MustGetToken('{'); if (!ParseConversation()) return false; } else if (sc.Compare("include")) { sc.MustGetToken('='); sc.MustGetToken(TK_StringConst); LoadScriptFile(sc.String, true); sc.MustGetToken(';'); } else { Skip(); } } return true; } }; bool P_ParseUSDF(int lumpnum, FileReader &lump, int lumplen) { USDFParser parse; try { if (!parse.Parse(lumpnum, lump, lumplen)) { // clean up the incomplete dialogue structures here return false; } return true; } catch(CRecoverableError &err) { Printf("%s", err.GetMessage()); return false; } }