File indexing completed on 2024-04-21 04:02:22

0001 /*
0002     SPDX-FileCopyrightText: 2006 Ian Wadham <iandw.au@gmail.com>
0003     SPDX-FileCopyrightText: 2009 Ian Wadham <iandw.au@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "kgrgameio.h"
0009 #include "kgoldrunner_debug.h"
0010 
0011 #include <QDir>
0012 #include <QWidget>
0013 
0014 #include <KLocalizedString>
0015 
0016 KGrGameIO::KGrGameIO (QWidget * pView)
0017     :
0018     view        (pView)
0019 {
0020 }
0021 
0022 IOStatus KGrGameIO::fetchGameListData
0023         (const Owner o, const QString & dir, QList<KGrGameData *> & gameList,
0024                               QString & filePath)
0025 {
0026     QDir directory (dir);
0027     const QStringList pattern {QStringLiteral("game_*")};
0028     QStringList files = directory.entryList (pattern, QDir::Files, QDir::Name);
0029 
0030     // KGr 3 has a game's data and all its levels in one file.
0031     // KGr 2 has all game-data in "games.dat" and each level in a separate file.
0032     bool kgr3Format = (files.count() > 0);
0033     if (! kgr3Format) {
0034         files << QStringLiteral("games.dat");
0035     }
0036 
0037     // Loop to read each file containing game-data.
0038     for (const QString &filename : std::as_const(files)) {
0039         if (filename == QLatin1String("game_ende.txt")) {
0040             continue;           // Skip the "ENDE" file.
0041         }
0042 
0043         filePath = dir + filename;
0044         KGrGameData * g = initGameData (o);
0045         gameList.append (g);
0046         // //qCDebug(KGOLDRUNNER_LOG)<< "GAME PATH:" << filePath;
0047 
0048         openFile.setFileName (filePath);
0049 
0050         // Check that the game-file exists.
0051         if (! openFile.exists()) {
0052             return (NotFound);
0053         }
0054 
0055         // Open the file for read-only.
0056         if (! openFile.open (QIODevice::ReadOnly)) {
0057             return (NoRead);
0058         }
0059 
0060         char c;
0061         QByteArray textLine;
0062         QByteArray gameName;
0063 
0064         // Find the first line of game-data.
0065         c = getALine (kgr3Format, textLine);
0066         if (kgr3Format) {
0067             while ((c != 'G') && (c != '\0')) {
0068                 c = getALine (kgr3Format, textLine);
0069             }
0070         }
0071         if (c == '\0') {
0072             openFile.close();
0073             return (UnexpectedEOF); // We reached end-of-file unexpectedly.
0074         }
0075 
0076         // Loop to extract the game-data for each game on the file.
0077         while (c != '\0') {
0078             if (kgr3Format && (c == 'L')) {
0079                 break;          // End of KGr 3 game-file header.
0080             }
0081             // Decode line 1 of the game-data.
0082             QList<QByteArray> fields = textLine.split (' ');
0083             g->nLevels = fields.at (0).toInt();
0084             g->rules   = fields.at (1).at (0);
0085             g->prefix  = QString::fromLatin1(fields.at (2));
0086             // //qCDebug(KGOLDRUNNER_LOG) << "Levels:" << g->nLevels << "Rules:" << g->rules <<
0087                 // "Prefix:" << g->prefix;
0088 
0089             if (kgr3Format) {
0090                 // KGr 3 Format: get skill, get game-name from a later line.
0091                 g->skill = fields.at (3).at (0);
0092             }
0093             else {
0094                 // KGr 2 Format: get game-name from end of line 1.
0095                 int n = 0;
0096                 // Skip the first 3 fields and extract the rest of the line.
0097                 n = textLine.indexOf (' ', n) + 1;
0098                 n = textLine.indexOf (' ', n) + 1;
0099                 n = textLine.indexOf (' ', n) + 1;
0100                 gameName = removeNewline (textLine.right (textLine.size() - n));
0101                 g->name  = i18n (gameName.constData());
0102             }
0103 
0104             // Check for further settings in this game.
0105             // bool usedDwfOpt = false;     // For debug.
0106             while ((c = getALine (kgr3Format, textLine)) == '.') {
0107                 if (textLine.startsWith ("dwf ")) {
0108                     // Dig while falling is allowed in this game, or not.
0109                     g->digWhileFalling = textLine.endsWith (" false\n") ?
0110                                          false : true;
0111                     // usedDwfOpt = true;       // For debug.
0112                 }
0113             }
0114 
0115             if (kgr3Format && (c == ' ')) {
0116                 gameName = removeNewline (textLine);
0117                 g->name  = i18n (gameName.constData());
0118                 c = getALine (kgr3Format, textLine);
0119             }
0120             //qCDebug(KGOLDRUNNER_LOG) << "Dig while falling" << g->digWhileFalling
0121                      // << "usedDwfOpt" << usedDwfOpt << "Game" << g->name;
0122             //qCDebug(KGOLDRUNNER_LOG) << "Skill:" << g->skill << "Name:" << g->name;
0123 
0124             // Loop to accumulate lines of about-data.  If kgr3Format, exit on
0125             // EOF or 'L' line.  If not kgr3Format, exit on EOF or numeric line.
0126             while (c != '\0') {
0127                 if ((c == '\0') ||
0128                     (kgr3Format && (c == 'L')) ||
0129                     ((! kgr3Format) &&
0130                     (textLine.at (0) >= '0') && (textLine.at (0) <= '9'))) {
0131                     break;
0132                 }
0133                 g->about.append (textLine);
0134                 c = getALine (kgr3Format, textLine);
0135             }
0136             g->about = removeNewline (g->about);    // Remove final '\n'.
0137             // //qCDebug(KGOLDRUNNER_LOG) << "Info about: [" + g->about + "]";
0138 
0139             if ((! kgr3Format) && (c != '\0')) {
0140                 filePath = dir + filename;
0141                 g = initGameData (o);
0142                 gameList.append (g);
0143             }
0144         } // END: game-data loop
0145 
0146         openFile.close();
0147 
0148     } // END: filename loop
0149 
0150     return (OK);
0151 }
0152 
0153 bool KGrGameIO::readLevelData (const QString & dir,
0154                                const QString & prefix,
0155                                const int levelNo, KGrLevelData & d)
0156 {
0157     //qCDebug(KGOLDRUNNER_LOG) << "dir" << dir << "Level" << prefix << levelNo;
0158     QString filePath;
0159     IOStatus stat = fetchLevelData
0160                         (dir, prefix, levelNo, d, filePath);
0161     switch (stat) {
0162     case NotFound:
0163         KGrMessage::information (view, i18n ("Read Level Data"),
0164             i18n ("Cannot find file '%1'.", filePath));
0165         break;
0166     case NoRead:
0167     case NoWrite:
0168         KGrMessage::information (view, i18n ("Read Level Data"),
0169             i18n ("Cannot open file '%1' for read-only.", filePath));
0170         break;
0171     case UnexpectedEOF:
0172         KGrMessage::information (view, i18n ("Read Level Data"),
0173             i18n ("Reached end of file '%1' without finding level data.",
0174             filePath));
0175         break;
0176     case OK:
0177         break;
0178     }
0179 
0180     return (stat == OK);
0181 }
0182 
0183 IOStatus KGrGameIO::fetchLevelData
0184         (const QString & dir, const QString & prefix,
0185                 const int level, KGrLevelData & d, QString & filePath)
0186 {
0187     filePath = getFilePath (dir, prefix, level);
0188     d.level  = level;       // Level number.
0189     d.width  = FIELDWIDTH;  // Default width of layout grid (28 cells).
0190     d.height = FIELDHEIGHT; // Default height of layout grid (20 cells).
0191     d.layout = "";      // Codes for the level layout (mandatory).
0192     d.name   = "";      // Level name (optional).
0193     d.hint   = "";      // Level hint (optional).
0194 
0195     // //qCDebug(KGOLDRUNNER_LOG)<< "LEVEL PATH:" << filePath;
0196     openFile.setFileName (filePath);
0197 
0198     // Check that the level-file exists.
0199     if (! openFile.exists()) {
0200         return (NotFound);
0201     }
0202 
0203     // Open the file for read-only.
0204     if (! openFile.open (QIODevice::ReadOnly)) {
0205         return (NoRead);
0206     }
0207 
0208     char c;
0209     QByteArray textLine;
0210     IOStatus result = UnexpectedEOF;
0211 
0212     // Determine whether the file is in KGoldrunner v3 or v2 format.
0213     bool kgr3Format = (filePath.endsWith (QLatin1String(".txt")));
0214 
0215     if (kgr3Format) {
0216         // In KGr 3 format, if a line starts with 'L', check the number.
0217         while ((c = getALine (kgr3Format, textLine)) != '\0') {
0218             if ((c == 'L') && (textLine.left (3).toInt() == level)) {
0219                 break;          // We have found the required level.
0220             }
0221         }
0222         if (c == '\0') {
0223             openFile.close();       // We reached end-of-file.
0224             return (UnexpectedEOF);
0225         }
0226     }  
0227 
0228     // Check for further settings in this level.
0229     while ((c = getALine (kgr3Format, textLine)) == '.') {
0230         if (textLine.startsWith ("dwf ")) {
0231             // Dig while falling is allowed in this level, or not.
0232             d.digWhileFalling = textLine.endsWith (" false\n") ? false : true;
0233         }
0234     }
0235 
0236     // Get the character-codes for the level layout.
0237     if (c  == ' ') {
0238         result = OK;
0239         d.layout = removeNewline (textLine);        // Remove '\n'.
0240 
0241         // Look for a line containing a level name (optional).
0242         if ((c = getALine (kgr3Format, textLine)) == ' ') {
0243             d.name = removeNewline (textLine);      // Remove '\n'.
0244 
0245             // Look for one or more lines containing a hint (optional).
0246             while ((c = getALine (kgr3Format, textLine)) == ' ') {
0247                 d.hint.append (textLine);
0248             }
0249             d.hint = removeNewline (d.hint);        // Remove final '\n'.
0250         }
0251     }
0252 
0253     // //qCDebug(KGOLDRUNNER_LOG) << "Level:" << level << "Layout length:" << d.layout.size();
0254     // //qCDebug(KGOLDRUNNER_LOG) << "Name:" << "[" + d.name + "]";
0255     // //qCDebug(KGOLDRUNNER_LOG) << "Hint:" << "[" + d.hint + "]";
0256 
0257     openFile.close();
0258     return (result);
0259 }
0260 
0261 QString KGrGameIO::getFilePath
0262         (const QString & dir, const QString & prefix, const int level)
0263 {
0264     QString filePath = ((level == 0) ? QStringLiteral("ende") : prefix);
0265     filePath = dir + QLatin1String("game_") + filePath + QLatin1String(".txt");
0266     QFile test (filePath);
0267 
0268     // See if there is a game-file or "ENDE" screen in KGoldrunner 3 format.
0269     if (test.exists()) {
0270         return (filePath);
0271     }
0272 
0273     // If not, we are looking for a file in KGoldrunner 2 format.
0274     if (level == 0) {
0275         // End of game: show the "ENDE" screen.
0276         filePath = dir + QStringLiteral("levels/level000.grl");
0277     }
0278     else {
0279         QString num = QString::number (level).rightJustified (3, QLatin1Char('0'));
0280         filePath = dir + QLatin1String("levels/") + prefix + num + QLatin1String(".grl");
0281     }
0282 
0283     return (filePath);
0284 }
0285 
0286 char KGrGameIO::getALine (const bool kgr3, QByteArray & line)
0287 {
0288     char c;
0289     line = "";
0290     while (openFile.getChar (&c)) {
0291         line.append (c);
0292         if (c == '\n') {
0293             break;
0294         }
0295     }
0296 
0297     // //qCDebug(KGOLDRUNNER_LOG) << "Raw line:" << line;
0298     if (line.size() <= 0) {
0299         // Return a '\0' byte if end-of-file.
0300         return ('\0');
0301     }
0302     if (kgr3) {
0303         // In KGr 3 format, strip off leading and trailing syntax.
0304         if (line.startsWith ("// ")) {
0305             line = line.right (line.size() - 3);
0306             // //qCDebug(KGOLDRUNNER_LOG) << "Stripped comment is:" << line;
0307         }
0308         else {
0309             if (line.startsWith (" i18n(\"")) {
0310                 line = ' ' + line.right (line.size() - 7);
0311             }
0312             else if (line.startsWith (" NOTi18n(\"")) {
0313                 line = ' ' + line.right (line.size() - 10);
0314             }
0315             else if (line.startsWith (" \"")) {
0316                 line = ' ' + line.right (line.size() - 2);
0317             }
0318             if (line.endsWith ("\");\n")) {
0319                 line = line.left (line.size() - 4) + '\n';
0320             }
0321             else if (line.endsWith ("\\n\"\n")) {
0322                 line = line.left (line.size() - 4) + '\n';
0323             }
0324             else if (line.endsWith ("\"\n")) {
0325                 line = line.left (line.size() - 2);
0326             }
0327             // //qCDebug(KGOLDRUNNER_LOG) << "Stripped syntax is:" << line;
0328         }
0329         // In Kgr 3 format, return the first byte if not end-of-file.
0330         c = line.at (0);
0331         line = line.right (line.size() - 1);
0332     }
0333     else {
0334         // In KGr 2 format, return a space if not end-of-file.
0335         c = ' ';
0336         if (line.startsWith (".")) {    // Line to set an option.
0337             c = line.at (0);
0338             line = line.right (line.size() - 1);
0339         }
0340     }
0341     return (c);
0342 }
0343 
0344 QByteArray KGrGameIO::removeNewline (const QByteArray & line)
0345 {
0346     int len = line.size();
0347     if ((len > 0) && (line.endsWith ('\n'))) {
0348         return (line.left (len -1));
0349     }
0350     else {
0351         return (line);
0352     }
0353 }
0354 
0355 KGrGameData * KGrGameIO::initGameData (Owner o)
0356 {
0357     KGrGameData * g = new KGrGameData;
0358     g->owner    = o;    // Owner of the game: "System" or "User".
0359     g->nLevels  = 0;    // Number of levels in the game.
0360     g->rules    = 'T';  // Game's rules: KGoldrunner or Traditional.
0361     g->digWhileFalling = true;  // The default: allow "dig while falling".
0362     g->prefix   = QString();    // Game's filename prefix.
0363     g->skill    = 'N';  // Game's skill: Tutorial, Normal or Champion.
0364     g->width    = FIELDWIDTH;   // Default width of layout grid (28 cells).
0365     g->height   = FIELDHEIGHT;  // Default height of layout grid (20 cells).
0366     g->name     = QString();    // Name of the game (translated, if System game).
0367     g->about    = "";   // Optional text about the game (untranslated).
0368     return (g);
0369 }
0370 
0371 bool KGrGameIO::safeRename (QWidget * theView, const QString & oldName,
0372                             const QString & newName)
0373 {
0374     QFile newFile (newName);
0375     if (newFile.exists()) {
0376         // On some file systems we cannot rename if a file with the new name
0377         // already exists.  We must delete the existing file, otherwise the
0378         // upcoming QFile::rename will fail, according to Qt4 docs.  This
0379         // seems to be true with reiserfs at least.
0380         if (! newFile.remove()) {
0381             KGrMessage::information (theView, i18n ("Rename File"),
0382                 i18n ("Cannot delete previous version of file '%1'.", newName));
0383             return false;
0384         }
0385     }
0386     QFile oldFile (oldName);
0387     if (! oldFile.rename (newName)) {
0388         KGrMessage::information (theView, i18n ("Rename File"),
0389             i18n ("Cannot rename file '%1' to '%2'.", oldName, newName));
0390         return false;
0391     }
0392     return true;
0393 }
0394 
0395 #include "moc_kgrgameio.cpp"