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"