mirror of
https://github.com/huderlem/porymap.git
synced 2026-03-22 01:54:46 -05:00
429 lines
15 KiB
C++
429 lines
15 KiB
C++
#include "history.h"
|
|
#include "map.h"
|
|
#include "imageproviders.h"
|
|
#include "scripting.h"
|
|
#include "utility.h"
|
|
#include "editcommands.h"
|
|
|
|
#include <QTime>
|
|
#include <QPainter>
|
|
#include <QImage>
|
|
#include <QRegularExpression>
|
|
|
|
|
|
Map::Map(QObject *parent) : QObject(parent)
|
|
{
|
|
m_editHistory = new QUndoStack(this);
|
|
|
|
resetEvents();
|
|
|
|
m_header = new MapHeader(this);
|
|
connect(m_header, &MapHeader::modified, this, &Map::modified);
|
|
}
|
|
|
|
Map::Map(const Map &other, QObject *parent) : Map(parent) {
|
|
m_name = other.m_name;
|
|
m_constantName = other.m_constantName;
|
|
m_sharedEventsMap = other.m_sharedEventsMap;
|
|
m_sharedScriptsMap = other.m_sharedScriptsMap;
|
|
m_customAttributes = other.m_customAttributes;
|
|
*m_header = *other.m_header;
|
|
m_layout = other.m_layout;
|
|
m_isPersistedToFile = false;
|
|
|
|
// Copy events
|
|
for (auto i = other.m_events.constBegin(); i != other.m_events.constEnd(); i++) {
|
|
for (const auto &event : i.value())
|
|
addEvent(event->duplicate());
|
|
}
|
|
|
|
// Duplicating the map connections is probably not desirable, so we skip them.
|
|
}
|
|
|
|
Map::~Map() {
|
|
qDeleteAll(m_ownedEvents);
|
|
m_ownedEvents.clear();
|
|
deleteConnections();
|
|
}
|
|
|
|
// Note: Map does not take ownership of layout
|
|
void Map::setLayout(Layout *layout) {
|
|
if (layout == m_layout)
|
|
return;
|
|
m_layout = layout;
|
|
if (layout) {
|
|
m_layoutId = layout->id;
|
|
}
|
|
emit layoutChanged();
|
|
}
|
|
|
|
// We don't enforce this for existing maps, but for creating new maps we need to formulaically generate a new MAP_NAME ID.
|
|
QString Map::mapConstantFromName(const QString &name) {
|
|
return projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix) + Util::toDefineCase(name);
|
|
}
|
|
|
|
// Get the portion of the map that can be rendered when rendered as a map connection.
|
|
// Cardinal connections render the nearest segment of their map and within the bounds of the border draw distance,
|
|
// Dive/Emerge connections are rendered normally within the bounds of their parent map.
|
|
QRect Map::getConnectionRect(const QString &direction, Layout * fromLayout) const {
|
|
int x = 0, y = 0;
|
|
int w = pixelWidth(), h = pixelHeight();
|
|
|
|
QMargins viewDistance = Project::getPixelViewDistance();
|
|
if (direction == "up") {
|
|
h = qMin(h, viewDistance.top());
|
|
y = pixelHeight() - h;
|
|
} else if (direction == "down") {
|
|
h = qMin(h, viewDistance.bottom());
|
|
} else if (direction == "left") {
|
|
w = qMin(w, viewDistance.left());
|
|
x = pixelWidth() - w;
|
|
} else if (direction == "right") {
|
|
w = qMin(w, viewDistance.right());
|
|
} else if (MapConnection::isDiving(direction)) {
|
|
if (fromLayout) {
|
|
w = qMin(w, fromLayout->pixelWidth());
|
|
h = qMin(h, fromLayout->pixelHeight());
|
|
}
|
|
} else {
|
|
// Unknown direction
|
|
return QRect();
|
|
}
|
|
return QRect(x, y, w, h);
|
|
}
|
|
|
|
QPixmap Map::renderConnection(const QString &direction, Layout * fromLayout) {
|
|
if (!m_layout)
|
|
return QPixmap();
|
|
|
|
QRect bounds = getConnectionRect(direction, fromLayout);
|
|
if (!bounds.isValid())
|
|
return QPixmap();
|
|
|
|
// 'fromLayout' will be used in 'render' to get the palettes from the parent map.
|
|
// Dive/Emerge connections render normally with their own palettes, so we ignore this.
|
|
if (MapConnection::isDiving(direction))
|
|
fromLayout = nullptr;
|
|
|
|
QPixmap connectionPixmap = m_layout->render(true, fromLayout, bounds);
|
|
return connectionPixmap.copy(bounds);
|
|
}
|
|
|
|
void Map::openScript(const QString &label) {
|
|
emit openScriptRequested(label);
|
|
}
|
|
|
|
void Map::setSharedScriptsMap(const QString &sharedScriptsMap) {
|
|
if (m_sharedScriptsMap == sharedScriptsMap)
|
|
return;
|
|
m_sharedScriptsMap = sharedScriptsMap;
|
|
invalidateScripts();
|
|
}
|
|
|
|
void Map::invalidateScripts() {
|
|
m_scriptsLoaded = false;
|
|
|
|
// m_scriptFileWatcher is a QPointer so clearing it shouldn't be necessary,
|
|
// but it's possible that Map::getScriptLabels will be called before events are processed.
|
|
delete m_scriptFileWatcher;
|
|
m_scriptFileWatcher = nullptr;
|
|
|
|
emit scriptsModified();
|
|
}
|
|
|
|
QStringList Map::getScriptLabels(Event::Group group) {
|
|
if (!m_scriptsLoaded && m_isPersistedToFile) {
|
|
const QString scriptsFilepath = getScriptsFilepath();
|
|
QString error;
|
|
m_scriptLabels = ParseUtil::getGlobalScriptLabels(scriptsFilepath, &error);
|
|
|
|
if (!error.isEmpty() && !m_loggedScriptsFileError) {
|
|
logWarn(QString("Failed to read scripts file '%1' for %2: %3")
|
|
.arg(Util::stripPrefix(scriptsFilepath, projectConfig.projectDir() + "/"))
|
|
.arg(m_name)
|
|
.arg(error));
|
|
|
|
// Setting this flag here (and below) lets us skip some steps and logging if we already know it failed.
|
|
// Script labels may be re-requested often, so we don't want to fill the log with warnings.
|
|
m_loggedScriptsFileError = true;
|
|
}
|
|
|
|
if (porymapConfig.monitorFiles && !m_loggedScriptsFileError) {
|
|
if (!m_scriptFileWatcher) {
|
|
// Only create the file watcher when it's first needed (even an empty QFileSystemWatcher will consume system resources).
|
|
// The other option would be for Porymap to have a single global QFileSystemWatcher, but that has complications of its own.
|
|
m_scriptFileWatcher = new QFileSystemWatcher(this);
|
|
connect(m_scriptFileWatcher, &QFileSystemWatcher::fileChanged, this, &Map::invalidateScripts);
|
|
}
|
|
// m_scriptFileWatcher can stil be nullptr here if the inotify limit was reached on Linux.
|
|
// Porymap isn't using enough resources in general for this to be a problem, but the user may have lowered the inotify limit.
|
|
if (!m_scriptFileWatcher) {
|
|
logWarn(QString("Failed to add scripts file '%1' to file watcher for %2: Reached system resource limit.")
|
|
.arg(Util::stripPrefix(scriptsFilepath, projectConfig.projectDir() + "/"))
|
|
.arg(m_name));
|
|
m_loggedScriptsFileError = true;
|
|
} else if (!m_scriptFileWatcher->files().contains(scriptsFilepath) && !m_scriptFileWatcher->addPath(scriptsFilepath)) {
|
|
logWarn(QString("Failed to add scripts file '%1' to file watcher for %2.")
|
|
.arg(Util::stripPrefix(scriptsFilepath, projectConfig.projectDir() + "/"))
|
|
.arg(m_name));
|
|
m_loggedScriptsFileError = true;
|
|
}
|
|
}
|
|
|
|
m_scriptsLoaded = true;
|
|
}
|
|
|
|
QStringList scriptLabels = m_scriptLabels;
|
|
|
|
// Add script labels currently in-use by the map's events
|
|
for (const auto &event : getEvents(group)) {
|
|
scriptLabels.append(event->getScripts());
|
|
}
|
|
|
|
scriptLabels.sort(Qt::CaseInsensitive);
|
|
scriptLabels.removeAll("");
|
|
scriptLabels.removeAll("0");
|
|
scriptLabels.removeAll("0x0");
|
|
scriptLabels.removeDuplicates();
|
|
|
|
return scriptLabels;
|
|
}
|
|
|
|
QString Map::getScriptsFilepath() const {
|
|
const bool usePoryscript = projectConfig.usePoryScript;
|
|
auto path = QDir::cleanPath(QString("%1/%2/%3/scripts")
|
|
.arg(projectConfig.projectDir())
|
|
.arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders))
|
|
.arg(!m_sharedScriptsMap.isEmpty() ? m_sharedScriptsMap : m_name));
|
|
auto extension = Project::getScriptFileExtension(usePoryscript);
|
|
if (usePoryscript && !QFile::exists(path + extension))
|
|
extension = Project::getScriptFileExtension(false);
|
|
path += extension;
|
|
return path;
|
|
}
|
|
|
|
QString Map::getJsonFilepath(const QString &mapName) {
|
|
return QDir::cleanPath(QString("%1/%2/%3/map.json")
|
|
.arg(projectConfig.projectDir())
|
|
.arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders))
|
|
.arg(mapName));
|
|
}
|
|
|
|
void Map::resetEvents() {
|
|
m_events[Event::Group::Object].clear();
|
|
m_events[Event::Group::Warp].clear();
|
|
m_events[Event::Group::Coord].clear();
|
|
m_events[Event::Group::Bg].clear();
|
|
m_events[Event::Group::Heal].clear();
|
|
}
|
|
|
|
QList<Event *> Map::getEvents(Event::Group group) const {
|
|
if (group == Event::Group::None) {
|
|
// Get all events
|
|
QList<Event *> all_events;
|
|
for (const auto &event_list : m_events) {
|
|
all_events << event_list;
|
|
}
|
|
return all_events;
|
|
}
|
|
return m_events[group];
|
|
}
|
|
|
|
Event* Map::getEvent(Event::Group group, int index) const {
|
|
return m_events[group].value(index, nullptr);
|
|
}
|
|
|
|
Event* Map::getEvent(Event::Group group, const QString &idName) const {
|
|
if (idName.isEmpty())
|
|
return nullptr;
|
|
|
|
bool idIsNumber;
|
|
int id = idName.toInt(&idIsNumber, 0);
|
|
if (idIsNumber)
|
|
return getEvent(group, id - Event::getIndexOffset(group));
|
|
|
|
auto events = getEvents(group);
|
|
for (const auto &event : events) {
|
|
if (event->getIdName() == idName) {
|
|
return event;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Returns a list of ID names for the given event group (or all events, if no group is given).
|
|
// For events with no explicit ID name, their index string is given instead.
|
|
QStringList Map::getEventIdNames(Event::Group group) const {
|
|
QList<Event::Group> groups;
|
|
if (group == Event::Group::None) {
|
|
groups = Event::groups();
|
|
} else {
|
|
groups.append(group);
|
|
}
|
|
|
|
QStringList idNames;
|
|
for (const auto &group : groups) {
|
|
const auto events = m_events[group];
|
|
int indexOffset = Event::getIndexOffset(group);
|
|
for (int i = 0; i < events.length(); i++) {
|
|
QString idName = events.at(i)->getIdName();
|
|
if (idName.isEmpty()) {
|
|
idName = QString::number(i + indexOffset);
|
|
}
|
|
idNames.append(idName);
|
|
}
|
|
}
|
|
return idNames;
|
|
}
|
|
|
|
int Map::getNumEvents(Event::Group group) const {
|
|
if (group == Event::Group::None) {
|
|
// Total number of events
|
|
int numEvents = 0;
|
|
for (auto it = m_events.constBegin(); it != m_events.constEnd(); it++) {
|
|
numEvents += it.value().length();
|
|
}
|
|
return numEvents;
|
|
}
|
|
return m_events[group].length();
|
|
}
|
|
|
|
bool Map::hasEvents() const {
|
|
for (auto it = m_events.constBegin(); it != m_events.constEnd(); it++) {
|
|
if (!it.value().isEmpty()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Map::removeEvent(Event *event) {
|
|
for (auto i = m_events.begin(); i != m_events.end(); i++) {
|
|
i.value().removeAll(event);
|
|
}
|
|
}
|
|
|
|
void Map::addEvent(Event *event) {
|
|
event->setMap(this);
|
|
m_events[event->getEventGroup()].append(event);
|
|
if (!m_ownedEvents.contains(event)) m_ownedEvents.insert(event);
|
|
}
|
|
|
|
int Map::getIndexOfEvent(Event *event) const {
|
|
return m_events.value(event->getEventGroup()).indexOf(event);
|
|
}
|
|
|
|
bool Map::hasEvent(Event *event) const {
|
|
return getIndexOfEvent(event) >= 0;
|
|
}
|
|
|
|
void Map::deleteConnections() {
|
|
qDeleteAll(m_ownedConnections);
|
|
m_ownedConnections.clear();
|
|
m_connections.clear();
|
|
}
|
|
|
|
void Map::addConnection(MapConnection *connection) {
|
|
if (!connection || m_connections.contains(connection))
|
|
return;
|
|
|
|
// Maps should only have one Dive/Emerge connection at a time.
|
|
// (Users can technically have more by editing their data manually, but we will only display one at a time)
|
|
// Any additional connections being added (this can happen via mirroring) are tracked for deleting but otherwise ignored.
|
|
if (connection->isDiving()) {
|
|
for (const auto &i : m_connections) {
|
|
if (i->direction() == connection->direction()) {
|
|
trackConnection(connection);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
loadConnection(connection);
|
|
modify();
|
|
emit connectionAdded(connection);
|
|
return;
|
|
}
|
|
|
|
void Map::loadConnection(MapConnection *connection) {
|
|
if (!connection)
|
|
return;
|
|
|
|
if (!m_connections.contains(connection))
|
|
m_connections.append(connection);
|
|
|
|
trackConnection(connection);
|
|
}
|
|
|
|
void Map::trackConnection(MapConnection *connection) {
|
|
connection->setParentMap(this, false);
|
|
|
|
if (!m_ownedConnections.contains(connection)) {
|
|
m_ownedConnections.insert(connection);
|
|
connect(connection, &MapConnection::parentMapChanged, [=](Map *, Map *after) {
|
|
if (after != this && after != nullptr) {
|
|
// MapConnection's parent has been reassigned, it's no longer our responsibility
|
|
m_ownedConnections.remove(connection);
|
|
QObject::disconnect(connection, &MapConnection::parentMapChanged, this, nullptr);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// We retain ownership of this MapConnection until it's assigned to a new parent map.
|
|
void Map::removeConnection(MapConnection *connection) {
|
|
if (!m_connections.removeOne(connection))
|
|
return;
|
|
connection->setParentMap(nullptr, false);
|
|
modify();
|
|
emit connectionRemoved(connection);
|
|
}
|
|
|
|
// Return the first map connection that has the given direction.
|
|
MapConnection* Map::getConnection(const QString &direction) const {
|
|
for (const auto &connection : m_connections) {
|
|
if (connection->direction() == direction)
|
|
return connection;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Map::commit(QUndoCommand *cmd) {
|
|
m_editHistory->push(cmd);
|
|
}
|
|
|
|
void Map::modify() {
|
|
emit modified();
|
|
}
|
|
|
|
void Map::setClean() {
|
|
m_editHistory->setClean();
|
|
m_hasUnsavedDataChanges = false;
|
|
m_isPersistedToFile = true;
|
|
}
|
|
|
|
bool Map::hasUnsavedChanges() const {
|
|
return !m_editHistory->isClean() || (m_layout && m_layout->hasUnsavedChanges()) || m_hasUnsavedDataChanges || !m_isPersistedToFile;
|
|
}
|
|
|
|
void Map::pruneEditHistory() {
|
|
// Edit history for map connections gets messy because edits on other maps can affect the current map.
|
|
// To avoid complications we clear MapConnection edit history when the user opens a different map.
|
|
// No other edits within a single map depend on MapConnections so they can be pruned safely.
|
|
static const QSet<int> mapConnectionIds = {
|
|
ID_MapConnectionMove,
|
|
ID_MapConnectionChangeDirection,
|
|
ID_MapConnectionChangeMap,
|
|
ID_MapConnectionAdd,
|
|
ID_MapConnectionRemove
|
|
};
|
|
for (int i = 0; i < m_editHistory->count(); i++) {
|
|
// Qt really doesn't expect editing commands in the stack to be valid (fair).
|
|
// A better future design might be to have separate edit histories per map tab,
|
|
// and dumping the entire Connections tab history with QUndoStack::clear.
|
|
auto command = const_cast<QUndoCommand*>(m_editHistory->command(i));
|
|
if (mapConnectionIds.contains(command->id()))
|
|
command->setObsolete(true);
|
|
}
|
|
}
|