File indexing completed on 2024-10-13 03:43:40
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"