mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
Stacked on #9799, so the base is `fix-db-index-string` and this retargets to development once that merges. - `table.contains()` keeps a set of the tables it has walked, so a self-referential one (every Geyser object holds its container, which holds it back) answers instead of overflowing the stack, and `Geyser.Label:setDoubleClickCallback()` stores the `doubleClickCallback` key the label's own re-registration reads rather than one nothing reads. - db: a `UNIQUE` with no `ON CONFLICT` clause is now seen, so a change in uniqueness rebuilds the sheet; a sheet given as a list of column names takes the sheet options instead of swallowing `_index` as a phantom column; and an `_index` naming a column the sheet does not have is refused rather than quietly dropping the indexes the sheet already had. - `saveMap()` resolves a relative location against the profile directory the way `importMap()` does instead of against the directory Mudlet was started in, `loadMap()` looks in the same place, and a format version below the oldest one Mudlet can write is refused the way one that is too new already was. Test case: `lua local t = {} t.self = t display(table.contains(t, "x"))` answers `false` instead of raising, and `lua saveMap(42)` writes into `getMudletHomeDir()` rather than the directory Mudlet was started in. Worth knowing: `db:create` now hard-errors on an `_index` naming a column the sheet does not declare, where it used to load and silently lose the sheet's indexes. Closes #9777, Closes #9779, Closes #9780, Closes #9781, Closes #9782, Closes #9800, Closes #9801 Assisted-by: Claude:claude-opus-5
3731 lines
151 KiB
C++
3731 lines
151 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-2024, 2026 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 "TMapLabel.h"
|
|
#include "TMapViewManager.h"
|
|
#include "TRoomDB.h"
|
|
#include "XMLimport.h"
|
|
#include "dlgMapper.h"
|
|
#include "TLuaInterpreter.h"
|
|
#include "mapInfoContributorManager.h"
|
|
#include "mudlet.h"
|
|
|
|
#include <QBuffer>
|
|
#include <QDataStream>
|
|
#include <QElapsedTimer>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonParseError>
|
|
#include <QJsonValue>
|
|
#include <QMetaMethod>
|
|
#include <QPainter>
|
|
#include <QPixmap>
|
|
#include <QSaveFile>
|
|
#include <QSizeF>
|
|
#include <chrono>
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace {
|
|
// Restores font information from userData that was stored during binary serialization.
|
|
// Font data is stored as "family|pointSize|weight|italic" to avoid binary format version changes.
|
|
void restoreLabelFontFromUserData(TMapLabel& label, int labelId, QMap<QString, QString>& userData)
|
|
{
|
|
const QString fontKey = qsl("system.labelFont_%1").arg(labelId);
|
|
if (userData.contains(fontKey)) {
|
|
const QStringList fontParts = userData.take(fontKey).split(QLatin1Char('|'));
|
|
if (fontParts.size() == 4) {
|
|
label.font = QFont(fontParts.at(0), fontParts.at(1).toInt(), fontParts.at(2).toInt(), fontParts.at(3).toInt() != 0);
|
|
} else {
|
|
qWarning("TMap: Failed to parse font data for label %d, expected 4 parts but got %lld", labelId, fontParts.size());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Outline color data is stored as "r|g|b|a" to avoid binary format version changes.
|
|
void restoreLabelOutlineColorFromUserData(TMapLabel& label, int labelId, QMap<QString, QString>& userData)
|
|
{
|
|
const QString colorKey = qsl("system.labelOutlineColor_%1").arg(labelId);
|
|
if (userData.contains(colorKey)) {
|
|
const QStringList colorParts = userData.take(colorKey).split(QLatin1Char('|'));
|
|
if (colorParts.size() == 4) {
|
|
label.outlineColor = QColor(colorParts.at(0).toInt(), colorParts.at(1).toInt(), colorParts.at(2).toInt(), colorParts.at(3).toInt());
|
|
} else {
|
|
qWarning("TMap: Failed to parse outline color data for label %d, expected 4 parts but got %lld", labelId, colorParts.size());
|
|
}
|
|
}
|
|
}
|
|
} // anonymous namespace
|
|
|
|
TMap::TMap(Host* pH, const QString& profileName)
|
|
: mDefaultAreaName(tr("Default Area"))
|
|
, mUnnamedAreaName(tr("Unnamed Area"))
|
|
, mpRoomDB(std::make_unique<TRoomDB>(this))
|
|
, mpViewManager(new TMapViewManager(pH, this))
|
|
, mpHost(pH)
|
|
, mProfileName(profileName)
|
|
{
|
|
restore16ColorSet();
|
|
|
|
// TODO: https://github.com/Mudlet/Mudlet/issues/6436
|
|
// 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);
|
|
|
|
mMapInfoContributorManager = new MapInfoContributorManager(this, pH);
|
|
|
|
connect(mpNetworkAccessManager, &QNetworkAccessManager::finished, this, &TMap::slot_replyFinished);
|
|
}
|
|
|
|
TMap::~TMap()
|
|
{
|
|
if (!mStoredMessages.isEmpty()) {
|
|
qWarning() << "TMap::~TMap() Instance being destroyed before it could display some messages,\n"
|
|
<< "messages are:\n"
|
|
<< "------------";
|
|
for (const auto& message : std::as_const(mStoredMessages)) {
|
|
qWarning() << message << "\n------------";
|
|
}
|
|
}
|
|
}
|
|
|
|
void TMap::mapClear()
|
|
{
|
|
mpRoomDB->clearMapDB();
|
|
mEnvColors.clear();
|
|
mRoomIdHash.clear();
|
|
mTargetID = 0;
|
|
mPathList.clear();
|
|
mDirList.clear();
|
|
mWeightList.clear();
|
|
mCustomEnvColors.clear();
|
|
// Need to restore the default colours:
|
|
restore16ColorSet();
|
|
roomidToIndex.clear();
|
|
edgeHash.clear();
|
|
locations.clear();
|
|
mMapGraphNeedsUpdate = true;
|
|
mNewMove = true;
|
|
mVersion = mDefaultVersion;
|
|
mUserData.clear();
|
|
|
|
// mSaveVersion is not reset - so that any new Mudlet map file saves are to
|
|
// whatever version was previously set/deduced
|
|
|
|
// Must also reset the mapper area selection control to reflect that it now
|
|
// only has the "Default Area" after TRoomDB::clearMapDB() has been run.
|
|
if (mpMapper) {
|
|
mpMapper->updateAreaComboBox();
|
|
|
|
auto map = mpMapper->mp2dMap;
|
|
if (map) {
|
|
map->mMultiSelectionListWidget.clear();
|
|
map->mMultiSelectionListWidget.hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
// The supplied message should contain a localised message and no "WARNING:" or other prefixes:
|
|
void TMap::logError(const QString& msg)
|
|
{
|
|
if (mpHost->mpEditorDialog) {
|
|
/*: Used to print a map error in the Errors console in the Editor, %1 is the
|
|
message text and a line-feed is also appended.*/
|
|
mpHost->mpEditorDialog->mpErrorConsole->print(tr("[MAP ERROR:] %1").arg(msg).append(QChar::LineFeed), 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 deferAreaRecalculations)
|
|
{
|
|
TRoom* pR = mpRoomDB->getRoom(id);
|
|
if (!pR) {
|
|
logError(tr("Can not set room with RoomID %1 to AreaID %2. Room does not exist!").arg(QString::number(id), QString::number(area)));
|
|
return false;
|
|
}
|
|
|
|
TArea* pA = mpRoomDB->getArea(area);
|
|
if (!pA) {
|
|
// Uh oh, the area doesn't seem to exist as a TArea instance, let's check
|
|
// to see if it exists as a name only:
|
|
if (!mpRoomDB->getAreaNamesMap().contains(area)) {
|
|
// Ah, no it doesn't so moan:
|
|
logError(tr("Can not set room with RoomID %1 to AreaID %2. Area does not exist!").arg(QString::number(id), QString::number(area)));
|
|
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...
|
|
}
|
|
|
|
const bool result = pR->setArea(area, deferAreaRecalculations);
|
|
if (result) {
|
|
mMapGraphNeedsUpdate = true;
|
|
setUnsaved(__func__);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool TMap::addRoom(int id)
|
|
{
|
|
if (mpRoomDB->addRoom(id)) {
|
|
mMapGraphNeedsUpdate = true;
|
|
setUnsaved(__func__);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TMap::setRoomCoordinates(int id, int x, int y, int z)
|
|
{
|
|
TRoom* pR = mpRoomDB->getRoom(id);
|
|
if (!pR) {
|
|
return false;
|
|
}
|
|
|
|
const int oldX = pR->x();
|
|
const int oldY = pR->y();
|
|
const int oldZ = pR->z();
|
|
|
|
if (oldX != x || oldY != y || oldZ != z) {
|
|
TArea* pA = mpRoomDB->getArea(pR->getArea());
|
|
if (pA) {
|
|
// Atomically update both indices for any coordinate change.
|
|
pA->moveRoom(id, oldZ, oldX, oldY, z, x, y);
|
|
}
|
|
}
|
|
|
|
pR->setCoordinates(x, y, z);
|
|
|
|
setUnsaved(__func__);
|
|
return true;
|
|
}
|
|
|
|
int compSign(int a, int b)
|
|
{
|
|
return (a < 0) == (b < 0);
|
|
}
|
|
|
|
// Will connect the exit stub in the indicated direction to a suitable room
|
|
// i.e. in the "right" (x,y,z) location AND with a stub in the reverse direction
|
|
// IN THE SAME AREA as the fromRoomId numbered room and also create the exit in
|
|
// the reverse direction from the other room - otherwise it will report the
|
|
// reason why it cannot.
|
|
QString TMap::connectExitStubByDirection(const int fromRoomId, const int dirType)
|
|
{
|
|
Q_ASSERT_X(scmUnitVectors.contains(dirType), "TMap::connectExitStubByDirection(...)", "there is no unitVector.value() for the given dirType");
|
|
Q_ASSERT_X(scmReverseDirections.contains(dirType), "TMap::connectExitStubByDirection(...)", "there is no scmReverseDirections.value() for the given dirType");
|
|
|
|
TRoom* pFromR = mpRoomDB->getRoom(fromRoomId);
|
|
if (!pFromR) {
|
|
return qsl("fromID (%1) does not exist").arg(fromRoomId);
|
|
}
|
|
const int area = pFromR->getArea();
|
|
// This will get converted to a positive value on first use:
|
|
int minDistance = -1;
|
|
int minDistanceRoom = 0;
|
|
int meanSquareDistance = 0;
|
|
|
|
if (!pFromR->exitStubs.contains(dirType)) {
|
|
return qsl("fromID (%1) does not have an exit stub in the given direction '%2' (%3)").arg(QString::number(fromRoomId), TRoom::dirCodeToString(dirType), QString::number(dirType));
|
|
}
|
|
|
|
const int reverseDir = scmReverseDirections.value(dirType);
|
|
const QVector3D unitVector = scmUnitVectors.value(dirType);
|
|
// QVector3D is composed of floating point values so we need to round them
|
|
// if we want to assign them to integral variables without compiler warnings!
|
|
const int ux = qRound(unitVector.x());
|
|
const int uy = qRound(unitVector.y());
|
|
const int uz = qRound(unitVector.z());
|
|
const int rx = pFromR->x();
|
|
const int ry = pFromR->y();
|
|
const int rz = pFromR->z();
|
|
int dx = 0;
|
|
int dy = 0;
|
|
int dz = 0;
|
|
TArea* pA = mpRoomDB->getArea(area);
|
|
if (!pA) {
|
|
return qsl("fromID (%1) room does not have an area").arg(fromRoomId);
|
|
}
|
|
|
|
QSetIterator<int> itRoom(pA->getAreaRooms());
|
|
while (itRoom.hasNext()) {
|
|
auto toRoom = itRoom.next();
|
|
auto pToR = mpRoomDB->getRoom(toRoom);
|
|
if (!pToR || pToR->getId() == fromRoomId) {
|
|
continue;
|
|
}
|
|
|
|
// New test - does this room have a stub exit in the wanted reverse
|
|
// direction:
|
|
if (!pToR->exitStubs.contains(reverseDir)) {
|
|
continue;
|
|
}
|
|
|
|
if (uz) {
|
|
dz = pToR->z() - rz;
|
|
if (!compSign(dz, uz) || !dz) {
|
|
continue;
|
|
}
|
|
|
|
} else {
|
|
//to avoid lower/upper floors from stealing stubs
|
|
if (pToR->z() != rz) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (ux) {
|
|
dx = pToR->x() - rx;
|
|
if (!compSign(dx, ux) || !dx) {
|
|
//we do !dx pRto make sure we have a component in the desired direction
|
|
continue;
|
|
}
|
|
|
|
} else {
|
|
//to avoid rooms on same plane from stealing stubs
|
|
if (pToR->x() != rx) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (uy) {
|
|
dy = pToR->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 (pToR->y() != ry) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
meanSquareDistance = dx * dx + dy * dy + dz * dz;
|
|
if (Q_UNLIKELY(minDistance == -1) || (meanSquareDistance < minDistance)) {
|
|
// The first alternative above is the initialisaton case:
|
|
minDistanceRoom = toRoom;
|
|
minDistance = meanSquareDistance;
|
|
}
|
|
}
|
|
|
|
if (minDistanceRoom) {
|
|
auto pToR = mpRoomDB->getRoom(minDistanceRoom);
|
|
if (!pToR) {
|
|
// Technically this should be redundant as we have already checked
|
|
// that this room existed in the above while() loop!
|
|
return qsl("nearest room in the indicated direction (%1) does not exist").arg(minDistanceRoom);
|
|
}
|
|
|
|
setExit(fromRoomId, minDistanceRoom, dirType);
|
|
setExit(minDistanceRoom, fromRoomId, scmReverseDirections.value(dirType));
|
|
setUnsaved(__func__);
|
|
return {};
|
|
}
|
|
|
|
return qsl("fromID (%1) does not have another room in the indicated direction '%2' (%3) with an exit stub in the reverse direction to connect to in its area")
|
|
.arg(QString::number(fromRoomId), TRoom::dirCodeToString(dirType), QString::number(dirType));
|
|
}
|
|
|
|
// Will connect an exit stub from the fromRoomId numbered room to the toRoomId
|
|
// numbered room and also connect the corresponding stub exit in the reverse
|
|
// direction of the toRoomId room back to the fromRoomId provided the second
|
|
// room has a stub in the reverse direction.
|
|
// Unlike the connectExitStubByDirection(...) method the relative placement of
|
|
// the two rooms is not considered - and indeed the toRoomId room need not be
|
|
// IN THE SAME AREA as the fromRoomId numbered room - otherwise it will report
|
|
// the reason why it cannot.
|
|
// It will only work if there is a single matching pair of stub exits between
|
|
// the two rooms - if there are more than one it will fail and invite the
|
|
// use of the Lua function with three arguments that include a direction and
|
|
// thus use connectExitStubByDirectionAndToId(...) instead:
|
|
QString TMap::connectExitStubByToId(const int fromRoomId, const int toRoomId)
|
|
{
|
|
auto pFromR = mpRoomDB->getRoom(fromRoomId);
|
|
if (!pFromR) {
|
|
return qsl("fromID (%1) does not exist").arg(fromRoomId);
|
|
}
|
|
|
|
if (toRoomId == fromRoomId) {
|
|
return qsl("fromID and toID are the same (%1)").arg(fromRoomId);
|
|
}
|
|
|
|
auto pToR = mpRoomDB->getRoom(toRoomId);
|
|
if (!pToR) {
|
|
return qsl("toID (%1) room does not exist").arg(toRoomId);
|
|
}
|
|
|
|
if (pFromR->exitStubs.isEmpty()) {
|
|
return qsl("fromID (%1) does not have any stub exits").arg(fromRoomId);
|
|
}
|
|
|
|
if (pToR->exitStubs.isEmpty()) {
|
|
return qsl("toID (%1) does not have any stub exits").arg(toRoomId);
|
|
}
|
|
|
|
QSet<int> const fromRoomStubs{pFromR->exitStubs.cbegin(), pFromR->exitStubs.cend()};
|
|
QListIterator<int> itToRoomStubs{pToR->exitStubs};
|
|
QSet<int> toReverseStubDirections;
|
|
while (itToRoomStubs.hasNext()) {
|
|
auto direction = itToRoomStubs.next();
|
|
Q_ASSERT_X(scmReverseDirections.contains(direction), "TMap::connectExitStubByToId(...)", "there is no scmReverseDirections.value() for a particular direction encountered");
|
|
toReverseStubDirections.insert(scmReverseDirections.value(direction));
|
|
}
|
|
|
|
QSet<int> usableStubDirections{fromRoomStubs};
|
|
usableStubDirections.detach();
|
|
usableStubDirections = usableStubDirections.intersect(toReverseStubDirections);
|
|
// Now we need to count how big this set is:
|
|
if (usableStubDirections.isEmpty()) {
|
|
return qsl("no pairs of reverse stubs found between rooms %1 and %2").arg(QString::number(fromRoomId), QString::number(toRoomId));
|
|
}
|
|
if (usableStubDirections.count() > 1) {
|
|
QStringList useableStubDirectionTexts;
|
|
QSetIterator<int> itUseableStub(usableStubDirections);
|
|
while (itUseableStub.hasNext()) {
|
|
auto direction = itUseableStub.next();
|
|
useableStubDirectionTexts << qsl("'%1' (%2)").arg(TRoom::dirCodeToString(direction), QString::number(direction));
|
|
}
|
|
return qsl("multiple pairs of reverse stubs found between rooms %1 and %2, please try again with the three argument function and one of the follow directions: %3")
|
|
.arg(QString::number(fromRoomId), QString::number(toRoomId), useableStubDirectionTexts.join(QLatin1String(", ")));
|
|
}
|
|
|
|
// else we must have just one direction:
|
|
const int usableStubDirection = *(usableStubDirections.constBegin());
|
|
setExit(fromRoomId, toRoomId, usableStubDirection);
|
|
setExit(toRoomId, fromRoomId, scmReverseDirections.value(usableStubDirection));
|
|
setUnsaved(__func__);
|
|
return {};
|
|
}
|
|
|
|
// Will connect an exit stub in the indicated direction from the fromRoomId
|
|
// numbered room to the toRoomId numbered room and also connect the
|
|
// corresponding stub exit in the reverse direction of the toRoomId room back to
|
|
// the fromRoomId provided the second room has a stub in the reverse direction.
|
|
// Unlike the connectExitStubByDirection(...) method the relative placement of
|
|
// the two rooms is not considered - and indeed the toRoomId room need not be
|
|
// IN THE SAME AREA as the fromRoomId numbered room - otherwise it will report
|
|
// the reason why it cannot.
|
|
QString TMap::connectExitStubByDirectionAndToId(const int fromRoomId, const int dirType, const int toRoomId)
|
|
{
|
|
Q_ASSERT_X(scmReverseDirections.contains(dirType), "TMap::connectExitStubByDirectionAndToId(...)", "there is no scmReverseDirections.value() for the given dirType");
|
|
|
|
auto pFromR = mpRoomDB->getRoom(fromRoomId);
|
|
if (!pFromR) {
|
|
return qsl("fromID (%1) does not exist").arg(fromRoomId);
|
|
}
|
|
|
|
if (toRoomId == fromRoomId) {
|
|
return qsl("fromID and toID are the same (%1)").arg(fromRoomId);
|
|
}
|
|
|
|
if (!pFromR->exitStubs.contains(dirType)) {
|
|
return qsl("fromID (%1) does not have an exit stub in the given direction '%2' (%3)").arg(QString::number(fromRoomId), TRoom::dirCodeToString(dirType), QString::number(dirType));
|
|
}
|
|
|
|
auto pToR = mpRoomDB->getRoom(toRoomId);
|
|
if (!pToR) {
|
|
return qsl("toID (%1) room does not exist").arg(toRoomId);
|
|
}
|
|
|
|
if (!pToR->exitStubs.contains(scmReverseDirections.value(dirType))) {
|
|
return qsl("toID (%1) does not have an exit stub in the reverse direction '%2' (%3) of that given '%4' (%5)")
|
|
.arg(QString::number(toRoomId),
|
|
TRoom::dirCodeToString(scmReverseDirections.value(dirType)),
|
|
QString::number(scmReverseDirections.value(dirType)),
|
|
TRoom::dirCodeToString(dirType),
|
|
QString::number(dirType));
|
|
}
|
|
|
|
setExit(fromRoomId, toRoomId, dirType);
|
|
setExit(toRoomId, fromRoomId, scmReverseDirections.value(dirType));
|
|
setUnsaved(__func__);
|
|
return {};
|
|
}
|
|
|
|
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);
|
|
setUnsaved(__func__);
|
|
return ret;
|
|
}
|
|
|
|
void TMap::audit()
|
|
{
|
|
// init areas
|
|
QElapsedTimer _time;
|
|
_time.start();
|
|
|
|
{ // Blocked - just to limit the scope of infoMsg...!
|
|
const 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();
|
|
const int areaID = itArea.key();
|
|
TArea* pArea = mpRoomDB->getArea(areaID);
|
|
if (!pArea) {
|
|
continue;
|
|
}
|
|
if (!pArea->mMapLabels.isEmpty()) {
|
|
QList<int> const labelIDList = pArea->mMapLabels.keys();
|
|
for (const int& i : labelIDList) {
|
|
TMapLabel const l = pArea->mMapLabels.value(i);
|
|
if (l.pix.isNull()) {
|
|
// Note that two of the last three arguments here
|
|
// (false, 40.0) are not the defaults (true, 30.0) used
|
|
// now:
|
|
const int newID = createMapLabel(areaID, l.text, l.pos.x(), l.pos.y(), l.pos.z(), l.fgColor, l.bgColor, true, false, false, 40.0, 50, std::nullopt, l.fgColor);
|
|
if (newID > -1) {
|
|
if (mudlet::self()->showMapAuditErrors()) {
|
|
const 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));
|
|
pArea->mMapLabels[i] = pArea->mMapLabels.take(newID);
|
|
|
|
} else {
|
|
if (mudlet::self()->showMapAuditErrors()) {
|
|
const 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())) {
|
|
pArea->mMapLabels[i].size.setWidth(l.pix.width());
|
|
}
|
|
if ((l.size.height() > std::numeric_limits<qreal>::max()) || (l.size.height() < -std::numeric_limits<qreal>::max())) {
|
|
pArea->mMapLabels[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()->clean();
|
|
}
|
|
|
|
{ // Blocked - just to limit the scope of infoMsg...!
|
|
const 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);
|
|
}
|
|
|
|
mpHost->getLuaInterpreter()->condenseMapLoad();
|
|
}
|
|
|
|
// This may be duplicating TArea class functionality:
|
|
QList<int> TMap::detectRoomCollisions(int id)
|
|
{
|
|
QList<int> collList;
|
|
TRoom* pR = mpRoomDB->getRoom(id);
|
|
if (!pR) {
|
|
return collList;
|
|
}
|
|
const int area = pR->getArea();
|
|
const int x = pR->x();
|
|
const int y = pR->y();
|
|
const int z = pR->z();
|
|
TArea* pA = mpRoomDB->getArea(area);
|
|
if (!pA) {
|
|
return collList;
|
|
}
|
|
|
|
QSetIterator<int> itRoom(pA->getAreaRooms());
|
|
while (itRoom.hasNext()) {
|
|
const 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::addDirectionalRoute(QHash<unsigned int, route>& bestRoutes,
|
|
const QMap<QString, int>& exitWeights,
|
|
unsigned int source,
|
|
TRoom* pSourceR,
|
|
int target,
|
|
quint8 direction,
|
|
const QString& exitKey,
|
|
const QSet<unsigned int>& unUsableRoomSet)
|
|
{
|
|
// Skip self-edges and any exits that lead to rooms already known to be unusable.
|
|
if (target <= 0 || static_cast<int>(source) == target) {
|
|
return;
|
|
}
|
|
|
|
TLuaInterpreter* interpreter = mpHost ? mpHost->getLuaInterpreter() : nullptr;
|
|
TLuaInterpreter::ExitWeightFilterResult filterResult;
|
|
TLuaInterpreter::ExitWeightFilterResult* filterResultPtr = nullptr;
|
|
if (interpreter && interpreter->hasExitWeightFilter()) {
|
|
filterResult = interpreter->applyExitWeightFilter(static_cast<int>(source), exitKey);
|
|
if (filterResult.blocked) {
|
|
return;
|
|
}
|
|
filterResultPtr = &filterResult;
|
|
}
|
|
|
|
const bool filterOverridesBlocks = filterResultPtr && filterResultPtr->weightOverride.has_value();
|
|
|
|
if (pSourceR->isLocked && !filterOverridesBlocks) {
|
|
return;
|
|
}
|
|
|
|
const bool isSpecialExit = direction == DIR_OTHER;
|
|
|
|
if (!filterOverridesBlocks) {
|
|
if (isSpecialExit) {
|
|
if (pSourceR->hasSpecialExitLock(exitKey)) {
|
|
return;
|
|
}
|
|
} else if (pSourceR->hasExitLock(direction)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
TRoom* pTargetR = mpRoomDB->getRoom(target);
|
|
if (!pTargetR) {
|
|
return;
|
|
}
|
|
|
|
if (!filterOverridesBlocks && (pTargetR->isLocked || unUsableRoomSet.contains(target))) {
|
|
return;
|
|
}
|
|
|
|
route r;
|
|
r.direction = direction;
|
|
if (isSpecialExit) {
|
|
r.specialExitName = exitKey;
|
|
}
|
|
|
|
int cost = exitWeights.value(exitKey, pTargetR->getWeight());
|
|
if (filterOverridesBlocks) {
|
|
cost = filterResultPtr->weightOverride.value();
|
|
}
|
|
r.cost = cost;
|
|
|
|
if (!bestRoutes.contains(target) || bestRoutes.value(target).cost > r.cost) {
|
|
bestRoutes.insert(target, r);
|
|
}
|
|
}
|
|
|
|
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;
|
|
TLuaInterpreter* interpreter = mpHost ? mpHost->getLuaInterpreter() : nullptr;
|
|
const bool exitWeightFilterActive = interpreter && interpreter->hasExitWeightFilter();
|
|
// Keep track of the unusable rather than the usable 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) {
|
|
unUsableRoomSet.insert(itRoom.key());
|
|
continue;
|
|
}
|
|
|
|
if (pR->isLocked) {
|
|
unUsableRoomSet.insert(itRoom.key());
|
|
if (!exitWeightFilterActive) {
|
|
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);
|
|
// This command maps usable TRooms (key) to index of entry in locations (for route finding).
|
|
// It loses invalid and unusable (i.e. locked) rooms
|
|
roomidToIndex.insert(itRoom.key(), roomCount++);
|
|
}
|
|
|
|
for (unsigned int i = 0; i < roomCount; ++i) {
|
|
boost::add_vertex(g);
|
|
}
|
|
|
|
// Now identify the routes between rooms, and pick out the best edges of parallel ones
|
|
for (auto l : locations) {
|
|
unsigned const 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> const exitWeights = pSourceR->getExitWeights();
|
|
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getNorth(), DIR_NORTH, qsl("n"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getEast(), DIR_EAST, qsl("e"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getSouth(), DIR_SOUTH, qsl("s"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getWest(), DIR_WEST, qsl("w"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getUp(), DIR_UP, qsl("up"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getDown(), DIR_DOWN, qsl("down"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getNortheast(), DIR_NORTHEAST, qsl("ne"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getSoutheast(), DIR_SOUTHEAST, qsl("se"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getSouthwest(), DIR_SOUTHWEST, qsl("sw"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getNorthwest(), DIR_NORTHWEST, qsl("nw"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getIn(), DIR_IN, qsl("in"), unUsableRoomSet);
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, pSourceR->getOut(), DIR_OUT, qsl("out"), unUsableRoomSet);
|
|
|
|
QMapIterator<QString, int> itSpecialExit(pSourceR->getSpecialExits());
|
|
while (itSpecialExit.hasNext()) {
|
|
itSpecialExit.next();
|
|
|
|
addDirectionalRoute(bestRoutes, exitWeights, source, pSourceR, itSpecialExit.value(), DIR_OTHER, itSpecialExit.key(), unUsableRoomSet);
|
|
} // End of while(itSpecialExit.hasNext())
|
|
|
|
// Now we have eliminated possible 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 usable rooms and found:" << edgeCount << "distinct, usable edges in:" << _time.nsecsElapsed() * 1.0e-6 << "ms.";
|
|
}
|
|
|
|
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 trivial "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->getSpecialExits().keys();
|
|
while (!specialExitCommands.isEmpty()) {
|
|
if (!pFrom->hasSpecialExitLock(specialExitCommands.at(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 const 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 const goal = roomidToIndex.value(to);
|
|
|
|
const auto vertexCount = static_cast<std::size_t>(num_vertices(g));
|
|
if (vertexCount == 0) {
|
|
qDebug() << "TMap::findPath(" << from << "," << to << ") FAIL: map graph has no vertices.";
|
|
return false;
|
|
}
|
|
|
|
if (static_cast<std::size_t>(start) >= vertexCount || static_cast<std::size_t>(goal) >= vertexCount) {
|
|
qWarning().nospace().noquote() << "TMap::findPath(" << from << "," << to << ") FAIL: start or target vertex outside of graph range (vertexCount=" << vertexCount << ").";
|
|
return false;
|
|
}
|
|
|
|
std::vector<vertex> p(vertexCount);
|
|
// 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(vertexCount);
|
|
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 (const found_goal&) {
|
|
qDebug() << "TMap::findPath(" << from << "," << to << ") INFO: time elapsed in A*:" << t.nsecsElapsed() * 1.0e-6 << "ms.";
|
|
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-6 << "ms.";
|
|
mPathList.clear();
|
|
mDirList.clear();
|
|
mWeightList.clear(); // Reset any partial results...
|
|
return false;
|
|
}
|
|
const unsigned int previousRoomId = (locations.at(previousVertex)).id;
|
|
QPair<unsigned int, unsigned int> const edgeRoomIdPair = qMakePair(previousRoomId, currentRoomId);
|
|
const 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) {
|
|
/*
|
|
* Do not translate the directions into the user's locale here,
|
|
* that is to be done in the profile specific doSpeedwalk()
|
|
* function of the mapper package as the language of the MUD
|
|
* need not be the native language of the user - translating
|
|
* them here makes the mapper harder to code as it has to
|
|
* accommodate all the possible languages the GUI of Mudlet was
|
|
* configured to support!
|
|
*/
|
|
case DIR_NORTH:
|
|
mDirList.prepend(qsl("n"));
|
|
break;
|
|
case DIR_NORTHEAST:
|
|
mDirList.prepend(qsl("ne"));
|
|
break;
|
|
case DIR_EAST:
|
|
mDirList.prepend(qsl("e"));
|
|
break;
|
|
case DIR_SOUTHEAST:
|
|
mDirList.prepend(qsl("se"));
|
|
break;
|
|
case DIR_SOUTH:
|
|
mDirList.prepend(qsl("s"));
|
|
break;
|
|
case DIR_SOUTHWEST:
|
|
mDirList.prepend(qsl("sw"));
|
|
break;
|
|
case DIR_WEST:
|
|
mDirList.prepend(qsl("w"));
|
|
break;
|
|
case DIR_NORTHWEST:
|
|
mDirList.prepend(qsl("nw"));
|
|
break;
|
|
case DIR_UP:
|
|
mDirList.prepend(qsl("up"));
|
|
break;
|
|
case DIR_DOWN:
|
|
mDirList.prepend(qsl("down"));
|
|
break;
|
|
case DIR_IN:
|
|
mDirList.prepend(qsl("in"));
|
|
break;
|
|
case DIR_OUT:
|
|
mDirList.prepend(qsl("out"));
|
|
break;
|
|
case DIR_OTHER:
|
|
mDirList.prepend(r.specialExitName);
|
|
break;
|
|
default:
|
|
qWarning().nospace().noquote() << "TMap::findPath(" << from << ", " << to << ") WARNING - 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-6 << "ms.";
|
|
return true;
|
|
}
|
|
|
|
qDebug() << "TMap::findPath(" << from << "," << to << ") INFO: did NOT find path in:" << t.nsecsElapsed() * 1.0e-6 << "ms.";
|
|
return false;
|
|
}
|
|
|
|
bool TMap::serialize(QDataStream& ofs, int saveVersion)
|
|
{
|
|
if (saveVersion > mMaxVersion) {
|
|
const QString errMsg = tr("[ ERROR ] - The format version \"%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;
|
|
}
|
|
if (saveVersion != 0 && saveVersion < mMinVersion) {
|
|
//: Shown when a map save asks for a format version older than this Mudlet can write. %1 is the version asked for, %2 the oldest one supported.
|
|
const QString errMsg = tr("[ ERROR ] - The format version \"%1\" you are trying to save the map with is too old\n"
|
|
"for this version of Mudlet. Supported are only formats from version %2.")
|
|
.arg(QString::number(saveVersion), QString::number(mMinVersion));
|
|
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) {
|
|
const QString message = tr("[ ALERT ] - Saving map in format version \"%1\" that is different than \"%2\" which\n"
|
|
"it was loaded as. 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) {
|
|
const QString message = tr("[ WARN ] - Saving map in format version \"%1\" different from the\n"
|
|
"recommended map version %2 for this version of Mudlet.")
|
|
.arg(mSaveVersion)
|
|
.arg(mDefaultVersion);
|
|
appendErrorMsgWithNoLf(message, false);
|
|
postMessage(message);
|
|
}
|
|
|
|
ofs << mSaveVersion;
|
|
ofs << mEnvColors;
|
|
ofs << mpRoomDB->getAreaNamesMap();
|
|
ofs << mCustomEnvColors;
|
|
ofs << mpRoomDB->hashToRoomID;
|
|
if (mSaveVersion < 19) {
|
|
// Save the data in the map user data for older versions - use a local
|
|
// copy so that saving does not modify the live map's user data:
|
|
QMap<QString, QString> userData{mUserData};
|
|
userData.insert(qsl("system.fallback_mapSymbolFont"), mMapSymbolFont.toString());
|
|
userData.insert(qsl("system.fallback_mapSymbolFontFudgeFactor"), QString::number(mMapSymbolFontFudgeFactor));
|
|
userData.insert(qsl("system.fallback_onlyUseMapSymbolFont"), mIsOnlyMapSymbolFontToBeUsed ? qsl("true") : qsl("false"));
|
|
ofs << userData;
|
|
} else {
|
|
ofs << mUserData;
|
|
// Save the data directly in supported format versions (19 and above)
|
|
ofs << mMapSymbolFont;
|
|
ofs << mMapSymbolFontFudgeFactor;
|
|
ofs << mIsOnlyMapSymbolFontToBeUsed;
|
|
}
|
|
|
|
// Qt 6 changed the return type of QMap<T1, T2>::size() to qsizetype which
|
|
// is an int64 rather than the int32 of Qt5 - but we need to use the latter
|
|
// to retain compatibility:
|
|
ofs << static_cast<qint32>(mpRoomDB->getAreaMap().size());
|
|
|
|
// serialize area table
|
|
QMapIterator<int, TArea*> itAreaList(mpRoomDB->getAreaMap());
|
|
while (itAreaList.hasNext()) {
|
|
itAreaList.next();
|
|
const 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> const _oldList = pA->rooms.values();
|
|
ofs << _oldList;
|
|
}
|
|
ofs << pA->zLevels;
|
|
ofs << pA->mAreaExits;
|
|
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;
|
|
if (mSaveVersion >= 21) {
|
|
// Revised in version 21 to store the value directly:
|
|
ofs << pA->mLast2DMapZoom;
|
|
} else {
|
|
pA->mUserData.insert(QLatin1String("system.fallback_map2DZoom"), QString::number(pA->get2DMapZoom()));
|
|
}
|
|
// Store font and outline color info for labels in userData (avoids binary format version change)
|
|
const auto permanentLabelsList{pA->getPermanentLabelIds()};
|
|
for (const auto labelID : permanentLabelsList) {
|
|
const auto label = pA->mMapLabels.value(labelID);
|
|
if (!label.font.family().isEmpty()) {
|
|
if (label.font.family().contains(QLatin1Char('|'))) {
|
|
qWarning("TMap::serialize() - Font family '%s' for label %d contains pipe character, font may not deserialize correctly", qUtf8Printable(label.font.family()), labelID);
|
|
}
|
|
const QString fontKey = qsl("system.labelFont_%1").arg(labelID);
|
|
const QString fontValue = qsl("%1|%2|%3|%4").arg(label.font.family()).arg(label.font.pointSize()).arg(label.font.weight()).arg(label.font.italic() ? 1 : 0);
|
|
pA->mUserData.insert(fontKey, fontValue);
|
|
}
|
|
const QString outlineColorKey = qsl("system.labelOutlineColor_%1").arg(labelID);
|
|
const QString outlineColorValue = qsl("%1|%2|%3|%4").arg(label.outlineColor.red()).arg(label.outlineColor.green()).arg(label.outlineColor.blue()).arg(label.outlineColor.alpha());
|
|
pA->mUserData.insert(outlineColorKey, outlineColorValue);
|
|
}
|
|
ofs << pA->mUserData;
|
|
if (mSaveVersion >= 21) {
|
|
// Revised in version 21 to store labels within the TArea class:
|
|
// Also we now have temporary labels, so we need to count the
|
|
// permanent ones first to use as the count for ones to store:
|
|
ofs << static_cast<qint32>(permanentLabelsList.size());
|
|
QListIterator<int> itMapLabelId(permanentLabelsList);
|
|
while (itMapLabelId.hasNext()) {
|
|
const auto labelID = itMapLabelId.next();
|
|
const auto label = pA->mMapLabels.value(labelID);
|
|
ofs << labelID;
|
|
ofs << label.pos;
|
|
ofs << label.size;
|
|
ofs << label.text;
|
|
ofs << label.fgColor;
|
|
ofs << label.bgColor;
|
|
ofs << label.pix;
|
|
ofs << label.noScaling;
|
|
ofs << label.showOnTop;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if (mSaveVersion < 21) {
|
|
// Before version 21 the map labels were stored within this class:
|
|
// First we have the number of labels per area - we need this as there
|
|
// is no delimiter between each area's map labels
|
|
QMap<int, TArea*> areasWithPermanentLabels;
|
|
// Need to count the areas that have mapLabels:
|
|
QMapIterator<int, TArea*> itArea(mpRoomDB->getAreaMap());
|
|
while (itArea.hasNext()) {
|
|
// Now we have temporary labels we need to identify areas with
|
|
// permanent ones:
|
|
itArea.next();
|
|
auto pArea = itArea.value();
|
|
if (pArea && !pArea->mMapLabels.isEmpty() && pArea->hasPermanentLabels()) {
|
|
areasWithPermanentLabels.insert(itArea.key(), itArea.value());
|
|
}
|
|
}
|
|
ofs << static_cast<qint32>(areasWithPermanentLabels.count());
|
|
QMapIterator<int, TArea*> itAreaWithLabels(areasWithPermanentLabels);
|
|
while (itAreaWithLabels.hasNext()) {
|
|
itAreaWithLabels.next();
|
|
auto pArea = itAreaWithLabels.value();
|
|
auto permanentLabelIdsList = pArea->getPermanentLabelIds();
|
|
// number of (permanent) labels in this area:
|
|
ofs << static_cast<qint32>(permanentLabelIdsList.size());
|
|
|
|
// only used to assign labels to the area:
|
|
ofs << itAreaWithLabels.key();
|
|
QListIterator<int> itPerminentMapLabelIds(permanentLabelIdsList);
|
|
while (itPerminentMapLabelIds.hasNext()) {
|
|
auto labelID = itPerminentMapLabelIds.next();
|
|
ofs << labelID; //label ID
|
|
TMapLabel const label = pArea->mMapLabels.value(labelID);
|
|
ofs << label.pos;
|
|
ofs << QPointF(); // dummy value - not actually used
|
|
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();
|
|
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;
|
|
if (mSaveVersion >= 21) {
|
|
ofs << pR->hidden;
|
|
ofs << pR->getSpecialExits();
|
|
} else {
|
|
QMultiMap<int, QString> oldSpecialExits;
|
|
QMapIterator<QString, int> itSpecialExit(pR->getSpecialExits());
|
|
while (itSpecialExit.hasNext()) {
|
|
itSpecialExit.next();
|
|
oldSpecialExits.insert(itSpecialExit.value(), (pR->hasSpecialExitLock(itSpecialExit.key()) ? QLatin1Char('1') : QLatin1Char('0')) % itSpecialExit.key());
|
|
}
|
|
ofs << oldSpecialExits;
|
|
}
|
|
if (mSaveVersion >= 19) {
|
|
ofs << pR->mSymbol;
|
|
} else {
|
|
qint8 oldCharacterCode = 0;
|
|
if (!pR->mSymbol.isEmpty()) {
|
|
// There is something for a symbol
|
|
const 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;
|
|
}
|
|
|
|
if (mSaveVersion >= 21) {
|
|
ofs << pR->mSymbolColor;
|
|
}
|
|
|
|
// Border properties are stored in userData (not binary stream) to avoid map bloat
|
|
if (pR->mBorderColor.isValid()) {
|
|
pR->userData.insert(ROOM_UI_BORDERCOLOR, pR->mBorderColor.name(QColor::HexArgb));
|
|
} else {
|
|
pR->userData.remove(ROOM_UI_BORDERCOLOR);
|
|
}
|
|
if (pR->mBorderThickness > 0) {
|
|
pR->userData.insert(ROOM_UI_BORDERTHICKNESS, QString::number(pR->mBorderThickness));
|
|
} else {
|
|
pR->userData.remove(ROOM_UI_BORDERTHICKNESS);
|
|
}
|
|
|
|
// Formats before 21 carry the hidden flag and symbol color - and
|
|
// formats before 19 the symbol - as user data fallbacks; use a local
|
|
// copy so that saving does not modify the live room's user data.
|
|
// TRoom::restore() strips each key again when loading a format that
|
|
// carries it, so none may appear in formats which store the value
|
|
// directly in the stream:
|
|
QMap<QString, QString> userData{pR->userData};
|
|
if (mSaveVersion < 21) {
|
|
if (pR->hidden) {
|
|
userData.insert(QLatin1String("system.fallback_hidden"), QLatin1String("true"));
|
|
}
|
|
if (pR->mSymbolColor.isValid()) {
|
|
userData.insert(QLatin1String("system.fallback_symbol_color"), pR->mSymbolColor.name());
|
|
}
|
|
}
|
|
if (mSaveVersion < 19 && !pR->mSymbol.isEmpty()) {
|
|
userData.insert(QLatin1String("system.fallback_symbol"), pR->mSymbol);
|
|
}
|
|
ofs << 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();
|
|
const 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();
|
|
const 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();
|
|
const 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:
|
|
[[fallthrough]];
|
|
default:
|
|
oldLineStyleData.insert(direction, QLatin1String("solid line"));
|
|
}
|
|
}
|
|
ofs << oldLineStyleData;
|
|
}
|
|
if (mSaveVersion >= 21) {
|
|
ofs << pR->getSpecialExitLocks();
|
|
}
|
|
ofs << pR->exitLocks;
|
|
ofs << pR->exitStubs;
|
|
ofs << pR->getExitWeights();
|
|
ofs << pR->doors;
|
|
}
|
|
|
|
// reset to the old map version
|
|
mSaveVersion = oldSaveVersion;
|
|
return true;
|
|
}
|
|
|
|
// file is expected to be linked to a file name but not be opened; ifs is not
|
|
// expected to be linked to any IODevice. On success file will be opened and
|
|
// ifs will be part way through it (has read the first 4 bytes which encode the
|
|
// map file version). On failure both will be in the same states as initial one:
|
|
bool TMap::validatePotentialMapFile(QFile& file, QDataStream& ifs)
|
|
{
|
|
int version = 0;
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
const QString errMsg = tr(R"([ ERROR ] - Unable to open map file for reading: "%1"!)").arg(file.fileName());
|
|
appendErrorMsg(errMsg, false);
|
|
postMessage(errMsg);
|
|
return false;
|
|
}
|
|
|
|
ifs.setDevice(&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 >> version;
|
|
if ((version < 1) || (version > 127)) {
|
|
const QString errMsg = tr("[ ALERT ] - File does not seem to be a Mudlet Map file. The part that indicates\n"
|
|
"its format version seems to be \"%1\" and that doesn't make sense. The file is:\n"
|
|
"\"%2\".")
|
|
.arg(version)
|
|
.arg(file.fileName());
|
|
appendErrorMsgWithNoLf(errMsg);
|
|
postMessage(errMsg);
|
|
const QString infoMsg = tr("[ INFO ] - Ignoring this unlikely map file.");
|
|
appendErrorMsgWithNoLf(infoMsg);
|
|
postMessage(infoMsg);
|
|
ifs.setDevice(nullptr);
|
|
file.close();
|
|
return false;
|
|
}
|
|
if (version > mMaxVersion) {
|
|
const QString errMsg = tr("[ ALERT ] - Map file is too new. Its format version \"%1\" is higher than this version of\n"
|
|
"Mudlet can handle (%2)! The file is:\n\"%3\".")
|
|
.arg(version)
|
|
.arg(mMaxVersion)
|
|
.arg(file.fileName());
|
|
appendErrorMsgWithNoLf(errMsg);
|
|
postMessage(errMsg);
|
|
const QString infoMsg = tr("[ INFO ] - You will need to update your Mudlet to read the map file.");
|
|
appendErrorMsgWithNoLf(infoMsg);
|
|
postMessage(infoMsg);
|
|
ifs.setDevice(nullptr);
|
|
file.close();
|
|
return false;
|
|
}
|
|
|
|
if (version < 4) {
|
|
const QString alertMsg = tr("[ ALERT ] - Map file is really old. Its format version \"%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(version)
|
|
.arg(file.fileName());
|
|
appendErrorMsgWithNoLf(alertMsg, false);
|
|
postMessage(alertMsg);
|
|
const 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);
|
|
} else {
|
|
// Less than (but not less than 4) or equal to default version
|
|
const QString infoMsg = tr("[ INFO ] - Reading map. Format version: %1. File:\n"
|
|
"\"%2\",\n"
|
|
"please wait...")
|
|
.arg(version)
|
|
.arg(file.fileName());
|
|
appendErrorMsg(tr(R"([ INFO ] - Reading map. Format version: %1. File: "%2".)").arg(version).arg(file.fileName()), false);
|
|
postMessage(infoMsg);
|
|
}
|
|
mVersion = version;
|
|
mSaveVersion = mDefaultVersion; // Make the save version the default one - unless the user intervenes
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TMap::restore(QString location)
|
|
{
|
|
const MapOperationScope operationScope(this);
|
|
qDebug().noquote().nospace() << "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(enums::profileMapsPath, mProfileName);
|
|
const QDir dir(folder);
|
|
QStringList filters;
|
|
filters << qsl("*.[dD][aA][tT]");
|
|
filters << qsl("*.[jJ][sS][oO][nN]");
|
|
entries = dir.entryList(filters, QDir::Files, QDir::Time);
|
|
}
|
|
|
|
bool canRestore = true;
|
|
if (entries.empty() && location.isEmpty()) {
|
|
canRestore = false;
|
|
}
|
|
|
|
QDataStream ifs;
|
|
QFile file;
|
|
if (canRestore && (!entries.empty() || !location.isEmpty())) {
|
|
// We get to here if there is one or more entries OR location is
|
|
// supplied - if the latter then there is only one file to consider but
|
|
// if the former we may have to check more than one to find a valid
|
|
// map file:
|
|
bool foundValidFile = false;
|
|
if (location.isEmpty()) {
|
|
// Look through the entries:
|
|
QStringListIterator itFileName(entries);
|
|
auto fileName = qsl("%1/%2").arg(folder, itFileName.next());
|
|
if (!fileName.endsWith(qsl(".json"), Qt::CaseInsensitive)) {
|
|
file.setFileName(fileName);
|
|
if (validatePotentialMapFile(file, ifs)) {
|
|
foundValidFile = true;
|
|
}
|
|
|
|
} else {
|
|
if (auto [isOk, message] = readJsonMapFile(fileName, true); !isOk) {
|
|
// Failed to read the JSON file
|
|
const QString errMsg = tr("[ ALERT ] - Failed to load a Mudlet JSON Map file, reason:\n"
|
|
"%1; the file is:\n"
|
|
"\"%2\".")
|
|
.arg(message, fileName);
|
|
appendErrorMsgWithNoLf(errMsg);
|
|
postMessage(errMsg);
|
|
const QString infoMsg = tr("[ INFO ] - Ignoring this map file.");
|
|
appendErrorMsgWithNoLf(infoMsg);
|
|
postMessage(infoMsg);
|
|
} else {
|
|
// immediately leave on success:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Allow for somethings to be updated - especially on Windows?
|
|
qApp->processEvents();
|
|
} else {
|
|
file.setFileName(location);
|
|
if (validatePotentialMapFile(file, ifs)) {
|
|
foundValidFile = true;
|
|
}
|
|
}
|
|
if (!foundValidFile) {
|
|
canRestore = false;
|
|
}
|
|
} else if (canRestore && !location.isEmpty()) {
|
|
file.setFileName(location);
|
|
canRestore = validatePotentialMapFile(file, ifs);
|
|
}
|
|
|
|
if (canRestore) {
|
|
// 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 >> mEnvColors;
|
|
mpRoomDB->restoreAreaMap(ifs);
|
|
}
|
|
if (mVersion >= 5) {
|
|
ifs >> mCustomEnvColors;
|
|
}
|
|
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;
|
|
if ((mVersion < 21) && mMapSymbolFont.toString().split(QLatin1String(",")).size() > 15) {
|
|
// We need to clean up the effects of using QFont(string)
|
|
// for a format 17 or 18 below - as this fix went in before
|
|
// 21 was used it only has to be used for map formats 19 and
|
|
// 20:
|
|
mMapSymbolFont.fromString(mMapSymbolFont.toString().split(QLatin1String(",")).mid(0, 10).join(QLatin1String(",")));
|
|
}
|
|
ifs >> mMapSymbolFontFudgeFactor;
|
|
ifs >> mIsOnlyMapSymbolFontToBeUsed;
|
|
// Clean up stale fallback keys that past versions could leave
|
|
// behind in the live map's user data (and thus in files saved
|
|
// from it) after saving in a format before 19:
|
|
mUserData.remove(qsl("system.fallback_mapSymbolFont"));
|
|
mUserData.remove(qsl("system.fallback_mapSymbolFontFudgeFactor"));
|
|
mUserData.remove(qsl("system.fallback_onlyUseMapSymbolFont"));
|
|
} else {
|
|
// Fallback to reading the data from the map user data - and
|
|
// remove it from the data the user will see:
|
|
// BUGFIX: Using QFont::toString() and then using that to
|
|
// construct a font again afterwards via a QFont(string) was
|
|
// incorrect as it seemed to cause the last to duplicated the
|
|
// last nine elements each time. The details of the ::toString()
|
|
// ::fromString() methods are not currently documented so the
|
|
// only details are documented in the source:
|
|
// https://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/text/qfont.cpp?h=5.15#n2070
|
|
// and:
|
|
// https://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/text/qfont.cpp?h=5.15#n2128
|
|
// this suggests that only one or ten elements are accepted so
|
|
// we CAN fix past mistakes by only considering the first ten
|
|
// elements:
|
|
const QStringList fontStrings{mUserData.take(qsl("system.fallback_mapSymbolFont")).split(QLatin1Char(','))};
|
|
const QString fontString{fontStrings.mid(0, 10).join(QLatin1Char(','))};
|
|
const QString fontFudgeFactorString = mUserData.take(qsl("system.fallback_mapSymbolFontFudgeFactor"));
|
|
const QString onlyUseSymbolFontString = mUserData.take(qsl("system.fallback_onlyUseMapSymbolFont"));
|
|
if (!fontString.isEmpty()) {
|
|
mMapSymbolFont.fromString(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 | QFont::PreferNoShaping));
|
|
if (mVersion >= 14) {
|
|
int areaSize = 0;
|
|
ifs >> areaSize;
|
|
// restore area table
|
|
for (int i = 0; i < areaSize; i++) {
|
|
auto pA = new TArea(this, mpRoomDB.get());
|
|
int areaID = 0;
|
|
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 = QSet<int>{oldRoomsList.begin(), oldRoomsList.end()};
|
|
}
|
|
// Can be useful when analysing suspect map files!
|
|
// qDebug() << "TMap::restore(...)" << "Area:" << areaID;
|
|
// qDebug() << "Rooms:" << pA->rooms;
|
|
ifs >> pA->zLevels;
|
|
ifs >> pA->mAreaExits;
|
|
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 >= 21) {
|
|
ifs >> pA->mLast2DMapZoom;
|
|
ifs >> pA->mUserData;
|
|
} else if (mVersion >= 17) {
|
|
ifs >> pA->mUserData;
|
|
const qreal fallback_map2DZoom = pA->mUserData.take(QLatin1String("system.fallback_map2DZoom")).toDouble();
|
|
pA->mLast2DMapZoom = (fallback_map2DZoom >= T2DMap::csmMinXYZoom) ? fallback_map2DZoom : T2DMap::csmDefaultXYZoom;
|
|
}
|
|
if (mVersion >= 21) {
|
|
int mapLabelsCount = -1;
|
|
ifs >> mapLabelsCount;
|
|
for (int i = 0; i < mapLabelsCount; ++i) {
|
|
int labelId = -1;
|
|
ifs >> labelId;
|
|
TMapLabel label;
|
|
ifs >> label.pos;
|
|
ifs >> label.size;
|
|
ifs >> label.text;
|
|
ifs >> label.fgColor;
|
|
ifs >> label.bgColor;
|
|
ifs >> label.pix;
|
|
ifs >> label.noScaling;
|
|
ifs >> label.showOnTop;
|
|
restoreLabelFontFromUserData(label, labelId, pA->mUserData);
|
|
restoreLabelOutlineColorFromUserData(label, labelId, pA->mUserData);
|
|
pA->mMapLabels.insert(labelId, label);
|
|
}
|
|
}
|
|
mpRoomDB->restoreSingleArea(areaID, pA);
|
|
}
|
|
}
|
|
|
|
if (!mpRoomDB->getAreaMap().keys().contains(-1)) {
|
|
auto pDefaultA = new TArea(this, mpRoomDB.get());
|
|
mpRoomDB->restoreSingleArea(-1, pDefaultA);
|
|
const 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 = 0;
|
|
ifs >> oldRoomId;
|
|
mRoomIdHash[mProfileName] = oldRoomId;
|
|
}
|
|
|
|
if (mVersion >= 11 && mVersion <= 20) {
|
|
// After version 20 the map labels have been moved to each area
|
|
int areasWithLabelsTotal = 0;
|
|
ifs >> areasWithLabelsTotal;
|
|
int areasWithLabelsCounter = 0;
|
|
while (!ifs.atEnd() && areasWithLabelsCounter < areasWithLabelsTotal) {
|
|
int areaID = -1;
|
|
int areaLabelsTotal = 0;
|
|
ifs >> areaLabelsTotal;
|
|
// Only used to identify the area for this batch of labels:
|
|
ifs >> areaID;
|
|
int areaLabelCounter = 0;
|
|
auto pA = mpRoomDB->getArea(areaID);
|
|
while (!ifs.atEnd() && areaLabelCounter < areaLabelsTotal) {
|
|
int labelID = 0;
|
|
ifs >> labelID;
|
|
TMapLabel label;
|
|
if (mVersion >= 12) {
|
|
// From version 12 labels could be placed on any level,
|
|
// so they have a z coordinate:
|
|
ifs >> label.pos;
|
|
} else {
|
|
QPointF labelPos2D;
|
|
ifs >> labelPos2D;
|
|
label.pos = QVector3D(labelPos2D);
|
|
}
|
|
// There was an unused QPointF in versions prior to 21
|
|
QPointF dummyPointF;
|
|
ifs >> dummyPointF;
|
|
ifs >> label.size;
|
|
ifs >> label.text;
|
|
ifs >> label.fgColor;
|
|
ifs >> label.bgColor;
|
|
ifs >> label.pix;
|
|
if (mVersion >= 15) {
|
|
ifs >> label.noScaling;
|
|
ifs >> label.showOnTop;
|
|
}
|
|
if (pA) {
|
|
restoreLabelFontFromUserData(label, labelID, pA->mUserData);
|
|
restoreLabelOutlineColorFromUserData(label, labelID, pA->mUserData);
|
|
pA->mMapLabels.insert(labelID, label);
|
|
}
|
|
++areaLabelCounter;
|
|
// Else: we dump labels for areas not in map - this should
|
|
// not be happening nowadays but did in the past - see
|
|
// PR #4369
|
|
}
|
|
++areasWithLabelsCounter;
|
|
}
|
|
}
|
|
|
|
while (!ifs.atEnd()) {
|
|
int i = 0;
|
|
ifs >> i;
|
|
auto pT = new TRoom(mpRoomDB.get());
|
|
pT->restore(ifs, i, mVersion);
|
|
mpRoomDB->restoreSingleRoom(i, pT);
|
|
}
|
|
|
|
restore16ColorSet();
|
|
|
|
// Recalculate area bounds to fix any corrupted yminForZ/ymaxForZ values
|
|
// from older map files (bug where first room's Y wasn't negated)
|
|
const QList<int> areaIds = mpRoomDB->getAreaIDList();
|
|
for (int areaId : areaIds) {
|
|
TArea* pA = mpRoomDB->getArea(areaId);
|
|
if (pA) {
|
|
pA->calcSpan();
|
|
}
|
|
}
|
|
|
|
const 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;
|
|
}
|
|
}
|
|
|
|
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, qsizetype* areaCount = nullptr, qsizetype* roomCount = nullptr)
|
|
{
|
|
if (profile.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
QString folder;
|
|
QStringList entries;
|
|
folder = mudlet::getMudletPath(enums::profileMapsPath, profile);
|
|
QDir dir(folder);
|
|
dir.setSorting(QDir::Time);
|
|
entries = dir.entryList(QDir::Filters(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(qsl("%1/%2").arg(folder, entries.at(0)));
|
|
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
const QString errMsg = tr(R"([ ERROR ] - Unable to open map file for reading: "%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;
|
|
|
|
const 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::self()->releaseVersion || mudlet::self()->publicTestVersion) {
|
|
// 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;
|
|
}
|
|
// 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;
|
|
}
|
|
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) {
|
|
// mCustomEnvColors
|
|
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 >= 19) {
|
|
QFont _dummyQFont;
|
|
ifs >> _dummyQFont;
|
|
double _dummyDouble;
|
|
ifs >> _dummyDouble;
|
|
bool _dummyBool;
|
|
ifs >> _dummyBool;
|
|
}
|
|
}
|
|
|
|
if (otherProfileVersion >= 14) {
|
|
int readAreaSize;
|
|
ifs >> readAreaSize;
|
|
qsizetype areaSize = static_cast<qsizetype>(readAreaSize);
|
|
if (areaCount) {
|
|
*areaCount = areaSize;
|
|
}
|
|
// read each area
|
|
for (qsizetype i = 0; i < areaSize; ++i) {
|
|
TArea pA(nullptr, nullptr);
|
|
int areaID;
|
|
ifs >> areaID;
|
|
ifs >> pA.rooms;
|
|
ifs >> pA.zLevels;
|
|
ifs >> pA.mAreaExits;
|
|
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 >= 21) {
|
|
ifs >> pA.mLast2DMapZoom;
|
|
}
|
|
if (otherProfileVersion >= 17) {
|
|
ifs >> pA.mUserData;
|
|
}
|
|
if (otherProfileVersion >= 21) {
|
|
int mapLabelsCount = -1;
|
|
ifs >> mapLabelsCount;
|
|
for (int i = 0; i < mapLabelsCount; ++i) {
|
|
int labelId = -1;
|
|
ifs >> labelId;
|
|
TMapLabel label;
|
|
ifs >> label.pos;
|
|
ifs >> label.size;
|
|
ifs >> label.text;
|
|
ifs >> label.fgColor;
|
|
ifs >> label.bgColor;
|
|
ifs >> label.pix;
|
|
ifs >> label.noScaling;
|
|
ifs >> label.showOnTop;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 && otherProfileVersion <= 20) {
|
|
int areasWithLabelsTotal = 0;
|
|
ifs >> areasWithLabelsTotal;
|
|
int areasWithLabelsCounter = 0;
|
|
while (!ifs.atEnd() && areasWithLabelsCounter < areasWithLabelsTotal) {
|
|
int areaID = -1;
|
|
int areaLabelsTotal = 0;
|
|
ifs >> areaLabelsTotal;
|
|
ifs >> areaID;
|
|
int areaLabelCounter = 0;
|
|
while (!ifs.atEnd() && areaLabelCounter < areaLabelsTotal) {
|
|
int labelID;
|
|
ifs >> labelID;
|
|
TMapLabel label;
|
|
if (otherProfileVersion >= 12) {
|
|
ifs >> label.pos;
|
|
} else {
|
|
QPointF oldLabelPos;
|
|
ifs >> oldLabelPos;
|
|
label.pos = QVector3D(oldLabelPos);
|
|
}
|
|
QPointF dummyPointF;
|
|
ifs >> dummyPointF;
|
|
ifs >> label.size;
|
|
ifs >> label.text;
|
|
ifs >> label.fgColor;
|
|
ifs >> label.bgColor;
|
|
ifs >> label.pix;
|
|
if (otherProfileVersion >= 15) {
|
|
ifs >> label.noScaling;
|
|
ifs >> label.showOnTop;
|
|
}
|
|
++areaLabelCounter;
|
|
}
|
|
++areasWithLabelsCounter;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
//NOLINT(readability-make-member-function-const)
|
|
int TMap::createMapLabel(int area,
|
|
const QString& text,
|
|
float x,
|
|
float y,
|
|
float z,
|
|
QColor fg,
|
|
QColor bg,
|
|
bool showOnTop,
|
|
bool noScaling,
|
|
bool temporary,
|
|
qreal zoom,
|
|
int fontSize,
|
|
std::optional<QString> fontName,
|
|
QColor outline)
|
|
{
|
|
auto pA = mpRoomDB->getArea(area);
|
|
if (!pA) {
|
|
return -1;
|
|
}
|
|
|
|
if (text.isEmpty()) {
|
|
return -1;
|
|
}
|
|
|
|
TMapLabel label;
|
|
label.text = text;
|
|
label.bgColor = bg;
|
|
label.fgColor = fg;
|
|
label.outlineColor = outline;
|
|
label.size = QSizeF(100, 100);
|
|
label.pos = QVector3D(x, y, z);
|
|
label.showOnTop = showOnTop;
|
|
label.noScaling = noScaling;
|
|
label.temporary = temporary;
|
|
|
|
const QRectF lr = QRectF(0, 0, 2000, 2000);
|
|
QPixmap pix(lr.size().toSize());
|
|
pix.fill(Qt::transparent);
|
|
|
|
QPainter lp(&pix);
|
|
lp.fillRect(lr, label.bgColor);
|
|
lp.setRenderHint(QPainter::Antialiasing);
|
|
|
|
QFont font(fontName.has_value() ? fontName.value() : QString(), fontSize);
|
|
label.font = font;
|
|
lp.setFont(font);
|
|
|
|
QPen outlinePen(label.outlineColor);
|
|
outlinePen.setWidth(1);
|
|
lp.setPen(outlinePen);
|
|
|
|
QRectF br;
|
|
|
|
if (label.fgColor != label.outlineColor) {
|
|
lp.drawText(QRect(19, 70, 2000, 2000), Qt::AlignLeft | Qt::AlignTop, label.text, &br);
|
|
lp.drawText(QRect(21, 70, 2000, 2000), Qt::AlignLeft | Qt::AlignTop, label.text, &br);
|
|
lp.drawText(QRect(20, 69, 2000, 2000), Qt::AlignLeft | Qt::AlignTop, label.text, &br);
|
|
lp.drawText(QRect(20, 71, 2000, 2000), Qt::AlignLeft | Qt::AlignTop, label.text, &br);
|
|
}
|
|
lp.setPen(label.fgColor);
|
|
lp.drawText(QRect(20, 70, 2000, 2000), Qt::AlignLeft | Qt::AlignTop, label.text, &br);
|
|
|
|
label.size = br.normalized().size();
|
|
const QRect brRect = br.normalized().toRect();
|
|
label.pix = pix.copy(brRect.topLeft().x(), brRect.topLeft().y(), brRect.width(), brRect.height());
|
|
const QSizeF s = QSizeF(label.size.width() / zoom, label.size.height() / zoom);
|
|
label.size = s;
|
|
label.clickSize = s;
|
|
|
|
const int labelId = pA->createLabelId();
|
|
if (Q_LIKELY(labelId >= 0)) {
|
|
pA->mMapLabels.insert(labelId, label);
|
|
if (mpMapper) {
|
|
mpMapper->mp2dMap->update();
|
|
}
|
|
}
|
|
|
|
if (!temporary) {
|
|
setUnsaved(__func__);
|
|
}
|
|
return labelId;
|
|
}
|
|
|
|
int TMap::createMapImageLabel(int area, QString imagePath, float x, float y, float z, float width, float height, float zoom, bool showOnTop, bool temporary)
|
|
{
|
|
auto pA = mpRoomDB->getArea(area);
|
|
if (!pA) {
|
|
return -1;
|
|
}
|
|
|
|
TMapLabel label;
|
|
label.size = QSizeF(width, height);
|
|
label.pos = QVector3D(x, y, z);
|
|
label.showOnTop = showOnTop;
|
|
// This method is only called from the TLuaInterpreter class and the value
|
|
// passed was hard-coded to this value:
|
|
label.noScaling = false;
|
|
label.temporary = temporary;
|
|
|
|
const QRectF drawRect = QRectF(0, 0, static_cast<qreal>(width * zoom), static_cast<qreal>(height * zoom));
|
|
const 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;
|
|
|
|
const int labelId = pA->createLabelId();
|
|
if (Q_LIKELY(labelId >= 0)) {
|
|
pA->mMapLabels.insert(labelId, label);
|
|
if (mpMapper) {
|
|
mpMapper->mp2dMap->update();
|
|
}
|
|
}
|
|
|
|
if (!temporary) {
|
|
setUnsaved(__func__);
|
|
}
|
|
return labelId;
|
|
}
|
|
|
|
void TMap::deleteMapLabel(int area, int labelId)
|
|
{
|
|
auto pA = mpRoomDB->getArea(area);
|
|
if (!pA) {
|
|
return;
|
|
}
|
|
|
|
auto label = pA->mMapLabels.take(labelId);
|
|
if (!label.pos.isNull() || !label.text.isEmpty() || label.bgColor != QColorConstants::Black || label.fgColor != QColorConstants::Black) {
|
|
// If any of the above tests are false then we can take it that we do
|
|
// not have a "default constructed" label - i.e. a real one and not
|
|
// one that the QMap<T1, T2>::take(const T1&) has created for us in the
|
|
// absence of an actual TMapLabel.
|
|
// The TMapLabel default constructor sets the 'temporary' class member
|
|
// to false so we can safely rely on it being true for a temporary one:
|
|
if (!label.temporary) {
|
|
setUnsaved(__func__);
|
|
}
|
|
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)
|
|
if (mpM) {
|
|
if (auto* glWidget = dynamic_cast<GLWidget*>(mpM.data())) {
|
|
glWidget->setViewCenter(areaId, xPos, yPos, zPos);
|
|
} else if (auto* modernWidget = dynamic_cast<ModernGLWidget*>(mpM.data())) {
|
|
modernWidget->setViewCenter(areaId, xPos, yPos, zPos);
|
|
}
|
|
}
|
|
#else
|
|
Q_UNUSED(areaId)
|
|
Q_UNUSED(xPos)
|
|
Q_UNUSED(yPos)
|
|
Q_UNUSED(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 = qsl("%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(enums::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(enums::profileLogErrorsFilePath, mProfileName), title));
|
|
}
|
|
|
|
mIsFileViewingRecommended = false;
|
|
}
|
|
|
|
void TMap::downloadMap(const QString& remoteUrl, const QString& localFileName)
|
|
{
|
|
const MapOperationScope operationScope(this);
|
|
Host* pHost = mpHost;
|
|
if (!pHost) {
|
|
return;
|
|
}
|
|
|
|
// Incidentally this should address: https://bugs.launchpad.net/mudlet/+bug/852861
|
|
if (mImportRunning) {
|
|
const QString warnMsg = tr("[ 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;
|
|
}
|
|
|
|
if (mMapProgressStandalone) {
|
|
//: Shown in the main console when a map download is refused
|
|
const QString warnMsg = tr("[ WARN ] - Attempt made to download an XML map while a map import or\n"
|
|
"export is already in progress - wait for that operation to complete\n"
|
|
"before retrying!");
|
|
postMessage(warnMsg);
|
|
return;
|
|
}
|
|
mImportRunning = true;
|
|
// MUST clear this flag when done under ALL circumstances
|
|
|
|
QUrl url;
|
|
if (remoteUrl.isEmpty()) {
|
|
if (!getMmpMapLocation().isEmpty()) {
|
|
url = QUrl::fromUserInput(getMmpMapLocation());
|
|
} else {
|
|
url = QUrl::fromUserInput(qsl("https://www.%1/maps/map.xml").arg(pHost->mUrl));
|
|
}
|
|
} else {
|
|
url = QUrl::fromUserInput(remoteUrl);
|
|
}
|
|
|
|
if (!url.isValid()) {
|
|
const QString errMsg = tr("[ 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);
|
|
mImportRunning = false;
|
|
return;
|
|
}
|
|
|
|
// Check to ensure we have a map directory to save the map files to.
|
|
const QDir toProfileDir;
|
|
const QString toProfileDirPathString = mudlet::getMudletPath(enums::profileMapsPath, mProfileName);
|
|
if (!toProfileDir.mkpath(toProfileDirPathString)) {
|
|
const 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);
|
|
mImportRunning = false;
|
|
return;
|
|
}
|
|
|
|
if (localFileName.isEmpty()) {
|
|
if (url.toString().endsWith(QLatin1String("xml"))) {
|
|
mLocalMapFileName = mudlet::getMudletPath(enums::profileXmlMapPathFileName, mProfileName);
|
|
} else {
|
|
mLocalMapFileName = mudlet::getMudletPath(enums::profileMapPathFileName, mProfileName, qsl("map.dat"));
|
|
}
|
|
} else {
|
|
mLocalMapFileName = localFileName;
|
|
}
|
|
|
|
QNetworkRequest request = QNetworkRequest(url);
|
|
pHost->updateProxySettings(mpNetworkAccessManager);
|
|
mudlet::self()->setNetworkRequestDefaults(url, request);
|
|
|
|
mExpectedFileSize = 4000000;
|
|
|
|
const 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);
|
|
//: %1 is the name of the current Mudlet profile
|
|
const QString label = tr("Downloading map file for use in %1...").arg(mProfileName);
|
|
//: This is a title of a progress window.
|
|
createTransferProgress(tr("Map download"), label, true);
|
|
|
|
connect(mpNetworkReply, &QNetworkReply::downloadProgress, this, &TMap::slot_setDownloadProgress);
|
|
connect(mpNetworkReply, &QNetworkReply::errorOccurred, this, &TMap::slot_downloadError);
|
|
}
|
|
|
|
// 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 (mImportRunning) {
|
|
if (errMsg) {
|
|
*errMsg = tr("loadMap: unable to perform request, a map is already being downloaded or\n"
|
|
"imported at user request.");
|
|
} else {
|
|
const QString warnMsg = qsl("[ 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;
|
|
}
|
|
|
|
if (mMapProgressStandalone) {
|
|
// readXmlMapFile() would see the JSON operation's progress dialog as its
|
|
// own, skip creating one, and then mapClear() the map out from under it:
|
|
if (errMsg) {
|
|
//: Error returned by the loadMap() Lua function
|
|
*errMsg = tr("loadMap: unable to perform request, a map import or export is\n"
|
|
"already in progress.");
|
|
} else {
|
|
//: Shown in the main console when a map import is refused
|
|
const QString warnMsg = tr("[ WARN ] - Attempt made to import an XML map while a map import or\n"
|
|
"export is already in progress - wait for that operation to complete\n"
|
|
"before retrying!");
|
|
postMessage(warnMsg);
|
|
}
|
|
return false;
|
|
}
|
|
mImportRunning = true;
|
|
// MUST clear this flag when done under ALL circumstances
|
|
|
|
const bool result = readXmlMapFile(file, errMsg);
|
|
mImportRunning = false;
|
|
|
|
return result;
|
|
}
|
|
|
|
bool TMap::readXmlMapFile(QFile& file, QString* errMsg)
|
|
{
|
|
const MapOperationScope operationScope(this);
|
|
Host* pHost = mpHost;
|
|
bool isLocalImport = false;
|
|
if (!pHost) {
|
|
return false;
|
|
}
|
|
|
|
if (!hasActiveTransferProgress()) {
|
|
isLocalImport = true;
|
|
//: This is a title of a progress window.
|
|
createTransferProgress(tr("Map import"), tr("Importing XML map file for use in %1...").arg(mProfileName), false);
|
|
}
|
|
|
|
// It is NOW safe to delete the map as we are in a position to load one
|
|
mapClear();
|
|
|
|
XMLimport reader(pHost);
|
|
auto [success, message] = reader.importPackage(&file);
|
|
|
|
if (!mpMapper.isNull() && mpMapper->mp2dMap) {
|
|
// 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 (success) {
|
|
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 (!success && errMsg) {
|
|
*errMsg = tr("loadMap: failure to import XML map file, further information may be available\n"
|
|
"in main console!");
|
|
}
|
|
|
|
if (isLocalImport) {
|
|
clearTransferProgress();
|
|
}
|
|
|
|
if (!mpMapper.isNull()) {
|
|
mpMapper->show();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void TMap::slot_setDownloadProgress(qint64 got, qint64 total)
|
|
{
|
|
if (!hasActiveTransferProgress()) {
|
|
return;
|
|
}
|
|
|
|
if (!transferProgressMaximum()) {
|
|
// First call, range has not been set;
|
|
updateTransferProgressRange(0, mExpectedFileSize);
|
|
} else if (total != -1 && transferProgressMaximum() != static_cast<int>(total)) {
|
|
// total will stick at -1 when we do not know how big the download is
|
|
// which seems to be the case for the IRE MUDS - *sigh* - Slysven
|
|
updateTransferProgressRange(0, static_cast<int>(total));
|
|
}
|
|
|
|
updateTransferProgressValue(static_cast<int>(got));
|
|
}
|
|
|
|
void TMap::slot_downloadCancel()
|
|
{
|
|
const QString alertMsg = tr("[ ALERT ] - Map download was canceled, on user's request.");
|
|
postMessage(alertMsg);
|
|
clearTransferProgress();
|
|
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
|
|
const 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 = nullptr;
|
|
|
|
// We don't dismiss the progress display until here as we now use it to
|
|
// inform about post-download operations
|
|
clearTransferProgress();
|
|
|
|
mLocalMapFileName.clear();
|
|
mExpectedFileSize = 0;
|
|
// We have finished with the XMLimporter so must clear the flag
|
|
mImportRunning = false;
|
|
};
|
|
|
|
|
|
if (reply != mpNetworkReply) {
|
|
qWarning() << "TMap::slot_replyFinished( QNetworkReply * ) ERROR - received argument was not the expected stored pointer.";
|
|
}
|
|
|
|
if (reply->error() != QNetworkReply::NoError && reply->error() != QNetworkReply::OperationCanceledError) {
|
|
// Don't report on any errors here as we've already done so in slot_downloadError(...) previously.
|
|
cleanup();
|
|
return;
|
|
// else was QNetworkReply::OperationCanceledError and we already handle
|
|
// THAT in slot_downloadCancel()
|
|
}
|
|
// Separate the two kinds of files to gain QSaveFile's atomic write behavior
|
|
QSaveFile writeFile(mLocalMapFileName);
|
|
QFile readFile(mLocalMapFileName);
|
|
if (!writeFile.open(QFile::WriteOnly)) {
|
|
const QString alertMsg = tr("[ ALERT ] - Map download failed, unable to open destination file:\n%1.").arg(mLocalMapFileName);
|
|
postMessage(alertMsg);
|
|
cleanup();
|
|
return;
|
|
}
|
|
// The QNetworkReply is Ok here:
|
|
if (writeFile.write(reply->readAll()) == -1) {
|
|
const QString alertMsg = tr("[ ALERT ] - Map download failed, unable to write destination file:\n%1.").arg(mLocalMapFileName);
|
|
postMessage(alertMsg);
|
|
cleanup();
|
|
return;
|
|
}
|
|
if (!writeFile.commit()) {
|
|
const QString alertMsg = tr("[ ALERT ] - Map download failed, unable to save destination file:\n%1\nreason: %2").arg(mLocalMapFileName, writeFile.errorString());
|
|
postMessage(alertMsg);
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
Host* pHost = mpHost;
|
|
if (!pHost) {
|
|
qWarning() << "TMap::slot_replyFinished( QNetworkReply * ) ERROR - NULL Host pointer - something is really wrong!";
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
const QString infoMsg = tr("[ INFO ] - ... map downloaded and stored, now parsing it...");
|
|
postMessage(infoMsg);
|
|
|
|
// Since the download is complete but we do not offer to
|
|
// cancel the required post-processing we should now hide
|
|
// the cancel/abort button:
|
|
disableTransferProgressCancel();
|
|
|
|
bool parsingWasSuccessful;
|
|
QString parsingFileName;
|
|
if (!readFile.fileName().endsWith(qsl("xml"), Qt::CaseInsensitive)) {
|
|
parsingFileName = readFile.fileName();
|
|
parsingWasSuccessful = pHost->mpConsole->loadMap(parsingFileName);
|
|
} else {
|
|
parsingFileName = mLocalMapFileName;
|
|
if (!readFile.open(QFile::OpenMode(QFile::ReadOnly | QFile::Text))) {
|
|
const QString alertMsg = tr("[ ERROR ] - Map download problem, unable to read destination file:\n%1.").arg(parsingFileName);
|
|
postMessage(alertMsg);
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
// 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.
|
|
parsingWasSuccessful = readXmlMapFile(readFile);
|
|
readFile.close();
|
|
}
|
|
|
|
if (parsingWasSuccessful) {
|
|
TEvent mapDownloadEvent{};
|
|
mapDownloadEvent.mArgumentList.append(qsl("sysMapDownloadEvent"));
|
|
mapDownloadEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
|
|
pHost->raiseEvent(mapDownloadEvent);
|
|
} else {
|
|
// Failure in parse file...
|
|
const QString alertMsg = tr("[ ERROR ] - Map download problem, failure in parsing destination file:\n%1.").arg(parsingFileName);
|
|
postMessage(alertMsg);
|
|
}
|
|
if (mpMapper) {
|
|
mpMapper->updateEmptyStateOverlay();
|
|
}
|
|
cleanup();
|
|
}
|
|
|
|
void TMap::reportStringToProgressDialog(const QString text)
|
|
{
|
|
if (hasActiveTransferProgress()) {
|
|
updateTransferProgressLabel(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 (hasActiveTransferProgress()) {
|
|
if (transferProgressMaximum() != maximum) {
|
|
updateTransferProgressRange(0, maximum);
|
|
}
|
|
updateTransferProgressValue(current);
|
|
}
|
|
}
|
|
|
|
void TMap::createTransferProgress(const QString& title, const QString& label, bool cancelable)
|
|
{
|
|
if (mpMapper && mpMapper->isVisible()) {
|
|
mpMapper->showMapProgress(label, cancelable);
|
|
connect(mpMapper, &dlgMapper::signal_mapProgressCanceled, this, &TMap::slot_downloadCancel, Qt::UniqueConnection);
|
|
return;
|
|
}
|
|
|
|
mMapProgressStandalone = true;
|
|
mMapProgressIsTransfer = true;
|
|
mMapProgressCancelRequested = false;
|
|
mMapProgressStandaloneMaximum = 0;
|
|
warnIfMapProgressUnwired(__func__, true);
|
|
emit signal_mapTransferProgressStart(title, label, cancelable ? tr("Abort") : QString());
|
|
}
|
|
|
|
void TMap::updateTransferProgressLabel(const QString& text)
|
|
{
|
|
if (mMapProgressStandalone) {
|
|
emit signal_mapProgressSetLabel(text);
|
|
} else if (mpMapper) {
|
|
mpMapper->setMapProgressLabel(text);
|
|
}
|
|
}
|
|
|
|
void TMap::updateTransferProgressRange(int minimum, int maximum)
|
|
{
|
|
if (mMapProgressStandalone) {
|
|
mMapProgressStandaloneMaximum = maximum;
|
|
emit signal_mapProgressSetRange(minimum, maximum);
|
|
} else if (mpMapper) {
|
|
mpMapper->setMapProgressRange(minimum, maximum);
|
|
}
|
|
}
|
|
|
|
void TMap::updateTransferProgressValue(int value)
|
|
{
|
|
if (mMapProgressStandalone) {
|
|
emit signal_mapProgressSetValue(value);
|
|
} else if (mpMapper) {
|
|
mpMapper->setMapProgressValue(value);
|
|
}
|
|
}
|
|
|
|
int TMap::transferProgressMaximum() const
|
|
{
|
|
if (mMapProgressStandalone) {
|
|
return mMapProgressStandaloneMaximum;
|
|
}
|
|
if (mpMapper) {
|
|
return mpMapper->mapProgressMaximum();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool TMap::hasActiveTransferProgress() const
|
|
{
|
|
return mMapProgressStandalone || (mpMapper && mpMapper->isMapProgressVisible());
|
|
}
|
|
|
|
void TMap::disableTransferProgressCancel()
|
|
{
|
|
if (mMapProgressStandalone) {
|
|
emit signal_mapProgressDisableCancel();
|
|
} else if (mpMapper) {
|
|
mpMapper->setMapProgressCancelable(false);
|
|
}
|
|
}
|
|
|
|
void TMap::clearTransferProgress()
|
|
{
|
|
// Only close a transfer-owned standalone dialog: a concurrent JSON
|
|
// import/export owns the standalone progress state and must keep it.
|
|
if (mMapProgressStandalone && mMapProgressIsTransfer) {
|
|
mMapProgressStandalone = false;
|
|
mMapProgressIsTransfer = false;
|
|
emit signal_mapProgressClose();
|
|
return;
|
|
}
|
|
if (mpMapper) {
|
|
disconnect(mpMapper, &dlgMapper::signal_mapProgressCanceled, this, &TMap::slot_downloadCancel);
|
|
mpMapper->hideMapProgress();
|
|
}
|
|
}
|
|
|
|
void TMap::requestMapOperationAbort()
|
|
{
|
|
if (!mMapOperationDepth || mMapOperationAbortRequested) {
|
|
return;
|
|
}
|
|
mMapOperationAbortRequested = true;
|
|
// Deliberately not slot_mapProgressDialogCancelled(): that is the user
|
|
// pressing Abort and says so in the console, whereas this is the profile
|
|
// going away and has no console left to say it to. What it does share is
|
|
// the flag the JSON import and export poll at their next progress step, and
|
|
// dropping a download that would otherwise hold the close up on the network.
|
|
mMapProgressCancelRequested = true;
|
|
if (mMapProgressIsTransfer && mpNetworkReply) {
|
|
mpNetworkReply->abort();
|
|
}
|
|
}
|
|
|
|
void TMap::slot_mapProgressDialogCancelled()
|
|
{
|
|
// The JSON path polls mMapProgressCancelRequested in its increment loop; the
|
|
// transfer path needs its network reply aborted here.
|
|
mMapProgressCancelRequested = true;
|
|
if (mMapProgressIsTransfer) {
|
|
slot_downloadCancel();
|
|
}
|
|
}
|
|
|
|
void TMap::warnIfMapProgressUnwired(const char* context, const bool transferPath)
|
|
{
|
|
static const QMetaMethod transferStart = QMetaMethod::fromSignal(&TMap::signal_mapTransferProgressStart);
|
|
static const QMetaMethod jsonStart = QMetaMethod::fromSignal(&TMap::signal_mapJsonProgressStart);
|
|
static const QMetaMethod progressClose = QMetaMethod::fromSignal(&TMap::signal_mapProgressClose);
|
|
if (isSignalConnected(transferPath ? transferStart : jsonStart) && isSignalConnected(progressClose)) {
|
|
return;
|
|
}
|
|
qWarning().nospace() << "TMap::" << context
|
|
<< "() WARNING - no frontend is connected to show the map progress dialog; the operation will run without a visible progress dialog and cannot be canceled from one.";
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (mMmpMapLocation == location) {
|
|
return;
|
|
}
|
|
mMmpMapLocation = location;
|
|
|
|
qDebug() << "MMP map registered at" << mMmpMapLocation;
|
|
emit signal_mmpMapLocationChanged();
|
|
}
|
|
|
|
QString TMap::getMmpMapLocation() const
|
|
{
|
|
return mMmpMapLocation;
|
|
}
|
|
|
|
bool TMap::getRoomNamesPresent()
|
|
{
|
|
return mUserData.contains(ROOM_UI_SHOWNAME);
|
|
}
|
|
|
|
bool TMap::getRoomNamesShown()
|
|
{
|
|
return getUserDataBool(mUserData, ROOM_UI_SHOWNAME, false);
|
|
}
|
|
|
|
void TMap::setRoomNamesShown(bool shown)
|
|
{
|
|
setUserDataBool(mUserData, ROOM_UI_SHOWNAME, shown);
|
|
}
|
|
|
|
/*
|
|
* Notes on the format version numbers in JSON files - we use this to track any
|
|
* changes in a major.minor number format, the minor number is to be three
|
|
* digits long.
|
|
*
|
|
* 0.002 was the first published draft
|
|
* 0.003 changed the format to encapsulate the room symbol as an object
|
|
* which contains text and a color which was added separately during the
|
|
* development of the JSON handling code. Also refactored the storage of
|
|
* colors to identify whether there is an alpha component or not in the
|
|
* array of values.
|
|
* 1.000 is identical to 0.003 - but changed to make sense from a release point
|
|
* of view.
|
|
*
|
|
* Currently only version 1.000 is expected or handled
|
|
*/
|
|
std::pair<bool, QString> TMap::writeJsonMapFile(const QString& dest)
|
|
{
|
|
const MapOperationScope operationScope(this);
|
|
QString destination{dest};
|
|
|
|
if (destination.isEmpty()) {
|
|
const QString destFolder = mudlet::getMudletPath(enums::profileMapsPath, mProfileName);
|
|
const QDir destDir(destFolder);
|
|
if (!destDir.exists()) {
|
|
destDir.mkdir(destFolder);
|
|
}
|
|
destination = mudlet::getMudletPath(enums::profileDateTimeStampedJsonMapPathFileName, mProfileName, QDateTime::currentDateTime().toString(qsl("yyyy-MM-dd#HH-mm-ss")));
|
|
}
|
|
|
|
if (!destination.endsWith(QLatin1String(".json"), Qt::CaseInsensitive)) {
|
|
destination.append(QLatin1String(".json"));
|
|
}
|
|
|
|
if (mMapProgressStandalone) {
|
|
return {false, qsl("import or export already in progress")};
|
|
}
|
|
|
|
mProgressDialogRoomsTotal = mpRoomDB->getRoomMap().count();
|
|
mProgressDialogAreasTotal = mpRoomDB->getAreaMap().count();
|
|
mProgressDialogLabelsTotal = 0;
|
|
for (const auto area : mpRoomDB->getAreaMap()) {
|
|
if (area) {
|
|
mProgressDialogLabelsTotal += area->getPermanentLabelIds().count();
|
|
}
|
|
}
|
|
|
|
mMapProgressStandalone = true;
|
|
mMapProgressIsTransfer = false;
|
|
mMapProgressCancelRequested = false;
|
|
mMapProgressStandaloneMaximum = static_cast<int>(mProgressDialogRoomsTotal);
|
|
warnIfMapProgressUnwired(__func__, false);
|
|
//: This is a title of a progress window.
|
|
emit signal_mapJsonProgressStart(tr("Map JSON export"),
|
|
tr("Exporting JSON map data from %1\n"
|
|
"Areas: %2 of: %3 Rooms: %4 of: %5 Labels: %6 of: %7...")
|
|
.arg(mProfileName,
|
|
QLatin1String("0"),
|
|
QString::number(mProgressDialogAreasTotal),
|
|
QLatin1String("0"),
|
|
QString::number(mProgressDialogRoomsTotal),
|
|
QLatin1String("0"),
|
|
QString::number(mProgressDialogLabelsTotal)),
|
|
tr("Abort"),
|
|
static_cast<int>(mProgressDialogRoomsTotal));
|
|
emit signal_mapProgressSetValue(0);
|
|
qApp->processEvents();
|
|
QSaveFile file(destination);
|
|
if (!file.open(QFile::OpenMode(QFile::Text | QFile::WriteOnly))) {
|
|
qWarning().noquote().nospace() << "TMap::writeJsonMapFile(...) WARNING - Could not open save file \"" << destination << "\".";
|
|
emit signal_mapProgressClose();
|
|
mMapProgressStandalone = false;
|
|
return {false, qsl("could not open save file \"%1\", reason: %2").arg(destination.toHtmlEscaped(), file.errorString())};
|
|
}
|
|
|
|
QJsonObject mapObj;
|
|
mapObj.insert(QLatin1String("formatVersion"), static_cast<double>(1.000));
|
|
|
|
writeJsonUserData(mapObj);
|
|
|
|
QList<int> areaRawIdsList{mpRoomDB->getAreaMap().keys()};
|
|
QList<int> areaNameRawIdsList{mpRoomDB->getAreaNamesMap().keys()};
|
|
|
|
QSet<int> areaIdsSet{areaRawIdsList.begin(), areaRawIdsList.end()};
|
|
areaIdsSet.unite(QSet<int>{areaNameRawIdsList.begin(), areaNameRawIdsList.end()});
|
|
QList<int> areaIdsList{areaIdsSet.begin(), areaIdsSet.end()};
|
|
if (areaIdsList.count() > 1) {
|
|
std::sort(areaIdsList.begin(), areaIdsList.end());
|
|
}
|
|
|
|
mProgressDialogAreasCount = 0;
|
|
mProgressDialogRoomsCount = 0;
|
|
mProgressDialogLabelsCount = 0;
|
|
bool abort = false;
|
|
QJsonArray areasArray;
|
|
for (const auto area : mpRoomDB->getAreaMap()) {
|
|
if (area) {
|
|
area->writeJsonArea(areasArray);
|
|
}
|
|
++mProgressDialogAreasCount;
|
|
if (incrementJsonProgressDialog(true, true, 0)) {
|
|
abort = true;
|
|
break;
|
|
}
|
|
}
|
|
if (abort) {
|
|
file.cancelWriting();
|
|
emit signal_mapProgressClose();
|
|
mMapProgressStandalone = false;
|
|
return {false, qsl("aborted by user")};
|
|
}
|
|
|
|
const QJsonValue areasValue{areasArray};
|
|
mapObj.insert(QLatin1String("areas"), areasValue);
|
|
|
|
// Should Qt change things so that the order in the file is not
|
|
// alphabetically sorted but instead dependent on actually insertion order
|
|
// then these must be precalculated and put first - as they are needed to
|
|
// drive the progress dialogue:
|
|
mapObj.insert(QLatin1String("areaCount"), static_cast<double>(areaIdsList.count()));
|
|
mapObj.insert(QLatin1String("roomCount"), static_cast<double>(mProgressDialogRoomsCount));
|
|
mapObj.insert(QLatin1String("labelCount"), static_cast<double>(mProgressDialogLabelsTotal));
|
|
|
|
const QJsonValue defaultAreaNameValue{mDefaultAreaName};
|
|
mapObj.insert(QLatin1String("defaultAreaName"), defaultAreaNameValue);
|
|
|
|
const QJsonValue anonymousAreaNameValue{mUnnamedAreaName};
|
|
mapObj.insert(QLatin1String("anonymousAreaName"), anonymousAreaNameValue);
|
|
|
|
if (!mEnvColors.isEmpty()) {
|
|
QJsonObject envColorObj;
|
|
QMapIterator<int, int> itEnvColor(mEnvColors);
|
|
while (itEnvColor.hasNext()) {
|
|
itEnvColor.next();
|
|
envColorObj.insert(QString::number(itEnvColor.key()), static_cast<double>(itEnvColor.value()));
|
|
}
|
|
const QJsonValue mEnvColorsValue{envColorObj};
|
|
mapObj.insert(QLatin1String("envToColorMapping"), mEnvColorsValue);
|
|
}
|
|
|
|
QJsonObject playerRoomIdHashObj;
|
|
QHashIterator<QString, int> itplayerRoomIdHash(mRoomIdHash);
|
|
while (itplayerRoomIdHash.hasNext()) {
|
|
itplayerRoomIdHash.next();
|
|
playerRoomIdHashObj.insert(itplayerRoomIdHash.key(), static_cast<double>(itplayerRoomIdHash.value()));
|
|
}
|
|
const QJsonValue playerRoomIdHashsValue{playerRoomIdHashObj};
|
|
mapObj.insert(QLatin1String("playersRoomId"), playerRoomIdHashsValue);
|
|
|
|
QJsonArray customEnvColorArray;
|
|
QMapIterator<int, QColor> itCustomEnvColor(mCustomEnvColors);
|
|
while (itCustomEnvColor.hasNext()) {
|
|
itCustomEnvColor.next();
|
|
QJsonObject customEnvColorObj{};
|
|
// Should insert an array value into the customEnvColorObj with the key
|
|
// "colorRGBA"
|
|
writeJsonColor(customEnvColorObj, itCustomEnvColor.value());
|
|
customEnvColorObj.insert(QLatin1String("id"), QJsonValue{itCustomEnvColor.key()});
|
|
// Convert the customEnvColorObj into a QJsonValue:
|
|
const QJsonValue customEnvColorValue{customEnvColorObj};
|
|
// Now append this object onto the array:
|
|
customEnvColorArray.append(customEnvColorValue);
|
|
}
|
|
// Convert the array of all the mCustomEnvColors into a QJsonValue so we
|
|
// can add it to the map object:
|
|
const QJsonValue mCustomEnvColorsValue{customEnvColorArray};
|
|
mapObj.insert(QLatin1String("customEnvColors"), mCustomEnvColorsValue);
|
|
|
|
mapObj.insert(QLatin1String("mapSymbolFontDetails"), mMapSymbolFont.toString());
|
|
mapObj.insert(QLatin1String("mapSymbolFontFudgeFactor"), static_cast<double>(mMapSymbolFontFudgeFactor));
|
|
mapObj.insert(QLatin1String("onlyMapSymbolFontToBeUsed"), mIsOnlyMapSymbolFontToBeUsed);
|
|
|
|
QJsonArray playerRoomColorsArray;
|
|
QJsonObject playerRoomOuterColorObj;
|
|
QJsonObject playerRoomInnerColorObj;
|
|
writeJsonColor(playerRoomOuterColorObj, mPlayerRoomOuterColor);
|
|
writeJsonColor(playerRoomInnerColorObj, mPlayerRoomInnerColor);
|
|
const QJsonValue playerRoomOuterColorValue{playerRoomOuterColorObj};
|
|
const QJsonValue playerRoomInnerColorValue{playerRoomInnerColorObj};
|
|
playerRoomColorsArray.append(playerRoomOuterColorValue);
|
|
playerRoomColorsArray.append(playerRoomInnerColorValue);
|
|
const QJsonValue playerRoomColorsValue{playerRoomColorsArray};
|
|
mapObj.insert(QLatin1String("playerRoomColors"), playerRoomColorsValue);
|
|
mapObj.insert(QLatin1String("playerRoomStyle"), static_cast<double>(mPlayerRoomStyle));
|
|
mapObj.insert(QLatin1String("playerRoomOuterDiameterPercentage"), static_cast<double>(mPlayerRoomOuterDiameterPercentage));
|
|
mapObj.insert(QLatin1String("playerRoomInnerDiameterPercentage"), static_cast<double>(mPlayerRoomInnerDiameterPercentage));
|
|
|
|
emit signal_mapProgressSetLabel(tr("Exporting JSON map file from %1 - writing data to file:\n"
|
|
"%2 ...")
|
|
.arg(mProfileName, destination));
|
|
emit signal_mapProgressSetValue(0);
|
|
// Hide the cancel button as we can't stop now:
|
|
emit signal_mapProgressDisableCancel();
|
|
file.write(QJsonDocument(mapObj).toJson(QJsonDocument::Indented));
|
|
if (!file.commit()) {
|
|
qDebug() << "TMap::writeJsonMapFile: error saving JSON map: " << file.errorString();
|
|
}
|
|
|
|
emit signal_mapProgressClose();
|
|
mMapProgressStandalone = false;
|
|
|
|
return {file.error() == QFileDevice::NoError, ((file.error() == QFileDevice::NoError) ? QString() : qsl("could not export file, reason: %1").arg(file.errorString()))};
|
|
}
|
|
|
|
// The translatable messages are used within this file and do not need to
|
|
// mention the file concerned whereas the untranslated messages are used by the
|
|
// Lua sub-system and do need to report the file:
|
|
std::pair<bool, QString> TMap::readJsonMapFile(const QString& source, const bool translatableTexts)
|
|
{
|
|
const MapOperationScope operationScope(this);
|
|
const QString oldDefaultAreaName{mDefaultAreaName};
|
|
const QString oldUnnamedName{mUnnamedAreaName};
|
|
|
|
if (mMapProgressStandalone) {
|
|
return {false, (translatableTexts ? tr("import or export already in progress") : qsl("import or export already in progress"))};
|
|
}
|
|
|
|
QFile file(source);
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
qWarning().noquote().nospace() << "TMap::readJsonMapFile(...) WARNING - Could not open JSON file \"" << source << "\".";
|
|
return {false, (translatableTexts ? tr("could not open file") : qsl("could not open file \"%1\"").arg(source))};
|
|
}
|
|
|
|
const QByteArray mapData = file.readAll();
|
|
file.close();
|
|
QJsonParseError jsonErr;
|
|
const QJsonDocument doc(QJsonDocument::fromJson(mapData, &jsonErr));
|
|
if (jsonErr.error != QJsonParseError::NoError) {
|
|
return {false,
|
|
(translatableTexts ? tr("could not parse file, reason: \"%1\" at offset %2").arg(jsonErr.errorString(), QString::number(jsonErr.offset))
|
|
: qsl("could not parse file \"%1\", reason: \"%2\" at offset %3").arg(source, jsonErr.errorString(), QString::number(jsonErr.offset)))};
|
|
}
|
|
|
|
if (doc.isEmpty()) {
|
|
qDebug().nospace().noquote() << "TMap::readJsonMapFile(\"" << source << "\") INFO - no Json file data detected, this is not a Mudlet JSON map file.";
|
|
return {false, (translatableTexts ? tr("empty Json file, no map data detected") : qsl("empty Json file, no map data detected"))};
|
|
}
|
|
|
|
// Read all the base level stuff:
|
|
QJsonObject mapObj{doc.object()};
|
|
double formatVersion = 0.0f;
|
|
if (mapObj.contains(QLatin1String("formatVersion")) && mapObj[QLatin1String("formatVersion")].isDouble()) {
|
|
formatVersion = mapObj[QLatin1String("formatVersion")].toDouble();
|
|
if (qFuzzyCompare(1.0, formatVersion + 1.0) || formatVersion < 1.0000 || formatVersion > 1.0000) {
|
|
// We only handle 1.000f right now (0.001f was borked, 0.002f
|
|
// didn't include room symbol color, 0.003 is the same as 1.000
|
|
// but the numbered was changed for release into the wild):
|
|
qDebug().nospace().noquote() << "TMap::readJsonMapFile(\"" << source << "\") INFO - Version information \"" << formatVersion << "\" was found, and it is not okay.";
|
|
return {false,
|
|
(translatableTexts ? tr("invalid format version \"%1\" detected").arg(formatVersion, 0, 'f', 3, QLatin1Char('0'))
|
|
: qsl("invalid format version \"%1\" detected").arg(formatVersion, 0, 'f', 3, QLatin1Char('0')))};
|
|
}
|
|
} else {
|
|
qDebug().nospace().noquote() << "TMap::readJsonMapFile(\"" << source << "\") INFO - Version information was not found. This is not likely to be a Mudlet JSON map file.";
|
|
return {false, (translatableTexts ? tr("no format version detected") : qsl("no format version detected"))};
|
|
}
|
|
|
|
if (!mapObj.contains(QLatin1String("areas")) || !mapObj.value(QLatin1String("areas")).isArray()) {
|
|
return {false, (translatableTexts ? tr("no areas detected") : qsl("no areas detected"))};
|
|
}
|
|
|
|
mProgressDialogAreasTotal = qRound(mapObj[QLatin1String("areaCount")].toDouble());
|
|
mProgressDialogAreasCount = 0;
|
|
mProgressDialogRoomsTotal = qRound(mapObj[QLatin1String("roomCount")].toDouble());
|
|
mProgressDialogRoomsCount = 0;
|
|
mProgressDialogLabelsTotal = qRound(mapObj[QLatin1String("labelCount")].toDouble());
|
|
mProgressDialogLabelsCount = 0;
|
|
mMapProgressStandalone = true;
|
|
mMapProgressIsTransfer = false;
|
|
mMapProgressCancelRequested = false;
|
|
mMapProgressStandaloneMaximum = static_cast<int>(mProgressDialogRoomsTotal);
|
|
warnIfMapProgressUnwired(__func__, false);
|
|
//: This is a title of a progress window.
|
|
emit signal_mapJsonProgressStart(tr("Map JSON import"),
|
|
tr("Importing JSON map data to %1\n"
|
|
"Areas: %2 of: %3 Rooms: %4 of: %5 Labels: %6 of: %7...")
|
|
.arg(mProfileName,
|
|
QLatin1String("0"),
|
|
QString::number(mProgressDialogAreasTotal),
|
|
QLatin1String("0"),
|
|
QString::number(mProgressDialogRoomsTotal),
|
|
QLatin1String("0"),
|
|
QString::number(mProgressDialogLabelsTotal)),
|
|
tr("Abort"),
|
|
static_cast<int>(mProgressDialogRoomsTotal));
|
|
emit signal_mapProgressSetValue(0);
|
|
qApp->processEvents();
|
|
|
|
mDefaultAreaName = mapObj[QLatin1String("defaultAreaName")].toString();
|
|
mUnnamedAreaName = mapObj[QLatin1String("anonymousAreaName")].toString();
|
|
if (mapObj.contains(QLatin1String("userData"))) {
|
|
readJsonUserData(mapObj[QLatin1String("userData")].toObject());
|
|
}
|
|
const QString mapSymbolFontText = mapObj[QLatin1String("mapSymbolFontDetails")].toString();
|
|
const float mapSymbolFontFudgeFactor = (qRound(mapObj[QLatin1String("mapSymbolFontFudgeFactor")].toDouble() * 1000.0)) / 1000;
|
|
const bool isOnlyMapSymbolFontToBeUsed = mapObj[QLatin1String("onlyMapSymbolFontToBeUsed")].toBool();
|
|
const int playerRoomStyle = qRound(mapObj[QLatin1String("playerRoomStyle")].toDouble());
|
|
quint8 const playerRoomOuterDiameterPercentage = qRound(mapObj[QLatin1String("playerRoomOuterDiameterPercentage")].toDouble());
|
|
quint8 const playerRoomInnerDiameterPercentage = qRound(mapObj[QLatin1String("playerRoomInnerDiameterPercentage")].toDouble());
|
|
QColor playerRoomOuterColor;
|
|
QColor playerRoomInnerColor;
|
|
|
|
if (mapObj.contains(QLatin1String("playerRoomColors")) && mapObj.value(QLatin1String("playerRoomColors")).isArray()) {
|
|
const QJsonArray playerRoomColorArray = mapObj.value(QLatin1String("playerRoomColors")).toArray();
|
|
if (playerRoomColorArray.size() == 2 && playerRoomColorArray.at(0).isObject() && playerRoomColorArray.at(1).isObject()) {
|
|
playerRoomOuterColor = readJsonColor(playerRoomColorArray.at(0).toObject());
|
|
playerRoomInnerColor = readJsonColor(playerRoomColorArray.at(1).toObject());
|
|
}
|
|
}
|
|
|
|
QMap<int, int> envColors;
|
|
if (mapObj.contains(QLatin1String("envToColorMapping")) && mapObj.value(QLatin1String("envToColorMapping")).isObject()) {
|
|
const QJsonObject envColorObj{mapObj.value(QLatin1String("envToColorMapping")).toObject()};
|
|
if (!envColorObj.isEmpty()) {
|
|
for (auto& key : envColorObj.keys()) {
|
|
bool isOk = false;
|
|
const int index = key.toInt(&isOk);
|
|
if (isOk && envColorObj.value(key).isDouble()) {
|
|
const int value = envColorObj.value(key).toInt();
|
|
envColors.insert(index, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QMap<int, QColor> customEnvColors;
|
|
if (mapObj.contains(QLatin1String("customEnvColors")) && mapObj.value(QLatin1String("customEnvColors")).isArray()) {
|
|
const QJsonArray customEnvColorArray = mapObj.value(QLatin1String("customEnvColors")).toArray();
|
|
if (!customEnvColorArray.isEmpty()) {
|
|
for (const auto& customEnvColorValue : std::as_const(customEnvColorArray)) {
|
|
const QJsonObject customEnvColorObj{customEnvColorValue.toObject()};
|
|
if (customEnvColorObj.contains(QLatin1String("id"))
|
|
&& ((customEnvColorObj.contains(QLatin1String("color32RGBA")) && customEnvColorObj.value(QLatin1String("color32RGBA")).isArray())
|
|
|| (customEnvColorObj.contains(QLatin1String("color24RGB")) && customEnvColorObj.value(QLatin1String("color24RGB")).isArray()))
|
|
&& customEnvColorObj.value(QLatin1String("id")).isDouble()) {
|
|
const int id{customEnvColorObj.value(QLatin1String("id")).toInt()};
|
|
const QColor color{readJsonColor(customEnvColorObj)};
|
|
customEnvColors.insert(id, color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QHash<QString, int> playersRoomId;
|
|
if (mapObj.contains(QLatin1String("playersRoomId")) && mapObj.value(QLatin1String("playersRoomId")).isObject()) {
|
|
const QJsonObject playersRoomIdObj{mapObj.value(QLatin1String("playersRoomId")).toObject()};
|
|
if (!playersRoomIdObj.isEmpty()) {
|
|
for (auto& profileName : playersRoomIdObj.keys()) {
|
|
if (playersRoomIdObj.value(profileName).isDouble()) {
|
|
playersRoomId.insert(profileName, playersRoomIdObj.value(profileName).toInt());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto pNewRoomDB = std::make_unique<TRoomDB>(this);
|
|
bool abort = false;
|
|
for (int i = 0, total = mapObj.value(QLatin1String("areas")).toArray().count(); i < total; ++i) {
|
|
std::unique_ptr<TArea> pArea = std::make_unique<TArea>(this, pNewRoomDB.get());
|
|
auto [id, name] = pArea->readJsonArea(mapObj.value(QLatin1String("areas")).toArray(), i);
|
|
++mProgressDialogAreasCount;
|
|
if (incrementJsonProgressDialog(false, true, 0)) {
|
|
abort = true;
|
|
break;
|
|
}
|
|
// This will populate the TRoomDB::areas and TRoomDB::areaNameMap:
|
|
pNewRoomDB->addArea(pArea.release(), id, name);
|
|
}
|
|
if (abort) {
|
|
emit signal_mapProgressClose();
|
|
mMapProgressStandalone = false;
|
|
mDefaultAreaName = oldDefaultAreaName;
|
|
mUnnamedAreaName = oldUnnamedName;
|
|
return {false, (translatableTexts ? tr("aborted by user") : qsl("aborted by user"))};
|
|
}
|
|
|
|
mCustomEnvColors.swap(customEnvColors);
|
|
mEnvColors.swap(envColors);
|
|
mIsOnlyMapSymbolFontToBeUsed = isOnlyMapSymbolFontToBeUsed;
|
|
QFont mapSymbolFont;
|
|
mapSymbolFont.fromString(mapSymbolFontText);
|
|
mapSymbolFont.setStyleStrategy(static_cast<QFont::StyleStrategy>((isOnlyMapSymbolFontToBeUsed ? QFont::NoFontMerging : 0) | QFont::PreferOutline | QFont::PreferAntialias | QFont::PreferQuality
|
|
| QFont::PreferNoShaping));
|
|
|
|
mMapSymbolFont.swap(mapSymbolFont);
|
|
mMapSymbolFontFudgeFactor = mapSymbolFontFudgeFactor;
|
|
mPlayerRoomInnerColor = playerRoomInnerColor;
|
|
mPlayerRoomInnerDiameterPercentage = playerRoomInnerDiameterPercentage;
|
|
mPlayerRoomOuterColor = playerRoomOuterColor;
|
|
mPlayerRoomOuterDiameterPercentage = playerRoomOuterDiameterPercentage;
|
|
mPlayerRoomStyle = playerRoomStyle;
|
|
mRoomIdHash = playersRoomId;
|
|
qDebug().nospace().noquote() << "TMap::readJsonMapFile(...) INFO - parsed a file (version: " << formatVersion << ") containing " << mProgressDialogRoomsCount << " rooms.";
|
|
|
|
// This is it - the point at which the new map gets activated:
|
|
mpRoomDB = std::move(pNewRoomDB);
|
|
// Need to update the master copy of these details in the Host class:
|
|
mpHost->setPlayerRoomStyleDetails(mPlayerRoomStyle, mPlayerRoomOuterDiameterPercentage, mPlayerRoomInnerDiameterPercentage, mPlayerRoomOuterColor, mPlayerRoomInnerColor);
|
|
// And redraw the indicator if a 2D map is being shown:
|
|
if (mpMapper && mpMapper->mp2dMap) {
|
|
mpMapper->mp2dMap->setPlayerRoomStyle(mPlayerRoomStyle);
|
|
}
|
|
emit signal_mapProgressClose();
|
|
mMapProgressStandalone = false;
|
|
return {true, QString()};
|
|
}
|
|
|
|
void TMap::writeJsonUserData(QJsonObject& obj) const
|
|
{
|
|
QJsonObject userDataObj;
|
|
if (mUserData.isEmpty()) {
|
|
// Skip creating a user data array if it will be empty:
|
|
return;
|
|
}
|
|
QMapIterator<QString, QString> itDataItem(mUserData);
|
|
while (itDataItem.hasNext()) {
|
|
itDataItem.next();
|
|
const QJsonValue jsonValue{itDataItem.value()};
|
|
userDataObj.insert(itDataItem.key(), jsonValue);
|
|
}
|
|
const QJsonValue jsonValue{userDataObj};
|
|
obj.insert(QLatin1String("userData"), jsonValue);
|
|
}
|
|
|
|
// Takes a userData object and parses all its elements
|
|
void TMap::readJsonUserData(const QJsonObject& obj)
|
|
{
|
|
if (obj.isEmpty()) {
|
|
// Skip doing anything more if there is nothing to do:
|
|
return;
|
|
}
|
|
|
|
for (auto& key : obj.keys()) {
|
|
if (obj.value(key).isString()) {
|
|
mUserData.insert(key, obj.value(key).toString());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Inserts a color as an array of 3 or 4 ints (cast to doubles) into the
|
|
// supplied object.
|
|
void TMap::writeJsonColor(QJsonObject& obj, const QColor& color)
|
|
{
|
|
QJsonArray colorRGBAArray;
|
|
colorRGBAArray.append(static_cast<double>(color.red()));
|
|
colorRGBAArray.append(static_cast<double>(color.green()));
|
|
colorRGBAArray.append(static_cast<double>(color.blue()));
|
|
if (color.alpha() < 255) {
|
|
colorRGBAArray.append(static_cast<double>(color.alpha()));
|
|
const QJsonValue colorRGBAValue{colorRGBAArray};
|
|
obj.insert(QLatin1String("color32RGBA"), colorRGBAValue);
|
|
} else {
|
|
const QJsonValue colorRGBAValue{colorRGBAArray};
|
|
obj.insert(QLatin1String("color24RGB"), colorRGBAValue);
|
|
}
|
|
}
|
|
|
|
QColor TMap::readJsonColor(const QJsonObject& obj)
|
|
{
|
|
if (!((obj.contains(QLatin1String("color32RGBA")) && obj.value(QLatin1String("color32RGBA")).isArray())
|
|
|| (obj.contains(QLatin1String("color24RGB")) && obj.value(QLatin1String("color24RGB")).isArray()))) {
|
|
// Return a null color if one was not found
|
|
return QColor();
|
|
}
|
|
|
|
QJsonArray colorRGBAArray;
|
|
bool hasAlpha = false;
|
|
int red = 0;
|
|
int green = 0;
|
|
int blue = 0;
|
|
int alpha = 255;
|
|
if (obj.contains(QLatin1String("color32RGBA"))) {
|
|
colorRGBAArray = obj.value(QLatin1String("color32RGBA")).toArray();
|
|
hasAlpha = true;
|
|
} else {
|
|
colorRGBAArray = obj.value(QLatin1String("color24RGB")).toArray();
|
|
}
|
|
const int size = colorRGBAArray.size();
|
|
if ((size == 3 || size == 4) && colorRGBAArray.at(0).isDouble() && colorRGBAArray.at(1).isDouble() && colorRGBAArray.at(2).isDouble()) {
|
|
red = qRound(colorRGBAArray.at(0).toDouble());
|
|
green = qRound(colorRGBAArray.at(1).toDouble());
|
|
blue = qRound(colorRGBAArray.at(2).toDouble());
|
|
return QColor(red, green, blue);
|
|
}
|
|
|
|
if (hasAlpha && size == 4 && colorRGBAArray.at(3).isDouble()) {
|
|
alpha = qRound(colorRGBAArray.at(3).toDouble());
|
|
return QColor(red, green, blue, alpha);
|
|
}
|
|
|
|
return QColor();
|
|
}
|
|
|
|
|
|
bool TMap::incrementJsonProgressDialog(const bool isExportNotImport, const bool isRoomNotLabel, const int increment)
|
|
{
|
|
if (isRoomNotLabel) {
|
|
mProgressDialogRoomsCount += increment;
|
|
} else {
|
|
mProgressDialogLabelsCount += increment;
|
|
}
|
|
|
|
emit signal_mapProgressSetValue(static_cast<int>(mProgressDialogRoomsCount));
|
|
if (isExportNotImport) {
|
|
emit signal_mapProgressSetLabel(tr("Exporting JSON map data from %1\n"
|
|
"Areas: %2 of: %3 Rooms: %4 of: %5 Labels: %6 of: %7...")
|
|
.arg(mProfileName,
|
|
QString::number(mProgressDialogAreasCount),
|
|
QString::number(mProgressDialogAreasTotal),
|
|
QString::number(mProgressDialogRoomsCount),
|
|
QString::number(mProgressDialogRoomsTotal),
|
|
QString::number(mProgressDialogLabelsCount),
|
|
QString::number(mProgressDialogLabelsTotal)));
|
|
} else {
|
|
emit signal_mapProgressSetLabel(tr("Importing JSON map data to %1\n"
|
|
"Areas: %2 of: %3 Rooms: %4 of: %5 Labels: %6 of: %7...")
|
|
.arg(mProfileName,
|
|
QString::number(mProgressDialogAreasCount),
|
|
QString::number(mProgressDialogAreasTotal),
|
|
QString::number(mProgressDialogRoomsCount),
|
|
QString::number(mProgressDialogRoomsTotal),
|
|
QString::number(mProgressDialogLabelsCount),
|
|
QString::number(mProgressDialogLabelsTotal)));
|
|
}
|
|
qApp->processEvents();
|
|
return mMapProgressCancelRequested;
|
|
}
|
|
|
|
void TMap::updateArea(int areaId)
|
|
{
|
|
static bool debounce;
|
|
if (!debounce) {
|
|
debounce = true;
|
|
QTimer::singleShot(0ms, this, [this, areaId]() {
|
|
debounce = false;
|
|
|
|
#if defined(INCLUDE_3DMAPPER)
|
|
if (mpM) {
|
|
mpM->update();
|
|
}
|
|
#endif
|
|
if (mpMapper) {
|
|
if (mpMapper->mp2dMap) {
|
|
mpMapper->mp2dMap->mNewMoveAction = true;
|
|
mpMapper->mp2dMap->update();
|
|
}
|
|
}
|
|
|
|
emit signal_areaChanged(areaId);
|
|
});
|
|
}
|
|
}
|
|
|
|
QColor TMap::getColor(int id)
|
|
{
|
|
QColor color;
|
|
|
|
TRoom* room = mpRoomDB->getRoom(id);
|
|
if (!room) {
|
|
return color;
|
|
}
|
|
|
|
int env = room->environment;
|
|
if (mEnvColors.contains(env)) {
|
|
env = mEnvColors.value(env);
|
|
} else {
|
|
if (!mCustomEnvColors.contains(env)) {
|
|
env = 1;
|
|
}
|
|
}
|
|
switch (env) {
|
|
case 1:
|
|
color = mpHost->mRed_2;
|
|
break;
|
|
case 2:
|
|
color = mpHost->mGreen_2;
|
|
break;
|
|
case 3:
|
|
color = mpHost->mYellow_2;
|
|
break;
|
|
case 4:
|
|
color = mpHost->mBlue_2;
|
|
break;
|
|
case 5:
|
|
color = mpHost->mMagenta_2;
|
|
break;
|
|
case 6:
|
|
color = mpHost->mCyan_2;
|
|
break;
|
|
case 7:
|
|
color = mpHost->mWhite_2;
|
|
break;
|
|
case 8:
|
|
color = mpHost->mBlack_2;
|
|
break;
|
|
case 9:
|
|
color = mpHost->mLightRed_2;
|
|
break;
|
|
case 10:
|
|
color = mpHost->mLightGreen_2;
|
|
break;
|
|
case 11:
|
|
color = mpHost->mLightYellow_2;
|
|
break;
|
|
case 12:
|
|
color = mpHost->mLightBlue_2;
|
|
break;
|
|
case 13:
|
|
color = mpHost->mLightMagenta_2;
|
|
break;
|
|
case 14:
|
|
color = mpHost->mLightCyan_2;
|
|
break;
|
|
case 15:
|
|
color = mpHost->mLightWhite_2;
|
|
break;
|
|
case 16:
|
|
color = mpHost->mLightBlack_2;
|
|
break;
|
|
default: //user defined room color
|
|
if (!mCustomEnvColors.contains(env)) {
|
|
if (16 < env && env < 232) {
|
|
quint8 const base = env - 16;
|
|
quint8 r = base / 36;
|
|
quint8 g = (base - (r * 36)) / 6;
|
|
quint8 b = (base - (r * 36)) - (g * 6);
|
|
|
|
r = r == 0 ? 0 : (r - 1) * 40 + 95;
|
|
g = g == 0 ? 0 : (g - 1) * 40 + 95;
|
|
b = b == 0 ? 0 : (b - 1) * 40 + 95;
|
|
color = QColor(r, g, b, 255);
|
|
} else if (231 < env && env < 256) {
|
|
quint8 const k = ((env - 232) * 10) + 8;
|
|
color = QColor(k, k, k, 255);
|
|
}
|
|
break;
|
|
}
|
|
color = mCustomEnvColors.value(env);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
void TMap::restore16ColorSet()
|
|
{
|
|
mCustomEnvColors[257] = mpHost->mRed_2;
|
|
mCustomEnvColors[258] = mpHost->mGreen_2;
|
|
mCustomEnvColors[259] = mpHost->mYellow_2;
|
|
mCustomEnvColors[260] = mpHost->mBlue_2;
|
|
mCustomEnvColors[261] = mpHost->mMagenta_2;
|
|
mCustomEnvColors[262] = mpHost->mCyan_2;
|
|
mCustomEnvColors[263] = mpHost->mWhite_2;
|
|
mCustomEnvColors[264] = mpHost->mBlack_2;
|
|
mCustomEnvColors[265] = mpHost->mLightRed_2;
|
|
mCustomEnvColors[266] = mpHost->mLightGreen_2;
|
|
mCustomEnvColors[267] = mpHost->mLightYellow_2;
|
|
mCustomEnvColors[268] = mpHost->mLightBlue_2;
|
|
mCustomEnvColors[269] = mpHost->mLightMagenta_2;
|
|
mCustomEnvColors[270] = mpHost->mLightCyan_2;
|
|
mCustomEnvColors[271] = mpHost->mLightWhite_2;
|
|
mCustomEnvColors[272] = mpHost->mLightBlack_2;
|
|
}
|
|
|
|
void TMap::setUnsaved(const char* fromWhere)
|
|
{
|
|
#if !defined(DEBUG_MAPAUTOSAVE)
|
|
Q_UNUSED(fromWhere)
|
|
#else
|
|
QString nowString = QDateTime::currentDateTimeUtc().toString("HH:mm:ss.zzz");
|
|
qDebug().nospace().noquote() << "TMap::setUnsaved(...) INFO - called at: " << nowString << " from: " << fromWhere << ".";
|
|
#endif
|
|
mUnsavedMap = true;
|
|
}
|
|
|
|
void TMap::setSaveError(bool state)
|
|
{
|
|
if (mSaveError != state) {
|
|
mSaveError = state;
|
|
emit signal_saveErrorChanged(state);
|
|
}
|
|
}
|
|
|
|
void TMap::setDefaultAreaShown(bool state)
|
|
{
|
|
if (mShowDefaultArea != state) {
|
|
mShowDefaultArea = state;
|
|
if (!mpMapper.isNull()) {
|
|
mpMapper->updateAreaComboBox();
|
|
}
|
|
}
|
|
}
|