From df98d6233960dfcc5efacf8064fbdb2cf10743dc Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 7 Dec 2025 20:18:08 -0500 Subject: [PATCH] Add unlockable icon --- include/mainwindow.h | 6 +- include/ui/unlockableicon.h | 57 ++++++++ porymap.pro | 2 + resources/images.qrc | 1 + resources/images/unlockable_tab_icon.dat | 1 + src/mainwindow.cpp | 22 +++ src/ui/unlockableicon.cpp | 178 +++++++++++++++++++++++ 7 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 include/ui/unlockableicon.h create mode 100644 resources/images/unlockable_tab_icon.dat create mode 100644 src/ui/unlockableicon.cpp diff --git a/include/mainwindow.h b/include/mainwindow.h index 0f9cc76e..a4d3a731 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -34,6 +34,7 @@ #include "newlayoutdialog.h" #include "message.h" #include "resizelayoutpopup.h" +#include "unlockableicon.h" #if __has_include() #include @@ -354,6 +355,8 @@ private: MapNavigation forwardNavigation; bool ignoreNavigationRecords = false; + UnlockableIcon unlockableMainTabIcon; + QAction *copyAction = nullptr; QAction *pasteAction = nullptr; @@ -460,7 +463,8 @@ private: MapListToolBar* getCurrentMapListToolBar(); MapTree* getCurrentMapList(); void setLocationComboBoxes(const QStringList &locations); - + void overrideMainTabIcons(const QIcon& icon); + void tryUnlockMainTabIcon(const Map* map); QObjectList shortcutableObjects() const; void addCustomHeaderValue(QString key, QJsonValue value, bool isNew = false); diff --git a/include/ui/unlockableicon.h b/include/ui/unlockableicon.h new file mode 100644 index 00000000..2ec8687e --- /dev/null +++ b/include/ui/unlockableicon.h @@ -0,0 +1,57 @@ +#ifndef UNLOCKABLEICON_H +#define UNLOCKABLEICON_H + +// Manages an icon loaded from an obfuscated data file containing the icon's image data and a key. +// The icon can only be accessed by inputting the correct key. + +#include +#include +#include +#include + +class UnlockableIcon : public QObject +{ + Q_OBJECT +public: + UnlockableIcon(QObject* parent = nullptr); + UnlockableIcon(const QString& dataFilepath, QObject* parent = nullptr); + ~UnlockableIcon() {}; + + // Create the obfuscated data file to load an unlockable icon from. + // Normally unused, this is only needed to update the resource data file. + static bool createDataFile(const QString& inputFilepath, const QString& outputFilepath, const QString& key); + + bool load(const QString& dataFilepath); + void clear(); + + // Try to unlock the icon by matching the next character in the key. + // Progress resets if the character is not a match. + void tryUnlock(const QChar& c); + + // Try to unlock the icon by matching the next character in the key. + // Progress resets if none of the characters in the set are a match. + void tryUnlock(const QSet& cSet); + + // Try to unlock the icon by matching the remaining characters in the key. + // Progress resets if any character in the string is not a match. + void tryUnlock(const QString& key); + + bool isUnlocked() const; + QIcon icon() const; + +signals: + void unlocked(const QIcon& icon); + +private: + QIcon m_icon; + QString m_key; + quint32 m_keyIndex = 0; + bool m_loaded = false; + + bool canUnlock() const; + bool tryKeyMatch(const QSet& cSet); +}; + + + +#endif // UNLOCKABLEICON_H diff --git a/porymap.pro b/porymap.pro index 4e0e9321..7285db6c 100644 --- a/porymap.pro +++ b/porymap.pro @@ -148,6 +148,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/regionmappropertiesdialog.cpp \ src/ui/colorpicker.cpp \ src/ui/loadingscreen.cpp \ + src/ui/unlockableicon.cpp \ src/config.cpp \ src/editor.cpp \ src/main.cpp \ @@ -268,6 +269,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/regionmappropertiesdialog.h \ include/ui/colorpicker.h \ include/ui/loadingscreen.h \ + include/ui/unlockableicon.h \ include/config.h \ include/editor.h \ include/mainwindow.h \ diff --git a/resources/images.qrc b/resources/images.qrc index 856a3fbe..55be0e4f 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -102,6 +102,7 @@ images/Entities_16x16.png images/pokemon_icon_placeholder.png images/porysplash.gif + images/unlockable_tab_icon.dat icons/clipboard.ico icons/map_go.ico diff --git a/resources/images/unlockable_tab_icon.dat b/resources/images/unlockable_tab_icon.dat new file mode 100644 index 00000000..a440f93e --- /dev/null +++ b/resources/images/unlockable_tab_icon.dat @@ -0,0 +1 @@ +ļę㻺ƒ»ŗßƒ ½ßŚæč\嫆¹­ ½˜ß¤Øˆ€®®®®”¼ŗ‡Ŗŗˆ®®®«ˆ®®®®Ū¬®¢®®®¬‰ø¢œœ®®®­†Żƒ«¾Ž­§¼ŗŽ¾¦Ø„ŽŽø—ߣøƒš¦§”¼½ß¦®®¬†½‰µŖÖ¼¢”®§¢¹‰ŗŲŗ†©¾Œ†«‡„žŖŚŻŗ¹©—žƒŗ€¾€¹¾¤Ų»ž¶§£Ÿ©•½Ÿ¼©„Œ§®··ˆ€¢‰†Žŗ§©Ż‹‹§¹˜©¾‰®«—©Ž˜ŗ¼½Ŗ™Ä·©ƒŖŠ§«Œ…܉ܧ‰™®¤©ø¶Ÿ¹Ŗ¾ŗßܻ޻׵†¶•ž–¤ˆ¹ŒŖß¶ŗ­»Ø”øµŸ¶—„߄„Š¦Ł™ŠĄ…ŪŠ‡‰‡ø‹ŲĄ‡–ÖŽ›µ†ˆŖ×„……£«›¦„܆¤ŒŻ¶æ•æØ¦©ø¼¹Ä„—Ū•¤¾£Ŗ…Ö–·ø˜ØÄŠ×–˜£æ«„‚ŸŚ«——†©…¢›Ų§¼—ž—ˆŽœ¼»—Ø©¹ß–ƒ‰¼«‹¶Ś•©ø¼›¹øæ ŠĄ¦·­£Ł–•§øŽ˜Ū‡…Ŗŗš¾¦Ŗ„­­ŗøŗ¶¬”¬žß٤‡¼»›—••×¾ŁŚ‰¦Ÿ‹¬†¦¶ ½µ¾‡ˆµÖ¶æĄ˜ŠÖš‹•ŖŠ¬¢ŸØ®¢Ł·—•¶˜¾¦Ų®£Žžš”×§•› Ą¾»˜æ˜”·Šœ›‰ˆŖ•Ł½·øŽŪ¬ …­†ŁšøŚž–­Ž•𮦔懂•¤š¼¤Š½–˜æœµ‰¹¢ØŁ£×©Š›¶Ž™»·ÜŒ‰€®Ÿ¤†—®Ž˜Œ®†¢Ś†ƒŲ܊æ‹ÜŠŻÖĄ·š‚݋׿ØŲÖ–Ł„§®×„Ŗ®®®®¼ŗŖ—ŗ½¾¬®Ø­ˆ¾Ŗæ…€Ä¦­˜‚欘†®®®®§ØŲ†›ß®®®®­‹©„ ŗ˜­®Ś›‡‚®®®®¬·­¦ø·¢®®®ŁĄ®®® ™˜ŖŪ­¹¢„®®®®—·Ÿŗø§½¼¶·ŒˆŒ§„™µ‚ƒœµ¼­ßŠ·­ƒ¦Ø¹ŪŽø¶®®­†¹¹­­«œ¢ˆ«£™•†…߇—¬§®ŒÄ…޼‰™­×®€øžøŒ¦”‹‚¼Ž‡ æ•‰€¹§­ßŒ„„£½ž„€Øž¹¤Śø¹©€€ˆŻ „¦¢§œ£—ŪŽ×”ŸœŖ€‰”ŽŽæš·§œÄ®ÄøƒøŸŗ›¾Šž—ܶߞƒÖĈ™˜‡Ö«Ö†¤Ü¶æž‡ŪŖ„ßˆ«§¦žßƒ·ß‰¶«ž…•­¢ŁŽŠ–†‡Ą•Š„‹™ŻÖ‚»™ˆæƒ®­­ƒ…£ˆø¢‰ƒ„®¢Ū ”¬øž”ŖøŸŽ¾‡Ų¤Ø›ŗø×‚Öæ¬ÄŖ£Ą¼›µ”Ųˆ‡†¾®®®¢ŠŽ¹½¶‹©‡”»«Ÿ…Żßš¶ø½™¶‚ŗšŠØŽ˜®®Ŗ®¹¹½Ø£»ˆ®øŖŽ¢ ‚”™¼Ś‡µØÖ†µ¼ŚŪ·®®¼¦›¹×ŽšŻ•ˆ¾™šŒŸ­æø–ŠŁ­¦ßµ¦ŒŖ¹Ž£¦„ŁÜ ˆ”؂ ŲŽ—ŖŚ‹¦‡¶æ–œ—œµ”·Ż×¢ÄŗƒÖ‡¼¼ƒŽŚ€ˆß¶„ÜŖ‚¶Ą‰”•¢Œ«ž»©žÜß£×ˆŚ›ŗ £ØŽŖ·ŪŁµÖĄ˜×¬€¦™–¶›†ž™Œ­×ĄŽŸ˜Śš‹Ż™Ś‹¬‹•©ŚŁŠ¶†Üܹ¹‚ˆ‡Ż©‰š‡‰Ų¦ˆ»¹ƒ¬…Œš•‰»¢­ˆˆÖžŪ¦ĄŽŸ‹­—‹”‡ßŖ¼„½Ŗ…ƒÜ™•˜¢ ·µ×؆ŗŽŸ×ø£ŚŚŸ¬¦–Ž›‡øŗŲŽŲޫУ‚‚ګە•椊 ŗŒÄ¦¾Ÿ§Äœ£Š—„܍Ą˜©¬æ§Ŗ™µ‚‹ŽŽŸˆŲרœ‚܇µ‡Łæ•­ŚĄ†ž‹ŗŁß”‹Ö”Ŗ¾–ŽŽ†ŠŻ‡Ÿ§«©Ž¹·¼Ł€ŲŠ·«Ą§ ĄŽ¶Ū™ß®Ą”‰¼‰—׈©ÄŻ„¤×Œ‰¹Ł™€€Ÿ¶ŻŸŻ†ŖŸ©×ٽ‚„¦§·§™‰¾Š¢„‡¼Üø«¼Üž©½Ü‡›æŚÄ”Ū•­æ‹¹Ö…Ą µ®®æ”½££½›ŚŲߎ›ƒŖ‰ŸŪ‡øž„ŗø„ہ¶ Ž¦Š¶—–‰¦µ„Ū‚ Ø¾Ø¾„„†µ¤«‚§øŒ•Ŗ„޹‰ŒøŽ€‚Š–ƒ„†Ū܅‚¬Œ˜Ūµ Œ„—ŗ–»„«·„ŖÖµ½›†Œˆµµ¢ˆ›½×¾™ØÄ¢ŁŪ¶»š”–ƒ¢Ÿ½Ś·„¢¶€„ø¾¦Ÿ¶œ‚Ž˜»†¹­Ø¶ŸŽ˜™©½ŽœŪ§¤Ū”Ū‰·Ž¹‡«–Žø¤ŁÜ¶§·»¹ŗøŪ®‡‚×™Ą†øµœ¤–•Ūžœ·•£Ø·Ÿ Š‹Ÿ”œ›£æœ¬Ä‡ Ų܁øŒ„ŪŽµ”«œ‰æŖž¤”ŪŪ¼· Š¶¬…µ¦ŻÜ€• æ–ŚÄؗŒ«‚¬§‰§”¢¹©‡Ż¬›ß߯–£ßܦ؁棇·Ų…ŪŸœ£®Ŗ†ßŁ”×¹»Ŗ·£­œ§øŒ†¾šøŖŸ¶¤”œŖÜ–š…§ĄßæŲŻ—¢œ—†žĄ”Ü»‰——¢Ūڸ٘Ą§Ž…Œ‚Ŗ½«£‹¬ˆ­—»Ł Ŗ­¹·…„„‹¾Ö¬”«ÜŸœ”ÖĄŸ­¹µž–„”¼ŖžŚ»Ž˜œœ»›•µˆ¼¦Ś¾Ÿ˜˜ƒ®–Ž×Œœ‰Žæ›«…Ÿ×™·©Ž‹£¹–»»ŗ¤™‡›æ¢£‰…ÄØŲøŁ•§ØŸ¬¶„ŻÖ¾„וŸ› ŗ©Ą¼€€–®–µ…—ßŪœÄ…–×Ü«Šƒ‡§«Ū·Ų¼ŒĄ„ŚØ»–Ŗ„„§¦¼ ¶‚Œ½§ŪŸ„æ©Ū¶Œ¤ŽĄŒµ›§ Ą¼Ą×­žÄŻŽ¤¦¤ ¾˜®®®Ø€¼ŗ½­¹Ŗ†„ƒ¹¶„…ˆ¢—¬¬™§ĄÖÄ×®ŗ–¼Ü¹ŗ¹˜ß˜™ž½ŪŒ¢ŖŪā–ׇž‚ž‚©‚Ą—Ä™ŽĄ‰ŁŖ„«©­­µ×«ŖŽ¬”ؾŖ·Ø¼¢ß«•¼Ÿ‹®Ÿ¹‰€Żš™œƒ£¼¾æØŁœžŽ»žŸŸ·Ś®¦˜¤„Žƒ·„؜ø£ŽŻ«…„ŁŠ¤«µŽæ†æž†Ž¦ †ˆ ¦¼×¼®ŁŸµ¢ŚŸœ€„ޠڣߗÄŪŽƒøØ—‹„‚ŗŻ§§Ł€ŁŁ‚ ƒø–Ą„‚ŁŠŽŒÖŲž©øŸ„‚µ©®×Ž»»¶€Ÿ µß”Œœŗ¤æ¢µ˜¤Ł¤½æƒ˜¤—¶ƒ•”ŖšŪŽ—›”Żž©£–Ųž½æ¶„‹½ž™‰Šŗ×„‚¹®©–¬Ÿ½š­Ž†ÄŽÄ‡ŻƒŽ‹ƒ—ݼŗ‚Ż™¢© ›–¬žœŪŚ„ÖŁ…„­…Ö¶­œ½Ż½×¤ŁŠ¢‰Ž·»§©¢ŽžŁŸƒ •˜Ö„Ų§™…©«‚š›×˜ß×¼€·‹Ž¢ĄÜ؛©§‹ŒŒ„€æ»˜Ö‡•©™€ˆŚ—ض‡§¤ßÖŻ¬Üܞƒø…¾‚ž®§ž˜»¼Šæ†Ł©žµ†ž»øĄ¢–­¬…ž‹ØŖ­™Ż ŒŒø…›ŸšßŪ›®®®®‹€µ–¹ßœŖ—œ•¤½ ž‰ŗ®š »øĄ™ŚŖ§Ÿ©¢¾¾Ä›£Œ—¼˜„Ł›æˆŒœ¹›‹­««›…æ¶…£Ż¬ŠĄ©¹ƒ¹µ†œĄ…€æ¹ŗ€‹·™–‰•ø»žŠ‹¾Ųߊ׉‹Ąæ×¾¾¬•ŗ‡ÜŸ¢Ą®®®®®Ŗƒ©»„¼š¾‚¬¬ \ No newline at end of file diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 637c4caf..08ce1a7a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -334,6 +334,8 @@ void MainWindow::initCustomUI() { ui->mainTabBar->setTabIcon(i, mainTabIcons.value(i)); } + this->unlockableMainTabIcon.load(":/images/unlockable_tab_icon.dat"); + // Create map header data widget this->mapHeaderForm = new MapHeaderForm(); ui->layout_HeaderData->addWidget(this->mapHeaderForm); @@ -343,6 +345,12 @@ void MainWindow::initCustomUI() { ui->graphicsView_Map->setResizeAnchor(QGraphicsView::ViewportAnchor::AnchorUnderMouse); } +void MainWindow::overrideMainTabIcons(const QIcon& icon) { + for (int i = 1; i < ui->mainTabBar->count(); i++) { + ui->mainTabBar->setTabIcon(i, icon); + } +} + void MainWindow::initExtraSignals() { connect(ui->tabWidget_EventType, &QTabWidget::currentChanged, this, &MainWindow::eventTabChanged); @@ -2285,6 +2293,8 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) ui->stackedWidget_MapEvents->setCurrentIndex(1); } else if (index == MainTab::Connections) { ui->graphicsView_Connections->setFocus(); // Avoid opening tab with focus on something editable + connect(this, &MainWindow::mapOpened, this, &MainWindow::tryUnlockMainTabIcon, Qt::UniqueConnection); + connect(&this->unlockableMainTabIcon, &UnlockableIcon::unlocked, this, &MainWindow::overrideMainTabIcons, Qt::UniqueConnection); } if (!editor->map) return; @@ -2294,6 +2304,18 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) } } +void MainWindow::tryUnlockMainTabIcon(const Map* map) { + if (!map || this->unlockableMainTabIcon.isUnlocked()) return; + const Layout* layout = map->layout(); + if (!layout) return; + + QSet chars; + if (!layout->name.isEmpty()) chars.insert(layout->name.at(0)); + const QString tilesetName = Tileset::stripPrefix(layout->tileset_secondary_label); + if (!tilesetName.isEmpty()) chars.insert(tilesetName.at(0)); + this->unlockableMainTabIcon.tryUnlock(chars); +} + void MainWindow::on_actionZoom_In_triggered() { editor->scaleMapView(1); } diff --git a/src/ui/unlockableicon.cpp b/src/ui/unlockableicon.cpp new file mode 100644 index 00000000..c13dbd6e --- /dev/null +++ b/src/ui/unlockableicon.cpp @@ -0,0 +1,178 @@ +#include "unlockableicon.h" +#include +#include +#include + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)) +constexpr int Version = QDataStream::Qt_6_8; +#endif + +UnlockableIcon::UnlockableIcon(QObject* parent) : QObject(parent) {}; +UnlockableIcon::UnlockableIcon(const QString& dataFilepath, QObject* parent) : UnlockableIcon(parent) { + load(dataFilepath); +}; + +#if (QT_VERSION < QT_VERSION_CHECK(6, 8, 0)) +bool UnlockableIcon::createDataFile(const QString&, const QString&, const QString&) { return false; } +#else +bool UnlockableIcon::createDataFile(const QString& inputFilepath, const QString& outputFilepath, const QString& key) { + if (inputFilepath.isEmpty() || outputFilepath.isEmpty() || key.isEmpty()) return false; + if (key.length() >= std::numeric_limits::max()) return false; + + QByteArray key64 = key.toUtf8().toBase64(); + if (key64.length() >= std::numeric_limits::max()) return false; + + QImage iconImage(inputFilepath); + if (iconImage.isNull()) return false; + + QByteArray iconData; + QBuffer buffer(&iconData); + buffer.open(QIODevice::WriteOnly); + iconImage.save(&buffer, "PNG"); + buffer.close(); + if (iconData.length() >= std::numeric_limits::max()) return false; + + QByteArray iconData64 = iconData.toBase64(); + if (iconData64.length() >= std::numeric_limits::max()) return false; + + QFile file(outputFilepath); + if (!file.open(QIODevice::WriteOnly)) return false; + QDataStream out(&file); + out.setVersion(Version); + + quint8 r = QRandomGenerator::global()->bounded(std::numeric_limits::max()); + + out << r; + out << static_cast(key.length() ^ r); + out << static_cast(key64.length() ^ r); + for (const auto& byte : key64) out << static_cast(byte ^ r); + + out << static_cast(iconData.length() ^ (r | (r << 8))); + out << static_cast(iconData64.length() ^ (r | (r << 8))); + for (const auto& byte : iconData64) out << static_cast(byte ^ r); + + file.close(); + return true; +} +#endif + +#if (QT_VERSION < QT_VERSION_CHECK(6, 8, 0)) +bool UnlockableIcon::load(const QString&) { return false; } +#else +bool UnlockableIcon::load(const QString& dataFilepath) { + clear(); + + QFile file(dataFilepath); + if (!file.open(QIODevice::ReadOnly)) return false; + + QDataStream in(&file); + in.setVersion(Version); + + quint8 r = 0; + in >> r; + + quint8 keyLength = 0; + in >> keyLength; + keyLength ^= r; + if (keyLength == 0) return false; + + quint8 key64Length = 0; + in >> key64Length; + key64Length ^= r; + if (key64Length == 0) return false; + + QByteArray key64(key64Length,0); + for (quint8 i = 0; i < key64Length; i++) { + in >> key64[i]; + key64[i] ^= r; + } + QString key = QString(QByteArray::fromBase64(key64)); + if (key.length() != keyLength) return false; + + quint16 iconDataLength = 0; + in >> iconDataLength; + iconDataLength ^= (r | r << 8); + if (iconDataLength == 0) return false; + + quint16 iconData64Length = 0; + in >> iconData64Length; + iconData64Length ^= (r | r << 8); + if (iconData64Length == 0) return false; + + QByteArray iconData64(iconData64Length,0); + for (quint16 i = 0; i < iconData64Length; i++) { + in >> iconData64[i]; + iconData64[i] ^= r; + } + QByteArray iconData = QByteArray::fromBase64(iconData64); + if (iconData.length() != iconDataLength) return false; + + QPixmap iconPixmap; + iconPixmap.loadFromData(iconData); + if (iconPixmap.isNull()) return false; + + m_icon = QIcon(iconPixmap); + m_key = key; + m_loaded = true; + return true; +} +#endif + +void UnlockableIcon::clear() { + m_icon = QIcon(); + m_key = QString(); + m_keyIndex = 0; + m_loaded = false; +} + + +#if (QT_VERSION < QT_VERSION_CHECK(6, 8, 0)) +bool UnlockableIcon::isUnlocked() const { return false; } +#else +bool UnlockableIcon::isUnlocked() const { + return m_loaded && m_keyIndex >= m_key.length(); +} +#endif + +#if (QT_VERSION < QT_VERSION_CHECK(6, 8, 0)) +bool UnlockableIcon::canUnlock() const { return false; } +#else +bool UnlockableIcon::canUnlock() const { + return m_loaded && m_keyIndex < m_key.length(); +} +#endif + +QIcon UnlockableIcon::icon() const { + return isUnlocked() ? m_icon : QIcon(); +} + +#if (QT_VERSION < QT_VERSION_CHECK(6, 8, 0)) +bool UnlockableIcon::tryKeyMatch(const QSet&) { return false; } +#else +bool UnlockableIcon::tryKeyMatch(const QSet& cSet) { + if (m_keyIndex >= m_key.length()) return false; + if (!cSet.contains(m_key.at(m_keyIndex))) { + m_keyIndex = 0; + return false; + } + if (++m_keyIndex == m_key.length()) { + emit unlocked(m_icon); + } + return true; +} +#endif + +void UnlockableIcon::tryUnlock(const QSet& cSet) { + if (canUnlock()) tryKeyMatch(cSet); +} + +void UnlockableIcon::tryUnlock(const QChar& c) { + tryUnlock(QSet{c}); +} + +void UnlockableIcon::tryUnlock(const QString& key) { + if (!canUnlock()) return; + for (const QChar& c : key) { + if (!tryKeyMatch(QSet{c})) return; + } +}