File indexing completed on 2024-04-28 04:05:21

0001 /*
0002     SPDX-FileCopyrightText: 2015 Jakob Gruber <jakob.gruber@gmail.com>
0003     SPDX-FileCopyrightText: 2011 Julian Helfferich <julian.helfferich@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include <config.h>
0009 #include "levelloader.h"
0010 
0011 #include <KLocalizedString>
0012 #include "picmi_debug.h"
0013 #include <QDir>
0014 #include <QDomDocument>
0015 #include <QFile>
0016 #include <QStandardPaths>
0017 
0018 #include "settings.h"
0019 #include "src/systemexception.h"
0020 
0021 class LevelList : public QList<QSharedPointer<Level> >
0022 {
0023 public:
0024     LevelList() : QList<QSharedPointer<Level> >() { }
0025     void append(const QList<QSharedPointer<Level> > &t);
0026 private:
0027     bool containsLevel(QSharedPointer<Level> level) const;
0028 };
0029 
0030 void LevelList::append(const QList<QSharedPointer<Level> > &t) {
0031     for (int i = 0; i < t.size(); i++) {
0032         QSharedPointer<Level> level = t[i];
0033         if (!containsLevel(level)) {
0034             QList<QSharedPointer<Level> >::append(level);
0035         }
0036     }
0037 }
0038 
0039 bool LevelList::containsLevel(QSharedPointer<Level> level) const {
0040     for (int i = 0; i < size(); i++) {
0041         if (*at(i) == *level) {
0042             return true;
0043         }
0044     }
0045     return false;
0046 }
0047 
0048 Level::Level() : m_solved(false), m_solved_time(0) { }
0049 
0050 QString Level::visibleName() const
0051 {
0052     if (solved()) {
0053         return name();
0054     }
0055     /* Mask the real name if unsolved. Unicode 0x26AB is the bullet point ('⚫') */
0056     return QString(name().length(), QChar(0x26AB));
0057 }
0058 
0059 QString Level::name() const {
0060     QByteArray bytes = m_name.toUtf8();
0061     return i18n(bytes.constData());
0062 }
0063 
0064 QString Level::author() const {
0065     QByteArray bytes = m_author.toUtf8();
0066     return i18n(bytes.constData());
0067 }
0068 
0069 QString Level::key() const {
0070     return QStringLiteral("preset_scores/%1_%2").arg(m_levelset, m_name);
0071 }
0072 
0073 void Level::writeSettings(int seconds) {
0074     QSharedPointer<QSettings> settings = Settings::instance()->qSettings();
0075     QString k = key();
0076 
0077     settings->setValue(k, seconds);
0078     settings->sync();
0079 }
0080 
0081 void Level::finalize() {
0082     constructPreview();
0083     readSettings();
0084 }
0085 
0086 void Level::readSettings() {
0087     QSharedPointer<QSettings> settings = Settings::instance()->qSettings();
0088     QString k = key();
0089 
0090     if (settings->contains(k)) {
0091         m_solved = true;
0092         m_solved_time = settings->value(k).toInt();
0093     }
0094 }
0095 
0096 void Level::constructPreview() {
0097     QImage preview(width(), height(), QImage::Format_Mono);
0098     preview.fill(Qt::color1);
0099 
0100     for (int y = 0; y < height(); y++) {
0101         for (int x = 0; x < width(); x++) {
0102             if (m_map[y * width() + x] == Board::Box) {
0103                 preview.setPixel(x, y, 0);
0104             }
0105         }
0106     }
0107 
0108     m_preview = QPixmap::fromImage(preview);
0109 }
0110 
0111 void Level::setSolved(int seconds) {
0112     if (m_solved_time > 0 && seconds >= m_solved_time) {
0113         return;
0114     }
0115     m_solved = true;
0116     m_solved_time = seconds;
0117     writeSettings(seconds);
0118 }
0119 
0120 bool Level::operator==(const Level &that) const {
0121     return (that.m_name == m_name && that.m_author == m_author);
0122 }
0123 
0124 QList<QSharedPointer<Level> > LevelLoader::load() {
0125     const QString prefix = QStringLiteral("levels/");
0126     QList<QString> paths;
0127     paths << QString(prefix)
0128           << QStringLiteral(FILEPATH) + QStringLiteral("/") + prefix
0129           << QStandardPaths::locate(QStandardPaths::AppDataLocation,
0130                                     prefix,
0131                                     QStandardPaths::LocateOption::LocateDirectory);
0132 
0133     LevelList list;
0134 
0135     for (int i = 0; i < paths.size(); i++) {
0136         QDir dir(paths[i]);
0137         if (!dir.exists()) {
0138             continue;
0139         }
0140 
0141         QStringList files = dir.entryList(QStringList(QStringLiteral("*.xml")));
0142 
0143         for (int j = 0; j < files.size(); j++) {
0144             LevelLoader loader(dir.absoluteFilePath(files[j]));
0145             list.append(loader.loadLevels());
0146         }
0147     }
0148 
0149     return list;
0150 }
0151 
0152 LevelLoader::LevelLoader(const QString &filename) :
0153     m_filename(filename), m_valid(true)
0154 {
0155     setLevelset(filename);
0156 }
0157 
0158 void LevelLoader::setLevelset(const QString& filename)
0159 {
0160     m_levelset = QSharedPointer<QDomDocument>(new QDomDocument());
0161 
0162     QFile file(filename);
0163     if (!file.open( QIODevice::ReadOnly)) {
0164         throw SystemException(QStringLiteral("Can't open file %1").arg(filename));
0165     }
0166 
0167     const QDomDocument::ParseResult parseResult = m_levelset->setContent(&file);
0168     file.close();
0169     if (!parseResult) {
0170         qCDebug(PICMIC_LOG) << QStringLiteral("Can't read levelset from %1 \nError: %2 in Line %3, Column %4")
0171                               .arg(filename, parseResult.errorMessage).arg(parseResult.errorLine).arg(parseResult.errorColumn);
0172         m_valid = false;
0173     }
0174 }
0175 
0176 QList<QSharedPointer<Level> > LevelLoader::loadLevels() {
0177     QList<QSharedPointer<Level> > l;
0178 
0179     if (!m_valid) {
0180         return l;
0181     }
0182 
0183     QDomElement levels = m_levelset->documentElement();
0184     if (!levels.hasAttribute(QStringLiteral("name"))) {
0185         qCDebug(PICMIC_LOG) << "Loading level failed: no levelset name specified";
0186         return l;
0187     }
0188     m_levelsetname = levels.attribute(QStringLiteral("name"));
0189 
0190     QDomNodeList childNodes = levels.childNodes();
0191     for (int i = 0; i < childNodes.size(); i++) {
0192         try {
0193             l.append(loadLevel(childNodes.at(i).toElement()));
0194         } catch (const SystemException &e) {
0195             qCDebug(PICMIC_LOG) << "Loading level failed: " << e.what();
0196         }
0197     }
0198     return l;
0199 }
0200 
0201 QSharedPointer<Level> LevelLoader::loadLevel(const QDomElement &node) const {
0202     if (node.isNull() || node.tagName() != QLatin1String("board")) {
0203         throw SystemException(QStringLiteral("Unexpected level node"));
0204     }
0205 
0206     if (!node.hasAttribute(QStringLiteral("name")) || !node.hasAttribute(QStringLiteral("author"))
0207             || !node.hasAttribute(QStringLiteral("difficulty"))) {
0208         throw SystemException(QStringLiteral("Level node missing attribute."));
0209     }
0210 
0211     QSharedPointer<Level> p(new Level);
0212     p->m_name = node.attribute(QStringLiteral("name"));
0213     p->m_author = node.attribute(QStringLiteral("author"));
0214     p->m_levelset = m_levelsetname;
0215     p->m_difficulty = node.attribute(QStringLiteral("difficulty")).toInt();
0216 
0217     QDomNodeList childNodes = node.childNodes();
0218 
0219     if (childNodes.isEmpty()) {
0220         throw SystemException(QStringLiteral("Empty level definition."));
0221     }
0222 
0223     const QString tag_name = childNodes.at(0).toElement().tagName();
0224     if (tag_name == QLatin1String("row")) {
0225         int i;
0226         QList<Board::State> l;
0227         for (i = 0; i < childNodes.size(); i++) {
0228             l = loadRow(childNodes.at(i).toElement());
0229             p->m_map.append(l);
0230         }
0231         p->m_width = l.size();
0232         p->m_height = i;
0233     } else if (tag_name == QLatin1String("xpm")) {
0234         QImage xpm = openXPM(childNodes.at(0).toElement());
0235         p->m_map = loadXPM(xpm);
0236         p->m_width = xpm.width();
0237         p->m_height = xpm.height();
0238     }
0239 
0240     if (p->m_map.size() != p->height() * p->width()) {
0241         throw SystemException(QStringLiteral("Invalid board size"));
0242     }
0243 
0244     p->finalize();
0245 
0246     return p;
0247 }
0248 
0249 static Board::State charToState(const QChar &c) {
0250     switch (c.toLatin1()) {
0251     case '-': return Board::Nothing;
0252     case '1': return Board::Box;
0253     default: throw SystemException(QStringLiteral("Invalid char in level definition"));
0254     }
0255 }
0256 
0257 QImage LevelLoader::openXPM(const QDomElement &node) const {
0258     if (node.isNull() || node.tagName() != QLatin1String("xpm")) {
0259         throw SystemException(QStringLiteral("Unexpected row node"));
0260     }
0261 
0262     QFileInfo file(m_filename);
0263     QString filepath = file.absolutePath() + QLatin1Char('/') + node.text();
0264 
0265     QImage xpm(filepath);
0266 
0267     if (xpm.isNull()) {
0268         throw SystemException(QStringLiteral("Could not load %1").arg(filepath));
0269     }
0270 
0271     return xpm;
0272 }
0273 
0274 QList<Board::State> LevelLoader::loadXPM(const QImage &xpm) const {
0275     QList<Board::State> list;
0276     for (int y = 0; y < xpm.height(); y++) {
0277         for (int x = 0; x < xpm.width(); x++) {
0278             QRgb pix = xpm.pixel(x, y);
0279             list.append((pix == 0) ? Board::Nothing : Board::Box);
0280         }
0281     }
0282 
0283     return list;
0284 }
0285 
0286 QList<Board::State> LevelLoader::loadRow(const QDomElement &node) const {
0287     if (node.isNull() || node.tagName() != QLatin1String("row")) {
0288         throw SystemException(QStringLiteral("Unexpected row node"));
0289     }
0290 
0291     const QString text = node.text();
0292     QList<Board::State> list;
0293     for (const QChar &c : text) {
0294         Board::State s = charToState(c);
0295         list.append(s);
0296     }
0297 
0298     return list;
0299 }