reflex2q3/ReflexToQ3/includes/EntityConverter.cpp
2017-07-06 17:30:21 -07:00

798 lines
23 KiB
C++

/*
* =====================================================================================
*
* 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 <exception>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <sstream>
/*-----------------------------------------------------------------------------
* PUBLIC
*-----------------------------------------------------------------------------*/
EntityConverter::EntityConverter(const std::string &entityMapFile) :
OFFSET_PLAYER(32.0), OFFSET_PICKUP(2.0), BRIGHTNESS_ADJUST(50.0)
{
//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)
{
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" );
}
fin.close();
haveMapInfo_ = true;
//DEBUG
//printMapping();
//printTargetSources();
}
void
EntityConverter::extractMapInfo(std::queue<std::vector<std::string>> entities)
{
if( haveMapInfo_ ) {
std::cerr << "Map info already extracted, doing nothing" << std::endl;
}
else {
while ( ! entities.empty() ) {
std::vector<std::string> entity( entities.front() );
entities.pop();
std::stringstream ss;
std::copy(entity.begin(), entity.end(),
std::ostream_iterator<std::string>(ss, "\n"));
std::string nextLine;
if ( getline(ss, nextLine )) {
extractFromEntity(nextLine, ss);
}
}
}
haveMapInfo_ = true;
}
void
EntityConverter::extractMapInfo(const std::vector<std::vector<std::string>> &entities)
{
if( haveMapInfo_ ) {
std::cerr << "Map info already extracted, doing nothing" << std::endl;
}
else {
std::vector<std::vector<std::string>>::const_iterator it;
for ( it=entities.begin(); it!=entities.end(); ++it ) {
std::vector<std::string> entity( *it );
std::stringstream ss;
std::copy(entity.begin(), entity.end(),
std::ostream_iterator<std::string>(ss, "\n"));
std::string nextLine;
if ( getline(ss, nextLine )) {
extractFromEntity(nextLine, ss);
}
}
}
haveMapInfo_ = true;
}
std::vector<std::string>
EntityConverter::convert(const std::vector<std::string> &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");
}
// 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( "error: Map info must be extracted prior to conversion" );
}
// If unsupported entity, return empty vector
std::vector<std::string> empty;
return empty;
}
/*-----------------------------------------------------------------------------
* PROTECTED
*-----------------------------------------------------------------------------*/
std::vector<std::string>
EntityConverter::convertPickup(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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 ) {
auto pickupIter = pickupMap_.find(pickupID);
if ( pickupIter == pickupMap_.end() ) {
throw std::runtime_error("error: Pickup ID must be valid");
}
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("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<std::string>
EntityConverter::convertPlayerSpawn(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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 = ws_.cts;
bool isModeCtf = ws_.ctf;
bool isModeTdm = ws_.tdm;
bool isModeFfa = ws_.ffa;
bool isModeDuel = ws_.duel;
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");
}
}
// 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 ( havePosition ) {
// Will convert all race points to dm/team spawns on maps that support race AND others
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\" \"" << angle << "\"" << std::endl;
convertedLines.push_back (angleStream.str() );
return convertedLines;
}
else {
throw std::runtime_error("error: PlayerSpawn entity requires position coordinates");
}
}
std::vector<std::string>
EntityConverter::convertJumpPad(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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<std::string>
EntityConverter::convertTeleporter(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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<std::string>
EntityConverter::convertTarget(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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) {
auto targetIter = targetMap_.find(targetName);
if ( targetIter == targetMap_.end() ) {
std::cerr << "EntityConverter doesn't know what the source of a Target entity with the "
<< "following attributes. It is probably an unsupported entity type or feature. "
<< "(game over camera, etc). Returning an empty vector." << std::endl;
std::vector<std::string>::const_iterator it;
for ( it=lines.begin(); it!=lines.end(); ++it ) {
std::cerr << *it << std::endl;
}
std::cerr << std::endl;
std::vector<std::string> 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 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<std::string>
EntityConverter::convertRaceStart(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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<std::string>
EntityConverter::convertRaceFinish(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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<std::string>
EntityConverter::convertPointLight(const std::vector<std::string> &lines) const
{
std::vector<std::string> 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
float backgroundColor = 0.5; //!! Just a guess
hexToRGB(color, backgroundColor, 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" );
}
pickupMap_.insert ( std::pair<int, std::string>(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<std::string, std::string>(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<std::string, std::string>(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 background, float &r, float &g, float &b) const
{
unsigned int value;
std::stringstream ss;
ss << std::hex << hex;
ss >> value;
// BYTE ORDER IS ARGB
// Get each value and normalize
float sourceAlpha = ((value >> 24) & 0xFF) / 255.0;
float sourceRed = ((value >> 16) & 0xFF) / 255.0;
float sourceGreen = ((value >> 8) & 0xFF) / 255.0;
float sourceBlue = ((value) & 0xFF) / 255.0;
// Convert RGB value close to something close to the source RGBA value
r = ((1 - sourceAlpha) * background) + (sourceAlpha * sourceRed);
g = ((1 - sourceAlpha) * background) + (sourceAlpha * sourceGreen);
b = ((1 - sourceAlpha) * background) + (sourceAlpha * sourceBlue);
}
int
EntityConverter::adjustBrightness(const std::string &value) const
{
float inputBright;
std::stringstream ss(value);
ss >> inputBright;
return static_cast<int>(inputBright * BRIGHTNESS_ADJUST);
}
// DEBUG
void
EntityConverter::printMapping() const
{
std::cout << std::endl << "Reflex pickup ID mapped to Xonotic pickup names: " << std::endl;
std::map<int, std::string>::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<std::string, std::string>::const_iterator it;
for ( it=targetMap_.begin(); it!=targetMap_.end(); ++it )
std::cout << it->first << " => " << it->second << std::endl;
}