Files
reflex2q3/src/EntityConverter.cpp

858 lines
24 KiB
C++

/*
* =====================================================================================
*
* 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 <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),
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<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);
}
}
}
if (haveRequiredMappings())
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 (
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<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] = {"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<std::string>
EntityConverter::convertPlayerSpawn (const std::vector<std::string> &lines) const
{
std::vector<std::string> 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<std::string, std::string>::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<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(
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<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 (
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<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] = {"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<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 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<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");
// While 0 is finish in Race, in CTS, checkpoints numbered from 0 to n
convertedLines.push_back ("\"cnt\" \"0\"\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");
// While 0 is finish in Race, in CTS, checkpoints numbered from 0 to n
convertedLines.push_back ("\"cnt\" \"1\"\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] = {"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<std::string, std::string> (id, pickup));
}
}
else {
throw std::ios::failure ("Error: EntityConverter failed to open Reflex Entity Mapping file");
}
fin.close();
}
bool
EntityConverter::haveRequiredMappings()
{
std::vector<std::string> 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<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::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<int>(inputBright * BRIGHTNESS_ADJUST);
}
std::string
EntityConverter::makeErrorMessage (const std::string message,
const std::vector<std::string> entity) const
{
std::stringstream ss;
ss << std::endl << message << ":" << std::endl;
std::vector<std::string>::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<std::string, std::string>::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<std::string, std::string>::const_iterator it;
for (it=targetMap_.begin(); it!=targetMap_.end(); ++it)
std::cout << it->first << " => " << it->second << std::endl;
}