From c1fa65a01d351c71c16cbba2eb3c5bc1b7c7c76d Mon Sep 17 00:00:00 2001 From: Celtic Minstrel Date: Mon, 29 Jun 2015 19:55:59 -0400 Subject: [PATCH] Much better error-checking when loading scenarios - First of all, the program no longer crashes after an error. - A lot more constraints are now checked for, such as required elements; even some constraints not expressed in the schema - Special node parsing now has actually useful error messages - Map data parsing now has error messages for the few invalid cases - Fix the XML utility exception classes originally used only by the dialog engine; they're now correctly caught and properly report the error - Fix loading of special shop entries - Fix accepting any character as the separator in dice constants - Verify that the monster ability type is on the right ability tag, that the extra element is present only if needed for general abilities, and that the missile and range elements are present for non-touch general abilities - Use "infinite" for quantity in special shop items, instead of 0 - Tweak ticpp to fill out file/line/col information for _all_ exceptions, not just parse errors - Raise error (ie, set stream failbit) when failing to convert an enumerator, instead of silently returning a default value (they do still return a default value though, if you don't bother to check stream state) - Fix status effect enumerator not being correctly saved/loaded --- rsrc/schemas/monsters.xsd | 3 +- rsrc/schemas/scenario.xsd | 4 +- src/classes/estreams.cpp | 125 ++++++++++------ src/dialogxml/dialog.cpp | 49 +++++-- src/dialogxml/dialog.hpp | 42 ++++-- src/dialogxml/xml-parser/ticpp.h | 6 + src/scenedit/scen.actions.cpp | 4 +- src/scenedit/scen.fileio.cpp | 4 +- src/tools/fileio_scen.cpp | 237 +++++++++++++++++++++++++++---- src/tools/map_parse.cpp | 68 +++++++-- src/tools/map_parse.hpp | 23 ++- src/tools/special_parse.hpp | 28 +++- src/tools/specials_parse.cpp | 127 ++++++++++++++--- 13 files changed, 579 insertions(+), 141 deletions(-) diff --git a/rsrc/schemas/monsters.xsd b/rsrc/schemas/monsters.xsd index d5cce5f8..2a75ef48 100644 --- a/rsrc/schemas/monsters.xsd +++ b/rsrc/schemas/monsters.xsd @@ -70,6 +70,7 @@ + @@ -105,7 +106,6 @@ - @@ -231,7 +231,6 @@ - diff --git a/rsrc/schemas/scenario.xsd b/rsrc/schemas/scenario.xsd index 81a079e0..40c56b6e 100644 --- a/rsrc/schemas/scenario.xsd +++ b/rsrc/schemas/scenario.xsd @@ -167,7 +167,7 @@ - + @@ -181,7 +181,7 @@ - + diff --git a/src/classes/estreams.cpp b/src/classes/estreams.cpp index 370f0b0e..008c13c0 100644 --- a/src/classes/estreams.cpp +++ b/src/classes/estreams.cpp @@ -20,10 +20,10 @@ class cEnumLookup { struct node { char c; mutable boost::ptr_set next; - mutable boost::optional value; + mutable boost::optional value; node(char c) : c(c) {} - node(char c, unsigned long v) : c(c), value(v) {} - bool find(unsigned long val, std::string& result) const { + node(char c, long long v) : c(c), value(v) {} + bool find(long long val, std::string& result) const { if(value && *value == val) { if(c >= ' ') result.push_back(c); return true; @@ -52,7 +52,7 @@ public: i++; } } - void insert(const std::string& str, unsigned long val) { + void insert(const std::string& str, long long val) { size_t i = 0; const node* cur = &root; while(i < str.size()) { @@ -67,7 +67,7 @@ public: cur->value = val; size_cached = false; } - unsigned long get(const std::string& str, unsigned long def = 0) const { + unsigned long get(const std::string& str, long long def = 0) const { size_t i = 0; const node* cur = &root; while(i < str.size()) { @@ -81,15 +81,14 @@ public: return *cur->value; return def; } - std::string find(unsigned long val, std::string def = "") const { + std::string find(long long val, std::string def = "") const { std::string result; if(root.find(val,result)) { - std::reverse(result.begin(), result.end()); - return result; + return std::string(result.rbegin(), result.rend()); } return def; } - bool contains(unsigned long val) const { + bool contains(long long val) const { if(find(val).empty()) return false; return true; } @@ -114,20 +113,22 @@ template void writeEnum(std::ostream& out, E val, cEnumLookup& tbl, out << tbl.find(int(val), def); } -template void readEnum(std::istream& in, E& to, cEnumLookup& tbl, E def = E()) { +template bool readEnum(std::istream& in, E& to, cEnumLookup& tbl, E def = E()) { std::string key; in >> key; - bool have_num = true; - for(char c : key) { - if(!isdigit(c)) { - have_num = false; - break; - } - } + if(in.fail() || key.empty()) return false; + bool have_num = (isdigit(key[0]) || key[0] == '-') && std::all_of(key.begin() + 1, key.end(), isdigit); + bool valid = true; if(have_num) { int n = boost::lexical_cast(key); - to = tbl.contains(n) ? E(n) : def; - } else to = E(tbl.get(key, int(def))); + bool valid = tbl.contains(n); + to = valid ? E(n) : def; + } else { + to = E(tbl.get(key, int(def))); + if(to == def && key != tbl.find(int(def))) + valid = false; + } + return valid; } // These operators have their prototypes declared all over the place, but I'm not including those headers, so silence the warnings @@ -148,7 +149,8 @@ std::ostream& operator << (std::ostream& out, eSkill e) { } std::istream& operator >> (std::istream& in, eSkill& e){ - readEnum(in, e, skill_names, eSkill::EDGED_WEAPONS); + if(!readEnum(in, e, skill_names, eSkill::EDGED_WEAPONS)) + in.setstate(std::ios::failbit); return in; } @@ -166,7 +168,8 @@ std::ostream& operator << (std::ostream& out, eItemType e) { } std::istream& operator >> (std::istream& in, eItemType& e) { - readEnum(in, e, item_types, eItemType::NO_ITEM); + if(!readEnum(in, e, item_types, eItemType::NO_ITEM)) + in.setstate(std::ios::failbit); return in; } @@ -180,7 +183,8 @@ std::ostream& operator << (std::ostream& out, eItemUse e) { } std::istream& operator >> (std::istream& in, eItemUse& e){ - readEnum(in, e, item_uses, eItemUse::HELP_ONE); + if(!readEnum(in, e, item_uses, eItemUse::HELP_ONE)) + in.setstate(std::ios::failbit); return in; } @@ -209,24 +213,32 @@ std::ostream& operator << (std::ostream& out, eItemAbil e) { } std::istream& operator >> (std::istream& in, eItemAbil& e){ - readEnum(in, e, item_abils, eItemAbil::NONE); + if(!readEnum(in, e, item_abils, eItemAbil::NONE)) + in.setstate(std::ios::failbit); return in; } // MARK: eStatus cEnumLookup pc_status = { - "poison-weap", "bless-curse", "poison", "haste-slow", "magic", "web", "disease", "invis", "dumb-smart", + "poison-weap", "bless-curse", "poison", "haste-slow", "invuln", "magic", "web", "disease", "invis", "dumb-smart", "martyr", "sleep", "paralysis", "acid", "cage", "charm", }; +struct finish_status_init { + finish_status_init() { + pc_status.insert("main", -1); + } +} finish_status_init; + std::ostream& operator << (std::ostream& out, eStatus e){ writeEnum(out, e, pc_status, "main"); return out; } std::istream& operator >> (std::istream& in, eStatus& e){ - readEnum(in, e, pc_status, eStatus::MAIN); + if(!readEnum(in, e, pc_status, eStatus::MAIN)) + in.setstate(std::ios::failbit); return in; } @@ -244,7 +256,8 @@ std::ostream& operator << (std::ostream& out, eRace e){ } std::istream& operator >> (std::istream& in, eRace& e){ - readEnum(in, e, race_names, eRace::HUMANOID); + if(!readEnum(in, e, race_names, eRace::HUMANOID)) + in.setstate(std::ios::failbit); return in; } @@ -262,7 +275,8 @@ std::ostream& operator << (std::ostream& out, eMonstTime e){ } std::istream& operator >> (std::istream& in, eMonstTime& e){ - readEnum(in, e, monst_times, eMonstTime::ALWAYS); + if(!readEnum(in, e, monst_times, eMonstTime::ALWAYS)) + in.setstate(std::ios::failbit); return in; } @@ -276,7 +290,8 @@ std::ostream& operator<<(std::ostream& out, eDirection dir) { } std::istream& operator>>(std::istream& in, eDirection& dir) { - readEnum(in, dir, dirs, DIR_HERE); + if(!readEnum(in, dir, dirs, DIR_HERE)) + in.setstate(std::ios::failbit); return in; } @@ -296,7 +311,8 @@ std::ostream& operator << (std::ostream& out, eFieldType e) { } std::istream& operator >> (std::istream& in, eFieldType& e) { - readEnum(in, e, field_names, FIELD_DISPEL); + if(!readEnum(in, e, field_names, FIELD_DISPEL)) + in.setstate(std::ios::failbit); return in; } @@ -312,7 +328,8 @@ std::ostream& operator << (std::ostream& out, eDamageType e) { } std::istream& operator >> (std::istream& in, eDamageType& e) { - readEnum(in, e, dmg_names, eDamageType::MARKED); + if(!readEnum(in, e, dmg_names, eDamageType::MARKED)) + in.setstate(std::ios::failbit); return in; } @@ -330,7 +347,8 @@ std::ostream& operator << (std::ostream& out, eMonstAbil e) { } std::istream& operator >> (std::istream& in, eMonstAbil& e) { - readEnum(in, e, monst_abils, eMonstAbil::NO_ABIL); + if(!readEnum(in, e, monst_abils, eMonstAbil::NO_ABIL)) + in.setstate(std::ios::failbit); return in; } @@ -344,7 +362,8 @@ std::ostream& operator << (std::ostream& out, eMonstGen e) { } std::istream& operator >> (std::istream& in, eMonstGen& e) { - readEnum(in, e, monst_abil_types, eMonstGen::TOUCH); + if(!readEnum(in, e, monst_abil_types, eMonstGen::TOUCH)) + in.setstate(std::ios::failbit); return in; } @@ -358,7 +377,8 @@ std::ostream& operator << (std::ostream& out, eMonstMelee e) { } std::istream& operator >> (std::istream& in, eMonstMelee& e) { - readEnum(in, e, monst_melee, eMonstMelee::PUNCH); + if(!readEnum(in, e, monst_melee, eMonstMelee::PUNCH)) + in.setstate(std::ios::failbit); return in; } @@ -372,7 +392,8 @@ std::ostream& operator << (std::ostream& out, eMonstMissile e) { } std::istream& operator >> (std::istream& in, eMonstMissile& e) { - readEnum(in, e, monst_missiles, eMonstMissile::ARROW); + if(!readEnum(in, e, monst_missiles, eMonstMissile::ARROW)) + in.setstate(std::ios::failbit); return in; } @@ -386,7 +407,8 @@ std::ostream& operator << (std::ostream& out, eMonstSummon e) { } std::istream& operator >> (std::istream& in, eMonstSummon& e) { - readEnum(in, e, monst_summons, eMonstSummon::TYPE); + if(!readEnum(in, e, monst_summons, eMonstSummon::TYPE)) + in.setstate(std::ios::failbit); return in; } @@ -395,7 +417,8 @@ std::istream& operator >> (std::istream& in, eMonstSummon& e) { cEnumLookup note_types = {"SCEN", "OUT", "TOWN"}; std::istream& operator>>(std::istream& in, eEncNoteType& type) { - readEnum(in, type, note_types, NOTE_SCEN); + if(!readEnum(in, type, note_types, NOTE_SCEN)) + in.setstate(std::ios::failbit); return in; } @@ -409,7 +432,8 @@ std::ostream& operator<<(std::ostream& out, eEncNoteType type) { cEnumLookup party_status = {"STEALTH", "FLIGHT", "DETECT", "FIREWALK"}; std::istream& operator>>(std::istream& in, ePartyStatus& type) { - readEnum(in, type, party_status, ePartyStatus::STEALTH); + if(!readEnum(in, type, party_status, ePartyStatus::STEALTH)) + in.setstate(std::ios::failbit); return in; } @@ -423,7 +447,8 @@ std::ostream& operator<<(std::ostream& out, ePartyStatus type) { cEnumLookup quest_status = {"avail", "start", "done", "fail"}; std::istream& operator>>(std::istream& in, eQuestStatus& type) { - readEnum(in, type, quest_status, eQuestStatus::AVAILABLE); + if(!readEnum(in, type, quest_status, eQuestStatus::AVAILABLE)) + in.setstate(std::ios::failbit); return in; } @@ -445,7 +470,8 @@ std::ostream& operator << (std::ostream& out, eMainStatus e){ } std::istream& operator >> (std::istream& in, eMainStatus& e){ - readEnum(in, e, main_status, eMainStatus::ABSENT); + if(!readEnum(in, e, main_status, eMainStatus::ABSENT)) + in.setstate(std::ios::failbit); return in; } @@ -454,7 +480,8 @@ std::istream& operator >> (std::istream& in, eMainStatus& e){ cEnumLookup shop_types = {"live", "dead", "rand"}; std::istream& operator>>(std::istream& in, eShopType& type) { - readEnum(in, type, shop_types, eShopType::NORMAL); + if(!readEnum(in, type, shop_types, eShopType::NORMAL)) + in.setstate(std::ios::failbit); return in; } @@ -468,7 +495,8 @@ std::ostream& operator<<(std::ostream& out, eShopType type) { cEnumLookup shop_prompts = {"shop", "heal", "mage", "priest", "spell", "alch", "train"}; std::istream& operator>>(std::istream& in, eShopPrompt& type) { - readEnum(in, type, shop_prompts, eShopPrompt::SHOPPING); + if(!readEnum(in, type, shop_prompts, eShopPrompt::SHOPPING)) + in.setstate(std::ios::failbit); return in; } @@ -491,7 +519,8 @@ std::ostream& operator << (std::ostream& out, eTerSpec e){ } std::istream& operator >> (std::istream& in, eTerSpec& e){ - readEnum(in, e, ter_types, eTerSpec::NONE); + if(!readEnum(in, e, ter_types, eTerSpec::NONE)) + in.setstate(std::ios::failbit); return in; } @@ -508,7 +537,8 @@ std::ostream& operator << (std::ostream& out, eTrimType e){ } std::istream& operator >> (std::istream& in, eTrimType& e){ - readEnum(in, e, ter_trims, eTrimType::NONE); + if(!readEnum(in, e, ter_trims, eTrimType::NONE)) + in.setstate(std::ios::failbit); return in; } @@ -522,7 +552,8 @@ std::ostream& operator<< (std::ostream& out, eTerObstruct block) { } std::istream& operator >> (std::istream& in, eTerObstruct& e){ - readEnum(in, e, ter_blocks, eTerObstruct::CLEAR); + if(!readEnum(in, e, ter_blocks, eTerObstruct::CLEAR)) + in.setstate(std::ios::failbit); return in; } @@ -536,7 +567,8 @@ std::ostream& operator<< (std::ostream& out, eStepSnd snd) { } std::istream& operator >> (std::istream& in, eStepSnd& e){ - readEnum(in, e, step_snds, eStepSnd::STEP); + if(!readEnum(in, e, step_snds, eStepSnd::STEP)) + in.setstate(std::ios::failbit); return in; } @@ -550,6 +582,7 @@ std::ostream& operator<< (std::ostream& out, eLighting light) { } std::istream& operator>> (std::istream& in, eLighting& light) { - readEnum(in, light, light_types, LIGHT_NORMAL); + if(!readEnum(in, light, light_types, LIGHT_NORMAL)) + in.setstate(std::ios::failbit); return in; } diff --git a/src/dialogxml/dialog.cpp b/src/dialogxml/dialog.cpp index ccde117b..3e1cd4ce 100644 --- a/src/dialogxml/dialog.cpp +++ b/src/dialogxml/dialog.cpp @@ -1421,21 +1421,21 @@ xBadNode::xBadNode(std::string t, int r, int c, std::string dlg) throw() : msg(NULL), dlg(dlg) {} -const char* xBadNode::what() throw() { +const char* xBadNode::what() const throw() { if(msg == NULL){ char* s = new (nothrow) char[200]; if(s == NULL){ std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl; abort(); } - sprintf(s,"XML Parse Error: Unknown element %s encountered (dialog %s, line %d, column %d).",type.c_str(),dlg.c_str(),row,col); + sprintf(s,"XML Parse Error: Unknown element %s encountered (file %s, line %d, column %d).",type.c_str(),dlg.c_str(),row,col); msg = s; } return msg; } xBadNode::~xBadNode() throw(){ - if(msg != NULL) delete msg; + if(msg != NULL) delete[] msg; } xBadAttr::xBadAttr(std::string t, std::string n, int r, int c, std::string dlg) throw() : @@ -1446,21 +1446,21 @@ xBadAttr::xBadAttr(std::string t, std::string n, int r, int c, std::string dlg) msg(NULL), dlg(dlg) {} -const char* xBadAttr::what() throw() { +const char* xBadAttr::what() const throw() { if(msg == NULL){ char* s = new (nothrow) char[200]; if(s == NULL){ std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl; abort(); } - sprintf(s,"XML Parse Error: Unknown attribute %s encountered on element %s (dialog %s, line %d, column %d).",name.c_str(),type.c_str(),dlg.c_str(),row,col); + sprintf(s,"XML Parse Error: Unknown attribute %s encountered on element %s (file %s, line %d, column %d).",name.c_str(),type.c_str(),dlg.c_str(),row,col); msg = s; } return msg; } xBadAttr::~xBadAttr() throw(){ - if(msg != NULL) delete msg; + if(msg != NULL) delete[] msg; } xMissingAttr::xMissingAttr(std::string t, std::string n, int r, int c, std::string dlg) throw() : @@ -1471,21 +1471,46 @@ xMissingAttr::xMissingAttr(std::string t, std::string n, int r, int c, std::stri msg(NULL), dlg(dlg) {} -const char* xMissingAttr::what() throw() { +const char* xMissingAttr::what() const throw() { if(msg == NULL){ char* s = new (nothrow) char[200]; if(s == NULL){ std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl; abort(); } - sprintf(s,"XML Parse Error: Required attribute %s missing on element %s (dialog %s, line %d, column %d).",name.c_str(),type.c_str(),dlg.c_str(),row,col); + sprintf(s,"XML Parse Error: Required attribute %s missing on element %s (file %s, line %d, column %d).",name.c_str(),type.c_str(),dlg.c_str(),row,col); msg = s; } return msg; } xMissingAttr::~xMissingAttr() throw(){ - if(msg != NULL) delete msg; + if(msg != NULL) delete[] msg; +} + +xMissingElem::xMissingElem(std::string p, std::string t, int r, int c, std::string dlg) throw() : + parent(p), + name(t), + row(r), + col(c), + msg(NULL), + dlg(dlg) {} + +const char* xMissingElem::what() const throw() { + if(msg == NULL){ + char* s = new (nothrow) char[200]; + if(s == NULL){ + std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl; + abort(); + } + sprintf(s,"XML Parse Error: Required element %s missing in element %s (file %s, line %d, column %d).",name.c_str(),parent.c_str(),dlg.c_str(),row,col); + msg = s; + } + return msg; +} + +xMissingElem::~xMissingElem() throw(){ + if(msg != NULL) delete[] msg; } xBadVal::xBadVal(std::string t, std::string n, std::string v, int r, int c, std::string dlg) throw() : @@ -1497,21 +1522,21 @@ xBadVal::xBadVal(std::string t, std::string n, std::string v, int r, int c, std: msg(NULL), dlg(dlg) {} -const char* xBadVal::what() throw() { +const char* xBadVal::what() const throw() { if(msg == NULL){ char* s = new (nothrow) char[200]; if(s == NULL){ std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl; abort(); } - sprintf(s,"XML Parse Error: Invalid value %s for attribute %s encountered on element %s (dialog %s, line %d, column %d).",val.c_str(),name.c_str(),type.c_str(),dlg.c_str(),row,col); + sprintf(s,"XML Parse Error: Invalid value %s for attribute %s encountered on element %s (file %s, line %d, column %d).",val.c_str(),name.c_str(),type.c_str(),dlg.c_str(),row,col); msg = s; } return msg; } xBadVal::~xBadVal() throw(){ - if(msg != NULL) delete msg; + if(msg != NULL) delete[] msg; } bool cDialog::doAnimations = false; diff --git a/src/dialogxml/dialog.hpp b/src/dialogxml/dialog.hpp index 090ba863..d9cd95bf 100644 --- a/src/dialogxml/dialog.hpp +++ b/src/dialogxml/dialog.hpp @@ -204,10 +204,10 @@ private: }; /// Thrown when an invalid element is found while parsing an XML dialog definition. -class xBadNode : std::exception { +class xBadNode : public std::exception { std::string type, dlg; int row, col; - const char* msg; + mutable const char* msg; public: /// Construct a new exception. /// @param t The tag name of the invalid element. @@ -217,14 +217,14 @@ public: xBadNode(std::string t, int r, int c, std::string dlg) throw(); ~xBadNode() throw(); /// @return The error message. - const char* what() throw(); + const char* what() const throw(); }; /// Thrown when an invalid attribute is found while parsing an XML dialog definition. -class xBadAttr : std::exception { +class xBadAttr : public std::exception { std::string type, name, dlg; int row, col; - const char* msg; + mutable const char* msg; public: /// Construct a new exception. /// @param t The tag name of the element with the invalid attribute. @@ -235,14 +235,14 @@ public: xBadAttr(std::string t,std::string n, int r, int c, std::string dlg) throw(); ~xBadAttr() throw(); /// @return The error message. - const char* what() throw(); + const char* what() const throw(); }; /// Thrown when an element is missing a required attribute while parsing an XML dialog definition. -class xMissingAttr : std::exception { +class xMissingAttr : public std::exception { std::string type, name, dlg; int row, col; - const char* msg; + mutable const char* msg; public: /// Construct a new exception. /// @param t The tag name of the element with the missing attribute. @@ -253,14 +253,32 @@ public: xMissingAttr(std::string t,std::string n, int r, int c, std::string dlg) throw(); ~xMissingAttr() throw(); /// @return The error message. - const char* what() throw(); + const char* what() const throw(); +}; + +/// Thrown when a required element is missing while parsing an XML dialog definition. +class xMissingElem : public std::exception { + std::string parent, name, dlg; + int row, col; + mutable const char* msg; +public: + /// Construct a new exception. + /// @param p The tag name of the parent element. + /// @param t The tag name of the missing element. + /// @param r The line number of the element in the source. + /// @param c The column number of the element in the source. + /// @param dlg The name of the file in which the element occurred. + xMissingElem(std::string p, std::string t, int r, int c, std::string dlg) throw(); + ~xMissingElem() throw(); + /// @return The error message. + const char* what() const throw(); }; /// Thrown when an invalid value is found anywhere within an element's or attribute's content. -class xBadVal : std::exception { +class xBadVal : public std::exception { std::string type, name, val, dlg; int row, col; - const char* msg; + mutable const char* msg; public: /// A magic value to indicate errors in an element's content, rather than an attribute's content. static const char*const CONTENT; @@ -275,7 +293,7 @@ public: xBadVal(std::string t,std::string n,std::string v, int r, int c, std::string dlg) throw(); ~xBadVal() throw(); /// @return The error message. - const char* what() throw(); + const char* what() const throw(); }; #endif diff --git a/src/dialogxml/xml-parser/ticpp.h b/src/dialogxml/xml-parser/ticpp.h index c3c900c4..a0ca3714 100644 --- a/src/dialogxml/xml-parser/ticpp.h +++ b/src/dialogxml/xml-parser/ticpp.h @@ -263,6 +263,12 @@ namespace ticpp << "\nLine: " << doc->ErrorRow() << "\nColumn: " << doc->ErrorCol(); } + else + { + full_message << "\nFile: " << (strlen( doc->Value() ) > 0 ? doc->Value() : "") + << "\nLine: " << node->Row() + << "\nColumn: " << node->Column(); + } } } #endif diff --git a/src/scenedit/scen.actions.cpp b/src/scenedit/scen.actions.cpp index 9da16126..66895513 100644 --- a/src/scenedit/scen.actions.cpp +++ b/src/scenedit/scen.actions.cpp @@ -173,7 +173,9 @@ static bool handle_lb_action(location the_point) { current_terrain = scenario.outdoors[cur_out.x][cur_out.y]; overall_mode = MODE_MAIN_SCREEN; set_up_main_screen(); - } + } else if(!file_to_load.empty()) + // If we tried to load but failed, the scenario record is messed up, so boot to start screen. + set_up_start_screen(); break; case LB_EDIT_TER: start_terrain_editing(); diff --git a/src/scenedit/scen.fileio.cpp b/src/scenedit/scen.fileio.cpp index 691be8e5..8d3c80d7 100644 --- a/src/scenedit/scen.fileio.cpp +++ b/src/scenedit/scen.fileio.cpp @@ -249,7 +249,9 @@ static void writeScenarioToXml(ticpp::Printer&& data) { data.PushElement("name", entry.item.full_name); data.PushElement("description", entry.item.desc); data.PushElement("node", entry.item.item_level); - data.PushElement("quantity", entry.quantity); + if(entry.quantity == 0) + data.PushElement("quantity", "infinite"); + else data.PushElement("quantity", entry.quantity); data.PushElement("cost", entry.item.value); data.PushElement("icon", entry.item.graphic_num); data.CloseElement("special"); diff --git a/src/tools/fileio_scen.cpp b/src/tools/fileio_scen.cpp index 16d4372c..88f8975c 100644 --- a/src/tools/fileio_scen.cpp +++ b/src/tools/fileio_scen.cpp @@ -63,10 +63,15 @@ bool load_scenario(fs::path file_to_load, cScenario& scenario, bool only_header) std::string fname = file_to_load.filename().string(); std::transform(fname.begin(), fname.end(), fname.begin(), tolower); size_t dot = fname.find_last_of('.'); - if(fname.substr(dot) == ".boes") - return load_scenario_v2(file_to_load, scenario, only_header); - else if(fname.substr(dot) == ".exs") - return load_scenario_v1(file_to_load, scenario, only_header); + try { + if(fname.substr(dot) == ".boes") + return load_scenario_v2(file_to_load, scenario, only_header); + else if(fname.substr(dot) == ".exs") + return load_scenario_v1(file_to_load, scenario, only_header); + } catch(std::exception& x) { + giveError("There was an error loading the scenario. The details of the error are given below; you may be able to decompress the scenario package, fix the error, and repack it.", x.what()); + return false; + } giveError("That is not a Blades of Exile scenario."); return false; } @@ -317,20 +322,24 @@ static location readLocFromXml(ticpp::Element& data, std::string prefix = "", st data.GetDocument()->GetValue(&fname); data.GetValue(&type); location pos = {-1000, -1000}; + bool got_extra = extra_val == nullptr; for(attr = attr.begin(&data); attr != attr.end(); attr++) { attr->GetName(&name); if(name == prefix + "x") attr->GetValue(&pos.x); else if(name == prefix + "y") attr->GetValue(&pos.y); - else if(name == extra && extra_val != nullptr) + else if(name == extra && extra_val != nullptr) { attr->GetValue(extra_val); - else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); + got_extra = true; + } else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); } if(pos.x == -1000) throw xMissingAttr(type, prefix + "x", data.Row(), data.Column(), fname); if(pos.y == -1000) throw xMissingAttr(type, prefix + "y", data.Row(), data.Column(), fname); + if(!got_extra) + throw xMissingAttr(type, extra, data.Row(), data.Column(), fname); return pos; } @@ -342,6 +351,7 @@ static rectangle readRectFromXml(ticpp::Element& data, std::string prefix = "", data.GetDocument()->GetValue(&fname); data.GetValue(&type); rectangle rect = {-1000, -1000, -1000, -1000}; + bool got_extra = extra_val == nullptr; for(attr = attr.begin(&data); attr != attr.end(); attr++) { attr->GetName(&name); if(name == prefix + "top") @@ -352,9 +362,10 @@ static rectangle readRectFromXml(ticpp::Element& data, std::string prefix = "", attr->GetValue(&rect.bottom); else if(name == prefix + "right") attr->GetValue(&rect.right); - else if(name == extra && extra_val != nullptr) + else if(name == extra && extra_val != nullptr) { attr->GetValue(extra_val); - else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); + got_extra = true; + } else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); } if(rect.top == -1000) throw xMissingAttr(type, prefix + "top", data.Row(), data.Column(), fname); @@ -364,6 +375,8 @@ static rectangle readRectFromXml(ticpp::Element& data, std::string prefix = "", throw xMissingAttr(type, prefix + "bottom", data.Row(), data.Column(), fname); if(rect.right == -1000) throw xMissingAttr(type, prefix + "right", data.Row(), data.Column(), fname); + if(!got_extra) + throw xMissingAttr(type, extra, data.Row(), data.Column(), fname); return rect; } @@ -389,15 +402,19 @@ static void readSpecItemFromXml(ticpp::Element& data, cSpecItem& item) { item.flags += 1; } else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); } + std::set reqs = {"name", "description"}; Iterator elem; for(elem = elem.begin(&data); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "name") { elem->GetText(&item.name, false); } else if(type == "description") { elem->GetText(&item.descr, false); } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("special-item", *reqs.begin(), data.Row(), data.Column(), fname); } static void readQuestFromXml(ticpp::Element& data, cQuest& quest) { @@ -415,10 +432,12 @@ static void readQuestFromXml(ticpp::Element& data, cQuest& quest) { quest.flags += 10; } else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); } + std::set reqs = {"name", "description"}; Iterator elem; int banks_found = 0; for(elem = elem.begin(&data); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "deadline") { for(attr = attr.begin(elem.Get()); attr != attr.end(); attr++) { attr->GetName(&name); @@ -453,6 +472,8 @@ static void readQuestFromXml(ticpp::Element& data, cQuest& quest) { elem->GetText(&quest.descr, false); } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("quest", *reqs.begin(), data.Row(), data.Column(), fname); } static void readShopFromXml(ticpp::Element& data, cShop& shop) { @@ -460,9 +481,11 @@ static void readShopFromXml(ticpp::Element& data, cShop& shop) { std::string type, name, val, fname; data.GetDocument()->GetValue(&fname); data.GetValue(&type); + std::set reqs = {"name", "type", "prompt", "face", "entries"}; Iterator elem; for(elem = elem.begin(&data); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "name") { elem->GetText(&val, false); shop.setName(val); @@ -503,28 +526,32 @@ static void readShopFromXml(ticpp::Element& data, cShop& shop) { entry->GetText(&num); shop.addItem(num, dummy_item, amount, chance); } else if(type == "special") { - int amount, node, cost, icon; + int amount, node, cost = 0, icon; std::string title, descr; - Iterator attr; + std::set reqs = {"quantity", "node", "icon", "name", "description"}; + Iterator attr; for(attr = attr.begin(entry.Get()); attr != attr.end(); attr++) { - attr->GetName(&name); + attr->GetValue(&name); + reqs.erase(name); if(name == "quantity") { - attr->GetValue(&val); + attr->GetText(&val); if(val == "infinite") amount = 0; else amount = boost::lexical_cast(val); } else if(name == "cost") { - attr->GetValue(&cost); + attr->GetText(&cost); } else if(name == "node") { - attr->GetValue(&node); + attr->GetText(&node); } else if(name == "icon") { - attr->GetValue(&icon); + attr->GetText(&icon); } else if(name == "name") { - attr->GetValue(&title); + attr->GetText(&title, false); } else if(name == "description") { - attr->GetValue(&descr); + attr->GetText(&descr, false); } else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("special", *reqs.begin(), entry->Row(), entry->Column(), fname); shop.addSpecial(name, descr, icon, node, cost, amount); } else { eShopItemType itype; @@ -557,6 +584,8 @@ static void readShopFromXml(ticpp::Element& data, cShop& shop) { } } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("shop", *reqs.begin(), data.Row(), data.Column(), fname); } static void readTimerFromXml(ticpp::Element& data, cTimer& timer) { @@ -609,8 +638,14 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { initialXmlRead(data, "scenario", maj, min, rev, fname); Iterator attr; Iterator elem; + std::set reqs = { + "title", "icon", "id", "version", + "language", "author", "text", "ratings", + "flags", "creator", "game", "editor" + }; for(elem = elem.begin(data.FirstChildElement()); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "title") { elem->GetText(&scenario.scen_name, false); } else if(type == "icon") { @@ -663,9 +698,11 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { throw xBadVal("difficulty", xBadVal::CONTENT, std::to_string(scenario.difficulty), difficulty->Row(),difficulty->Column(), fname); scenario.difficulty--; } else if(type == "flags") { + std::set reqs = {"adjust-difficulty", "custom-graphics", "legacy"}; Iterator flag; for(flag = flag.begin(elem.Get()); flag != flag.end(); flag++) { flag->GetValue(&type); + reqs.erase(type); if(type == "adjust-difficulty") { flag->GetText(&val); scenario.adjust_diff = val == "true"; @@ -677,6 +714,8 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { scenario.is_legacy = val == "true"; } else throw xBadNode(type,flag->Row(),flag->Column(),fname); } + if(!reqs.empty()) + throw xMissingElem("flags", *reqs.begin(), elem->Row(), elem->Column(), fname); } else if(type == "creator") { elem->FirstChildElement("version")->GetText(&val); int maj, min, rev; @@ -684,12 +723,22 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { scenario.format.prog_make_ver[0] = maj; scenario.format.prog_make_ver[1] = min; scenario.format.prog_make_ver[2] = rev; - // TODO: Type field (verify that it's "oboe"?) and OS field + std::string type; + Element* tp_elem = elem->FirstChildElement("type"); + tp_elem->GetText(&type); + std::transform(type.begin(), type.end(), type.begin(), tolower); + if( type != "oboe") + throw xBadVal("creator", "type", type, tp_elem->Row(), tp_elem->Column(), fname); + // TODO: Check OS field? } else if(type == "game") { + std::set reqs = { + "num-towns", "out-width", "out-height", "start-town", "town-start", "outdoor-start", "sector-start", + }; Iterator game; int store_rects = 0, town_mods = 0, spec_items = 0, quests = 0, shops = 0, timers = 0, strnum; for(game = game.begin(elem.Get()); game != game.end(); game++) { game->GetValue(&type); + reqs.erase(type); if(type == "num-towns") { int num; game->GetText(&num); @@ -756,11 +805,15 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { game->GetText(&scenario.journal_strs[strnum], false); } else throw xBadNode(type, game->Row(), game->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("game", *reqs.begin(), elem->Row(), elem->Column(), fname); } else if(type == "editor") { + std::set reqs = {"default-ground", "last-out-section", "last-town"}; Iterator edit; int num_storage = 0, num_pics = 0, sndnum = 0; for(edit = edit.begin(elem.Get()); edit != edit.end(); edit++) { edit->GetValue(&type); + reqs.erase(type); if(type == "default-ground") { edit->GetText(&scenario.default_ground); } else if(type == "last-out-section") { @@ -795,7 +848,7 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { if(i < 0) throw xMissingAttr(type, "index", pic->Row(), pic->Column(), fname); if(!valid_pictypes.count(pictype)) - throw xBadVal(type, "pic", std::to_string(pictype), pic->Row(), pic->Column(), fname); + throw xBadVal(type, xBadVal::CONTENT, std::to_string(pictype), pic->Row(), pic->Column(), fname); scenario.custom_graphics[i] = ePicType(pictype); num_pics++; } @@ -803,9 +856,11 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { if(num_storage >= 10) throw xBadNode(type, edit->Row(), edit->Column(), fname); int num_items = 0; + std::set reqs = {"on-terrain", "is-property", "item"}; Iterator store; for(store = store.begin(edit.Get()); store != store.end(); store++) { store->GetValue(&type); + reqs.erase(type); if(type == "on-terrain") { store->GetText(&scenario.storage_shortcuts[num_storage].ter_type); } else if(type == "is-property") { @@ -829,11 +884,17 @@ static void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) { num_items++; } } + if(!reqs.empty()) + throw xMissingElem("storage", *reqs.begin(), edit->Row(), edit->Column(), fname); num_storage++; } else throw xBadNode(type, edit->Row(), edit->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("editor", *reqs.begin(), elem->Row(), elem->Column(), fname); } else throw xBadNode(type,elem->Row(),elem->Column(),fname); } + if(!reqs.empty()) + throw xMissingElem("scenario", *reqs.begin(), data.FirstChildElement()->Row(), data.FirstChildElement()->Column(), fname); } static void readTerrainFromXml(ticpp::Document&& data, cScenario& scenario) { @@ -853,9 +914,11 @@ static void readTerrainFromXml(ticpp::Document&& data, cScenario& scenario) { scenario.ter_types.resize(which_ter + 1); cTerrain& the_ter = scenario.ter_types[which_ter]; the_ter = cTerrain(); + std::set reqs = {"name", "pic", "map", "blockage", "special", "trim", "arena"}; Iterator ter; for(ter = ter.begin(elem.Get()); ter != ter.end(); ter++) { ter->GetValue(&type); + reqs.erase(type); if(type == "name") { ter->GetText(&the_ter.name, false); } else if(type == "pic") { @@ -866,11 +929,13 @@ static void readTerrainFromXml(ticpp::Document&& data, cScenario& scenario) { ter->GetText(&the_ter.blockage); } else if(type == "special") { int num_flags = 0; + bool found_type = false; Iterator spec; for(spec = spec.begin(ter.Get()); spec != spec.end(); spec++) { spec->GetValue(&type); if(type == "type") { spec->GetText(&the_ter.special); + found_type = true; } else if(type == "flag") { if(num_flags == 0) spec->GetText(&the_ter.flag1); @@ -882,6 +947,8 @@ static void readTerrainFromXml(ticpp::Document&& data, cScenario& scenario) { num_flags++; } else throw xBadNode(type, spec->Row(), spec->Column(), fname); } + if(!found_type) + throw xMissingElem("special", "type", ter->Row(), ter->Column(), fname); } else if(type == "transform") { ter->GetText(&the_ter.trans_to_what); } else if(type == "fly") { @@ -921,9 +988,11 @@ static void readTerrainFromXml(ticpp::Document&& data, cScenario& scenario) { edit->GetText(&val, false); the_ter.shortcut_key = val.empty() ? 0 : val[0]; } else if(type == "object") { + std::set reqs = {"num", "pos", "size"}; Iterator obj; for(obj = obj.begin(edit.Get()); obj != obj.end(); obj++) { obj->GetValue(&type); + reqs.erase(type); if(type == "num") { obj->GetText(&the_ter.obj_num); } else if(type == "pos") { @@ -932,10 +1001,14 @@ static void readTerrainFromXml(ticpp::Document&& data, cScenario& scenario) { the_ter.obj_size = readLocFromXml(*obj); } else throw xBadNode(type, obj->Row(), obj->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("object", *reqs.begin(), edit->Row(), edit->Column(), fname); } else throw xBadNode(type, edit->Row(), edit->Column(), fname); } } else throw xBadNode(type, ter->Row(), ter->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("terrain", *reqs.begin(), elem->Row(), elem->Column(), fname); } } @@ -956,9 +1029,11 @@ static void readItemsFromXml(ticpp::Document&& data, cScenario& scenario) { scenario.scen_items.resize(which_item + 1); cItem& the_item = scenario.scen_items[which_item]; the_item = cItem(); + std::set reqs = {"variety", "level", "pic", "value", "weight", "name", "full-name"}; Iterator item; for(item = item.begin(elem.Get()); item != item.end(); item++) { item->GetValue(&type); + reqs.erase(type); if(type == "variety") { item->GetText(&the_item.variety); } else if(type == "level") { @@ -992,9 +1067,11 @@ static void readItemsFromXml(ticpp::Document&& data, cScenario& scenario) { } else if(type == "treasure") { item->GetText(&the_item.treas_class); } else if(type == "ability") { + std::set reqs = {"type", "strength", "data"}; Iterator abil; for(abil = abil.begin(item.Get()); abil != abil.end(); abil++) { abil->GetValue(&type); + reqs.erase(type); if(type == "type") { abil->GetText(&the_item.ability); } else if(type == "strength") { @@ -1005,6 +1082,8 @@ static void readItemsFromXml(ticpp::Document&& data, cScenario& scenario) { abil->GetText(&the_item.magic_use_type); } else throw xBadNode(type, abil->Row(), abil->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("ability", *reqs.begin(), item->Row(), item->Column(), fname); } else if(type == "properties") { Iterator prop; for(prop = prop.begin(item.Get()); prop != prop.end(); prop++) { @@ -1029,6 +1108,8 @@ static void readItemsFromXml(ticpp::Document&& data, cScenario& scenario) { item->GetText(&the_item.desc, false); } else throw xBadNode(type, item->Row(), item->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("item", *reqs.begin(), elem->Row(), elem->Column(), fname); } // Once we have the items, we have to go back and fill in the shops for(cShop& shop : scenario.shops) @@ -1047,7 +1128,7 @@ static std::pair parseDice(std::string str, std::string elem, std::stri count *= 10; count += c - '0'; } - } else if(!found_d) + } else if(!found_d && c == 'd') found_d = true; else throw xBadVal(elem, attr, str, row, col, fname); } @@ -1079,11 +1160,25 @@ static void readMonstAbilFromXml(ticpp::Element& data, cMonster& monst) { abil.active = true; Iterator elem; if(type == "general") { + if(getMonstAbilCategory(abil_type) != eMonstAbilCat::GENERAL) + throw xBadVal(type, "type", val, data.Row(), data.Column(), fname); + std::set reqs = {"type", "strength", "chance"}; + if(abil_type == eMonstAbil::DAMAGE || abil_type == eMonstAbil::DAMAGE2) + reqs.insert("extra"); + else if(abil_type == eMonstAbil::FIELD) + reqs.insert("extra"); + else if(abil_type == eMonstAbil::STATUS || abil_type == eMonstAbil::STATUS2 || abil_type == eMonstAbil::STUN) + reqs.insert("extra"); auto& general = abil.gen; for(elem = elem.begin(&data); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "type") { elem->GetText(&general.type); + if(general.type != eMonstGen::TOUCH) { + reqs.insert("missile"); + reqs.insert("range"); + } } else if(type == "missile") { elem->GetText(&general.pic); } else if(type == "strength") { @@ -1095,17 +1190,25 @@ static void readMonstAbilFromXml(ticpp::Element& data, cMonster& monst) { elem->GetText(&general.dmg); else if(abil_type == eMonstAbil::FIELD) elem->GetText(&general.fld); - else elem->GetText(&general.stat); + else if(abil_type == eMonstAbil::STATUS || abil_type == eMonstAbil::STATUS2 || abil_type == eMonstAbil::STUN) + elem->GetText(&general.stat); + else throw xBadNode(type, elem->Row(), elem->Column(), fname); } else if(type == "chance") { long double percent; elem->GetText(&percent); general.odds = percent * 10; } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("general", *reqs.begin(), data.Row(), data.Column(), fname); } else if(type == "missile") { + if(getMonstAbilCategory(abil_type) != eMonstAbilCat::MISSILE) + throw xBadVal(type, "type", val, data.Row(), data.Column(), fname); + std::set reqs = {"type", "missile", "strength", "skill", "range", "chance"}; auto& missile = abil.missile; for(elem = elem.begin(&data); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "type") { elem->GetText(&missile.type); } else if(type == "missile") { @@ -1123,10 +1226,16 @@ static void readMonstAbilFromXml(ticpp::Element& data, cMonster& monst) { missile.odds = percent * 10; } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("missile", *reqs.begin(), data.Row(), data.Column(), fname); } else if(type == "summon") { + if(getMonstAbilCategory(abil_type) != eMonstAbilCat::SUMMON) + throw xBadVal(type, "type", val, data.Row(), data.Column(), fname); + std::set reqs = {"type", "min", "max", "duration", "chance"}; auto& summon = abil.summon; for(elem = elem.begin(&data); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "type") { elem->GetText(&summon.type); } else if(type == "min") { @@ -1141,17 +1250,27 @@ static void readMonstAbilFromXml(ticpp::Element& data, cMonster& monst) { summon.chance = percent * 10; } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("summon", *reqs.begin(), data.Row(), data.Column(), fname); } else if(type == "radiate") { + if(getMonstAbilCategory(abil_type) != eMonstAbilCat::RADIATE) + throw xBadVal(type, "type", val, data.Row(), data.Column(), fname); + std::set reqs = {"type", "chance"}; auto& radiate = abil.radiate; for(elem = elem.begin(&data); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "type") { elem->GetText(&radiate.type); } else if(type == "chance") { elem->GetText(&radiate.chance); } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("radiate", *reqs.begin(), data.Row(), data.Column(), fname); } else if(type == "special") { + if(getMonstAbilCategory(abil_type) != eMonstAbilCat::SPECIAL) + throw xBadVal(type, "type", val, data.Row(), data.Column(), fname); auto& special = abil.special; int num_params = 0; for(elem = elem.begin(&data); elem != elem.end(); elem++) { @@ -1189,10 +1308,15 @@ static void readMonstersFromXml(ticpp::Document&& data, cScenario& scenario) { scenario.scen_monsters.resize(which_monst + 1); cMonster& the_mon = scenario.scen_monsters[which_monst]; the_mon = cMonster(); + std::set reqs = { + "name", "level", "armor", "skill", "hp", "speed", + "race", "attacks", "pic", "attitude", "immunity", + }; Iterator attr; Iterator monst; for(monst = monst.begin(elem.Get()); monst != monst.end(); monst++) { monst->GetValue(&type); + reqs.erase(type); if(type == "name") { monst->GetText(&the_mon.m_name, false); } else if(type == "level") { @@ -1228,24 +1352,32 @@ static void readMonstersFromXml(ticpp::Document&& data, cScenario& scenario) { cMonster::cAttack& the_atk = the_mon.a[num_attacks]; atk->GetText(&val); std::tie(the_atk.dice, the_atk.sides) = parseDice(val, type, xBadVal::CONTENT, fname, atk->Row(), atk->Column()); + bool found_type = false; for(attr = attr.begin(atk.Get()); attr != attr.end(); attr++) { attr->GetName(&name); if(name != "type") throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); attr->GetValue(&the_atk.type); + found_type = true; } + if(!found_type) + throw xMissingAttr("attack", "type", atk->Row(), atk->Column(), fname); num_attacks++; } } else if(type == "pic") { monst->GetText(&the_mon.picture_num); + std::set reqs = {"w", "h"}; for(attr = attr.begin(monst.Get()); attr != attr.end(); attr++) { attr->GetName(&name); + reqs.erase(name); if(name == "w") attr->GetValue(&the_mon.x_width); else if(name == "h") attr->GetValue(&the_mon.y_width); else throw xBadAttr(type, name, attr->Row(), attr->Column(), fname); } + if(!reqs.empty()) + throw xMissingAttr("pic", *reqs.begin(), monst->Row(), monst->Column(), fname); } else if(type == "default-face") { monst->GetText(&the_mon.default_facial_pic); } else if(type == "onsight") { @@ -1279,17 +1411,23 @@ static void readMonstersFromXml(ticpp::Document&& data, cScenario& scenario) { } else throw xBadNode(type, resist->Row(), resist->Column(), fname); } } else if(type == "loot") { + std::set reqs = {"type", "chance"}; Iterator loot; for(loot = loot.begin(monst.Get()); loot != loot.end(); loot++) { loot->GetValue(&type); + reqs.erase(type); if(type == "type") { loot->GetText(&the_mon.corpse_item); } else if(type == "chance") { loot->GetText(&the_mon.corpse_item_chance); } else throw xBadNode(type, loot->Row(), loot->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("loot", *reqs.begin(), monst->Row(), monst->Column(), fname); } else throw xBadNode(type, monst->Row(), monst->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("monster", *reqs.begin(), elem->Row(), elem->Column(), fname); } } @@ -1299,11 +1437,13 @@ static void readOutdoorsFromXml(ticpp::Document&& data, cOutdoors& out) { std::string fname, type, name, val; initialXmlRead(data, "sector", maj, min, rev, fname); int num_rects = 0, num_encs = 0, num_wand = 0; + bool found_name = false; Iterator elem; for(elem = elem.begin(data.FirstChildElement()); elem != elem.end(); elem++) { elem->GetValue(&type); if(type == "name") { elem->GetText(&out.out_name, false); + found_name = true; } else if(type == "comment") { elem->GetText(&out.comment, false); } else if(type == "sound") { @@ -1325,6 +1465,7 @@ static void readOutdoorsFromXml(ticpp::Document&& data, cOutdoors& out) { Iterator attr; Iterator enc; for(enc = enc.begin(elem.Get()); enc != enc.end(); enc++) { + std::string type; enc->GetValue(&type); if(type == "monster") { bool is_friendly = false; @@ -1357,6 +1498,8 @@ static void readOutdoorsFromXml(ticpp::Document&& data, cOutdoors& out) { enc_list[count].end_spec2 = sdf.y; } else throw xBadNode(type, enc->Row(), enc->Column(), fname); } + if(num_hostile + num_friendly == 0) + throw xMissingElem(type, "monster", elem->Row(), elem->Column(), fname); count++; } else if(type == "sign") { int sign; @@ -1378,6 +1521,8 @@ static void readOutdoorsFromXml(ticpp::Document&& data, cOutdoors& out) { elem->GetText(&out.spec_strs[str], false); } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!found_name) + throw xMissingElem("sector", "name", data.FirstChildElement()->Row(), data.FirstChildElement()->Column(), fname); } static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& scen) { @@ -1388,10 +1533,13 @@ static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& sce initialXmlRead(data, "town", maj, min, rev, fname); int num_cmt = 0, num_timers = 0, num_wand = 0, num_rects = 0; bool found_size = false; + std::set reqs = {"size", "name", "bounds", "difficulty", "lighting", "flags"}; + bool found_ondead = false, found_onalive = false; Iterator attr; Iterator elem; for(elem = elem.begin(data.FirstChildElement()); elem != elem.end(); elem++) { elem->GetValue(&type); + reqs.erase(type); if(type == "size") { elem->GetText(&val); if(val == "32") { @@ -1419,11 +1567,13 @@ static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& sce elem->GetText(&town->lighting_type); } else if(type == "onenter") { elem->GetAttribute("condition", &val); - if(val == "alive") + if(!found_onalive && val == "alive") { elem->GetText(&town->spec_on_entry); - else if(val == "dead") + found_onalive = true; + } else if(!found_ondead && val == "dead") { elem->GetText(&town->spec_on_entry_if_dead); - else throw xBadVal(type, "condition", val, elem->Row(), elem->Column(), fname); + found_ondead = true; + } else throw xBadVal(type, "condition", val, elem->Row(), elem->Column(), fname); } else if(type == "exit") { location loc = readLocFromXml(*elem, "", "dir", &val); size_t which = dirs.find_first_of(val); @@ -1479,6 +1629,8 @@ static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& sce monst->GetText(&town->wandering[num_wand].monst[num_monst]); num_monst++; } + if(num_monst == 0) + throw xMissingElem("wandering", "monster", elem->Row(), elem->Column(), fname); num_wand++; } else if(type == "sign") { int sign; @@ -1498,11 +1650,13 @@ static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& sce if(which_item >= town->preset_items.size()) town->preset_items.resize(which_item + 1); cTown::cItem& item = town->preset_items[which_item]; + bool found_type = false; Iterator preset; for(preset = preset.begin(elem.Get()); preset != preset.end(); preset++) { preset->GetValue(&type); if(type == "type") { preset->GetText(&item.code); + found_type = true; } else if(type == "mod") { preset->GetText(&item.ability); } else if(type == "charges") { @@ -1521,15 +1675,19 @@ static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& sce item.contained = true; } else throw xBadNode(type, preset->Row(), preset->Column(), fname); } + if(!found_type) + throw xMissingElem("item", "type", elem->Row(), elem->Column(), fname); } else if(type == "creature") { int who; elem->GetAttribute("id", &who); if(who >= town->creatures.size()) town->creatures.resize(who + 1); cTownperson& npc = town->creatures[who]; + std::set reqs = {"type", "attitude", "mobility"}; Iterator monst; for(monst = monst.begin(elem.Get()); monst != monst.end(); monst++) { monst->GetValue(&type); + reqs.erase(type); if(type == "type") { monst->GetText(&npc.number); } else if(type == "attitude") { @@ -1559,6 +1717,8 @@ static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& sce monst->GetText(&npc.special_on_talk); } else throw xBadNode(type, monst->Row(), monst->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("creature", *reqs.begin(), elem->Row(), elem->Column(), fname); } else if(type == "area") { if(num_rects >= town->room_rect.size()) town->room_rect.resize(num_rects + 1); @@ -1567,6 +1727,8 @@ static void readTownFromXml(ticpp::Document&& data, cTown*& town, cScenario& sce num_rects++; } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("item", *reqs.begin(), data.FirstChildElement()->Row(), data.FirstChildElement()->Column(), fname); } static void readDialogueFromXml(ticpp::Document&& data, cSpeech& talk, int town_num) { @@ -1585,9 +1747,11 @@ static void readDialogueFromXml(ticpp::Document&& data, cSpeech& talk, int town_ if(id < town_num * 10 || id >= (town_num + 1) * 10) throw xBadVal(type, "id", std::to_string(id), elem->Row(), elem->Column(), fname); id %= 10; + std::set reqs = {"title", "look", "name", "job"}; Iterator who; for(who = who.begin(elem.Get()); who != who.end(); who++) { who->GetValue(&type); + reqs.erase(type); if(type == "title") { who->GetText(&talk.people[id].title, false); } else if(type == "look") { @@ -1600,11 +1764,14 @@ static void readDialogueFromXml(ticpp::Document&& data, cSpeech& talk, int town_ who->GetText(&talk.people[id].dunno, false); } else throw xBadNode(type, who->Row(), who->Column(), fname); } + if(!reqs.empty()) + throw xMissingElem("personality", *reqs.begin(), elem->Row(), elem->Column(), fname); } else if(type == "node") { if(num_nodes >= talk.talk_nodes.size()) talk.talk_nodes.resize(num_nodes + 1); elem->GetAttribute("for", &talk.talk_nodes[num_nodes].personality); int num_keys = 0, num_params = 0, num_strs = 0; + bool got_type = false; Iterator node; for(node = node.begin(elem.Get()); node != node.end(); node++) { node->GetValue(&type); @@ -1622,6 +1789,7 @@ static void readDialogueFromXml(ticpp::Document&& data, cSpeech& talk, int town_ int type; node->GetText(&type); talk.talk_nodes[num_nodes].type = eTalkNode(type); + got_type = true; } else if(type == "param") { if(num_params >= 4) throw xBadNode(type, node->Row(), node->Column(), fname); @@ -1636,6 +1804,12 @@ static void readDialogueFromXml(ticpp::Document&& data, cSpeech& talk, int town_ num_strs++; } else throw xBadNode(type, node->Row(), node->Column(), fname); } + if(num_keys == 0) + throw xMissingElem("node", "keyword", elem->Row(), elem->Column(), fname); + if(!got_type) + throw xMissingElem("node", "type", elem->Row(), elem->Column(), fname); + if(num_strs == 0) + throw xMissingElem("node", "text", elem->Row(), elem->Column(), fname); num_nodes++; } else throw xBadNode(type, elem->Row(), elem->Column(), fname); } @@ -1681,6 +1855,7 @@ static void loadOutMapData(map_data&& data, location which, cScenario& scen) { case eMapFeature::FIELD: if(feat.second == SPECIAL_SPOT) out.special_spot[x][y] = true; + else throw xMapParseError(map_out_bad_field, feat.second, y, x, data.file); break; case eMapFeature::SIGN: if(feat.second >= out.sign_locs.size()) @@ -1769,13 +1944,13 @@ static void loadTownMapData(map_data&& data, int which, cScenario& scen) { town.set_up_lights(); } -static void readSpecialNodesFromStream(std::istream& stream, std::vector& nodes) { +static void readSpecialNodesFromStream(std::istream& stream, std::vector& nodes, std::string name) { std::string contents; stream.seekg(0, std::ios::end); contents.reserve(stream.tellg()); stream.seekg(0, std::ios::beg); contents.assign(std::istreambuf_iterator(stream), std::istreambuf_iterator()); - auto loaded = SpecialParser().parse(contents); + auto loaded = SpecialParser().parse(contents, name); if(loaded.size() == 0) return; // If there were no nodes, we're already done here. nodes.resize(loaded.rbegin()->first + 1); for(auto p : loaded) @@ -1845,7 +2020,7 @@ bool load_scenario_v2(fs::path file_to_load, cScenario& scenario, bool only_head // Finally, the special nodes. std::istream& nodes = getFile("scenario/scenario.spec"); - readSpecialNodesFromStream(nodes, scenario.scen_specials); + readSpecialNodesFromStream(nodes, scenario.scen_specials, "scenario.spec"); } // Next, read the outdoors. Note that the space has already been reserved for them. @@ -1859,11 +2034,11 @@ bool load_scenario_v2(fs::path file_to_load, cScenario& scenario, bool only_head // Then the map. std::istream& out_map = getFile("scenario/out/" + file_basename + ".map"); - loadOutMapData(load_map(out_map, false), loc(x,y), scenario); + loadOutMapData(load_map(out_map, false, file_basename + ".map"), loc(x,y), scenario); // And the special nodes. std::istream& out_spec = getFile("scenario/out/" + file_basename + ".spec"); - readSpecialNodesFromStream(out_spec, scenario.outdoors[x][y]->specials); + readSpecialNodesFromStream(out_spec, scenario.outdoors[x][y]->specials, file_basename + ".spec"); } } @@ -1876,11 +2051,11 @@ bool load_scenario_v2(fs::path file_to_load, cScenario& scenario, bool only_head // Then the map. std::istream& town_map = getFile("scenario/towns/" + file_basename + ".map"); - loadTownMapData(load_map(town_map, true), i, scenario); + loadTownMapData(load_map(town_map, true, file_basename + ".map"), i, scenario); // And the special nodes. std::istream& town_spec = getFile("scenario/towns/" + file_basename + ".spec"); - readSpecialNodesFromStream(town_spec, scenario.towns[i]->specials); + readSpecialNodesFromStream(town_spec, scenario.towns[i]->specials, file_basename + ".spec"); // Don't forget the dialogue nodes. std::istream& town_talk = getFile("scenario/towns/talk" + std::to_string(i) + ".xml"); diff --git a/src/tools/map_parse.cpp b/src/tools/map_parse.cpp index 9ff711ba..fa3f397d 100644 --- a/src/tools/map_parse.cpp +++ b/src/tools/map_parse.cpp @@ -15,8 +15,9 @@ using namespace std; -map_data load_map(std::istream& fin, bool isTown) { +map_data load_map(std::istream& fin, bool isTown, std::string name) { map_data data; + data.file = name; int row = 0; while(!fin.eof()) { std::string line; @@ -31,14 +32,22 @@ map_data load_map(std::istream& fin, bool isTown) { if(isdigit(c)) { n *= 10; n += c - '0'; - } else if(c == '^' && isTown) { - data.addFeature(col, row, eMapFeature::ENTRANCE_NORTH); - } else if(c == '<' && isTown) { - data.addFeature(col, row, eMapFeature::ENTRANCE_WEST); - } else if(c == 'v' && isTown) { - data.addFeature(col, row, eMapFeature::ENTRANCE_SOUTH); - } else if(c == '>' && isTown) { - data.addFeature(col, row, eMapFeature::ENTRANCE_EAST); + } else if(c == '^') { + if(isTown) + data.addFeature(col, row, eMapFeature::ENTRANCE_NORTH); + else throw xMapParseError(map_out_has_town_dir, c, row, col, name); + } else if(c == '<') { + if(isTown) + data.addFeature(col, row, eMapFeature::ENTRANCE_WEST); + else throw xMapParseError(map_out_has_town_dir, c, row, col, name); + } else if(c == 'v') { + if(isTown) + data.addFeature(col, row, eMapFeature::ENTRANCE_SOUTH); + else throw xMapParseError(map_out_has_town_dir, c, row, col, name); + } else if(c == '>') { + if(isTown) + data.addFeature(col, row, eMapFeature::ENTRANCE_EAST); + else throw xMapParseError(map_out_has_town_dir, c, row, col, name); } else { if(curFeature == eMapFeature::NONE) data.set(col, row, n); @@ -80,7 +89,7 @@ map_data load_map(std::istream& fin, bool isTown) { col++; curFeature = eMapFeature::NONE; } else { - // TODO: This is an error! + throw xMapParseError(map_bad_feature, c, row, col, name); } } } @@ -160,3 +169,42 @@ void map_data::writeTo(std::ostream& out) { out << '\n'; } } + +const char*const xMapParseError::messages[NUM_MAP_ERR] = { + "Unrecognized map feature character found: ", + "Outdoors map has illegal town entrance direction feature: ", + "Outdoors map has illegal field type: ", +}; + +xMapParseError::xMapParseError(eMapError err, char c, int line, int col, std::string file) : + err(err), + c(c), + line(line), + col(col), + file(file), + msg(nullptr) {} + +xMapParseError::~xMapParseError() throw() { + if(msg != nullptr) + delete[] msg; +} + +const char* xMapParseError::what() const throw() { + if(msg == nullptr) { + char* s = new(std::nothrow) char[201]; + std::fill_n(s, 201, 0); + size_t len = strlen(messages[err]); + strncpy(s, messages[err], std::min(201, len + 1)); + if(err == map_out_bad_field) { + if(c < 100 && len < 200) + s[len++] = c / 100; + if(c < 10 && len < 200) + s[len++] = c / 10; + if(len < 200) + s[len++] = c % 10; + } else s[len++] = c; + s[len] = 0; + msg = s; + } + return msg; +} diff --git a/src/tools/map_parse.hpp b/src/tools/map_parse.hpp index faa3272a..5b13dd32 100644 --- a/src/tools/map_parse.hpp +++ b/src/tools/map_parse.hpp @@ -37,6 +37,7 @@ class map_data { using feature_t = std::pair; std::multimap features; public: + std::string file; void set(unsigned int x, unsigned int y, unsigned int val); unsigned int get(unsigned int x, unsigned int y); void addFeature(unsigned int x, unsigned int y, eMapFeature feature, int val = 0); @@ -44,6 +45,26 @@ public: void writeTo(std::ostream& out); }; -map_data load_map(std::istream& stream, bool isTown); +map_data load_map(std::istream& stream, bool isTown, std::string name); + +enum eMapError { + map_bad_feature, + map_out_has_town_dir, + map_out_bad_field, + NUM_MAP_ERR +}; + +class xMapParseError : public std::exception { + static const char*const messages[NUM_MAP_ERR]; + int line, col; + char c; + std::string file; + eMapError err; + mutable const char* msg; +public: + xMapParseError(eMapError err, char c, int line, int col, std::string file); + ~xMapParseError() throw(); + const char* what() const throw(); +}; #endif diff --git a/src/tools/special_parse.hpp b/src/tools/special_parse.hpp index 2cb45793..7354ba44 100644 --- a/src/tools/special_parse.hpp +++ b/src/tools/special_parse.hpp @@ -14,12 +14,15 @@ enum eParseError { generic_error, + double_def, expect_op, - expect_def, + expect_sym, expect_dat, expect_eq, expect_int, expect_val, + expect_nl, + NUM_PARSE_ERR }; namespace spirit = boost::spirit::classic; @@ -47,6 +50,7 @@ class SpecialParser { static void set_second(int i); static void set_third(int i); static void next_line(Iter, Iter); + static void maybe_throw(Iter, Iter); static std::string temp_symbol; static int cur_node, cur_fld; static cSpecial curSpec; @@ -54,15 +58,29 @@ class SpecialParser { static Iter file_start; static std::map specials; static spirit::symbols<> defn; - static Rule ws, comment, symbol, symbol_ch, eol; + static Rule ws, eq, comment, symbol, symbol_ch, eol, op_assign; static Rule datcode, command, init_line, null_line, def_line, cmd_line, op_line, cmd_block, nodes_file; - static Err err; + static Rule cmd_1st, cmd_2nd, cmd_3rd; + static Err err, assert_op, assert_sym, assert_dat, assert_eq, assert_int, assert_val; static Guard guard; static bool grammar_built; - static ErrStatus on_error(const Rule::scanner_t&, spirit::parser_error); + static ErrStatus check_error(const Rule::scanner_t&, spirit::parser_error); + static eParseError last_err; public: SpecialParser(); - std::map parse(std::string code); + std::map parse(std::string code, std::string context); +}; + +class xSpecParseError : public std::exception { + static const char*const messages[NUM_PARSE_ERR]; + eParseError err; + int line, col; + std::string found, file; + mutable const char* msg; +public: + xSpecParseError(std::string found, eParseError expect, int line, int col, std::string file); + ~xSpecParseError() throw(); + const char* what() const throw(); }; #endif diff --git a/src/tools/specials_parse.cpp b/src/tools/specials_parse.cpp index 14461c71..7f363aa3 100644 --- a/src/tools/specials_parse.cpp +++ b/src/tools/specials_parse.cpp @@ -27,11 +27,15 @@ std::map SpecialParser::specials; spirit::symbols<> SpecialParser::defn; Rule SpecialParser::ws, SpecialParser::comment, SpecialParser::symbol, SpecialParser::symbol_ch, SpecialParser::eol; Rule SpecialParser::datcode, SpecialParser::command, SpecialParser::def_line, SpecialParser::cmd_line; +Rule SpecialParser::eq, SpecialParser::cmd_1st, SpecialParser::cmd_2nd, SpecialParser::cmd_3rd, SpecialParser::op_assign; Rule SpecialParser::init_line, SpecialParser::null_line; Rule SpecialParser::op_line, SpecialParser::cmd_block, SpecialParser::nodes_file; int SpecialParser::lineno, SpecialParser::last_line_start; SpecialParser::Iter SpecialParser::file_start; +eParseError SpecialParser::last_err = NUM_PARSE_ERR; Err SpecialParser::err(generic_error); +Err SpecialParser::assert_op(expect_op), SpecialParser::assert_sym(expect_sym), SpecialParser::assert_dat(expect_dat); +Err SpecialParser::assert_eq(expect_eq), SpecialParser::assert_int(expect_int), SpecialParser::assert_val(expect_val); Guard SpecialParser::guard; SpecialParser::SpecialParser() { @@ -42,24 +46,29 @@ SpecialParser::SpecialParser() { symbol_ch = chset<>("A-Za-z$_-"); symbol = symbol_ch >> *symbol_ch; eol = eol_p[_(next_line)]; + eq = assert_eq(ch_p('=')); datcode = str_p("sdf")[_(for_sdf)] | str_p("pic")[_(for_pic)] | str_p("msg")[_(for_msg)] | str_p("ex1")[_(for_ex1)] | str_p("ex2")[_(for_ex2)] | str_p("goto")[_(for_goto)]; - command = datcode >> +ws >> (int_p[_(set_first)] | defn[_(set_first)]) >> - !(*ws >> ch_p(',') >> *ws >> (int_p[_(set_second)] | defn[_(set_second)]) >> - !(*ws >> ch_p(',') >> *ws >> (int_p[_(set_third)] | defn[_(set_third)]))); + cmd_1st = assert_val(int_p[_(set_first)] | defn[_(set_first)])[_(maybe_throw)]; + cmd_2nd = assert_val(int_p[_(set_second)] | defn[_(set_second)])[_(maybe_throw)]; + cmd_3rd = assert_val(int_p[_(set_third)] | defn[_(set_third)])[_(maybe_throw)]; + command = guard(assert_dat(datcode))[_(check_error)] >> +ws >> cmd_1st + >> !(*ws >> ch_p(',') >> *ws >> cmd_2nd >> !(*ws >> ch_p(',') >> *ws >> cmd_3rd)); null_line = !comment >> eol; init_line = null_line | def_line; - def_line = str_p("def") >> +ws >> symbol[_(prep_add_symbol)] >> *ws >> ch_p('=') >> *ws >> int_p[_(add_symbol)] >> *ws >> !comment >> eol; - cmd_line = command >> *ws >> !comment >> eol; - op_line = ch_p('@') >> opcode[_(set_type)] >> *ws >> !(ch_p('=') >> *ws >> int_p[_(skip_to)]) >> *ws >> !comment >> eol; + def_line = str_p("def") >> +ws >> assert_sym(symbol)[_(prep_add_symbol)] >> *ws + >> eq >> *ws >> assert_int(int_p)[_(add_symbol)] >> *ws >> !comment >> err(eol); + cmd_line = command >> *ws >> !comment >> err(eol); + op_assign = guard(eq >> *ws >> assert_int(int_p[_(skip_to)]))[_(check_error)]; + op_line = ch_p('@') >> assert_op(opcode[_(set_type)]) >> *ws >> !op_assign >> *ws >> !comment >> err(eol); cmd_block = eps_p[_(init_block)] >> op_line >> *(*ws >> (cmd_line | def_line | null_line)); // TODO: This fails if the file doesn't end in a newline. - nodes_file = /*guard*/err(eps_p[_(init_file)] >> *(*ws >> init_line) >> *cmd_block[_(add_command)] >> end_p);//[_(on_error)]; + nodes_file = err(eps_p[_(init_file)] >> *(*ws >> init_line) >> *cmd_block[_(add_command)] >> end_p); // Debugging. If BOOST_SPIRIT_DEBUG not defined, all these expand to nothing. BOOST_SPIRIT_DEBUG_NODE(ws); @@ -67,13 +76,18 @@ SpecialParser::SpecialParser() { BOOST_SPIRIT_DEBUG_NODE(comment); BOOST_SPIRIT_DEBUG_NODE(symbol); BOOST_SPIRIT_DEBUG_NODE(symbol_ch); + BOOST_SPIRIT_DEBUG_NODE(eq); BOOST_SPIRIT_DEBUG_NODE(datcode); BOOST_SPIRIT_DEBUG_NODE(command); + BOOST_SPIRIT_DEBUG_NODE(cmd_1st); + BOOST_SPIRIT_DEBUG_NODE(cmd_2nd); + BOOST_SPIRIT_DEBUG_NODE(cmd_3rd); BOOST_SPIRIT_DEBUG_NODE(def_line); BOOST_SPIRIT_DEBUG_NODE(cmd_line); BOOST_SPIRIT_DEBUG_NODE(init_line); BOOST_SPIRIT_DEBUG_NODE(null_line); BOOST_SPIRIT_DEBUG_NODE(op_line); + BOOST_SPIRIT_DEBUG_NODE(op_assign); BOOST_SPIRIT_DEBUG_NODE(cmd_block); BOOST_SPIRIT_DEBUG_NODE(nodes_file); @@ -82,8 +96,12 @@ SpecialParser::SpecialParser() { #undef _ -auto SpecialParser::on_error(const Rule::scanner_t&, spirit::parser_error) -> ErrStatus { - return ErrStatus(ErrStatus::fail); +auto SpecialParser::check_error(const Rule::scanner_t&, spirit::parser_error e) -> ErrStatus { + if(e.descriptor == expect_dat && (*e.where == '@' || *e.where == 0 || *e.where == '#' || isspace(*e.where))) + return ErrStatus(ErrStatus::fail); + if(e.descriptor == expect_eq && (isspace(*e.where) || *e.where == '#')) + return ErrStatus(ErrStatus::fail); + return ErrStatus(ErrStatus::rethrow); } static void init_specials_parse() { @@ -104,12 +122,34 @@ static void init_specials_parse() { } } +void SpecialParser::maybe_throw(Iter start, Iter) { + if(last_err != NUM_PARSE_ERR) { + Iter tmp = start; + // Do some fiddling so that we can locate the parser position as close to the error as possible + while(*start != '\n' && *start != 0) + start++; + if(*start == 0) start--; + while(isspace(*start)) + start--; + while(!isspace(*start) && *start != ',') + start--; + if(isdigit(*start)) + start++; + spirit::throw_(tmp, last_err); + } + last_err = NUM_PARSE_ERR; +} + void SpecialParser::init_file(Iter start, Iter) { file_start = start; specials.clear(); + // For some reason, symbol tables don't have a "clear" option, so reconstruct it instead. + defn.~symbols(); + new(&defn) spirit::symbols<>; cur_node = -1; lineno = 1; last_line_start = 0; + last_err = NUM_PARSE_ERR; } void SpecialParser::next_line(Iter, Iter end) { @@ -139,6 +179,8 @@ void SpecialParser::init_block(Iter, Iter) { void SpecialParser::prep_add_symbol(Iter start, Iter end) { temp_symbol.assign(start, end); + if(find(defn, temp_symbol.c_str()) != nullptr) + spirit::throw_(start, double_def); } void SpecialParser::add_symbol(int i) { @@ -190,8 +232,7 @@ void SpecialParser::set_first(int i) { case 3: curSpec.ex1a = i; break; case 4: curSpec.ex2a = i; break; case 5: curSpec.jumpto = i; break; - // TODO: Figure out how to get an iterator to the matched number - //default: throw spirit::parser_error(start, generic_error); + default: last_err = expect_nl; } } @@ -202,8 +243,7 @@ void SpecialParser::set_second(int i) { case 2: curSpec.m2 = i; break; case 3: curSpec.ex1b = i; break; case 4: curSpec.ex2b = i; break; - // TODO: Figure out how to get an iterator to the matched number - //default: throw spirit::parser_error(start, generic_error); + default: last_err = expect_nl; } } @@ -212,14 +252,14 @@ void SpecialParser::set_third(int i) { case 2: curSpec.m3 = i; break; case 3: curSpec.ex1c = i; break; case 4: curSpec.ex2c = i; break; - // TODO: Figure out how to get an iterator to the matched number - //default: throw spirit::parser_error(start, generic_error); + default: last_err = expect_nl; } } -std::map SpecialParser::parse(std::string code) { +std::map SpecialParser::parse(std::string code, std::string context) { static bool inited = false; if(!inited) init_specials_parse(); + code += '\n'; const char* code_raw = code.c_str(); try { auto result = spirit::parse(code_raw, nodes_file); @@ -227,12 +267,63 @@ std::map SpecialParser::parse(std::string code) { } catch(spirit::parser_error x) { int offset = x.where - code_raw; int col = offset - last_line_start; - (void) col; // Mark the variable unused std::cerr << "Parse error on line " << lineno << std::endl; + std::string found; + auto iter = x.where; + while(iter != code_raw + code.length() && !isspace(*iter) && *iter != '=') + found += *iter++; + throw xSpecParseError(found, x.descriptor, lineno, col, context); } return specials; } +const char*const xSpecParseError::messages[NUM_PARSE_ERR] = { + "Unable to parse special node due to unexpected token - ", + "Redefinition of symbol ", + "opcode", + "identifier", + "one of ['sdf', 'msg', 'pic', 'ex1', 'ex2', 'goto']", + "'='", + "integer", + "value (integer or known symbol)", + "end of line", +}; + +xSpecParseError::xSpecParseError(std::string found, eParseError expect, int line, int col, std::string file) : + found(found), + err(expect), + line(line), + col(col), + file(file), + msg(nullptr) {} + +xSpecParseError::~xSpecParseError() throw() { + if(msg != nullptr) + delete[] msg; +} + +const char* xSpecParseError::what() const throw() { + if(msg == nullptr) { + std::stringstream msgbld; + if(err > double_def) + msgbld << "Expected "; + msgbld << messages[err]; + if(err > double_def) + msgbld << " but "; + if(err != double_def) + msgbld << "found "; + msgbld << "'" << found << "' (in " << file << '@' << line << ':' << col << ')'; + size_t len = msgbld.tellp(); + msgbld.seekg(0); + char* s = new(std::nothrow) char[len + 10]; + std::fill_n(s, len + 10, 0); + std::copy(std::istreambuf_iterator(msgbld), std::istreambuf_iterator(), s); + msg = s; + } + return msg; +} + + void test_special_parse(std::string file); // Suppress "no prototype" warning void test_special_parse(std::string file) { std::ostringstream code; @@ -240,7 +331,7 @@ void test_special_parse(std::string file) { code << fin.rdbuf(); fin.close(); SpecialParser parser; - auto specials = parser.parse(code.str()); + auto specials = parser.parse(code.str(), file); std::ofstream fout(file + ".out"); for(auto p : specials) { fout << "Special node ID " << p.first << ":\n";