/* * ===================================================================================== * * 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: surkeh@protonmail.com * * ===================================================================================== */ #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(6) { //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(6) { 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(); if (haveRequiredMappings()) haveMapInfo_ = true; } 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); } } } if (haveRequiredMappings()) 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"}; std::string pickupID; std::string trash; bool havePickupID = false; if (lines.size() < 2) { throw std::runtime_error ( makeErrorMessage ("error: Pickup entity requires minimum of two lines (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: Invalid Pickup position", lines)); } } else if (type == "pickupType") { std::istringstream iss (lines[i]); // UInt8 pickupType ID if ( ! (iss >> trash >> trash >> pickupID)) { throw std::runtime_error ( makeErrorMessage ("error: Format of Pickup ID line is invalid", lines)); } havePickupID = true; } } if (havePickupID) { auto pickupIter = entityMap_.find (pickupID); if ( pickupIter == entityMap_.end()) { throw std::runtime_error( makeErrorMessage ("error: Pickup ID is invalid", 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 ID was not in the entity", 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: Invalid PlayerSpawn position", lines)); } } else if (type == "angles") { std::istringstream iss (lines[i]); // UInt8 pickupType ID if ( ! (iss >> trash >> trash >> angle)) { throw std::runtime_error ( makeErrorMessage ("error: Invalid PlayerSpawn angle", 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) { std::stringstream ss; std::map::const_iterator it; switch (team) { case 0: convertedLines.push_back ("\"classname\" \"info_player_deathmatch\"\n"); break; case 1: it = entityMap_.find ("team1"); ss << "\"classname\" \"" << it->second << "\"" << std::endl; convertedLines.push_back (ss.str()); break; case 2: it = entityMap_.find ("team2"); ss << "\"classname\" \"" << it->second << "\"" << std::endl; convertedLines.push_back (ss.str()); break; } } else { convertedLines.push_back ("\"classname\" \"info_player_start\"\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 minimum of two lines (type and target)", lines)); } std::istringstream iss (lines[1]); // String32 target targetName if ( ! (iss >> trash >> trash >> targetName)) { throw std::runtime_error( makeErrorMessage ("error: Invalid JumpPad target", 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 minimum of two lines (type and target)", lines)); } std::istringstream iss (lines[1]); // String32 target targetName if ( ! (iss >> trash >> trash >> targetName)) { throw std::runtime_error ( makeErrorMessage ("error: Invalid Teleport target", 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 minimum of two lines (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: Invalid Target position", lines)); } } else if (type == "name") { std::istringstream iss (lines[i]); // UInt8 name uniqueName if ( ! (iss >> trash >> trash >> targetName)) { throw std::runtime_error ( makeErrorMessage ("error: Invalid Target \"target\"", 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: Invalid Target angle", lines)); } } } if (haveName) { auto targetIter = targetMap_.find (targetName); if (targetIter == targetMap_.end()) { //std::cerr << makeErrorMessage ("End-game camera Target is not a supported feature in id Tech games. Skipping", 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\" was not found in this Target entity", 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"); // While 0 is finish in Race, in CTS, checkpoints numbered from 0 to n convertedLines.push_back ("\"cnt\" \"0\"\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"); // While 0 is finish in Race, in CTS, checkpoints numbered from 0 to n convertedLines.push_back ("\"cnt\" \"1\"\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 one line (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: Invalid PointLight position", lines)); } } else if (type == "intensity") { std::istringstream iss (lines[i]); // Float intensity validFloat if ( ! (iss >> trash >> trash >> intensity)) { throw std::runtime_error ( makeErrorMessage ("error: Invalid PointLight intensity", lines)); } } else if (type == "color") { std::istringstream iss (lines[i]); // ColourXRGB32 color eightDigitHexValue if ( ! (iss >> trash >> trash >> color)) { throw std::runtime_error ( makeErrorMessage ("error: Invalid PointLight color", 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; // 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 Reflex Entity Mapping file contents into pickup map std::string line; while (std::getline (fin, line)) { std::istringstream iss (line); // Reflex ID corresponds to xonotic pickup name std::string id; std::string pickup; if ( ! (iss >> id >> pickup)) { throw std::runtime_error ("format error in Pickup .pck file " + mapFile); } entityMap_.insert (std::pair (id, pickup)); } } else { throw std::ios::failure ("Error: EntityConverter failed to open Reflex Entity Mapping file"); } fin.close(); } bool EntityConverter::haveRequiredMappings() { std::vector required = { "team1", "team2" }; for (std::string id : required) { auto pickupIter = entityMap_.find (id); if ( pickupIter == entityMap_.end()) { throw std::runtime_error ("error: Missing required entity mappings"); return false; } } return true; } 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=entityMap_.begin(); it!=entityMap_.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; }