mudlet/src/TMap.cpp
2019-06-07 18:58:52 +02:00

2542 lines
99 KiB
C++

/***************************************************************************
* Copyright (C) 2008-2013 by Heiko Koehn - KoehnHeiko@googlemail.com *
* Copyright (C) 2014-2017 by Ahmed Charles - acharles@outlook.com *
* Copyright (C) 2014-2019 by Stephen Lyons - slysven@virginmedia.com *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "TMap.h"
#include "Host.h"
#include "TArea.h"
#include "TConsole.h"
#include "TEvent.h"
#include "TRoomDB.h"
#include "XMLimport.h"
#include "dlgMapper.h"
#include "dlgTriggerEditor.h"
#include "mudlet.h"
#include "pre_guard.h"
#include <QElapsedTimer>
#include <QFileDialog>
#include <QMessageBox>
#include <QProgressDialog>
#include "post_guard.h"
TMap::TMap(Host* pH, const QString& profileName)
: mpRoomDB(new TRoomDB(this))
, mpHost(pH)
, mProfileName(profileName)
, m2DPanMode(false)
, mLeftDown(false)
, mRightDown(false)
, m2DPanXStart(0.0f)
, m2DPanYStart(0.0f)
, mTargetID(0)
, mpM(Q_NULLPTR)
, mpMapper(Q_NULLPTR)
, mMapGraphNeedsUpdate(true)
, mNewMove(true)
// default map version that new maps will get
, mDefaultVersion(18)
// maximum version of the map format that this Mudlet can understand and will
// allow the user to load
, mMaxVersion(20)
// minimum version this instance of Mudlet will allow the user to save maps in
, mMinVersion(16)
, mMapSymbolFont(QFont(QStringLiteral("Bitstream Vera Sans Mono"), 12, QFont::Normal))
, mMapSymbolFontFudgeFactor(1.0)
, mIsOnlyMapSymbolFontToBeUsed(false)
, mIsFileViewingRecommended(false)
, mpNetworkAccessManager(Q_NULLPTR)
, mpProgressDialog(Q_NULLPTR)
, mpNetworkReply(Q_NULLPTR)
, mExpectedFileSize(0)
{
mSaveVersion = mDefaultVersion; // Can not be set initialiser list because of ordering issues (?)
// It needs to be set (for when writing new
// map files) as it triggers some version features
// that NEED a new map file format to be usable, it
// can be changed by control in last tab of profile
// preference dialog.
mVersion = mDefaultVersion; // This is overwritten during a map restore and
// is the loaded file version
customEnvColors[257] = mpHost->mRed_2;
customEnvColors[258] = mpHost->mGreen_2;
customEnvColors[259] = mpHost->mYellow_2;
customEnvColors[260] = mpHost->mBlue_2;
customEnvColors[261] = mpHost->mMagenta_2;
customEnvColors[262] = mpHost->mCyan_2;
customEnvColors[263] = mpHost->mWhite_2;
customEnvColors[264] = mpHost->mBlack_2;
customEnvColors[265] = mpHost->mLightRed_2;
customEnvColors[266] = mpHost->mLightGreen_2;
customEnvColors[267] = mpHost->mLightYellow_2;
customEnvColors[268] = mpHost->mLightBlue_2;
customEnvColors[269] = mpHost->mLightMagenta_2;
customEnvColors[270] = mpHost->mLightCyan_2;
customEnvColors[271] = mpHost->mLightWhite_2;
customEnvColors[272] = mpHost->mLightBlack_2;
unitVectors[1] = QVector3D(0, -1, 0);
unitVectors[2] = QVector3D(1, -1, 0);
unitVectors[3] = QVector3D(-1, -1, 0);
unitVectors[4] = QVector3D(1, 0, 0);
unitVectors[5] = QVector3D(-1, 0, 0);
unitVectors[6] = QVector3D(0, 1, 0);
unitVectors[7] = QVector3D(1, 1, 0);
unitVectors[8] = QVector3D(-1, 1, 0);
unitVectors[9] = QVector3D(0, 0, 1);
unitVectors[10] = QVector3D(0, 0, -1);
reverseDirections[1] = 6; //contains complementary directions
reverseDirections[2] = 8;
reverseDirections[3] = 7;
reverseDirections[4] = 5;
reverseDirections[5] = 4;
reverseDirections[6] = 1;
reverseDirections[7] = 3;
reverseDirections[8] = 2;
reverseDirections[9] = 10;
reverseDirections[10] = 9;
reverseDirections[11] = 12;
reverseDirections[12] = 11;
// According to Qt Docs we should really only have one of these
// (QNetworkAccessManager) for the whole application, but: each profile's
// TLuaInterpreter; each profile's ctelnet and now each profile's TMap
// (was dlgMapper) instance has one...!
mpNetworkAccessManager = new QNetworkAccessManager(this);
connect(mpNetworkAccessManager, &QNetworkAccessManager::finished, this, &TMap::slot_replyFinished);
}
TMap::~TMap()
{
delete mpRoomDB;
if (!mStoredMessages.isEmpty()) {
qWarning() << "TMap::~TMap() Instance being destroyed before it could display some messages,\n"
<< "messages are:\n"
<< "------------";
foreach (QString message, mStoredMessages) {
qWarning() << message << "\n------------";
}
}
}
void TMap::mapClear()
{
mpRoomDB->clearMapDB();
envColors.clear();
mRoomIdHash.clear();
mTargetID = 0;
mPathList.clear();
mDirList.clear();
mWeightList.clear();
customEnvColors.clear();
// Need to restore the default colours:
customEnvColors[257] = mpHost->mRed_2;
customEnvColors[258] = mpHost->mGreen_2;
customEnvColors[259] = mpHost->mYellow_2;
customEnvColors[260] = mpHost->mBlue_2;
customEnvColors[261] = mpHost->mMagenta_2;
customEnvColors[262] = mpHost->mCyan_2;
customEnvColors[263] = mpHost->mWhite_2;
customEnvColors[264] = mpHost->mBlack_2;
customEnvColors[265] = mpHost->mLightRed_2;
customEnvColors[266] = mpHost->mLightGreen_2;
customEnvColors[267] = mpHost->mLightYellow_2;
customEnvColors[268] = mpHost->mLightBlue_2;
customEnvColors[269] = mpHost->mLightMagenta_2;
customEnvColors[270] = mpHost->mLightCyan_2;
customEnvColors[271] = mpHost->mLightWhite_2;
customEnvColors[272] = mpHost->mLightBlack_2;
roomidToIndex.clear();
// Not used: pixNameTable.clear();
// Not used: pixTable.clear();
edgeHash.clear();
locations.clear();
mMapGraphNeedsUpdate = true;
mNewMove = true;
mapLabels.clear();
mVersion = mDefaultVersion;
mUserData.clear();
// mSaveVersion is not reset - so that any new Mudlet map file saves are to
// whatever version was previously set/deduced
}
void TMap::logError(QString& msg)
{
if (mpHost->mpEditorDialog) {
mpHost->mpEditorDialog->mpErrorConsole->print(tr("[MAP ERROR:]%1\n").arg(msg), QColor(255, 128, 0), QColor(Qt::black));
}
}
// Not used:
//void TMap::exportMapToDatabase()
//{
// QString dbName = QFileDialog::getSaveFileName( 0, "Chose db file name." );
// QString script = QString("exportMapToDatabse([[%1]])").arg(dbName);
// mpHost->mLuaInterpreter.compileAndExecuteScript( script );
//}
//void TMap::importMapFromDatabase()
//{
// QString dbName = QFileDialog::getOpenFileName( 0, "Chose db file name." );
// QString script = QString("importMapFromDatabase([[%1]])").arg(dbName);
// mpHost->mLuaInterpreter.compileAndExecuteScript( script );
//}
bool TMap::setRoomArea(int id, int area, bool isToDeferAreaRelatedRecalculations)
{
TRoom* pR = mpRoomDB->getRoom(id);
if (!pR) {
QString msg = tr("RoomID=%1 does not exist, can not set AreaID=%2 for non-existing room!").arg(id).arg(area);
logError(msg);
return false;
}
TArea* pA = mpRoomDB->getArea(area);
if (!pA) {
// Uh oh, the area doesn't seem to exist as a TArea instance, lets check
// to see if it exists as a name only:
if (!mpRoomDB->getAreaNamesMap().contains(area)) {
// Ah, no it doesn't so moan:
QString msg = tr("AreaID=%2 does not exist, can not set RoomID=%1 to non-existing area!").arg(id).arg(area);
logError(msg);
return false;
}
// If got to this point then there is NOT a TArea instance for the given
// area Id but there is a Name - and the pR->setArea(...) call WILL
// instantiate the required TArea structure - this seems a bit twisty
// and convoluted, but it was how previous code was wired up and we need
// to retain the API for the lua subsystem...
}
bool result = pR->setArea(area, isToDeferAreaRelatedRecalculations);
if (result) {
mMapGraphNeedsUpdate = true;
}
return result;
}
bool TMap::addRoom(int id)
{
bool ret = mpRoomDB->addRoom(id);
if (ret) {
mMapGraphNeedsUpdate = true;
}
return ret;
}
bool TMap::setRoomCoordinates(int id, int x, int y, int z)
{
TRoom* pR = mpRoomDB->getRoom(id);
if (!pR) {
return false;
}
pR->x = x;
pR->y = y;
pR->z = z;
return true;
}
int compSign(int a, int b)
{
return (a < 0) == (b < 0);
}
void TMap::connectExitStub(int roomId, int dirType)
{
TRoom* pR = mpRoomDB->getRoom(roomId);
if (!pR) {
return;
}
int area = pR->getArea();
int minDistance = 999999;
int minDistanceRoom = 0, meanSquareDistance = 0;
if (!unitVectors.contains(dirType)) {
return;
}
QVector3D unitVector = unitVectors[dirType];
int ux = unitVector.x(), uy = unitVector.y(), uz = unitVector.z();
int rx = pR->x, ry = pR->y, rz = pR->z;
int dx = 0, dy = 0, dz = 0;
TArea* pA = mpRoomDB->getArea(area);
if (!pA) {
return;
}
QSetIterator<int> itRoom(pA->getAreaRooms());
while (itRoom.hasNext()) {
pR = mpRoomDB->getRoom(itRoom.next());
if (!pR) {
continue;
}
if (pR->getId() == roomId) {
continue;
}
if (uz) {
dz = pR->z - rz;
if (!compSign(dz, uz) || !dz) {
continue;
}
} else {
//to avoid lower/upper floors from stealing stubs
if (pR->z != rz) {
continue;
}
}
if (ux) {
dx = pR->x - rx;
if (!compSign(dx, ux) || !dx) //we do !dx to make sure we have a component in the desired direction
{
continue;
}
} else {
//to avoid rooms on same plane from stealing stubs
if ((int)pR->x != rx) {
continue;
}
}
if (uy) {
dy = pR->y - ry;
//if the sign is the SAME here we keep it b/c we flip our y coordinate.
if (compSign(dy, uy) || !dy) {
continue;
}
} else {
//to avoid rooms on same plane from stealing stubs
if (pR->y != ry) {
continue;
}
}
meanSquareDistance = dx * dx + dy * dy + dz * dz;
if (meanSquareDistance < minDistance) {
minDistanceRoom = pR->getId();
minDistance = meanSquareDistance;
}
}
if (minDistanceRoom) {
pR = mpRoomDB->getRoom(minDistanceRoom);
if (!pR) {
return;
}
if (pR->exitStubs.contains(reverseDirections[dirType])) {
setExit(roomId, minDistanceRoom, dirType);
setExit(minDistanceRoom, roomId, reverseDirections[dirType]);
}
}
}
int TMap::createNewRoomID(int minimumId)
{
int _id = 0;
if (minimumId > 0) {
_id = minimumId - 1;
}
do {
; // Empty loop as increment done in test
} while (mpRoomDB->getRoom(++_id));
return _id;
}
bool TMap::setExit(int from, int to, int dir)
{
// FIXME: This along with TRoom->setExit need to be unified to a controller.
TRoom* pR = mpRoomDB->getRoom(from);
TRoom* pR_to = mpRoomDB->getRoom(to);
if (!pR) {
return false;
}
if (!pR_to && to > 0) {
return false;
}
if (to < 1) {
to = -1;
}
bool ret = true;
switch (dir) {
case DIR_NORTH:
pR->setNorth(to);
break;
case DIR_NORTHEAST:
pR->setNortheast(to);
break;
case DIR_NORTHWEST:
pR->setNorthwest(to);
break;
case DIR_EAST:
pR->setEast(to);
break;
case DIR_WEST:
pR->setWest(to);
break;
case DIR_SOUTH:
pR->setSouth(to);
break;
case DIR_SOUTHEAST:
pR->setSoutheast(to);
break;
case DIR_SOUTHWEST:
pR->setSouthwest(to);
break;
case DIR_UP:
pR->setUp(to);
break;
case DIR_DOWN:
pR->setDown(to);
break;
case DIR_IN:
pR->setIn(to);
break;
case DIR_OUT:
pR->setOut(to);
break;
default:
ret = false;
}
pR->setExitStub(dir, false);
mMapGraphNeedsUpdate = true;
TArea* pA = mpRoomDB->getArea(pR->getArea());
if (!pA) {
return false;
}
pA->determineAreaExitsOfRoom(pR->getId());
mpRoomDB->updateEntranceMap(pR);
return ret;
}
void TMap::audit()
{
// init areas
QElapsedTimer _time;
_time.start();
{ // Blocked - just to limit the scope of infoMsg...!
QString infoMsg = tr("[ INFO ] - Map audit starting...");
postMessage(infoMsg);
}
// The old mpRoomDB->initAreasForOldMaps() was a subset of these checks
QHash<int, int> roomRemapping; // These are populated by the auditRooms(...)
QHash<int, int> areaRemapping; // call and contain "Keys" of old ids and
// "Values" of new ids to use in their stead
if (mVersion < 16) {
// convert old style labels, wasn't made version conditional in past but
// not likely to be an issue in recent map file format versions (say 16+)
QMapIterator<int, TArea*> itArea(mpRoomDB->getAreaMap());
while (itArea.hasNext()) {
itArea.next();
int areaID = itArea.key();
if (mapLabels.contains(areaID)) {
QList<int> labelIDList = mapLabels.value(areaID).keys();
for (int& i : labelIDList) {
TMapLabel l = mapLabels.value(areaID).value(i);
if (l.pix.isNull()) {
int newID = createMapLabel(areaID, l.text, l.pos.x(), l.pos.y(), l.pos.z(), l.fgColor, l.bgColor, true, false, 40.0, 50);
if (newID > -1) {
if (mudlet::self()->showMapAuditErrors()) {
QString msg = tr("[ INFO ] - CONVERTING: old style label, areaID:%1 labelID:%2.").arg(areaID).arg(i);
postMessage(msg);
}
appendAreaErrorMsg(areaID, tr("[ INFO ] - Converting old style label id: %1.").arg(i));
mapLabels[areaID][i] = mapLabels[areaID][newID];
deleteMapLabel(areaID, newID);
} else {
if (mudlet::self()->showMapAuditErrors()) {
QString msg = tr("[ WARN ] - CONVERTING: cannot convert old style label in area with id: %1, label id is: %2.").arg(areaID).arg(i);
postMessage(msg);
}
appendAreaErrorMsg(areaID, tr("[ WARN ] - CONVERTING: cannot convert old style label with id: %1.").arg(i));
}
}
if ((l.size.width() > std::numeric_limits<qreal>::max()) || (l.size.width() < -std::numeric_limits<qreal>::max())) {
mapLabels[areaID][i].size.setWidth(l.pix.width());
}
if ((l.size.height() > std::numeric_limits<qreal>::max()) || (l.size.height() < -std::numeric_limits<qreal>::max())) {
mapLabels[areaID][i].size.setHeight(l.pix.height());
}
}
}
}
}
mpRoomDB->auditRooms(roomRemapping, areaRemapping);
// The second half of old mpRoomDB->initAreasForOldMaps() - needed to fixup
// all the (TArea *)->areaExits() that were built wrongly previously,
// calcSpan() may not be required to be done here and now but it is in my
// sights as a target for revision in the future. Slysven
QMapIterator<int, TArea*> itArea(mpRoomDB->getAreaMap());
while (itArea.hasNext()) {
itArea.next();
itArea.value()->determineAreaExits();
itArea.value()->calcSpan();
itArea.value()->mIsDirty = false;
}
{ // Blocked - just to limit the scope of infoMsg...!
QString infoMsg = tr("[ OK ] - Auditing of map completed (%1s). Enjoy your game...").arg(_time.nsecsElapsed() * 1.0e-9, 0, 'f', 2);
postMessage(infoMsg);
appendErrorMsg(infoMsg);
}
auto loadTime = mpHost->getLuaInterpreter()->condenseMapLoad();
if (loadTime != -1.0) {
QString msg = tr("[ OK ] - Map loaded successfully (%1s).").arg(loadTime);
postMessage(msg);
}
}
void TMap::setView(float x, float y, float z, float zoom)
{
}
void TMap::tidyMap(int areaID)
{
}
void TMap::solveRoomCollision(int id, int creationDirection, bool PCheck)
{
}
QList<int> TMap::detectRoomCollisions(int id)
{
QList<int> collList;
TRoom* pR = mpRoomDB->getRoom(id);
if (!pR) {
return collList;
}
int area = pR->getArea();
int x = pR->x;
int y = pR->y;
int z = pR->z;
TArea* pA = mpRoomDB->getArea(area);
if (!pA) {
return collList;
}
QSetIterator<int> itRoom(pA->getAreaRooms());
while (itRoom.hasNext()) {
int checkRoomId = itRoom.next();
pR = mpRoomDB->getRoom(checkRoomId);
if (!pR) {
continue;
}
if (pR->x == x && pR->y == y && pR->z == z) {
collList.push_back(checkRoomId);
}
}
return collList;
}
// Not used:
//void TMap::astBreitenAnpassung( int id, int id2 )
//{
//}
//void TMap::astHoehenAnpassung( int id, int id2 )
//{
//}
//void TMap::getConnectedNodesGreaterThanX( int id, int min )
//{
//}
//void TMap::getConnectedNodesSmallerThanX( int id, int min )
//{
//}
//void TMap::getConnectedNodesGreaterThanY( int id, int min )
//{
//}
//void TMap::getConnectedNodesSmallerThanY( int id, int min )
//{
//}
bool TMap::gotoRoom(int r)
{
mTargetID = r;
return findPath(mRoomIdHash.value(mProfileName), r);
}
// As can be seen this only sets the target and start point for a path find
// the speedwalk is instigated by the Host class caller...
bool TMap::gotoRoom(int r1, int r2)
{
return findPath(r1, r2);
}
void TMap::initGraph()
{
QElapsedTimer _time;
_time.start();
locations.clear();
roomidToIndex.clear();
g.clear();
g = mygraph_t();
unsigned int roomCount = 0;
unsigned int edgeCount = 0;
QSet<unsigned int> unUsableRoomSet;
// Keep track of the unusuable rather than the useable ones because that is
// hopefully a MUCH smaller set in normal situations!
QHashIterator<int, TRoom*> itRoom = mpRoomDB->getRoomMap();
while (itRoom.hasNext()) {
itRoom.next();
TRoom* pR = itRoom.value();
if (itRoom.key() < 1 || !pR || pR->isLocked) {
unUsableRoomSet.insert(itRoom.key());
continue;
}
location l;
l.pR = pR;
l.id = itRoom.key();
// locations is std::vector<location> and (locations.at( k )).id will give room ID value
locations.push_back(l);
// Map's usable TRooms (key) to index of entry in locations (for route finding), will lose invalid and unusable (through locking) rooms
roomidToIndex.insert(itRoom.key(), roomCount++);
}
// Now identify the routes between rooms, and pick out the best edges of parallel ones
foreach (location l, locations) {
unsigned int source = l.id;
TRoom* pSourceR = l.pR;
QHash<unsigned int, route> bestRoutes;
// key is target (destination room),
// value is data we will need to store later,
QMap<QString, int> exitWeights = pSourceR->getExitWeights();
int target = pSourceR->getNorth();
TRoom* pTargetR;
quint8 direction = DIR_NORTH;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
// In above tests the second test is to eliminate self-edges (they
// are of no use). The third test is to eliminate targets that we
// have already found to be unreachable because they are invalid or
// locked.
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) { // OK got something that is valid
route r;
r.cost = exitWeights.value(QStringLiteral("n"), pTargetR->getWeight());
r.direction = direction;
bestRoutes.insert(target, r);
}
}
target = pSourceR->getEast();
direction = DIR_EAST;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("e"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) { // Ah, this is a better route
r.direction = direction;
bestRoutes.insert(target, r); // If the second part of conditional is the truth this will replace previous best route to this target
}
}
}
target = pSourceR->getSouth();
direction = DIR_SOUTH;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("s"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getWest();
direction = DIR_WEST;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("w"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getUp();
direction = DIR_UP;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("up"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getDown();
direction = DIR_DOWN;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("down"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getNortheast();
direction = DIR_NORTHEAST;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("ne"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getSoutheast();
direction = DIR_SOUTHEAST;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("se"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getSouthwest();
direction = DIR_SOUTHWEST;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("sw"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getNorthwest();
direction = DIR_NORTHWEST;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("nw"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getIn();
direction = DIR_IN;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("in"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
target = pSourceR->getOut();
direction = DIR_OUT;
if (target > 0 && source != target && !unUsableRoomSet.contains(target) && !pSourceR->hasExitLock(direction)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
r.cost = exitWeights.value(QStringLiteral("out"), pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
QMapIterator<int, QString> itSpecialExit(pSourceR->getOtherMap());
while (itSpecialExit.hasNext()) {
itSpecialExit.next();
if ((itSpecialExit.value()).startsWith(QStringLiteral("1"))) {
continue; // Is a locked exit so forget it...
}
target = itSpecialExit.key();
direction = DIR_OTHER;
if (target > 0 && source != target && !unUsableRoomSet.contains(target)) {
pTargetR = mpRoomDB->getRoom(target);
if (pTargetR && !pTargetR->isLocked) {
route r;
if (Q_LIKELY((itSpecialExit.value()).startsWith(QStringLiteral("0")))) {
r.specialExitName = itSpecialExit.value().mid(1);
} else {
r.specialExitName = itSpecialExit.value();
}
r.cost = exitWeights.value(r.specialExitName, pTargetR->getWeight());
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
r.direction = direction;
bestRoutes.insert(target, r);
}
}
}
} // End of while(itSpecialExit.hasNext())
// Now we have eliminated possibe duplicate and useless edges we can create and
// insert the remainder into the BGL graph:
QHashIterator<unsigned int, route> itRoute = bestRoutes;
while (itRoute.hasNext()) {
itRoute.next();
edge_descriptor e;
bool inserted; // This is always going to be false as it gets set if
// we had tried to insert a parallel edge into a graph
// that does not support them - but we've just been
// and disposed of those already!
tie(e, inserted) = add_edge(roomidToIndex.value(source), roomidToIndex.value(itRoute.key()), itRoute.value().cost, g);
edgeHash.insert(qMakePair(source, itRoute.key()), itRoute.value());
// The key is made from the QPair<edgeSourceRoomId, edgeTargetRoomId>...
edgeCount++;
}
} // End of foreach(location l, locations)
mMapGraphNeedsUpdate = false;
qDebug() << "TMap::initGraph() INFO: built graph with:" << locations.size() << "(" << roomCount << ") locations(roomCount), and discarded" << unUsableRoomSet.count()
<< "other NOT useable rooms and found:" << edgeCount << "distinct, usable edges in:" << _time.nsecsElapsed() * 1.0e-9 << "seconds.";
}
bool TMap::findPath(int from, int to)
{
if (mMapGraphNeedsUpdate) {
initGraph();
}
QElapsedTimer t;
t.start();
mPathList.clear();
mDirList.clear();
mWeightList.clear();
// Clear the previous path data here so that if the following test is
// passed, the data is empty - and valid for THAT case!
if (from == to) {
return true; // Take a short-cut for trival "already there" case!
}
TRoom* pFrom = mpRoomDB->getRoom(from);
TRoom* pTo = mpRoomDB->getRoom(to);
if (!pFrom || !pTo) {
qDebug() << "TMap::findPath(" << from << "," << to << ") FAIL: NULL TRoom pointer for start or target rooms!";
return false;
}
bool hasUsableExit = false;
if (pFrom->getNorth() > 0 && (!pFrom->hasExitLock(DIR_NORTH))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getSouth() > 0 && (!pFrom->hasExitLock(DIR_SOUTH))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getWest() > 0 && (!pFrom->hasExitLock(DIR_WEST))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getEast() > 0 && (!pFrom->hasExitLock(DIR_EAST))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getUp() > 0 && (!pFrom->hasExitLock(DIR_UP))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getDown() > 0 && (!pFrom->hasExitLock(DIR_DOWN))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getNortheast() > 0 && (!pFrom->hasExitLock(DIR_NORTHEAST))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getNorthwest() > 0 && (!pFrom->hasExitLock(DIR_NORTHWEST))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getSoutheast() > 0 && (!pFrom->hasExitLock(DIR_SOUTHEAST))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getSouthwest() > 0 && (!pFrom->hasExitLock(DIR_SOUTHWEST))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getIn() > 0 && (!pFrom->hasExitLock(DIR_IN))) {
hasUsableExit = true;
}
if (!hasUsableExit && pFrom->getOut() > 0 && (!pFrom->hasExitLock(DIR_OUT))) {
hasUsableExit = true;
}
if (!hasUsableExit) {
// No available normal exits from this room so check the special ones
QStringList specialExitCommands = pFrom->getOtherMap().values();
while (!specialExitCommands.isEmpty()) {
if (specialExitCommands.at(0).mid(0, 1) == "0") {
hasUsableExit = true;
break;
}
specialExitCommands.removeFirst();
}
}
if (!hasUsableExit) {
qDebug() << "TMap::findPath(" << from << "," << to << ") FAIL: no usable exits from start room!";
return false; // No available exits from the start room so give up!
}
if (!roomidToIndex.contains(from)) {
qDebug() << "TMap::findPath(" << from << "," << to << ") FAIL: start room not in map graph!";
return false;
// The start room is NOT one that has been included in the BGL graph
// probably because it is locked - so no route finding can be done
}
vertex start = roomidToIndex.value(from);
if (!roomidToIndex.contains(to)) {
qDebug() << "TMap::findPath(" << from << "," << to << ") FAIL: target room not in map graph!";
return false;
// The target room is NOT one that has been included in the BGL graph
// probably because it is locked - so no route finding can be done
}
vertex goal = roomidToIndex.value(to);
std::vector<vertex> p(num_vertices(g));
// Somehow p is an ascending, monotonic series of numbers start at 0, it
// seems we have a redundant indirection in play there as p[0]=0, p[1]=1,..., p[n]=n ...!
std::vector<cost> d(num_vertices(g));
try {
astar_search(g, start, distance_heuristic<mygraph_t, cost, std::vector<location>>(locations, goal), predecessor_map(&p[0]).distance_map(&d[0]).visitor(astar_goal_visitor<vertex>(goal)));
} catch (found_goal) {
qDebug() << "TMap::findPath(" << from << "," << to << ") INFO: time elapsed in A*:" << t.nsecsElapsed() * 1.0e-9 << "seconds.";
t.restart();
if (!roomidToIndex.contains(to)) {
qDebug() << "TMap::findPath(" << from << "," << to << ") FAIL: target room not in map graph!";
return false;
}
vertex currentVertex = roomidToIndex.value(to);
unsigned int currentRoomId = (locations.at(currentVertex)).id;
// We step through the found path BACKWARDS so advance (well retard)
// the "previous" one first, and it will be the SOURCE vertex for the
// edge and current will be the TARGET vertex:
vertex previousVertex = currentVertex;
do {
previousVertex = p[currentVertex];
if (previousVertex == currentVertex) {
qDebug() << "TMap::findPath(" << from << "," << to << ") WARN: unable to build a path in:" << t.nsecsElapsed() * 1.0e-9 << "seconds.";
mPathList.clear();
mDirList.clear();
mWeightList.clear(); // Reset any partial results...
return false;
}
unsigned int previousRoomId = (locations.at(previousVertex)).id;
QPair<unsigned int, unsigned int> edgeRoomIdPair = qMakePair(previousRoomId, currentRoomId);
route r = edgeHash.value(edgeRoomIdPair);
mPathList.prepend(currentRoomId);
Q_ASSERT_X(r.cost > 0, "TMap::findPath()", "broken path {QPair made from source and target roomIds for a path step NOT found in QHash table of all possible steps.}");
// Above was found to be triggered by the situation described in:
// https://bugs.launchpad.net/mudlet/+bug/1263447 on 2015-07-17 but
// this is because previousVertex was the same as currentVertex after
// the "previousVertex = p[currentVertex]" operation at the start of
// the do{} loop - added a test for this so should bail out if it
// happens - Slysven
mWeightList.prepend( r.cost );
switch( r.direction ) { // TODO: Eventually this can instead drop in I18ned values set by country or user preference!
case DIR_NORTH: mDirList.prepend( tr( "n", "This translation converts the direction that DIR_NORTH codes for to a direction string that the game server will accept!" ) ); break;
case DIR_NORTHEAST: mDirList.prepend( tr( "ne", "This translation converts the direction that DIR_NORTHEAST codes for to a direction string that the game server will accept!" ) ); break;
case DIR_EAST: mDirList.prepend( tr( "e", "This translation converts the direction that DIR_EAST codes for to a direction string that the game server will accept!" ) ); break;
case DIR_SOUTHEAST: mDirList.prepend( tr( "se", "This translation converts the direction that DIR_SOUTHEAST codes for to a direction string that the game server will accept!" ) ); break;
case DIR_SOUTH: mDirList.prepend( tr( "s", "This translation converts the direction that DIR_SOUTH codes for to a direction string that the game server will accept!" ) ); break;
case DIR_SOUTHWEST: mDirList.prepend( tr( "sw", "This translation converts the direction that DIR_SOUTHWEST codes for to a direction string that the game server will accept!" ) ); break;
case DIR_WEST: mDirList.prepend( tr( "w", "This translation converts the direction that DIR_WEST codes for to a direction string that the game server will accept!" ) ); break;
case DIR_NORTHWEST: mDirList.prepend( tr( "nw", "This translation converts the direction that DIR_NORTHWEST codes for to a direction string that the game server will accept!" ) ); break;
case DIR_UP: mDirList.prepend( tr( "up", "This translation converts the direction that DIR_UP codes for to a direction string that the game server will accept!" ) ); break;
case DIR_DOWN: mDirList.prepend( tr( "down", "This translation converts the direction that DIR_DOWN codes for to a direction string that the game server will accept!" ) ); break;
case DIR_IN: mDirList.prepend( tr( "in", "This translation converts the direction that DIR_IN codes for to a direction string that the game server will accept!" ) ); break;
case DIR_OUT: mDirList.prepend( tr( "out", "This translation converts the direction that DIR_OUT codes for to a direction string that the game server will accept!" ) ); break;
case DIR_OTHER: mDirList.prepend( r.specialExitName ); break;
default: qWarning() << "TMap::findPath(" << from << "," << to << ") WARN: found route between rooms (from id:" << previousRoomId << ", to id:" << currentRoomId << ") with an invalid DIR_xxxx code:" << r.direction << " - the path will not be valid!" ;
}
currentVertex = previousVertex;
currentRoomId = previousRoomId;
} while (currentVertex != start);
qDebug() << "TMap::findPath(" << from << "," << to << ") INFO: found path in:" << t.nsecsElapsed() * 1.0e-9 << "seconds.";
return true;
}
qDebug() << "TMap::findPath(" << from << "," << to << ") INFO: did NOT find path in:" << t.nsecsElapsed() * 1.0e-9 << "seconds.";
return false;
}
bool TMap::serialize(QDataStream& ofs, int saveVersion)
{
// clamp version values
if (saveVersion < 0) {
saveVersion = 0;
} else if (saveVersion > mMaxVersion) {
saveVersion = mMaxVersion;
QString errMsg = tr("[ ERROR ] - The format {%1} you are trying to save the map with is too new\n"
"for this version of Mudlet. Supported are only formats up to version {%2}.")
.arg(QString::number(saveVersion), QString::number(mMaxVersion));
appendErrorMsgWithNoLf(errMsg, false);
postMessage(errMsg);
return false;
}
auto oldSaveVersion = mSaveVersion;
// if 0 we default to current version selected
if (saveVersion != 0) {
mSaveVersion = saveVersion;
}
if (mSaveVersion != mVersion) {
QString message = tr("[ ALERT ] - Saving map in a format {%1} that is different than the one it was\n"
"loaded as {%2}. This may be an issue if you want to share the resulting\n"
"map with others relying on the original format.")
.arg(mSaveVersion)
.arg(mVersion);
appendErrorMsgWithNoLf(message, false);
mpHost->mTelnet.postMessage(message);
}
if (mSaveVersion != mDefaultVersion) {
QString message = tr("[ WARN ] - Saving map in a format {%1} different from the\n"
"recommended format {%2} for this version of Mudlet.")
.arg(mSaveVersion)
.arg(mDefaultVersion);
appendErrorMsgWithNoLf(message, false);
postMessage(message);
}
ofs << mSaveVersion;
ofs << envColors;
ofs << mpRoomDB->getAreaNamesMap();
ofs << customEnvColors;
ofs << mpRoomDB->hashToRoomID;
if (mSaveVersion >= 17) {
if (mSaveVersion < 19) {
// Save the data in the map user data for older versions
mUserData.insert(QStringLiteral("system.fallback_mapSymbolFont"), mMapSymbolFont.toString());
mUserData.insert(QStringLiteral("system.fallback_mapSymbolFontFudgeFactor"), QString::number(mMapSymbolFontFudgeFactor));
mUserData.insert(QStringLiteral("system.fallback_onlyUseMapSymbolFont"), mIsOnlyMapSymbolFontToBeUsed ? QStringLiteral("true") : QStringLiteral("false"));
}
ofs << mUserData;
if (mSaveVersion >= 19) {
// Save the data directly in supported format versions (19 and above)
ofs << mMapSymbolFont;
ofs << mMapSymbolFontFudgeFactor;
ofs << mIsOnlyMapSymbolFontToBeUsed;
}
}
// TODO: Remove when versions < 17 are not an option...
else {
if (!mUserData.isEmpty()) {
QString message = tr("[ ALERT ] - Map User data has been lost in saved map file. Re-save in a\n"
"format of at least 17 to preserve it before quitting!")
.arg(mSaveVersion);
appendErrorMsgWithNoLf(message, false);
mpHost->mTelnet.postMessage(message);
}
}
// End of TODO:
ofs << mpRoomDB->getAreaMap().size();
// serialize area table
QMapIterator<int, TArea*> itAreaList(mpRoomDB->getAreaMap());
QList<int> areasWithData; // TODO: Remove when versions < 17 are not an option
while (itAreaList.hasNext()) {
itAreaList.next();
int areaID = itAreaList.key();
TArea* pA = itAreaList.value();
ofs << areaID;
if (mSaveVersion >= 18) {
ofs << pA->rooms;
} else {
// Switched to a (faster) QSet<int> from a QList<int> in version 18
QList<int> _oldList = pA->rooms.toList();
ofs << _oldList;
}
ofs << pA->zLevels;
ofs << pA->exits;
ofs << pA->gridMode;
ofs << pA->max_x;
ofs << pA->max_y;
ofs << pA->max_z;
ofs << pA->min_x;
ofs << pA->min_y;
ofs << pA->min_z;
ofs << pA->span;
if (mSaveVersion >= 17) {
ofs << pA->xmaxForZ;
ofs << pA->ymaxForZ;
ofs << pA->xminForZ;
ofs << pA->yminForZ;
} else { // Recreate the pointless z{min|max}ForZ items
QMap<int, int> dummyMinMaxForZ;
QListIterator<int> itZ(pA->zLevels);
while (itZ.hasNext()) {
int dummyEbenValue = itZ.next();
dummyMinMaxForZ.insert(dummyEbenValue, dummyEbenValue);
}
ofs << pA->xmaxForZ;
ofs << pA->ymaxForZ;
ofs << dummyMinMaxForZ;
ofs << pA->xminForZ;
ofs << pA->yminForZ;
ofs << dummyMinMaxForZ;
}
ofs << pA->pos;
ofs << pA->isZone;
ofs << pA->zoneAreaRef;
if (mSaveVersion >= 17) {
ofs << pA->mUserData;
}
// TODO: Remove when versions < 17 are not an option...
else {
if (!pA->mUserData.isEmpty()) {
areasWithData.append(areaID);
}
}
// End of TODO:
}
// TODO: Remove when versions < 17 are not an option...
if (!areasWithData.isEmpty()) {
if (areasWithData.size() > 1) {
std::sort(areasWithData.begin(), areasWithData.end());
}
QStringList areaIds;
do {
int areaId = areasWithData.takeFirst();
areaIds.append(QString::number(areaId));
appendAreaErrorMsg(areaId,
tr("[ ALERT ] - User data for this area has been lost in saved map file. Re-save in a\n"
"format of at least 17 to preserve it before quitting!\n"));
} while (!areasWithData.isEmpty());
QString message = tr("[ ALERT ] - Area User data has been lost in saved map file. Re-save in a\n"
"format of at least 17 to preserve it before quitting!\n"
"Areas id affected: %1.")
.arg(areaIds.join(tr(", "))); // Translatable in case list separators are locale dependant!
mpHost->mTelnet.postMessage(message);
}
// End of TODO
if (mSaveVersion >= 18) {
// Revised in version 18 to store mRoomId as a per profile case so that
// sharing/copying between profiles respects each profile's player
// location
ofs << mRoomIdHash;
} else {
ofs << mRoomIdHash.value(mProfileName);
}
ofs << mapLabels.size(); //anzahl der areas
QMapIterator<int, QMap<int, TMapLabel>> itL1(mapLabels);
while (itL1.hasNext()) {
itL1.next();
int i = itL1.key();
ofs << itL1.value().size(); //anzahl der labels pro area
ofs << itL1.key(); //area id
QMapIterator<int, TMapLabel> itL2(mapLabels[i]);
while (itL2.hasNext()) {
itL2.next();
ofs << itL2.key(); //label ID
TMapLabel label = itL2.value();
ofs << label.pos;
ofs << label.pointer;
ofs << label.size;
ofs << label.text;
ofs << label.fgColor;
ofs << label.bgColor;
ofs << label.pix;
ofs << label.noScaling;
ofs << label.showOnTop;
}
}
QHashIterator<int, TRoom*> it(mpRoomDB->getRoomMap());
while (it.hasNext()) {
it.next();
TRoom* pR = it.value();
if (!pR) {
qDebug() << "TMap::serialize(...) skipping a room with a NULL TRoom pointer:" << it.key();
continue;
}
ofs << pR->getId();
if (mSaveVersion <= 19) {
if (!pR->mSymbol.isEmpty()) {
pR->userData.insert(QLatin1String("system.fallback_symbol"), pR->mSymbol);
}
}
ofs << pR->getArea();
ofs << pR->x;
ofs << pR->y;
ofs << pR->z;
ofs << pR->getNorth();
ofs << pR->getNortheast();
ofs << pR->getEast();
ofs << pR->getSoutheast();
ofs << pR->getSouth();
ofs << pR->getSouthwest();
ofs << pR->getWest();
ofs << pR->getNorthwest();
ofs << pR->getUp();
ofs << pR->getDown();
ofs << pR->getIn();
ofs << pR->getOut();
ofs << pR->environment;
ofs << pR->getWeight();
ofs << pR->name;
ofs << pR->isLocked;
ofs << pR->getOtherMap();
if (mSaveVersion >= 19) {
ofs << pR->mSymbol;
} else {
qint8 oldCharacterCode = 0;
if (pR->mSymbol.length()) {
// There is something for a symbol
QChar firstChar = pR->mSymbol.at(0);
if (pR->mSymbol.length() == 1 && firstChar.row() == 0 && firstChar.cell() > 32) {
// It is something that can be represented by the past unsigned short
oldCharacterCode = firstChar.toLatin1();
} else {
// Not representable - put in a '?' for older Mudlet
// versions that cannot display the character and will not
// parse the value placed in the room's user data:
oldCharacterCode = QChar('?').toLatin1();
}
}
ofs << oldCharacterCode;
}
ofs << pR->userData;
if (mSaveVersion >= 20) {
// Before version 20 stored the style as an Latin1 string, the color
// as a QList<int> for the RGB components and used UPPER case for
// the NORMAL exit direction keys...
ofs << pR->customLines;
ofs << pR->customLinesArrow;
ofs << pR->customLinesColor;
ofs << pR->customLinesStyle;
} else {
QMap<QString, QList<QPointF>> oldLinesData;
QMapIterator<QString, QList<QPointF>> itCustomLine(pR->customLines);
while (itCustomLine.hasNext()) {
itCustomLine.next();
QString direction(itCustomLine.key());
if (direction == QLatin1String("n") || direction == QLatin1String("e") || direction == QLatin1String("s") || direction == QLatin1String("w") || direction == QLatin1String("up")
|| direction == QLatin1String("down")
|| direction == QLatin1String("ne")
|| direction == QLatin1String("se")
|| direction == QLatin1String("sw")
|| direction == QLatin1String("nw")
|| direction == QLatin1String("in")
|| direction == QLatin1String("out")) {
oldLinesData.insert(itCustomLine.key().toUpper(), itCustomLine.value());
} else {
oldLinesData.insert(itCustomLine.key(), itCustomLine.value());
}
}
ofs << oldLinesData;
QMap<QString, bool> oldLinesArrowData;
QMapIterator<QString, bool> itCustomLineArrow(pR->customLinesArrow);
while (itCustomLineArrow.hasNext()) {
itCustomLineArrow.next();
QString direction(itCustomLineArrow.key());
if (direction == QLatin1String("n") || direction == QLatin1String("e") || direction == QLatin1String("s") || direction == QLatin1String("w") || direction == QLatin1String("up")
|| direction == QLatin1String("down")
|| direction == QLatin1String("ne")
|| direction == QLatin1String("se")
|| direction == QLatin1String("sw")
|| direction == QLatin1String("nw")
|| direction == QLatin1String("in")
|| direction == QLatin1String("out")) {
oldLinesArrowData.insert(itCustomLineArrow.key().toUpper(), itCustomLineArrow.value());
} else {
oldLinesArrowData.insert(itCustomLineArrow.key(), itCustomLineArrow.value());
}
}
ofs << oldLinesArrowData;
QMap<QString, QList<int>> oldLinesColorData;
QMapIterator<QString, QColor> itCustomLineColor(pR->customLinesColor);
while (itCustomLineColor.hasNext()) {
itCustomLineColor.next();
QString direction(itCustomLineColor.key());
QList<int> colorComponents;
colorComponents << itCustomLineColor.value().red() << itCustomLineColor.value().green() << itCustomLineColor.value().blue();
if (direction == QLatin1String("n") || direction == QLatin1String("e") || direction == QLatin1String("s") || direction == QLatin1String("w") || direction == QLatin1String("up")
|| direction == QLatin1String("down")
|| direction == QLatin1String("ne")
|| direction == QLatin1String("se")
|| direction == QLatin1String("sw")
|| direction == QLatin1String("nw")
|| direction == QLatin1String("in")
|| direction == QLatin1String("out")) {
oldLinesColorData.insert(itCustomLineColor.key().toUpper(), colorComponents);
} else {
oldLinesColorData.insert(itCustomLineColor.key(), colorComponents);
}
}
ofs << oldLinesColorData;
QMap<QString, QString> oldLineStyleData;
QMapIterator<QString, Qt::PenStyle> itCustomLineStyle(pR->customLinesStyle);
while (itCustomLineStyle.hasNext()) {
itCustomLineStyle.next();
QString direction(itCustomLineStyle.key());
if (direction == QLatin1String("n") || direction == QLatin1String("e") || direction == QLatin1String("s") || direction == QLatin1String("w") || direction == QLatin1String("up")
|| direction == QLatin1String("down")
|| direction == QLatin1String("ne")
|| direction == QLatin1String("se")
|| direction == QLatin1String("sw")
|| direction == QLatin1String("nw")
|| direction == QLatin1String("in")
|| direction == QLatin1String("out")) {
direction = direction.toUpper();
}
switch (itCustomLineStyle.value()) {
case Qt::DotLine:
oldLineStyleData.insert(direction, QLatin1String("dot line"));
break;
case Qt::DashLine:
oldLineStyleData.insert(direction, QLatin1String("dash line"));
break;
case Qt::DashDotLine:
oldLineStyleData.insert(direction, QLatin1String("dash dot line"));
break;
case Qt::DashDotDotLine:
oldLineStyleData.insert(direction, QLatin1String("dash dot dot line"));
break;
case Qt::SolidLine:
[[clang::fallthrough]];
default:
oldLineStyleData.insert(direction, QLatin1String("solid line"));
}
}
ofs << oldLineStyleData;
}
ofs << pR->exitLocks;
ofs << pR->exitStubs;
ofs << pR->getExitWeights();
ofs << pR->doors;
}
// reset to the old map version
mSaveVersion = oldSaveVersion;
return true;
}
bool TMap::restore(QString location, bool downloadIfNotFound)
{
qDebug() << "TMap::restore(" << location << ") INFO: restoring map of Profile:" << mProfileName << " URL:" << mpHost->getUrl();
QElapsedTimer _time;
_time.start();
QString folder;
QStringList entries;
if (location.isEmpty()) {
folder = mudlet::getMudletPath(mudlet::profileMapsPath, mProfileName);
QDir dir(folder);
dir.setSorting(QDir::Time);
entries = dir.entryList(QDir::Files, QDir::Time);
}
bool canRestore = true;
if (!entries.empty() || !location.isEmpty()) {
QFile file(location.isEmpty() ? QStringLiteral("%1/%2").arg(folder, entries.at(0)) : location);
if (!file.open(QFile::ReadOnly)) {
QString errMsg = tr(R"([ ERROR ] - Unable to open (for reading) map file: "%1"!)").arg(file.fileName());
appendErrorMsg(errMsg, false);
postMessage(errMsg);
return false;
}
QDataStream ifs(&file);
ifs >> mVersion;
if (mVersion > mMaxVersion) {
QString errMsg = tr("[ ERROR ] - Map file is too new, its file format (%1) is higher than this version of\n"
"Mudlet can handle (%2)! The file is:\n\"%3\".")
.arg(mVersion)
.arg(mMaxVersion)
.arg(file.fileName());
appendErrorMsgWithNoLf(errMsg);
postMessage(errMsg);
QString infoMsg = tr("[ INFO ] - You will need to upgrade your Mudlet or find a map file saved in an\n"
"older format.");
appendErrorMsgWithNoLf(infoMsg);
postMessage(infoMsg);
file.close();
return false;
} else if (mVersion < 4) {
QString alertMsg = tr("[ ALERT ] - Map file is really old, it's file format (%1) is so ancient that\n"
"this version of Mudlet may not gain enough information from\n"
"it but it will try! The file is: \"%2\".")
.arg(mVersion)
.arg(file.fileName());
appendErrorMsgWithNoLf(alertMsg, false);
postMessage(alertMsg);
QString infoMsg = tr("[ INFO ] - You might wish to donate THIS map file to the Mudlet Museum!\n"
"There is so much data that it DOES NOT have that you could be\n"
"better off starting again...");
appendErrorMsgWithNoLf(infoMsg, false);
postMessage(infoMsg);
canRestore = false;
mSaveVersion = mDefaultVersion; // Make the save version the default one - unless the user intervenes
} else {
// Less than (but not less than 4) or equal to default version
QString infoMsg = tr("[ INFO ] - Reading map (format version:%1) file:\n\"%2\",\nplease wait...").arg(mVersion).arg(file.fileName());
appendErrorMsg(tr(R"([ INFO ] - Reading map (format version:%1) file: "%2".)").arg(mVersion).arg(file.fileName()), false);
postMessage(infoMsg);
mSaveVersion = mDefaultVersion; // Make the save version the default one - unless the user intervenes
}
// As all but the room reading have version checks the fact that sub-4
// files will still be parsed despite canRestore being false is probably OK
if (mVersion >= 4) {
ifs >> envColors;
mpRoomDB->restoreAreaMap(ifs);
}
if (mVersion >= 5) {
ifs >> customEnvColors;
}
if (mVersion >= 7) {
ifs >> mpRoomDB->hashToRoomID;
QMap<QString, int>::const_iterator i;
for (i = mpRoomDB->hashToRoomID.constBegin(); i != mpRoomDB->hashToRoomID.constEnd(); ++i) {
mpRoomDB->roomIDToHash.insert(i.value(), i.key());
}
}
if (mVersion >= 17) {
ifs >> mUserData;
if (mVersion >= 19) {
// Read the data from the file directly in version 19 or later
ifs >> mMapSymbolFont;
ifs >> mMapSymbolFontFudgeFactor;
ifs >> mIsOnlyMapSymbolFontToBeUsed;
} else {
// Fallback to reading the data from the map user data - and
// remove it from the data the user will see:
QString fontString = mUserData.take(QStringLiteral("system.fallback_mapSymbolFont"));
QString fontFudgeFactorString = mUserData.take(QStringLiteral("system.fallback_mapSymbolFontFudgeFactor"));
QString onlyUseSymbolFontString = mUserData.take(QStringLiteral("system.fallback_onlyUseMapSymbolFont"));
if (!fontString.isEmpty()) {
mMapSymbolFont = QFont(fontString);
}
if (!fontFudgeFactorString.isEmpty()) {
mMapSymbolFontFudgeFactor = fontFudgeFactorString.toDouble();
}
if (!onlyUseSymbolFontString.isEmpty()) {
mIsOnlyMapSymbolFontToBeUsed = (onlyUseSymbolFontString != QLatin1String("false"));
}
}
}
mMapSymbolFont.setStyleStrategy(static_cast<QFont::StyleStrategy>(( mIsOnlyMapSymbolFontToBeUsed ? QFont::NoFontMerging : 0)
| QFont::PreferOutline | QFont::PreferAntialias | QFont::PreferQuality
#if QT_VERSION >= 0x050a00
| QFont::PreferNoShaping
#endif
));
if (mVersion >= 14) {
int areaSize;
ifs >> areaSize;
// restore area table
for (int i = 0; i < areaSize; i++) {
auto pA = new TArea(this, mpRoomDB);
int areaID;
ifs >> areaID;
if (mVersion >= 18) {
// In version 18 changed from QList<int> to QSet<int> as the later is
// faster in many of the cases where we use it.
ifs >> pA->rooms;
} else {
QList<int> oldRoomsList;
ifs >> oldRoomsList;
pA->rooms = oldRoomsList.toSet();
}
// Can be useful when analysing suspect map files!
// qDebug() << "TMap::restore(...)" << "Area:" << areaID;
// qDebug() << "Rooms:" << pA->rooms;
ifs >> pA->zLevels;
ifs >> pA->exits;
ifs >> pA->gridMode;
ifs >> pA->max_x;
ifs >> pA->max_y;
ifs >> pA->max_z;
ifs >> pA->min_x;
ifs >> pA->min_y;
ifs >> pA->min_z;
ifs >> pA->span;
if (mVersion >= 17) {
ifs >> pA->xmaxForZ;
ifs >> pA->ymaxForZ;
ifs >> pA->xminForZ;
ifs >> pA->yminForZ;
} else {
QMap<int, int> dummyMinMaxForZ;
ifs >> pA->xmaxForZ;
ifs >> pA->ymaxForZ;
ifs >> dummyMinMaxForZ;
ifs >> pA->xminForZ;
ifs >> pA->yminForZ;
ifs >> dummyMinMaxForZ;
}
ifs >> pA->pos;
ifs >> pA->isZone;
ifs >> pA->zoneAreaRef;
if (mVersion >= 17) {
ifs >> pA->mUserData;
}
mpRoomDB->restoreSingleArea(areaID, pA);
}
}
if (!mpRoomDB->getAreaMap().keys().contains(-1)) {
auto pDefaultA = new TArea(this, mpRoomDB);
mpRoomDB->restoreSingleArea(-1, pDefaultA);
QString defaultAreaInsertionMsg = tr("[ INFO ] - Default (reset) area (for rooms that have not been assigned to an\n"
"area) not found, adding reserved -1 id.");
appendErrorMsgWithNoLf(defaultAreaInsertionMsg, false);
if (mudlet::self()->showMapAuditErrors()) {
postMessage(defaultAreaInsertionMsg);
}
}
if (mVersion >= 18) {
// In version 18 we changed to store the "userRoom" for each profile
// so that when copied/shared between profiles they do not interfere
// with each other's saved value
ifs >> mRoomIdHash;
} else if (mVersion >= 12) {
int oldRoomId;
ifs >> oldRoomId;
mRoomIdHash[mProfileName] = oldRoomId;
}
if (mVersion >= 11) {
int size;
ifs >> size; //size of mapLabels
int areaLabelCount = 0;
while (!ifs.atEnd() && areaLabelCount < size) {
int areaID;
int size_labels;
ifs >> size_labels;
ifs >> areaID;
int labelCount = 0;
QMap<int, TMapLabel> _map;
while (!ifs.atEnd() && labelCount < size_labels) {
int labelID;
ifs >> labelID;
TMapLabel label;
if (mVersion >= 12) {
ifs >> label.pos;
} else {
QPointF __label_pos;
ifs >> __label_pos;
label.pos = QVector3D(__label_pos.x(), __label_pos.y(), 0);
}
ifs >> label.pointer;
ifs >> label.size;
ifs >> label.text;
ifs >> label.fgColor;
ifs >> label.bgColor;
ifs >> label.pix;
if (mVersion >= 15) {
ifs >> label.noScaling;
ifs >> label.showOnTop;
}
_map.insert(labelID, label);
labelCount++;
}
mapLabels[areaID] = _map;
areaLabelCount++;
}
}
while (!ifs.atEnd()) {
int i;
ifs >> i;
auto pT = new TRoom(mpRoomDB);
pT->restore(ifs, i, mVersion);
mpRoomDB->restoreSingleRoom(i, pT);
}
customEnvColors[257] = mpHost->mRed_2;
customEnvColors[258] = mpHost->mGreen_2;
customEnvColors[259] = mpHost->mYellow_2;
customEnvColors[260] = mpHost->mBlue_2;
customEnvColors[261] = mpHost->mMagenta_2;
customEnvColors[262] = mpHost->mCyan_2;
customEnvColors[263] = mpHost->mWhite_2;
customEnvColors[264] = mpHost->mBlack_2;
customEnvColors[265] = mpHost->mLightRed_2;
customEnvColors[266] = mpHost->mLightGreen_2;
customEnvColors[267] = mpHost->mLightYellow_2;
customEnvColors[268] = mpHost->mLightBlue_2;
customEnvColors[269] = mpHost->mLightMagenta_2;
customEnvColors[270] = mpHost->mLightCyan_2;
customEnvColors[271] = mpHost->mLightWhite_2;
customEnvColors[272] = mpHost->mLightBlack_2;
QString okMsg = tr("[ INFO ] - Successfully read the map file (%1s), checking some\n"
"consistency details..." )
.arg(_time.nsecsElapsed() * 1.0e-9, 0, 'f', 2);
postMessage(okMsg);
appendErrorMsgWithNoLf(okMsg);
if (canRestore) {
return true;
}
}
if ((!canRestore || entries.empty()) && downloadIfNotFound) {
QMessageBox msgBox;
if (!getMmpMapLocation().isEmpty()) {
msgBox.setText(tr("No map found. Would you like to download the map or start your own?"));
QPushButton* yesButton = msgBox.addButton(tr("Download the map"), QMessageBox::ActionRole);
QPushButton* noButton = msgBox.addButton(tr("Start my own"), QMessageBox::ActionRole);
msgBox.exec();
if (msgBox.clickedButton() == yesButton) {
downloadMap();
} else if (msgBox.clickedButton() == noButton) {
; //No-op to avoid unused "noButton"
}
}
}
return canRestore; //FIXME
}
// Reads the newest map file from the profile and retrieves some stats and data,
// including the current player room - was mRoomId in 12 to pre-18 map files and
// is in mRoomIdHash since then so that it can be reinserted into a map that is
// copied across (if the room STILL exists! This is to avoid a replacement map
// (copied/shared) from one profile to another from repositioning the other
// player location. Though this is written as a member function it is intended
// also for use to retrieve details from maps from OTHER profiles, importantly
// it does (or should) NOT interact with this TMap instance...!
bool TMap::retrieveMapFileStats(QString profile, QString* latestFileName = nullptr, int* fileVersion = nullptr, int* roomId = nullptr, int* areaCount = nullptr, int* roomCount = nullptr)
{
if (profile.isEmpty()) {
return false;
}
QString folder;
QStringList entries;
folder = mudlet::getMudletPath(mudlet::profileMapsPath, profile);
QDir dir(folder);
dir.setSorting(QDir::Time);
entries = dir.entryList(QDir::Files | QDir::NoDotAndDotDot, QDir::Time);
if (entries.isEmpty()) {
return false;
}
// As the files are sorted by time this gets the latest one
QFile file(QStringLiteral("%1/%2").arg(folder, entries.at(0)));
if (!file.open(QFile::ReadOnly)) {
QString errMsg = tr(R"([ ERROR ] - Unable to open (for reading) map file: "%1"!)").arg(file.fileName());
appendErrorMsg(errMsg, false);
postMessage(errMsg);
return false;
}
if (latestFileName) {
*latestFileName = file.fileName();
}
int otherProfileVersion = 0;
QDataStream ifs(&file);
ifs >> otherProfileVersion;
QString infoMsg = tr(R"([ INFO ] - Checking map file: "%1", format version:%2...)").arg(file.fileName()).arg(otherProfileVersion);
appendErrorMsg(infoMsg, false);
if (mudlet::self()->showMapAuditErrors()) {
postMessage(infoMsg);
}
if (otherProfileVersion > mDefaultVersion) {
if (QByteArray(APP_BUILD).isEmpty()) {
// This is a release version - should not support any map file versions higher that it was built for
if (fileVersion) {
*fileVersion = otherProfileVersion;
}
file.close();
return true;
} else {
// Is a development version so check against mMaxVersion
if (otherProfileVersion > mMaxVersion) {
// Oh dear, can't handle THIS
if (fileVersion) {
*fileVersion = otherProfileVersion;
}
file.close();
return true;
} else {
if (fileVersion) {
*fileVersion = otherProfileVersion;
}
}
}
} else {
if (fileVersion) {
*fileVersion = otherProfileVersion;
}
}
if (otherProfileVersion >= 4) {
// envColorMap
QMap<int, int> _dummyQMapIntInt;
ifs >> _dummyQMapIntInt;
// AreaNamesMap
QMap<int, QString> _dummyQMapIntQString;
ifs >> _dummyQMapIntQString;
}
if (otherProfileVersion >= 5) {
// customEnvColors
QMap<int, QColor> _dummyQMapIntQColor;
ifs >> _dummyQMapIntQColor;
}
if (otherProfileVersion >= 7) {
// hashToRoomID
QMap<QString, int> _dummyQMapQStringInt;
ifs >> _dummyQMapQStringInt;
}
if (otherProfileVersion >= 17) {
// userMapData
QMap<QString, QString> _dummyQMapQStringQString;
ifs >> _dummyQMapQStringQString;
}
if (otherProfileVersion >= 14) {
int areaSize;
ifs >> areaSize;
if (areaCount) {
*areaCount = areaSize;
}
// read each area
for (int i = 0; i < areaSize; i++) {
TArea pA(nullptr, nullptr);
int areaID;
ifs >> areaID;
ifs >> pA.rooms;
ifs >> pA.zLevels;
ifs >> pA.exits;
ifs >> pA.gridMode;
ifs >> pA.max_x;
ifs >> pA.max_y;
ifs >> pA.max_z;
ifs >> pA.min_x;
ifs >> pA.min_y;
ifs >> pA.min_z;
ifs >> pA.span;
if (otherProfileVersion >= 17) {
ifs >> pA.xmaxForZ;
ifs >> pA.ymaxForZ;
ifs >> pA.xminForZ;
ifs >> pA.yminForZ;
} else {
QMap<int, int> dummyMinMaxForZ;
ifs >> pA.xmaxForZ;
ifs >> pA.ymaxForZ;
ifs >> dummyMinMaxForZ;
ifs >> pA.xminForZ;
ifs >> pA.yminForZ;
ifs >> dummyMinMaxForZ;
}
ifs >> pA.pos;
ifs >> pA.isZone;
ifs >> pA.zoneAreaRef;
if (otherProfileVersion >= 17) {
ifs >> pA.mUserData;
}
}
}
if (otherProfileVersion >= 18) {
// In version 18 we changed to store the "userRoom" for each profile
// so that when copied/shared between profiles they do not interfere
// with each other's saved value
QHash<QString, int> _dummyQHashQStringInt;
ifs >> _dummyQHashQStringInt;
if (roomId) {
*roomId = _dummyQHashQStringInt.value(profile);
}
} else if (otherProfileVersion >= 12) {
int oldRoomId;
ifs >> oldRoomId;
if (roomId) {
*roomId = oldRoomId;
}
} else {
if (roomId) {
*roomId = -1; // Not found value
}
}
if (otherProfileVersion >= 11) {
int size;
ifs >> size; //size of mapLabels
int areaLabelCount = 0;
while (!ifs.atEnd() && areaLabelCount < size) {
int areaID;
int size_labels;
ifs >> size_labels;
ifs >> areaID;
int labelCount = 0;
while (!ifs.atEnd() && labelCount < size_labels) {
int labelID;
ifs >> labelID;
TMapLabel label;
if (otherProfileVersion >= 12) {
ifs >> label.pos;
} else {
QPointF __label_pos;
ifs >> __label_pos;
label.pos = QVector3D(__label_pos.x(), __label_pos.y(), 0);
}
ifs >> label.pointer;
ifs >> label.size;
ifs >> label.text;
ifs >> label.fgColor;
ifs >> label.bgColor;
ifs >> label.pix;
if (otherProfileVersion >= 15) {
ifs >> label.noScaling;
ifs >> label.showOnTop;
}
labelCount++;
}
areaLabelCount++;
}
}
TRoom _pT(nullptr);
QSet<int> _dummyRoomIdSet;
while (!ifs.atEnd()) {
int i;
ifs >> i;
_pT.restore(ifs, i, otherProfileVersion);
// Can't do mpRoomDB->restoreSingleRoom( ifs, i, pT ) as it would mess up
// this TMap::mpRoomDB
// So emulate using _dummyRoomIdSet
if (i > 0 && !_dummyRoomIdSet.contains(i)) {
_dummyRoomIdSet.insert(i);
}
}
if (roomCount) {
*roomCount = _dummyRoomIdSet.count();
}
return true;
}
int TMap::createMapLabel(int area, QString text, float x, float y, float z, QColor fg, QColor bg, bool showOnTop, bool noScaling, qreal zoom, int fontSize)
{
if (!mpRoomDB->getArea(area)) {
return -1;
}
TMapLabel label;
label.text = text;
label.bgColor = bg;
label.bgColor.setAlpha(50);
label.fgColor = fg;
label.size = QSizeF(100, 100);
label.pos = QVector3D(x, y, z);
label.showOnTop = showOnTop;
label.noScaling = noScaling;
if (label.text.length() < 1) {
return -1;
}
QRectF lr = QRectF(0, 0, 1000, 1000);
QPixmap pix(lr.size().toSize());
pix.fill(Qt::transparent);
QPainter lp(&pix);
lp.fillRect(lr, label.bgColor);
QPen lpen;
lpen.setColor(label.fgColor);
QFont font;
font.setPointSize(fontSize); //good: font size = 50, zoom = 30.0
lp.setRenderHint(QPainter::TextAntialiasing, true);
lp.setPen(lpen);
lp.setFont(font);
QRectF br;
lp.drawText(lr, Qt::AlignLeft | Qt::AlignTop, label.text, &br);
label.size = br.normalized().size();
label.pix = pix.copy(br.normalized().topLeft().x(), br.normalized().topLeft().y(), br.normalized().width(), br.normalized().height());
QSizeF s = QSizeF(label.size.width() / zoom, label.size.height() / zoom);
label.size = s;
label.clickSize = s;
if (!mpRoomDB->getArea(area)) {
return -1;
}
int label_id;
// No labels exist for this area, so start from zero.
if (!mapLabels.contains(area)) {
QMap<int, TMapLabel> m;
label_id = 0;
m[label_id] = label;
mapLabels[area] = m;
} else {
label_id = createMapLabelID(area);
if (label_id > -1) {
mapLabels[area].insert(label_id, label);
}
}
if (mpMapper) {
mpMapper->mp2dMap->update();
}
return label_id;
}
int TMap::createMapImageLabel(int area, QString imagePath, float x, float y, float z, float width, float height, float zoom, bool showOnTop, bool noScaling)
{
if (!mpRoomDB->getArea(area)) {
return -1;
}
TMapLabel label;
label.size = QSizeF(width, height);
label.pos = QVector3D(x, y, z);
label.showOnTop = showOnTop;
label.noScaling = noScaling;
QRectF drawRect = QRectF(0, 0, width * zoom, height * zoom);
QPixmap imagePixmap = QPixmap(imagePath);
QPixmap pix = QPixmap(drawRect.size().toSize());
pix.fill(Qt::transparent);
QPainter lp(&pix);
lp.drawPixmap(QPoint(0, 0), imagePixmap.scaled(drawRect.size().toSize()));
label.size = QSizeF(width, height);
label.pix = pix;
if (!mpRoomDB->getArea(area)) {
return -1;
}
int label_id;
// No labels exist for this area, so start from zero.
if (!mapLabels.contains(area)) {
QMap<int, TMapLabel> m;
label_id = 0;
m[label_id] = label;
mapLabels[area] = m;
} else {
label_id = createMapLabelID(area);
if (label_id > -1) {
mapLabels[area].insert(label_id, label);
}
}
if (mpMapper) {
mpMapper->mp2dMap->update();
}
return label_id;
}
int TMap::createMapLabelID(int area)
{
if (mapLabels.contains(area)) {
const QList<int> idList = mapLabels.value(area).keys();
int id = 0;
// protect against integer overflow
while (id >= 0) {
if (!idList.contains(id)) {
return id;
}
id++;
}
}
return -1;
}
void TMap::deleteMapLabel(int area, int labelID)
{
if (!mpRoomDB->getArea(area)) {
return;
}
if (!mapLabels.contains(area)) {
return;
}
if (!mapLabels[area].contains(labelID)) {
return;
}
mapLabels[area].remove(labelID);
if (mpMapper) {
mpMapper->mp2dMap->update();
}
}
void TMap::postMessage(const QString text)
{
mStoredMessages.append(text);
Host* pHost = mpHost;
if (pHost) {
while (!mStoredMessages.isEmpty()) {
pHost->postMessage(mStoredMessages.takeFirst());
}
}
}
// Used by the 2D mapper to send view center coordinates to 3D one
void TMap::set3DViewCenter(const int areaId, const int xPos, const int yPos, const int zPos)
{
mpM->setViewCenter(areaId, xPos, yPos, zPos);
}
void TMap::appendRoomErrorMsg(const int roomId, const QString msg, const bool isToSetFileViewingRecommended)
{
mMapAuditRoomErrors[roomId].append(msg);
mIsFileViewingRecommended = isToSetFileViewingRecommended ? true : mIsFileViewingRecommended;
}
void TMap::appendAreaErrorMsg(const int areaId, const QString msg, const bool isToSetFileViewingRecommended)
{
mMapAuditAreaErrors[areaId].append(msg);
mIsFileViewingRecommended = isToSetFileViewingRecommended ? true : mIsFileViewingRecommended;
}
void TMap::appendErrorMsg(const QString msg, const bool isToSetFileViewingRecommended)
{
mMapAuditErrors.append(msg);
mIsFileViewingRecommended = isToSetFileViewingRecommended ? true : mIsFileViewingRecommended;
}
void TMap::appendErrorMsgWithNoLf(const QString msg, const bool isToSetFileViewingRecommended)
{
QString text = msg;
text.replace(QChar::LineFeed, QChar::Space);
mMapAuditErrors.append(text);
mIsFileViewingRecommended = isToSetFileViewingRecommended ? true : mIsFileViewingRecommended;
}
const QString TMap::createFileHeaderLine(const QString title, const QChar fillChar)
{
QString text;
if (title.length() <= 76) {
text = QStringLiteral("%1 %2 %1\n").arg(QString(fillChar).repeated((78 - title.length()) / 2), title);
} else {
text = title;
text.append(QChar::LineFeed);
}
return text;
}
void TMap::pushErrorMessagesToFile(const QString title, const bool isACleanup)
{
Host* pHost = mpHost;
if (!pHost) {
qWarning() << "TMap::pushErrorMessagesToFile( ... ) ERROR: called with a NULL HOST pointer - something is wrong!";
return;
}
// Replacement storage locations:
QMap<int, QList<QString>> mapAuditRoomErrors; // Key is room number (where renumbered is the original one), Value is the errors, appended as they are found
QMap<int, QList<QString>> mapAuditAreaErrors; // As for the Room ones but with key as the area number
QList<QString> mapAuditErrors; // For the whole map
// Switch message storage locations to freeze them so we can dump them to
// file; according to Qt documentation "Swaps XXX other with this XXX. This
// operation is very fast and never fails."
mapAuditErrors.swap(mMapAuditErrors);
mapAuditAreaErrors.swap(mMapAuditAreaErrors);
mapAuditRoomErrors.swap(mMapAuditRoomErrors);
if (mapAuditErrors.isEmpty() && mapAuditAreaErrors.isEmpty() && mapAuditRoomErrors.isEmpty() && isACleanup) {
mIsFileViewingRecommended = false;
return; // Nothing to do
}
pHost->mErrorLogStream << createFileHeaderLine(title, QLatin1Char('#'));
pHost->mErrorLogStream << createFileHeaderLine(tr("Map issues"), QLatin1Char('='));
QListIterator<QString> itMapMsg(mapAuditErrors);
while (itMapMsg.hasNext()) {
pHost->mErrorLogStream << itMapMsg.next() << QLatin1Char('\n');
;
}
pHost->mErrorLogStream << createFileHeaderLine(tr("Area issues"), QLatin1Char('='));
QMapIterator<int, QList<QString>> itAreasMsg(mapAuditAreaErrors);
while (itAreasMsg.hasNext()) {
itAreasMsg.next();
QString titleText;
if (!mpRoomDB->getAreaNamesMap().value(itAreasMsg.key()).isEmpty()) {
titleText = tr(R"(Area id: %1 "%2")").arg(itAreasMsg.key()).arg(mpRoomDB->getAreaNamesMap().value(itAreasMsg.key()));
} else {
titleText = tr("Area id: %1").arg(itAreasMsg.key());
}
pHost->mErrorLogStream << createFileHeaderLine(titleText, QLatin1Char('-'));
QListIterator<QString> itMapAreaMsg(itAreasMsg.value());
while (itMapAreaMsg.hasNext()) {
pHost->mErrorLogStream << itMapAreaMsg.next() << QLatin1Char('\n');
}
}
pHost->mErrorLogStream << createFileHeaderLine(tr("Room issues"), QLatin1Char('='));
QMapIterator<int, QList<QString>> itRoomsMsg(mapAuditRoomErrors);
while (itRoomsMsg.hasNext()) {
itRoomsMsg.next();
QString titleText;
TRoom* pR = mpRoomDB->getRoom(itRoomsMsg.key());
if (pR && !pR->name.isEmpty()) {
titleText = tr(R"(Room id: %1 "%2")").arg(itRoomsMsg.key()).arg(pR->name);
} else {
titleText = tr("Room id: %1").arg(itRoomsMsg.key());
}
pHost->mErrorLogStream << createFileHeaderLine(titleText, QLatin1Char('-'));
QListIterator<QString> itMapRoomMsg(itRoomsMsg.value());
while (itMapRoomMsg.hasNext()) {
pHost->mErrorLogStream << itMapRoomMsg.next() << QLatin1Char('\n');
;
}
}
pHost->mErrorLogStream << createFileHeaderLine(tr("End of report"), QLatin1Char('#'));
pHost->mErrorLogStream.flush();
mapAuditErrors.clear();
mapAuditAreaErrors.clear();
mapAuditRoomErrors.clear();
if (mIsFileViewingRecommended && (!mudlet::self()->showMapAuditErrors())) {
postMessage(tr("[ ALERT ] - At least one thing was detected during that last map operation\n"
"that it is recommended that you review the most recent report in\n"
"the file:\n"
"\"%1\"\n"
"- look for the (last) report with the title:\n"
"\"%2\".")
.arg(mudlet::getMudletPath(mudlet::profileLogErrorsFilePath, mProfileName), title));
} else if (mIsFileViewingRecommended && mudlet::self()->showMapAuditErrors()) {
postMessage(tr("[ INFO ] - The equivalent to the above information about that last map\n"
"operation has been saved for review as the most recent report in\n"
"the file:\n"
"\"%1\"\n"
"- look for the (last) report with the title:\n"
"\"%2\".")
.arg(mudlet::getMudletPath(mudlet::profileLogErrorsFilePath, mProfileName), title));
}
mIsFileViewingRecommended = false;
}
void TMap::downloadMap(const QString& remoteUrl, const QString& localFileName)
{
Host* pHost = mpHost;
if (!pHost) {
return;
}
// Incidentally this should address: https://bugs.launchpad.net/mudlet/+bug/852861
if (!mXmlImportMutex.tryLock(0)) {
QString warnMsg = QStringLiteral("[ WARN ] - Attempt made to download an XML map when one has already been\n"
"requested or is being imported from a local file - wait for that\n"
"operation to complete (if it cannot be canceled) before retrying!");
postMessage(warnMsg);
return;
}
// We have the mutex locked - MUST unlock it when done under ALL circumstances
QUrl url;
if (remoteUrl.isEmpty()) {
if (!getMmpMapLocation().isEmpty()) {
url = QUrl::fromUserInput(getMmpMapLocation());
} else {
url = QUrl::fromUserInput(QStringLiteral("https://www.%1/maps/map.xml").arg(pHost->mUrl));
}
} else {
url = QUrl::fromUserInput(remoteUrl);
}
if (!url.isValid()) {
QString errMsg = QStringLiteral("[ WARN ] - Attempt made to download an XML from an invalid URL. The URL was:\n"
"%1\n"
"and the error message (may contain technical details) was:"
"\"%2\".")
.arg(url.toString(), url.errorString());
postMessage(errMsg);
mXmlImportMutex.unlock();
return;
}
if (localFileName.isEmpty()) {
mLocalMapFileName = mudlet::getMudletPath(mudlet::profileXmlMapPathFileName, mProfileName);
} else {
mLocalMapFileName = localFileName;
}
QNetworkRequest request = QNetworkRequest(url);
// This should prevent similar problems to those mentioned in:
// https://bugs.launchpad.net/mudlet/+bug/1366781 although the fix for THAT
// is elsewhere and is to be inserted separately to the changeset that
// placed this code here:
request.setRawHeader(QByteArray("User-Agent"), QByteArray(QStringLiteral("Mozilla/5.0 (Mudlet/%1%2)").arg(APP_VERSION, APP_BUILD).toUtf8().constData()));
#ifndef QT_NO_SSL
if (url.scheme() == QStringLiteral("https")) {
QSslConfiguration config(QSslConfiguration::defaultConfiguration());
request.setSslConfiguration(config);
}
#endif
mExpectedFileSize = 4000000;
QString infoMsg = tr("[ INFO ] - Map download initiated, please wait...");
postMessage(infoMsg);
qApp->processEvents();
// Attempts to ensure INFO message gets shown before download is initiated!
mpNetworkReply = mpNetworkAccessManager->get(QNetworkRequest(QUrl(url)));
// Using zero for both min and max values should cause the bar to oscillate
// until the first update
mpProgressDialog = new QProgressDialog(tr("Downloading XML map file for use in %1...").arg(mProfileName), tr("Abort"), 0, 0);
mpProgressDialog->setWindowTitle(tr("Map download"));
mpProgressDialog->setWindowIcon(QIcon(QStringLiteral(":/icons/mudlet_map_download.png")));
mpProgressDialog->setMinimumWidth(300);
mpProgressDialog->setAutoClose(false);
mpProgressDialog->setAutoReset(false);
mpProgressDialog->setMinimumDuration(0); // Normally waits for 4 seconds before showing
connect(mpNetworkReply, &QNetworkReply::downloadProgress, this, &TMap::slot_setDownloadProgress);
// Not used: connect(mpNetworkReply, &QNetworkReply::readyRead, this, &TMap::slot_readyRead);
connect(mpNetworkReply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::error), this, &TMap::slot_downloadError);
// Not used: connect(mpNetworkReply, &QNetworkReply::sslErrors, this, &TMap::slot_sslErrors);
connect(mpProgressDialog, &QProgressDialog::canceled, this, &TMap::slot_downloadCancel);
mpProgressDialog->show();
}
// Called from TLuaInterpreter::loadFile() or dlgProfilePreferences's "loadMap"
// both via TConsole::importMap( QFile & ) - it is intended to prevent
// readXmlMapFile( QFile & ) from being used more than once at a time and to
// prevent the above callers from using that when a map download is in progress!
// errMsg if, non-null is for a suitable structured error message to return to
// the TLuaInterpreter::loadFile(...) usage and is also needed to suppress the
// error message to the console
bool TMap::importMap(QFile& file, QString* errMsg)
{
if (!mXmlImportMutex.tryLock(0)) {
if (errMsg) {
*errMsg = tr("loadMap: unable to perform request, a map is already being downloaded or\n"
"imported at user request.");
} else {
QString warnMsg = QStringLiteral("[ WARN ] - Attempt made to import an XML map when one is already being\n"
"downloaded or is being imported from a local file - wait for that\n"
"operation to complete (if it cannot be canceled) before retrying!");
postMessage(warnMsg);
}
return false;
}
// We have the mutex and MUST unlock it when we are done
bool result = readXmlMapFile(file, errMsg);
// Finally release the lock on the XMLimporter
mXmlImportMutex.unlock();
return result;
}
bool TMap::readXmlMapFile(QFile& file, QString* errMsg)
{
Host* pHost = mpHost;
bool isLocalImport = false;
if (!pHost) {
return false;
}
if (!mpProgressDialog) {
// This is the local import case - which has not got a progress dialog
// until now:
isLocalImport = true;
mpProgressDialog = new QProgressDialog(tr("Importing XML map file for use in %1...").arg(mProfileName), QString(), 0, 0);
mpProgressDialog->setWindowTitle(tr("Map import"));
mpProgressDialog->setWindowIcon(QIcon(QStringLiteral(":/icons/mudlet_map_download.png")));
mpProgressDialog->setMinimumWidth(300);
mpProgressDialog->setAutoClose(false);
mpProgressDialog->setAutoReset(false);
mpProgressDialog->setMinimumDuration(0); // Normally waits for 4 seconds before showing
} else {
; // This is the download file case which is a no-op
}
// It is NOW safe to delete the map as we are in a position to load one
mapClear();
XMLimport reader(pHost);
bool result = reader.importPackage(&file);
// probably not needed for the download but might be
// needed for local file case:
mpMapper->mp2dMap->init();
// No need to call audit() as XMLimport::importPackage() does it!
// audit() produces the successful ending [ OK ] message...!
mpMapper->updateAreaComboBox();
if (result) {
mpMapper->resetAreaComboBoxToPlayerRoomArea();
} else {
// Failed...
if (errMsg) {
*errMsg = tr("loadMap: failure to import XML map file, further information may be available\n"
"in main console!");
}
}
if (isLocalImport) {
// clean-up
mpProgressDialog->deleteLater();
mpProgressDialog = nullptr;
}
mpMapper->show();
return result;
}
void TMap::slot_setDownloadProgress(qint64 got, qint64 tot)
{
if (!mpProgressDialog) {
return;
}
if (!mpProgressDialog->maximum()) {
// First call, range has not been set;
mpProgressDialog->setRange(0, mExpectedFileSize);
} else if (tot != -1 && mpProgressDialog->maximum() != static_cast<int>(tot)) {
// tot will stuck at -1 when we do not know how big the download is
// which seems to be the case for the IRE MUDS - *sigh* - Slysven
mpProgressDialog->setRange(0, static_cast<int>(tot));
}
mpProgressDialog->setValue(static_cast<int>(got));
}
void TMap::slot_downloadCancel()
{
QString alertMsg = tr("[ ALERT ] - Map download was canceled, on user's request.");
postMessage(alertMsg);
if (mpProgressDialog) {
mpProgressDialog->deleteLater();
mpProgressDialog = Q_NULLPTR; // Must reset this so it can be reused
}
if (mpNetworkReply) {
mpNetworkReply->abort(); // Will indirectly cause error() AND replyFinished signals to be sent
}
}
void TMap::slot_downloadError(QNetworkReply::NetworkError error)
{
if (!mpNetworkReply) {
return;
}
if (error != QNetworkReply::OperationCanceledError) {
// No point in reporting Cancel as that is handled elsewhere
QString errMsg = tr("[ ERROR ] - Map download encountered an error:\n%1.").arg(mpNetworkReply->errorString());
postMessage(errMsg);
}
}
void TMap::slot_replyFinished(QNetworkReply* reply)
{
if (reply != mpNetworkReply) {
qWarning() << "TMap::slot_replyFinished( QNetworkReply * ) ERROR - received argument was not the expected stored pointer.";
}
if (reply->error() != QNetworkReply::NoError) {
if (reply->error() != QNetworkReply::OperationCanceledError) {
// Don't post an error for the cancel case - it has already been done
QString alertMsg = tr("[ ALERT ] - Map download failed, error reported was:\n%1.").arg(reply->errorString());
postMessage(alertMsg);
}
// else was QNetworkReply::OperationCanceledError and we already handle
// THAT in slot_downloadCancel()
} else {
QFile file(mLocalMapFileName);
if (!file.open(QFile::WriteOnly)) {
QString alertMsg = tr("[ ALERT ] - Map download failed, unable to open destination file:\n%1.").arg(mLocalMapFileName);
postMessage(alertMsg);
} else {
// The QNetworkReply is Ok here:
if (file.write(reply->readAll()) == -1) {
QString alertMsg = tr("[ ALERT ] - Map download failed, unable to write destination file:\n%1.").arg(mLocalMapFileName);
postMessage(alertMsg);
} else {
file.flush();
file.close();
if (file.open(QFile::ReadOnly | QFile::Text)) {
QString infoMsg = tr("[ INFO ] - ... map downloaded and stored, now parsing it...");
postMessage(infoMsg);
Host* pHost = mpHost;
if (!pHost) {
qWarning() << "TMap::slot_replyFinished( QNetworkReply * ) ERROR - NULL Host pointer - something is really wrong!";
mXmlImportMutex.unlock();
return;
}
// Since the download is complete but we do not offer to
// cancel the required post-processing we should now hide
// the cancel/abort button:
mpProgressDialog->setCancelButton(Q_NULLPTR);
// The action to parse the XML file has been refactored to
// a separate method so that it can be shared with the
// direct importation of a local copy of a map file.
if (readXmlMapFile(file)) {
TEvent mapDownloadEvent {};
mapDownloadEvent.mArgumentList.append(QLatin1String("sysMapDownloadEvent"));
mapDownloadEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
pHost->raiseEvent(mapDownloadEvent);
} else {
// Failure in parse file...
QString alertMsg = tr("[ ERROR ] - Map download problem, failure in parsing destination file:\n%1.").arg(mLocalMapFileName);
postMessage(alertMsg);
}
file.close();
} else {
QString alertMsg = tr("[ ERROR ] - Map download problem, unable to read destination file:\n%1.").arg(mLocalMapFileName);
postMessage(alertMsg);
}
}
}
}
reply->deleteLater();
mpNetworkReply = Q_NULLPTR;
// We don't delete the progress dialog until here as we now use it to inform
// about post-download operations
mpProgressDialog->deleteLater();
mpProgressDialog = Q_NULLPTR; // Must reset this so it can be reused
mLocalMapFileName.clear();
mExpectedFileSize = 0;
// We have finished with the XMLimporter so must release the lock on it
mXmlImportMutex.unlock();
}
void TMap::reportStringToProgressDialog(const QString text)
{
if (mpProgressDialog) {
mpProgressDialog->setLabelText(text);
// Needed to make the changed text show, it does increase the overall
// time a little but as the main usage is when parsing XML room data
// and that can take MORE THAN A MINUTE the activity is essential to
// inform the user that something IS happening...
qApp->processEvents();
}
}
void TMap::reportProgressToProgressDialog(const int current, const int maximum)
{
if (mpProgressDialog) {
if (mpProgressDialog->maximum() != maximum) {
mpProgressDialog->setMaximum(maximum);
}
mpProgressDialog->setValue(current);
}
}
QHash<QString, QSet<int>> TMap::roomSymbolsHash()
{
QHash<QString, QSet<int>> results;
QHashIterator<int, TRoom*> itRoom(mpRoomDB->getRoomMap());
while (itRoom.hasNext()) {
itRoom.next();
if (itRoom.value() && !itRoom.value()->mSymbol.isEmpty()) {
if (results.contains(itRoom.value()->mSymbol)) {
results[itRoom.value()->mSymbol].insert(itRoom.key());
} else {
QSet<int> newEntry;
newEntry << itRoom.key();
results.insert(itRoom.value()->mSymbol, newEntry);
}
}
}
return results;
}
void TMap::setMmpMapLocation(const QString &location)
{
mMmpMapLocation = location;
qDebug() << "MMP map registered at" << mMmpMapLocation;
}
QString TMap::getMmpMapLocation() const
{
return mMmpMapLocation;
}