/*************************************************************************** * 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 "mudlet.h" #include "pre_guard.h" #include #include #include #include #include #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) #if defined(INCLUDE_3DMAPPER) , mpM(Q_NULLPTR) #endif , mpMapper(Q_NULLPTR) , mMapGraphNeedsUpdate(true) , mNewMove(true) // default map version that new maps will get , mDefaultVersion(20) // 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(17) , 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 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 roomRemapping; // These are populated by the auditRooms(...) QHash 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 itArea(mpRoomDB->getAreaMap()); while (itArea.hasNext()) { itArea.next(); int areaID = itArea.key(); if (mapLabels.contains(areaID)) { QList 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::max()) || (l.size.width() < -std::numeric_limits::max())) { mapLabels[areaID][i].size.setWidth(l.pix.width()); } if ((l.size.height() > std::numeric_limits::max()) || (l.size.height() < -std::numeric_limits::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 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::tidyMap(int areaID) { } void TMap::solveRoomCollision(int id, int creationDirection, bool PCheck) { } QList TMap::detectRoomCollisions(int id) { QList 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 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 unUsableRoomSet; // Keep track of the unusuable rather than the useable ones because that is // hopefully a MUCH smaller set in normal situations! QHashIterator 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 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 bestRoutes; // key is target (destination room), // value is data we will need to store later, QMap 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 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 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... 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 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 d(num_vertices(g)); try { astar_search(g, start, distance_heuristic>(locations, goal), predecessor_map(&p[0]).distance_map(&d[0]).visitor(astar_goal_visitor(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 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 < 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; } ofs << mpRoomDB->getAreaMap().size(); // serialize area table QMapIterator itAreaList(mpRoomDB->getAreaMap()); QList 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 from a QList in version 18 QList _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; ofs << pA->xmaxForZ; ofs << pA->ymaxForZ; ofs << pA->xminForZ; ofs << pA->yminForZ; ofs << pA->pos; ofs << pA->isZone; ofs << pA->zoneAreaRef; ofs << pA->mUserData; } 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> 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 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 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 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> oldLinesData; QMapIterator> 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 oldLinesArrowData; QMapIterator 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> oldLinesColorData; QMapIterator itCustomLineColor(pR->customLinesColor); while (itCustomLineColor.hasNext()) { itCustomLineColor.next(); QString direction(itCustomLineColor.key()); QList 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 oldLineStyleData; QMapIterator 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: [[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); // Is the RUN-TIME version of the Qt libraries equal to or more than // Qt 5.13.0? Then force things to use the backwards compatible format // - for us - of Qt 5.12.0 - this is needed because the way that the // QFont class is stored in a binary format has changed at 5.13 and it // causes crashes when a new version of the Qt libraries tries to read // the older format: if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) { // 18 is the enum value corresponding to QDataStream::Qt_5_12 which // we want to force to be used but we cannot use the enum directly // because it will not be defined in older versions of the Qt // library when the code is compilated: ifs.setVersion(mudlet::scmQDataStreamFormat_5_12); } 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, its 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::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(( mIsOnlyMapSymbolFontToBeUsed ? QFont::NoFontMerging : 0) | QFont::PreferOutline | QFont::PreferAntialias | QFont::PreferQuality | QFont::PreferNoShaping )); 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 to QSet as the later is // faster in many of the cases where we use it. ifs >> pA->rooms; } else { QList 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 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 _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); if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) { ifs.setVersion(mudlet::scmQDataStreamFormat_5_12); } 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 (mudlet::scmIsReleaseVersion || mudlet::scmIsPublicTestVersion) { // This is a release/public test 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 _dummyQMapIntInt; ifs >> _dummyQMapIntInt; // AreaNamesMap QMap _dummyQMapIntQString; ifs >> _dummyQMapIntQString; } if (otherProfileVersion >= 5) { // customEnvColors QMap _dummyQMapIntQColor; ifs >> _dummyQMapIntQColor; } if (otherProfileVersion >= 7) { // hashToRoomID QMap _dummyQMapQStringInt; ifs >> _dummyQMapQStringInt; } if (otherProfileVersion >= 17) { // userMapData QMap _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 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 _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 _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 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 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 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) { #if defined(INCLUDE_3DMAPPER) mpM->setViewCenter(areaId, xPos, yPos, zPos); #endif } 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> mapAuditRoomErrors; // Key is room number (where renumbered is the original one), Value is the errors, appended as they are found QMap> mapAuditAreaErrors; // As for the Room ones but with key as the area number QList 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 itMapMsg(mapAuditErrors); while (itMapMsg.hasNext()) { pHost->mErrorLogStream << itMapMsg.next() << QLatin1Char('\n'); ; } pHost->mErrorLogStream << createFileHeaderLine(tr("Area issues"), QLatin1Char('=')); QMapIterator> 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 itMapAreaMsg(itAreasMsg.value()); while (itMapAreaMsg.hasNext()) { pHost->mErrorLogStream << itMapAreaMsg.next() << QLatin1Char('\n'); } } pHost->mErrorLogStream << createFileHeaderLine(tr("Room issues"), QLatin1Char('=')); QMapIterator> 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 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; } // Check to ensure we have a map directory to save the map files to. QDir toProfileDir; QString toProfileDirPathString = mudlet::getMudletPath(mudlet::profileMapsPath, mProfileName); if (!toProfileDir.mkpath(toProfileDirPathString)) { QString errMsg = tr("[ ERROR ] - Unable to use or create directory to store map.\n" "Please check that you have permissions/access to:\n" "\"%1\"\n" "and there is enough space. The download operation has failed.") .arg(toProfileDirPathString); pHost->postMessage(errMsg); mXmlImportMutex.unlock(); return; } if (localFileName.isEmpty()) { if (url.toString().endsWith(QLatin1String("xml"))) { mLocalMapFileName = mudlet::getMudletPath(mudlet::profileXmlMapPathFileName, mProfileName); } else { mLocalMapFileName = mudlet::getMudletPath(mudlet::profileMapPathFileName, mProfileName, QStringLiteral("map.dat")); } } else { mLocalMapFileName = localFileName; } QNetworkRequest request = QNetworkRequest(url); pHost->updateProxySettings(mpNetworkAccessManager); mudlet::self()->setNetworkRequestDefaults(url, request); 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(request); // Using zero for both min and max values should cause the bar to oscillate // until the first update mpProgressDialog = new QProgressDialog(tr("Downloading map file for use in %1...", "%1 is the name of the current Mudlet profile") .arg(mProfileName), tr("Abort"), 0, 0); mpProgressDialog->setWindowTitle(tr("Map download", "This is a title of a progress window.")); 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::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", "This is a title of a progress dialog.")); 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(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(tot)); } mpProgressDialog->setValue(static_cast(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) { auto cleanup = [this, reply](){ 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(); }; 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.fileName().endsWith(QStringLiteral("xml"), Qt::CaseInsensitive)) { auto pHost = mpHost; if (!pHost) { cleanup(); return; } QString infoMsg = tr("[ INFO ] - ... map downloaded and stored, now parsing it..."); postMessage(infoMsg); if (pHost->mpConsole->loadMap(file.fileName())) { TEvent mapDownloadEvent {}; mapDownloadEvent.mArgumentList.append(QStringLiteral("sysMapDownloadEvent")); mapDownloadEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); pHost->raiseEvent(mapDownloadEvent); } else { QString alertMsg = tr("[ ERROR ] - Map download problem, failure in parsing destination file:\n%1.").arg(file.fileName()); postMessage(alertMsg); } cleanup(); return; } 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!"; cleanup(); 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(QStringLiteral("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); } } } } cleanup(); } 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> TMap::roomSymbolsHash() { QHash> results; QHashIterator 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 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; }