/* * ===================================================================================== * * Filename: EntityConverter.cpp * * Description: Convert Reflex entities into Xonotic entities * * Version: 1.0 * 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.0), OUTPUT_PRECISION(10) { //MUST RUN extractMapInfo method after this constructor haveMapInfo_ = false; // game modes default to enabled ws_.cts = true; ws_.ctf = true; ws_.ffa = true; ws_.tdm = true; ws_.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.0), OUTPUT_PRECISION(10) { haveMapInfo_ = false; // game modes default to enabled ws_.cts = true; ws_.ctf = true; ws_.ffa = true; ws_.tdm = true; ws_.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 " + reflexMapFile ); } fin.close(); haveMapInfo_ = true; //DEBUG //printMapping(); //printTargetSources(); } 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; } void EntityConverter::extractMapInfo(const std::vector> &entities) { if( haveMapInfo_ ) { std::cerr << "Map info already extracted, doing nothing" << std::endl; } else { std::vector>::const_iterator it; for ( it=entities.begin(); it!=entities.end(); ++it ) { std::vector entity( *it ); 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( makeErrorMessage( "error: empty entity cannot be converted", lines )); } // second token is the type std::istringstream iss(lines[0]); if ( ! (iss >> trash >> type)) { throw std::runtime_error( makeErrorMessage( "error: type is required", lines )); } // If worldspawn, first reenable all gamemodes // then check worldspawn for disabled modes // then RETURN EMPTY VECTOR if ( type == "WorldSpawn" ) { ws_.cts = true; ws_.ctf = true; ws_.ffa = true; ws_.tdm = true; ws_.duel = true; // Each worldspawn can specify modes enabled/disabled for ( int i = 1; i < lines.size(); ++i ) { if ( lines[i].find("modeRace 0") != std::string::npos) { ws_.cts = false; } else if ( lines[i].find("modeCTF 0") != std::string::npos) { ws_.ctf = false; } else if ( lines[i].find("modeFFA 0") != std::string::npos) { ws_.ffa = false; } else if ( lines[i].find("modeTDM 0") != std::string::npos) { ws_.tdm = false; } else if ( lines[i].find("mode1v1 0") != std::string::npos) { ws_.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( makeErrorMessage( "error: Map info must be extracted prior to conversion", lines )); } // If unsupported entity, return empty vector std::vector empty; return empty; } /*----------------------------------------------------------------------------- * PROTECTED *-----------------------------------------------------------------------------*/ std::vector EntityConverter::convertPickup(const std::vector &lines) const { std::vector convertedLines; //can ignore angle of pickups in xonotic format std::string coords[3] = {"0.0", "0.0", "0.0"}; int pickupID; std::string trash; bool havePickupID = false; if ( lines.size() < 2 ) { throw std::runtime_error( makeErrorMessage( "error: Pickup entity requires at least type and ID", 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( makeErrorMessage( "error: Pickup entity requires coordinates", lines )); } } else if ( type == "pickupType" ) { std::istringstream iss(lines[i]); // UInt8 pickupType ID if ( ! (iss >> trash >> trash >> pickupID) ) { throw std::runtime_error( makeErrorMessage( "error: Pickup entity requires Pickup ID", lines )); } havePickupID = true; } } if ( havePickupID ) { auto pickupIter = pickupMap_.find(pickupID); if ( pickupIter == pickupMap_.end() ) { throw std::runtime_error( makeErrorMessage( "error: Pickup ID must be valid", lines )); } std::stringstream pickupStream; pickupStream << "\"classname\" \"" << pickupIter->second << "\"" << 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( makeErrorMessage( "error: Pickup requires position and pickup ID, missing 1 or both", lines )); } } /* *-------------------------------------------------------------------------------------- * 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) const { std::vector convertedLines; // Requires position coordinate std::string coords[3] = {"0.0", "0.0", "0.0"}; // Requires an angle, default to 0 degrees (floating point) std::string angle("0.0"); // 1-2 for corresponding team, 0 for deathmatch spawn int team = 0; std::string trash; bool isModeRace = ws_.cts; bool isModeCtf = ws_.ctf; bool isModeTdm = ws_.tdm; bool isModeFfa = ws_.ffa; bool isModeDuel = ws_.duel; 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( makeErrorMessage( "error: PlayerSpawn entity must have valid position coordinates if specified", lines )); } } else if ( type == "angles" ) { std::istringstream iss(lines[i]); // UInt8 pickupType ID if ( ! (iss >> trash >> trash >> angle )) { throw std::runtime_error( makeErrorMessage( "error: Pickup entity requires Pickup ID", lines )); } } // Bool8 modeX 0 indicates this spawn is not for game mode X else if ( type == "modeRace" ) { isModeRace = false; } else if ( type == "modeCTF" ) { isModeCtf = false; } else if ( type == "modeTDM" ) { isModeTdm = false; } else if ( type == "modeFFA" ) { isModeFfa = false; } else if ( type == "mode1v1" ) { isModeDuel = false; } 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 ( isModeCtf || isModeTdm || isModeFfa || isModeDuel ) { 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 { 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\" \"" << adjustAngleForHandedness(angle) << "\"" << std::endl; convertedLines.push_back (angleStream.str() ); return convertedLines; } std::vector EntityConverter::convertJumpPad(const std::vector &lines) const { std::vector convertedLines; std::string targetName; std::string trash; if ( lines.size() < 2 ) { throw std::runtime_error( makeErrorMessage( "error: JumpPad entity requires at least type and target name", lines )); } std::istringstream iss(lines[1]); // String32 target targetName if ( ! (iss >> trash >> trash >> targetName) ) { throw std::runtime_error( makeErrorMessage( "error: JumpPad entity requires target name", lines )); } 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) const { std::vector convertedLines; std::string targetName; std::string trash; if ( lines.size() < 2 ) { throw std::runtime_error( makeErrorMessage( "error: Teleport entity requires at least type and target name", lines )); } std::istringstream iss(lines[1]); // String32 target targetName if ( ! (iss >> trash >> trash >> targetName) ) { throw std::runtime_error( makeErrorMessage( "error: Teleport entity requires target name", lines )); } 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) const { std::vector convertedLines; //position and name required, angles optional std::string coords[3] = {"0.0", "0.0", "0.0"}; std::string targetName; std::string angle = "0.0"; std::string trash; bool haveName = false; if ( lines.size() < 3 ) { throw std::runtime_error( makeErrorMessage( "error: Target entity requires at least type and name", 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( makeErrorMessage( "error: Target entity requires coordinates", lines )); } } else if ( type == "name" ) { std::istringstream iss(lines[i]); // UInt8 name uniqueName if ( ! (iss >> trash >> trash >> targetName) ) { throw std::runtime_error( makeErrorMessage( "error: Target entity requires target name", lines )); } 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( makeErrorMessage( "error: Target entity requires target angle if specified", lines )); } } } if ( haveName) { auto targetIter = targetMap_.find(targetName); if ( targetIter == targetMap_.end() ) { std::cerr << makeErrorMessage("EntityConverter doesn't know what the source of a Target entity with the following attributes. This entity will not be converted. It is probably an unsupported entity type or feature. (e.g. game over camera)", lines); std::vector empty; return empty; } if ( targetIter->second == "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 ( targetIter->second == "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 every time std::stringstream angleStream; angleStream << "\"angle\" \"" << adjustAngleForHandedness(angle) << "\"" << std::endl; convertedLines.push_back( angleStream.str() ); return convertedLines; } else { throw std::runtime_error( makeErrorMessage( "error: Target entity requires position coordinates and targetname", lines )); } } std::vector EntityConverter::convertRaceStart(const std::vector &lines) const { 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) const { 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) const { std::vector convertedLines; //position and intensity required, color optional std::string coords[3] = {"0.0", "0.0", "0.0"}; //default to a typical value std::string intensity = "1.0"; //color is hex 8 digits //default to white if no color specified std::string color = "ff000000"; std::string trash; bool haveColor = false; if ( lines.size() < 2 ) { throw std::runtime_error( makeErrorMessage( "error: PointLight entity requires at least type", 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( makeErrorMessage( "error: PointLight entity requires valid position coordinates", lines )); } } else if ( type == "intensity" ) { std::istringstream iss(lines[i]); // Float intensity validFloat if ( ! (iss >> trash >> trash >> intensity) ) { throw std::runtime_error( makeErrorMessage( "error: PointLight intensity keyword must be followed by a value", lines )); } } else if ( type == "color" ) { std::istringstream iss(lines[i]); // ColourXRGB32 color eightDigitHexValue if ( ! (iss >> trash >> trash >> color) ) { throw std::runtime_error( makeErrorMessage( "error: PointLight color keyword must be followed by a value", lines )); } haveColor = true; } } 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() ); float red; float green; float blue; if ( haveColor ) { // Convert 32bit hex RGBA value (ALPHA ALWAYS FULL) into RGB values hexToRGB(color, red, green, blue); } std::stringstream colorStream; colorStream << "\"_color\" \"" << red << " " << green << " " << blue << "\"" << std::endl; convertedLines.push_back (colorStream.str() ); return convertedLines; } 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 Pickup .pck file " + mapFile ); } pickupMap_.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::fixed << std::setprecision(OUTPUT_PRECISION) << c; return ss.str(); } std::string EntityConverter::adjustAngleForHandedness(const std::string &angle) const { std::istringstream iss(angle); float a; iss >> a; a = -a + 90.0; std::stringstream ss; ss << std::fixed << std::fixed << std::setprecision(OUTPUT_PRECISION) << a; return ss.str(); } void EntityConverter::hexToRGB(const std::string &hex, float &r, float &g, float &b) const { unsigned int value; std::stringstream ss; ss << std::hex << hex; ss >> value; // BYTE ORDER IS ARGB // Alpha value is always full -> can be ignored safely // Get each value and normalize 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); } std::string EntityConverter::makeErrorMessage(const std::string message, const std::vector entity) const { std::stringstream ss; ss << std::endl << message << std::endl; std::vector::const_iterator it; for ( it=entity.begin(); it!=entity.end(); ++it ) { ss << *it << std::endl; } ss << std::endl; return ss.str(); } /*----------------------------------------------------------------------------- * PRIVATE *-----------------------------------------------------------------------------*/ // DEBUG void EntityConverter::printMapping() const { std::cout << std::endl << "Reflex pickup ID mapped to Xonotic pickup names: " << std::endl; std::map::const_iterator it; for ( it=pickupMap_.begin(); it!=pickupMap_.end(); ++it ) std::cout << it->first << " => " << it->second << std::endl; } // DEBUG void EntityConverter::printTargetSources() const { std::cout << std::endl << "Target and Sources: " << std::endl; std::map::const_iterator it; for ( it=targetMap_.begin(); it!=targetMap_.end(); ++it ) std::cout << it->first << " => " << it->second << std::endl; }