/* * ===================================================================================== * * 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(const std::string &entityMapFile) : OFFSET_PLAYER(32.0), OFFSET_PICKUP(2.0), BRIGHTNESS_ADJUST(50) { //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(const std::string &entityMapFile, const std::string &reflexMapFile) : OFFSET_PLAYER(32.0), OFFSET_PICKUP(2.0), BRIGHTNESS_ADJUST(50) { 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(const 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) { mapinfo_.cts = false; } else if ( lines[i].find("modeCTF 0") != std::string::npos) { mapinfo_.ctf = false; } else if ( lines[i].find("modeFFA 0") != std::string::npos) { mapinfo_.ffa = false; } else if ( lines[i].find("modeTDM 0") != std::string::npos) { mapinfo_.tdm = false; } else if ( lines[i].find("mode1v1 0") != std::string::npos) { 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 if ( type == "PointLight" ) { return convertPointLight(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(const 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 pickupStream; pickupStream << "\"classname\" \"" << pickupMapping_[pickupID] << "\"" << std::endl; convertedLines.push_back ( pickupStream.str() ); // coordinates reordered to x, z, y std::stringstream positionStream; positionStream << "\"origin\" \"" << coords[0] << " " << coords[2] << " " << offset(coords[1], OFFSET_PICKUP) << "\"" << std::endl; convertedLines.push_back ( positionStream.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(const 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 positionStream; // coordinates reordered to x, z, y positionStream << "\"origin\" \"" << coords[0] << " " << coords[2] << " " << offset(coords[1], OFFSET_PLAYER) << "\"" << std::endl; convertedLines.push_back ( positionStream.str() ); std::stringstream angleStream; angleStream << "\"angle\" \"" << angle << "\"" << std::endl; convertedLines.push_back (angleStream.str() ); return convertedLines; } else { throw std::runtime_error("error: PlayerSpawn entity requires position coordinates"); } } std::vector EntityConverter::convertJumpPad(const 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(const 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(const 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 targetStream; targetStream << "\"targetname\" \"" << targetName << "\"" << std::endl; convertedLines.push_back ( targetStream.str() ); // Write angle only if position and name exist if ( haveAngle ) { std::stringstream angleStream; angleStream << "\"angle\" \"" << angle << "\"" << std::endl; convertedLines.push_back( angleStream.str() ); } return convertedLines; } else { throw std::runtime_error("error: Target entity requires position coordinates and targetname"); } } std::vector EntityConverter::convertRaceStart(const 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(const 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; } std::vector EntityConverter::convertPointLight(const std::vector &lines) { std::vector convertedLines; //position and intensity required, color optional std::string coords[3]; std::string intensity; //color is hex 8 digits std::string color; std::string trash; bool havePosition = false; bool haveIntensity = false; bool haveColor = false; if ( lines.size() < 3 ) { throw std::runtime_error("error: PointLight 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: PointLight entity requires position coordinates" ); } havePosition = true; } else if ( type == "intensity" ) { std::istringstream iss(lines[i]); // Float intensity validFloat if ( ! (iss >> trash >> trash >> intensity) ) { throw std::runtime_error( "error: PointLight entity requires intensity value" ); } haveIntensity = true; } else if ( type == "color" ) { std::istringstream iss(lines[i]); // ColourXRGB32 color eightDigitHexValue if ( ! (iss >> trash >> trash >> color) ) { throw std::runtime_error( "error: PointLight entity requires valid float value if specified" ); } haveColor = true; } } if ( havePosition && haveIntensity) { convertedLines.push_back ( "\"classname\" \"light\"\n" ); // coordinates reordered to x, z, y std::stringstream positionStream; positionStream << "\"origin\" \"" << coords[0] << " " << coords[2] << " " << coords[1] << "\"" << std::endl; convertedLines.push_back ( positionStream.str() ); // convert intensity std::stringstream intensityStream; intensityStream << "\"light\" \"" << adjustBrightness(intensity) << "\"\n"; convertedLines.push_back ( intensityStream.str() ); if ( haveColor ) { std::stringstream colorStream; float red; float green; float blue; // Convert 32bit hex value into RGB values hexToRGB(color, red, green, blue); colorStream << "\"_color\" \"" << red << " " << green << " " << blue << "\"" << std::endl; convertedLines.push_back (colorStream.str() ); } return convertedLines; } else { throw std::runtime_error("error: Target entity requires position coordinates and targetname"); } } /*----------------------------------------------------------------------------- * PRIVATE *-----------------------------------------------------------------------------*/ std::string EntityConverter::getAttributeType(const std::string &line) const { std::string type; std::string dataType; std::istringstream iss(line); if ( ! (iss >> dataType >> type )) { return std::string(); } return type; } void EntityConverter::mapEntities(const 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(const std::string &line, std::istream &is) { std::string trash; std::string targetName; std::string nextLine; if ( line.find("type Teleporter") != std::string::npos) { std::getline(is, nextLine); std::istringstream iss(nextLine); 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, nextLine); std::istringstream iss(nextLine); if ( ! (iss >> trash >> trash >> targetName)) { throw std::runtime_error( "format error in .map file"); } targetMap_.insert ( std::pair(targetName, "JumpPad") ); } } std::string EntityConverter::offset(const std::string &value, const float amount) const { std::istringstream iss(value); float c; iss >> c; c += amount; std::stringstream ss; ss << std::fixed << std::setprecision(5) << c; return ss.str(); } void EntityConverter::hexToRGB(const std::string &hex, float &r, float &g, float &b) { unsigned int value; std::stringstream ss; ss << std::hex << hex; ss >> value; // get the necessary components from the right bytes r = ((value >> 16) & 0xFF) / 255.0; g = ((value >> 8) & 0xFF) / 255.0; b = ((value) & 0xFF) / 255.0; } int EntityConverter::adjustBrightness(const std::string &value) const { float inputBright; std::stringstream ss(value); ss >> inputBright; return static_cast(inputBright) * BRIGHTNESS_ADJUST; } // 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; }