#pragma region License /* License (OLC-3) ~~~~~~~~~~~~~~~ Copyright 2024 Joshua Sigona Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions or derivations of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions or derivative works in binary form must reproduce the above copyright notice. This list of conditions and the following disclaimer must be reproduced in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder 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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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. Portions of this software are copyright © 2024 The FreeType Project (www.freetype.org). Please see LICENSE_FT.txt for more information. All rights reserved. */ #pragma endregion #pragma once #include "olcUTIL_Geometry2D.h" #include "olcPixelGameEngine.h" #include #include #include "EnvironmentalAudio.h" #include "DEFINES.h" #include "safemap.h" #include "ItemMapData.h" #include "Class.h" #include "MonsterData.h" #include INCLUDE_MONSTER_DATA using MapName=std::string; using namespace olc; using namespace std::literals; struct XMLTag{ std::string tag; std::map data; const std::string FormatTagData(std::maptiles); friend std::ostream& operator << (std::ostream& os, XMLTag& rhs); std::string str(); int GetInteger(std::string dataTag); float GetFloat(std::string dataTag); double GetDouble(std::string dataTag); bool GetBool(std::string dataTag); std::string GetString(std::string dataTag)const; }; struct ForegroundTileTag:public XMLTag{ //Whether or not to hide this foreground tile bool hide=true; }; struct MapTag{ int width=0,height=0; int tilewidth=0,tileheight=0; bool optimized=false; //An optimized map will require us to flatten it out and use it as a single tile. bool provideOptimization=false; //An optimized map will require us to flatten it out and use it as a single tile. vi2d playerSpawnLocation; vi2d MapSize; //The number of tiles in width and height of this map. vi2d TileSize; //How large in pixels the map's tiles are. MapTag(); MapTag(int width,int height,int tilewidth,int tileheight); friend std::ostream& operator << (std::ostream& os, MapTag& rhs); }; struct LayerTag{ XMLTag tag; std::vector> tiles; std::string unlockCondition=""; std::string str(); }; struct SpawnerTag{ XMLTag ObjectData; std::vectormonsters; bool upperLevel=false; std::string bossNameDisplay; std::string str(); friend std::ostream& operator << (std::ostream& os, SpawnerTag& rhs); }; struct ZoneData{ geom2d::rectzone; bool isUpper=false; std::vectorproperties; }; struct NPCData{ std::string function=""; std::string name=""; std::string sprite=""; std::string unlockCondition=""; uint32_t roamingRange=0; vf2d spawnPos; NPCData(); NPCData(XMLTag npcTag); }; namespace MonsterTests{ class MonsterTest; } struct Map{ friend class AiL; friend class TMXParser; enum class MapType{ DUNGEON, BOSS, STORY, BLACKSMITH, WORLD_MAP, HUB }; private: MapTag MapData; std::string name; Renderable*optimizedTile=nullptr; std::vector TilesetData; std::vector LayerData; std::vectorenvironmentalAudioData; std::vectorstageLoot; std::vectornpcs; MapType mapType{}; std::string bgmSongName=""; std::string audioEvent{"Default Volume"}; std::unordered_mapdevCompletionTrialTime; BackdropName backdrop=""; std::setspawns; std::mapSpawnerData; //Spawn groups have IDs, mobs associate which spawner they are tied to via this ID. std::optional>spawnControllerIDs; std::map>ZoneData; public: Map(); void _SetMapData(MapTag data); bool skipLoadoutScreen=false; const MapTag&GetMapData()const; const MapType&GetMapType()const; const std::vector&GetStageLoot()const; const std::vector&GetLayers()const; const std::vector&GetEnvironmentalAudio()const; const float GetDevCompletionTime(Class cl)const; const MapName&GetMapName()const; const std::string_view GetMapDisplayName()const; const bool HasMoreSpawns()const; //Returns whether or not there are more spawns for the spawn controller. const int Spawn_pop(); //Grabs the next spawn controller ID and removes it from the stack. const Renderable*const GetOptimizedMap()const; const std::map>&GetZones()const; const std::string&GetDefaultAudioEvent()const; std::string FormatLayerData(std::ostream& os, std::vectortiles); std::string FormatSpawnerData(std::ostream& os, std::maptiles); friend std::ostream& operator << (std::ostream& os, Map& rhs); friend std::ostream& operator << (std::ostream& os, std::vector& rhs); }; struct Property{ std::string name; std::string value; int GetInteger(); float GetFloat(); double GetDouble(); bool GetBool(); }; struct StagePlate{ XMLTag tag; std::mapproperties; }; class TMXParser{ public: Map GetData(); private: Map parsedMapInfo; std::string fileName; bool buildingSpawner=false; SpawnerTag obj; int prevSpawner; ZoneData*prevZoneData=nullptr; EnvironmentalAudio*prevAudioData=nullptr; void ParseTag(std::string tag); int monsterPropertyTagCount=-1; bool inNPCTag=false; XMLTag monsterTag; XMLTag npcTag; XMLTag spawnerLinkTag; StagePlate*currentStagePlate=nullptr; std::optionalspawnControllerTag; std::vectoraccumulatedMonsterTags; std::mapstagePlates; bool infiniteMap=false; LayerTag*currentLayerTag=nullptr; public: TMXParser(std::string file); }; //#define TMX_PARSER_SETUP //Toggle for code-writing. #ifdef TMX_PARSER_SETUP #undef TMX_PARSER_SETUP #include "State_OverworldMap.h" extern bool _MAP_LOAD_INFO; extern bool _DEBUG_MAP_LOAD_INFO; const std::string XMLTag::FormatTagData(std::maptiles){ std::string displayStr=""; for (std::map::iterator it=data.begin();it!=data.end();it++) { displayStr+=" "+it->first+": "+it->second+"\n"; } return displayStr; } std::ostream& operator << (std::ostream& os, XMLTag& rhs){ os << rhs.tag <<"\n"<< rhs.FormatTagData(rhs.data) <<"\n"; return os; } std::ostream& operator << (std::ostream& os, MapTag& rhs){ os << "(width:"<tiles) { std::string displayStr; for (int i=0;itiles) { std::string displayStr; for (auto key:SpawnerData) { displayStr+=SpawnerData[key.first].str(); } return displayStr; } const Map::MapType&Map::GetMapType()const{ return mapType; } const MapName&Map::GetMapName()const{ return name; } void Map::_SetMapData(MapTag data){ MapData=data; } const MapTag&Map::GetMapData()const{ return MapData; } const std::vector&Map::GetEnvironmentalAudio()const{ return environmentalAudioData; } const std::map>&Map::GetZones()const{ return ZoneData; } const std::vector&Map::GetStageLoot()const{ return stageLoot; } const std::vector&Map::GetLayers()const{ return LayerData; } const std::string_view Map::GetMapDisplayName()const{ return name; } const bool Map::HasMoreSpawns()const{ return spawnControllerIDs.has_value()&&spawnControllerIDs.value().size()>0; } const int Map::Spawn_pop(){ if(!HasMoreSpawns())ERR("WARNING! Trying to pop from queue when there are no items! Make sure to use HasMoreSpawns() first!"); const int nextSpawnId=spawnControllerIDs.value().front(); spawnControllerIDs.value().pop(); return nextSpawnId; } const Renderable*const Map::GetOptimizedMap()const{ return optimizedTile; } const std::string&Map::GetDefaultAudioEvent()const{ return audioEvent; } NPCData::NPCData(){} NPCData::NPCData(XMLTag npcTag){ const std::arraytags={"Function","NPC Name","Roaming Range","Unlock Condition","Spritesheet","x","y"}; for(int i=0;i& rhs) { for(XMLTag&tag:rhs){ os << tag<<"\n"; } return os; } std::ostream& operator <<(std::ostream& os, Map& rhs) { os << rhs.MapData <<"\n"<< rhs.TilesetData <<"\n"<< rhs.FormatLayerData(os,rhs.LayerData) <<"\n"<< rhs.FormatSpawnerData(os,rhs.SpawnerData)<<"\n"; return os; } Map TMXParser::GetData() { return parsedMapInfo; } void TMXParser::ParseTag(std::string tag) { auto ReadNextTag=[&](){ XMLTag newTag; //First character is a '<' so we discard it. tag.erase(0,1); tag.erase(tag.length()-1,1); //Erase the first and last characters in the tag. Now parse by spaces. std::stringstream s(tag); //Turn it into a string stream to now parse into individual whitespaces. std::string data; while (s.good()) { int quotationMarkCount=0; bool pastEquals=false; data=""; bool valid=false; while(s.good()){ int character=s.get(); if(character=='"'){ quotationMarkCount++; } if(character==' '&"ationMarkCount%2==0){ valid=true; break; } data+=character; if(pastEquals&"ationMarkCount%2==0){ valid=true; break; } if(character=='='&"ationMarkCount%2==0){ pastEquals=true; } } if(valid&&data.length()>0){ if (newTag.tag.length()==0) { //Tag's empty, so first line is the tag. newTag.tag=data; #if _DEBUG if(_DEBUG_MAP_LOAD_INFO)LOG("Tag: "<unlockCondition=newTag.data["value"]; }else if(newTag.tag=="property"&&newTag.data["name"]=="Upper?"&&prevZoneData!=nullptr){ prevZoneData->isUpper=newTag.GetBool("value"); }else if (newTag.tag=="property"&&newTag.data["name"]=="Optimize"&&newTag.data["value"]=="true") { parsedMapInfo.MapData.optimized=true; }else if (newTag.tag=="property"&&newTag.data["name"]=="Create Optimization Map (Override)"&&newTag.data["value"]=="true") { parsedMapInfo.MapData.provideOptimization=true; }else if (newTag.tag=="property"&&newTag.data["name"]=="Boss Title Display") { parsedMapInfo.SpawnerData[prevSpawner].bossNameDisplay=newTag.data["value"]; }else if (newTag.tag=="property"&&newTag.data["name"]=="Level Type") { parsedMapInfo.mapType=Map::MapType(stoi(newTag.data["value"])); }else if (newTag.tag=="property"&&newTag.data["name"]=="Background Music") { if(newTag.data["value"]!="None"){ //None is a default value that we ignore. parsedMapInfo.bgmSongName=newTag.data["value"]; } }else if (newTag.tag=="property"&&newTag.data["name"].starts_with("Dev Completion Time")) { if(newTag.data.count("value")){ //None is a default value that we ignore. size_t classStartPos="Dev Completion Time - "s.length(); std::string className=newTag.data["name"].substr(classStartPos,newTag.data["name"].find(' ',classStartPos)-classStartPos); parsedMapInfo.devCompletionTrialTime[classutils::StringToClass(className)]=newTag.GetFloat("value"); } }else if (newTag.tag=="property"&&newTag.data["name"]=="Backdrop") { if(newTag.data["value"]!="None"){ //None is a default value that we ignore. parsedMapInfo.backdrop=newTag.data["value"]; } }else if(newTag.tag=="property"&&newTag.data["name"]=="Audio Event"){ parsedMapInfo.audioEvent=newTag.data["value"]; }else if (newTag.tag=="object"&&newTag.data["type"]=="AudioEnvironmentalSound") { parsedMapInfo.environmentalAudioData.emplace_back(); prevAudioData=&parsedMapInfo.environmentalAudioData.back(); prevAudioData->SetPos({newTag.GetFloat("x"),newTag.GetFloat("y")}); }else if (newTag.tag=="property"&&newTag.data["propertytype"]=="EnvironmentalSounds") { if(newTag.data["value"]!="None"){ //None is a default value that we ignore. prevAudioData->SetAudioName(newTag.data["value"]); } }else if (newTag.tag=="object"&&newTag.data["type"]=="PlayerSpawnLocation") { float width=1.f; float height=1.f; if(newTag.data.count("width")>0)width=newTag.GetFloat("width"); if(newTag.data.count("height")>0)height=newTag.GetFloat("height"); parsedMapInfo.MapData.playerSpawnLocation={int(newTag.GetFloat("x")+width/2),int(newTag.GetFloat("y")+height/2)}; }else if (newTag.tag=="object"&&newTag.data["type"]=="NPC") { if(inNPCTag)parsedMapInfo.npcs.push_back(NPCData{npcTag}); npcTag=newTag; inNPCTag=true; } else if (newTag.tag=="object"&&newTag.data["template"].starts_with("../maps/Monsters")) { monsterTag=newTag; monsterPropertyTagCount=0; }else if(newTag.tag=="property"&&spawnControllerTag.has_value()) { if(!parsedMapInfo.HasMoreSpawns())parsedMapInfo.spawnControllerIDs=std::queue{}; parsedMapInfo.spawnControllerIDs.value().push(newTag.GetInteger("value")); }else if (newTag.tag=="property"&&inNPCTag) { npcTag.data[newTag.data["name"]]=newTag.data["value"]; }else if (newTag.tag=="property"&&monsterPropertyTagCount==0) { std::string monsterName=ParseMonsterTemplateName(monsterTag); if(!MONSTER_DATA.count(monsterName))ERR(std::format("WARNING! Could not find monster type {}",monsterName)); parsedMapInfo.spawns.insert(monsterName); spawnerLinkTag=newTag; monsterTag.data["value"]=monsterName; monsterTag.data["spawnerLink"]=spawnerLinkTag.data["value"]; accumulatedMonsterTags.push_back(monsterTag); monsterPropertyTagCount=-1; }else if (newTag.tag=="object"&&newTag.data["type"]=="StagePlate") { if(newTag.GetInteger("id")!=0){ stagePlates[newTag.GetInteger("id")]={newTag}; currentStagePlate=&stagePlates.at(newTag.GetInteger("id")); } }else if(newTag.tag=="property"&¤tStagePlate!=nullptr){ currentStagePlate->properties[newTag.data["name"]]={newTag.data["name"],newTag.data["value"]}; }else if(newTag.tag=="property"&&prevZoneData!=nullptr){ //This is a property for a zone that doesn't fit into the other categories, we add it to the previous zone data encountered. prevZoneData->properties.push_back(newTag); }else if (newTag.tag=="object"&&newTag.data.find("type")!=newTag.data.end()){ //This is an object with a type that doesn't fit into other categories, we can add it to ZoneData. std::vector&zones=parsedMapInfo.ZoneData[newTag.data["type"]]; float width=1.f; float height=1.f; if(newTag.data.count("width")>0)width=newTag.GetFloat("width"); if(newTag.data.count("height")>0)height=newTag.GetFloat("height"); zones.emplace_back(geom2d::rect{{newTag.GetInteger("x"),newTag.GetInteger("y")},{int(width),int(height)}}); prevZoneData=&zones.back(); }else{ #ifdef _DEBUG if(_DEBUG_MAP_LOAD_INFO)LOG("Unsupported tag format! Ignoring."<<"\n"); #endif } #ifdef _DEBUG if(_DEBUG_MAP_LOAD_INFO)LOG("\n"<<"=============\n"); #endif } TMXParser::TMXParser(std::string file){ fileName=file; std::ifstream f(file,std::ios::in); std::string accumulator=""; //Initialize these so that they are valid entries for zones. parsedMapInfo.ZoneData["LowerZone"]; parsedMapInfo.ZoneData["UpperZone"]; parsedMapInfo.ZoneData["EndZone"]; parsedMapInfo.ZoneData["BossArena"]; while (f.good()&&!infiniteMap) { std::string data; f>>data; if (data.empty()) continue; if (accumulator.length()>0) { accumulator+=" "+data; //Check if it ends with '>' if (data[data.length()-1]=='>') { ParseTag(accumulator); accumulator=""; } } else if (data[0]=='<') { //Beginning of XML tag. accumulator=data; if(accumulator.length()>1&&accumulator.at(1)=='/'){ accumulator=""; //Restart because this is an end tag. } if(accumulator.length()>1&&accumulator.find('>')!=std::string::npos){ accumulator=""; //Restart because this tag has nothing in it! } }else{ //Start reading in data for this layer. std::vectorrowData; while (data.find(",")!=std::string::npos) { std::string datapiece = data.substr(0,data.find(",")); data = data.substr(data.find(",")+1,std::string::npos); rowData.push_back(stoi(datapiece)); } if (data.length()) { rowData.push_back(stoi(data)); } parsedMapInfo.LayerData[parsedMapInfo.LayerData.size()-1].tiles.push_back(rowData); } } if(inNPCTag){ parsedMapInfo.npcs.push_back(NPCData{npcTag}); } if(infiniteMap){ #if _DEBUG if(_DEBUG_MAP_LOAD_INFO)LOG("Infinite map detected. Parsing stopped early."); #endif } for(XMLTag&monster:accumulatedMonsterTags){ parsedMapInfo.SpawnerData[monster.GetInteger("spawnerLink")].monsters.push_back(monster); } for(auto&spawnerData:parsedMapInfo.SpawnerData){ SpawnerTag&spawner=spawnerData.second; for(auto&zoneData:parsedMapInfo.ZoneData){ if(zoneData.first=="UpperZone"){ std::vector&zones=zoneData.second; for(ZoneData&zone:zones){ if(geom2d::overlaps(zone.zone,geom2d::rect{{spawner.ObjectData.GetInteger("x"),spawner.ObjectData.GetInteger("y")},{spawner.ObjectData.GetInteger("width"),spawner.ObjectData.GetInteger("height")}})){ spawner.upperLevel=true; goto continueSpawnerLoop; } } } } continueSpawnerLoop: continue; } std::sort(parsedMapInfo.TilesetData.begin(),parsedMapInfo.TilesetData.end(),[](XMLTag&t1,XMLTag&t2){return t1.GetInteger("firstgid")idToIndexMap; //Since the original map data relies on IDs decided by Tiled and we are condensing all this data into a vector of connection points, each connection point is going to be in a different ID. //therefore, we need to convert the Tiled IDs into whatever vector index we insert each connection into for State_OverworldMap::connections. for(auto key:stagePlates){ StagePlate&plate=key.second; idToIndexMap[plate.tag.GetInteger("id")]=int(State_OverworldMap::connections.size()); ConnectionPoint newConnection={{{plate.tag.GetFloat("x"),plate.tag.GetFloat("y")},{plate.tag.GetFloat("width"),plate.tag.GetFloat("height")}},plate.properties["Type"].value,plate.tag.data["name"],plate.properties["Map"].value,plate.properties["Unlock Condition"].value,{}}; int iterationCount=0; for(auto key2:plate.properties){ if(key2.first.starts_with("Connection ")){ int direction=stoi(key2.first.substr("Connection "s.length(),1))-1; int&neighborElement=newConnection.neighbors[direction]; if(neighborElement!=-1)ERR(std::format("WARNING! Connection Point in direction {} is already occupied by node {}!",direction,neighborElement)); newConnection.neighbors[direction]=key2.second.GetInteger(); } } State_OverworldMap::connections.push_back(newConnection); } for(ConnectionPoint&connection:State_OverworldMap::connections){ for(int&val:connection.neighbors){ if(idToIndexMap.count(val)){ val=idToIndexMap.at(val); //Convert from given Tiled ID to indexed ID in State_OverworldMap::connections } } } for(int counter=0;ConnectionPoint&connection:State_OverworldMap::connections){ for(int directionInd=0;int val:connection.neighbors){ if(val!=-1){ ConnectionPoint&neighbor=State_OverworldMap::connections.at(val); //Find the opposite slot. int targetInd=-1; #pragma region Opposite Direction Calculation switch(directionInd){ case ConnectionPoint::NORTH:{ targetInd=ConnectionPoint::SOUTH; }break; case ConnectionPoint::EAST:{ targetInd=ConnectionPoint::WEST; }break; case ConnectionPoint::SOUTH:{ targetInd=ConnectionPoint::NORTH; }break; case ConnectionPoint::WEST:{ targetInd=ConnectionPoint::EAST; }break; } #pragma endregion if(neighbor.neighbors[targetInd]==-1){ //We insert our neighbor pairing here. neighbor.neighbors[targetInd]=counter; } } directionInd++; } counter++; } #ifdef _DEBUG if(_DEBUG_MAP_LOAD_INFO)LOG("Parsed Map Data:\n"<