/* * ===================================================================================== * * Filename: EntityConverter.cpp * * Description: Convert Reflex entities into Xonotic entities * * Version: 0.1 * Created: 06/05/2017 07:15:25 PM * Revision: none * Compiler: gcc * * Author: suhrke@teknik.io * * ===================================================================================== */ #include "EntityConverter.hpp" #include #include #include #include #include #include /*----------------------------------------------------------------------------- * PUBLIC *-----------------------------------------------------------------------------*/ EntityConverter::EntityConverter(std::string entityMapFile) : OFFSET_PLAYER(32.0), OFFSET_PICKUP(2.0) { //MUST RUN extractMapInfo method after this constructor haveMapInfo_ = false; // game modes default to enabled mapinfo_.cts = true; mapinfo_.ctf = true; mapinfo_.ffa = true; mapinfo_.tdm = true; mapinfo_.duel = true; mapEntities(entityMapFile); } EntityConverter::EntityConverter(std::string entityMapFile, std::string reflexMapFile) : OFFSET_PLAYER(32.0), OFFSET_PICKUP(2.0) { haveMapInfo_ = false; // game modes default to enabled mapinfo_.cts = true; mapinfo_.ctf = true; mapinfo_.ffa = true; mapinfo_.tdm = true; mapinfo_.duel = true; mapEntities(entityMapFile); // Pre-scan for info needed by converter std::ifstream fin; fin.open(reflexMapFile); if ( fin.is_open() ) { //Extract the source type of targets (teleporters or jump pads) std::string line; while (std::getline(fin, line)) { extractFromEntity(line, fin); } } else { throw std::ios::failure( "Error: EntityConverter failed to open .map file" ); } fin.close(); haveMapInfo_ = true; //DEBUG //printMapping(); //printTargetSources(); } /* *-------------------------------------------------------------------------------------- * Class: EntityConverter * Method: EntityConverter :: extractMapInfo * Description: Read through entities, matching related as necessary * Note: For now, accomplishes the same goal as the pre-scan * constructor *-------------------------------------------------------------------------------------- */ void EntityConverter::extractMapInfo(std::queue> entities) { if( haveMapInfo_ ) { std::cerr << "Map info already extracted, doing nothing" << std::endl; } else { while ( ! entities.empty() ) { std::vector entity = entities.front(); entities.pop(); std::stringstream ss; std::copy(entity.begin(), entity.end(), std::ostream_iterator(ss, "\n")); std::string nextLine; if ( getline(ss, nextLine )) { extractFromEntity(nextLine, ss); } } } haveMapInfo_ = true; } std::vector EntityConverter::convert(std::vector lines) { if ( haveMapInfo_ ) { std::string attribute; std::string trash; //unused tokens std::string type; if ( lines.size() < 1 ) { throw std::runtime_error("error: empty entity cannot be converted"); } // second token is the type std::istringstream iss(lines[0]); if ( ! (iss >> trash >> type)) { throw std::runtime_error("error: type is required"); } // RETURN EMPTY VECTOR if type is WorldSpawn if ( type == "WorldSpawn" ) { for ( int i = 1; i < lines.size(); ++i ) { // only works for maps that support race AND other modes if ( lines[i].find("modeRace 0") != std::string::npos) { std::cout << "disabled race because worldspawn" << std::endl; mapinfo_.cts = false; } else if ( lines[i].find("modeCTF 0") != std::string::npos) { std::cout << "disabled ctf because worldspawn" << std::endl; mapinfo_.ctf = false; } else if ( lines[i].find("modeFFA 0") != std::string::npos) { std::cout << "disabled ffa because worldspawn" << std::endl; mapinfo_.ffa = false; } else if ( lines[i].find("modeTDM 0") != std::string::npos) { std::cout << "disabled tdm because worldspawn" << std::endl; mapinfo_.tdm = false; } else if ( lines[i].find("mode1v1 0") != std::string::npos) { std::cout << "disabled duel because worldspawn" << std::endl; mapinfo_.duel = false; } } } else if ( type == "Pickup" ) { return convertPickup(lines); } else if ( type == "PlayerSpawn" ) { return convertPlayerSpawn(lines); } else if ( type == "JumpPad" ) { return convertJumpPad(lines); } else if ( type == "Teleporter" ) { return convertTeleporter(lines); } else if ( type == "Target" ) { return convertTarget(lines); } else if ( type == "RaceStart" ) { return convertRaceStart(lines); } else if ( type == "RaceFinish" ) { return convertRaceFinish(lines); } } else { throw std::runtime_error( "error: Map info must be extracted prior to conversion" ); } // If unsupported entity, return empty vector std::vector empty; return empty; } /*----------------------------------------------------------------------------- * PROTECTED *-----------------------------------------------------------------------------*/ std::vector EntityConverter::convertPickup(std::vector &lines) { std::vector convertedLines; //can ignore angle of pickups in xonotic format std::string coords[3]; int pickupID; std::string trash; bool havePosition = false; bool havePickupID = false; if ( lines.size() < 3 ) { throw std::runtime_error("error: Pickup entity requires at least 3 lines"); } for (int i = 1; i < lines.size(); i++) { std::string type = getAttributeType(lines[i]); if ( type == "position" ) { std::istringstream iss(lines[i]); // Vector3 position coord0 coord1 coord2 if ( ! (iss >> trash >> trash >> coords[0] >> coords[1] >> coords[2])) { throw std::runtime_error("error: Pickup entity requires coordinates"); } havePosition = true; } else if ( type == "pickupType" ) { std::istringstream iss(lines[i]); // UInt8 pickupType ID if ( ! (iss >> trash >> trash >> pickupID) ) { throw std::runtime_error("error: Pickup entity requires Pickup ID"); } havePickupID = true; } } if ( havePosition && havePickupID ) { std::stringstream oss; oss << "\"classname\" \"" << pickupMapping_[pickupID] << "\"" << std::endl; convertedLines.push_back ( oss.str() ); // coordinates reordered to x, z, y std::stringstream oss2; oss2 << "\"origin\" \"" << coords[0] << " " << coords[2] << " " << offset(coords[1], OFFSET_PICKUP) << "\"" << std::endl; convertedLines.push_back ( oss2.str() ); return convertedLines; } else { throw std::runtime_error("error: Pickup requires position and pickup ID, missing 1 or both"); } } /* *-------------------------------------------------------------------------------------- * Class: EntityConverter * Method: EntityConverter :: convertPlayerSpawn * Notes: REFLEX * -Optionally includes angle, team indicator, and 0 or more game * mode indicators (defaults to all modes enabled so this line * is used to disable a mode. eg Bool8 modeRace 0) *-------------------------------------------------------------------------------------- */ std::vector EntityConverter::convertPlayerSpawn(std::vector &lines) { std::vector convertedLines; // Requires position coordinate std::string coords[3]; // Requires an angle so if no reflex one is given, use 0 std::string angle("0"); // 1-2 for corresponding team, 0 for deathmatch spawn int team = 0; std::string trash; bool havePosition = false; bool isModeRace = true; if ( lines.size() < 2 ) { throw std::runtime_error("error: PlayerSpawn entity requires at least 2 lines"); } for (int i = 1; i < lines.size(); i++) { std::string type = getAttributeType(lines[i]); if ( type == "position" ) { std::istringstream iss(lines[i]); // Vector3 position coord0 coord1 coord2 if ( ! (iss >> trash >> trash >> coords[0] >> coords[1] >> coords[2])) { throw std::runtime_error("error: PlayerSpawn entity requires position coordinates"); } havePosition = true; } else if ( type == "angles" ) { std::istringstream iss(lines[i]); // UInt8 pickupType ID if ( ! (iss >> trash >> trash >> angle )) { throw std::runtime_error("error: Pickup entity requires Pickup ID"); } } else if ( type == "modeRace" ) { isModeRace = false; // Bool8 modeRace 0 indicates this spawn is not for race } else if ( type == "teamA" ) { team = 2; // Bool8 teamA 0 indicates teamB only } else if ( type == "teamB" ) { team = 1; // Bool8 teamB 0 indicates teamA only } } if ( havePosition ) { // Will convert all race points to dm/team spawns on maps that support race AND others if ( mapinfo_.ctf || mapinfo_.tdm || mapinfo_.ffa || mapinfo_.duel ) { switch (team) { case 0: convertedLines.push_back ( "\"classname\" \"info_player_deathmatch\"\n" ); break; case 1: convertedLines.push_back ( "\"classname\" \"info_player_team1\"\n" ); break; case 2: convertedLines.push_back ( "\"classname\" \"info_player_team2\"\n" ); break; } } else if ( mapinfo_.cts && isModeRace ) { convertedLines.push_back ( "\"classname\" \"info_player_race\"\n" ); // Reflex maps have only start and finish, point to start on spawn convertedLines.push_back ( "\"target\" \"cp1\"\n" ); // Reflex maps are only cts, set spawn to cts-only type convertedLines.push_back ( "\"race_place\" \"-1\"\n" ); } std::stringstream oss; // coordinates reordered to x, z, y oss << "\"origin\" \"" << coords[0] << " " << coords[2] << " " << offset(coords[1], OFFSET_PLAYER) << "\"" << std::endl; convertedLines.push_back ( oss.str() ); std::stringstream oss2; oss2 << "\"angle\" \"" << angle << "\"" << std::endl; convertedLines.push_back ( oss2.str() ); return convertedLines; } else { throw std::runtime_error("error: PlayerSpawn entity requires position coordinates"); } } std::vector EntityConverter::convertJumpPad(std::vector &lines) { std::vector convertedLines; std::string targetName; std::string trash; if ( lines.size() < 2 ) { throw std::runtime_error("error: JumpPad entity requires at least 2 lines"); } std::istringstream iss(lines[1]); // String32 target targetName if ( ! (iss >> trash >> trash >> targetName) ) { throw std::runtime_error("error: JumpPad entity requires target name"); } convertedLines.push_back ( "\"classname\" \"trigger_push\"\n" ); std::stringstream oss; oss << "\"target\" \"" << targetName << "\"" << std::endl; convertedLines.push_back ( oss.str() ); return convertedLines; } std::vector EntityConverter::convertTeleporter(std::vector &lines) { std::vector convertedLines; std::string targetName; std::string trash; if ( lines.size() < 2 ) { throw std::runtime_error("error: Teleport entity requires at least 2 lines"); } std::istringstream iss(lines[1]); // String32 target targetName if ( ! (iss >> trash >> trash >> targetName) ) { throw std::runtime_error( "error: Teleport entity requires target name" ); } convertedLines.push_back ( "\"classname\" \"trigger_teleport\"\n" ); std::stringstream oss; oss << "\"target\" \"" << targetName << "\"" << std::endl; convertedLines.push_back ( oss.str() ); return convertedLines; } std::vector EntityConverter::convertTarget(std::vector &lines) { std::vector convertedLines; //position and name required, angles optional std::string coords[3]; std::string targetName; std::string angle; std::string trash; bool havePosition = false; bool haveName = false; bool haveAngle = false; if ( lines.size() < 3 ) { throw std::runtime_error("error: Target entity requires at least 3 lines"); } for (int i = 1; i < lines.size(); i++) { std::string type = getAttributeType(lines[i]); if ( type == "position" ) { std::istringstream iss(lines[i]); // Vector3 position coord0 coord1 coord2 if ( ! (iss >> trash >> trash >> coords[0] >> coords[1] >> coords[2])) { throw std::runtime_error( "error: Target entity requires coordinates" ); } havePosition = true; } else if ( type == "name" ) { std::istringstream iss(lines[i]); // UInt8 name uniqueName if ( ! (iss >> trash >> trash >> targetName) ) { throw std::runtime_error( "error: Target entity requires target name" ); } haveName = true; } else if ( type == "angles" ) { std::istringstream iss(lines[i]); // Vector3 angles angle notapplicable notapplicable if ( ! (iss >> trash >> trash >> angle) ) { throw std::runtime_error( "error: Target entity requires target angle if specified" ); } haveAngle = true; } } if ( havePosition && haveName) { if ( targetMap_[targetName] == "Teleporter") { convertedLines.push_back ( "\"classname\" \"misc_teleporter_dest\"\n" ); // coordinates reordered to x, z, y // teleporter height is OFFSET std::stringstream oss; oss << "\"origin\" \"" << coords[0] << " " << coords[2] << " " << offset(coords[1], OFFSET_PLAYER) << "\"" << std::endl; convertedLines.push_back ( oss.str() ); } else if ( targetMap_[targetName] == "JumpPad") { convertedLines.push_back ( "\"classname\" \"target_position\"\n" ); // coordinates reordered to x, z, y std::stringstream oss; oss << "\"origin\" \"" << coords[0] << " " << coords[2] << " " << coords[1] << "\"" << std::endl; convertedLines.push_back ( oss.str() ); } std::stringstream oss; oss << "\"targetname\" \"" << targetName << "\"" << std::endl; convertedLines.push_back ( oss.str() ); // Write angle only if position and name exist if ( haveAngle ) { std::stringstream oss2; oss2 << "\"angle\" \"" << angle << "\"" << std::endl; convertedLines.push_back (oss2.str() ); } return convertedLines; } else { throw std::runtime_error("error: Target entity requires position coordinates and targetname"); } } std::vector EntityConverter::convertRaceStart(std::vector &lines) { std::vector convertedLines; convertedLines.push_back ("\"classname\" \"trigger_race_checkpoint\"\n"); convertedLines.push_back ("\"targetname\" \"cp1\"\n"); convertedLines.push_back ("\"cnt\" \"1\"\n"); return convertedLines; } std::vector EntityConverter::convertRaceFinish(std::vector &lines) { std::vector convertedLines; convertedLines.push_back ("\"classname\" \"trigger_race_checkpoint\"\n"); convertedLines.push_back ("\"targetname\" \"finish\"\n"); convertedLines.push_back ("\"cnt\" \"0\"\n"); return convertedLines; } /*----------------------------------------------------------------------------- * PRIVATE *-----------------------------------------------------------------------------*/ std::string EntityConverter::getAttributeType(std::string line) { std::string type; std::string dataType; std::istringstream iss(line); if ( ! (iss >> dataType >> type )) { return std::string(); } return type; } void EntityConverter::mapEntities(std::string mapFile) { std::ifstream fin; fin.open(mapFile); if ( fin.is_open() ) { //Read .ent contents into pickup map std::string line; while (std::getline(fin, line)) { std::istringstream iss(line); // Reflex ID corresponds to xonotic pickup name int id; std::string pickup; if ( ! (iss >> id >> pickup)) { throw std::runtime_error( "format error in .ent file" ); } pickupMapping_.insert ( std::pair(id, pickup) ); } } else { throw std::ios::failure( "Error: EntityConverter failed to open .ent file" ); } fin.close(); } void EntityConverter::extractFromEntity(std::string &line, std::istream &is) { std::string trash; std::string targetName; if ( line.find("type Teleporter") != std::string::npos) { std::getline(is, line); std::istringstream iss(line); if ( ! (iss >> trash >> trash >> targetName)) { throw std::runtime_error( "format error in .map file"); } targetMap_.insert ( std::pair(targetName, "Teleporter") ); } else if ( line.find("type JumpPad") != std::string::npos) { std::getline(is, line); std::istringstream iss(line); if ( ! (iss >> trash >> trash >> targetName)) { throw std::runtime_error( "format error in .map file"); } targetMap_.insert ( std::pair(targetName, "JumpPad") ); } } std::string EntityConverter::offset(std::string value, float amount) { std::istringstream iss(value); float c; iss >> c; c += amount; std::stringstream ss; ss << std::fixed << std::setprecision(5) << c; return ss.str(); } // DEBUG void EntityConverter::printMapping() { std::map::iterator it; for (it=pickupMapping_.begin(); it!=pickupMapping_.end(); ++it) std::cout << it->first << " => " << it->second << std::endl; } // DEBUG void EntityConverter::printTargetSources() { std::map::iterator it; for (it=targetMap_.begin(); it!=targetMap_.end(); ++it) std::cout << it->first << " => " << it->second << std::endl; }