File indexing completed on 2024-05-05 08:05:35

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