1 /*
2     SPDX-FileCopyrightText: 2006 Ian Wadham <iandw.au@gmail.com>
3     SPDX-FileCopyrightText: 2009 Ian Wadham <iandw.au@gmail.com>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "kgrgameio.h"
9 #include "kgoldrunner_debug.h"
10 
11 #include <QDir>
12 #include <QWidget>
13 
14 #include <KLocalizedString>
15 
KGrGameIO(QWidget * pView)16 KGrGameIO::KGrGameIO (QWidget * pView)
17     :
18     view        (pView)
19 {
20 }
21 
fetchGameListData(const Owner o,const QString & dir,QList<KGrGameData * > & gameList,QString & filePath)22 IOStatus KGrGameIO::fetchGameListData
23         (const Owner o, const QString & dir, QList<KGrGameData *> & gameList,
24                               QString & filePath)
25 {
26     QDir directory (dir);
27     QStringList pattern;
28     pattern << QStringLiteral("game_*");
29     QStringList files = directory.entryList (pattern, QDir::Files, QDir::Name);
30 
31     // KGr 3 has a game's data and all its levels in one file.
32     // KGr 2 has all game-data in "games.dat" and each level in a separate file.
33     bool kgr3Format = (files.count() > 0);
34     if (! kgr3Format) {
35         files << QStringLiteral("games.dat");
36     }
37 
38     // Loop to read each file containing game-data.
39     for (const QString &filename : std::as_const(files)) {
40         if (filename == QLatin1String("game_ende.txt")) {
41             continue;			// Skip the "ENDE" file.
42         }
43 
44         filePath = dir + filename;
45         KGrGameData * g = initGameData (o);
46         gameList.append (g);
47         // //qCDebug(KGOLDRUNNER_LOG)<< "GAME PATH:" << filePath;
48 
49         openFile.setFileName (filePath);
50 
51         // Check that the game-file exists.
52         if (! openFile.exists()) {
53             return (NotFound);
54         }
55 
56         // Open the file for read-only.
57         if (! openFile.open (QIODevice::ReadOnly)) {
58             return (NoRead);
59         }
60 
61         char c;
62         QByteArray textLine;
63         QByteArray gameName;
64 
65         // Find the first line of game-data.
66         c = getALine (kgr3Format, textLine);
67         if (kgr3Format) {
68             while ((c != 'G') && (c != '\0')) {
69                 c = getALine (kgr3Format, textLine);
70             }
71         }
72         if (c == '\0') {
73             openFile.close();
74             return (UnexpectedEOF);	// We reached end-of-file unexpectedly.
75         }
76 
77         // Loop to extract the game-data for each game on the file.
78         while (c != '\0') {
79             if (kgr3Format && (c == 'L')) {
80                 break;			// End of KGr 3 game-file header.
81             }
82             // Decode line 1 of the game-data.
83             QList<QByteArray> fields = textLine.split (' ');
84             g->nLevels = fields.at (0).toInt();
85             g->rules   = fields.at (1).at (0);
86             g->prefix  = QString::fromLatin1(fields.at (2));
87             // //qCDebug(KGOLDRUNNER_LOG) << "Levels:" << g->nLevels << "Rules:" << g->rules <<
88                 // "Prefix:" << g->prefix;
89 
90             if (kgr3Format) {
91                 // KGr 3 Format: get skill, get game-name from a later line.
92                 g->skill = fields.at (3).at (0);
93             }
94             else {
95                 // KGr 2 Format: get game-name from end of line 1.
96                 int n = 0;
97                 // Skip the first 3 fields and extract the rest of the line.
98                 n = textLine.indexOf (' ', n) + 1;
99                 n = textLine.indexOf (' ', n) + 1;
100                 n = textLine.indexOf (' ', n) + 1;
101                 gameName = removeNewline (textLine.right (textLine.size() - n));
102                 g->name  = i18n (gameName.constData());
103             }
104 
105             // Check for further settings in this game.
106             // bool usedDwfOpt = false;		// For debug.
107             while ((c = getALine (kgr3Format, textLine)) == '.') {
108                 if (textLine.startsWith ("dwf ")) {
109                     // Dig while falling is allowed in this game, or not.
110                     g->digWhileFalling = textLine.endsWith (" false\n") ?
111                                          false : true;
112                     // usedDwfOpt = true;		// For debug.
113                 }
114             }
115 
116             if (kgr3Format && (c == ' ')) {
117                 gameName = removeNewline (textLine);
118                 g->name  = i18n (gameName.constData());
119                 c = getALine (kgr3Format, textLine);
120             }
121             //qCDebug(KGOLDRUNNER_LOG) << "Dig while falling" << g->digWhileFalling
122                      // << "usedDwfOpt" << usedDwfOpt << "Game" << g->name;
123             //qCDebug(KGOLDRUNNER_LOG) << "Skill:" << g->skill << "Name:" << g->name;
124 
125             // Loop to accumulate lines of about-data.  If kgr3Format, exit on
126             // EOF or 'L' line.  If not kgr3Format, exit on EOF or numeric line.
127             while (c != '\0') {
128                 if ((c == '\0') ||
129                     (kgr3Format && (c == 'L')) ||
130                     ((! kgr3Format) &&
131                     (textLine.at (0) >= '0') && (textLine.at (0) <= '9'))) {
132                     break;
133                 }
134                 g->about.append (textLine);
135                 c = getALine (kgr3Format, textLine);
136             }
137             g->about = removeNewline (g->about);	// Remove final '\n'.
138             // //qCDebug(KGOLDRUNNER_LOG) << "Info about: [" + g->about + "]";
139 
140             if ((! kgr3Format) && (c != '\0')) {
141                 filePath = dir + filename;
142                 g = initGameData (o);
143                 gameList.append (g);
144             }
145         } // END: game-data loop
146 
147         openFile.close();
148 
149     } // END: filename loop
150 
151     return (OK);
152 }
153 
readLevelData(const QString & dir,const QString & prefix,const int levelNo,KGrLevelData & d)154 bool KGrGameIO::readLevelData (const QString & dir,
155                                const QString & prefix,
156                                const int levelNo, KGrLevelData & d)
157 {
158     //qCDebug(KGOLDRUNNER_LOG) << "dir" << dir << "Level" << prefix << levelNo;
159     QString filePath;
160     IOStatus stat = fetchLevelData
161                         (dir, prefix, levelNo, d, filePath);
162     switch (stat) {
163     case NotFound:
164         KGrMessage::information (view, i18n ("Read Level Data"),
165             i18n ("Cannot find file '%1'.", filePath));
166         break;
167     case NoRead:
168     case NoWrite:
169         KGrMessage::information (view, i18n ("Read Level Data"),
170             i18n ("Cannot open file '%1' for read-only.", filePath));
171         break;
172     case UnexpectedEOF:
173         KGrMessage::information (view, i18n ("Read Level Data"),
174             i18n ("Reached end of file '%1' without finding level data.",
175             filePath));
176         break;
177     case OK:
178         break;
179     }
180 
181     return (stat == OK);
182 }
183 
fetchLevelData(const QString & dir,const QString & prefix,const int level,KGrLevelData & d,QString & filePath)184 IOStatus KGrGameIO::fetchLevelData
185         (const QString & dir, const QString & prefix,
186                 const int level, KGrLevelData & d, QString & filePath)
187 {
188     filePath = getFilePath (dir, prefix, level);
189     d.level  = level;		// Level number.
190     d.width  = FIELDWIDTH;	// Default width of layout grid (28 cells).
191     d.height = FIELDHEIGHT;	// Default height of layout grid (20 cells).
192     d.layout = "";		// Codes for the level layout (mandatory).
193     d.name   = "";		// Level name (optional).
194     d.hint   = "";		// Level hint (optional).
195 
196     // //qCDebug(KGOLDRUNNER_LOG)<< "LEVEL PATH:" << filePath;
197     openFile.setFileName (filePath);
198 
199     // Check that the level-file exists.
200     if (! openFile.exists()) {
201         return (NotFound);
202     }
203 
204     // Open the file for read-only.
205     if (! openFile.open (QIODevice::ReadOnly)) {
206         return (NoRead);
207     }
208 
209     char c;
210     QByteArray textLine;
211     IOStatus result = UnexpectedEOF;
212 
213     // Determine whether the file is in KGoldrunner v3 or v2 format.
214     bool kgr3Format = (filePath.endsWith (QLatin1String(".txt")));
215 
216     if (kgr3Format) {
217         // In KGr 3 format, if a line starts with 'L', check the number.
218         while ((c = getALine (kgr3Format, textLine)) != '\0') {
219             if ((c == 'L') && (textLine.left (3).toInt() == level)) {
220                 break;			// We have found the required level.
221             }
222         }
223         if (c == '\0') {
224             openFile.close();		// We reached end-of-file.
225             return (UnexpectedEOF);
226         }
227     }
228 
229     // Check for further settings in this level.
230     while ((c = getALine (kgr3Format, textLine)) == '.') {
231         if (textLine.startsWith ("dwf ")) {
232             // Dig while falling is allowed in this level, or not.
233             d.digWhileFalling = textLine.endsWith (" false\n") ? false : true;
234         }
235     }
236 
237     // Get the character-codes for the level layout.
238     if (c  == ' ') {
239         result = OK;
240         d.layout = removeNewline (textLine);		// Remove '\n'.
241 
242         // Look for a line containing a level name (optional).
243         if ((c = getALine (kgr3Format, textLine)) == ' ') {
244             d.name = removeNewline (textLine);		// Remove '\n'.
245 
246             // Look for one or more lines containing a hint (optional).
247             while ((c = getALine (kgr3Format, textLine)) == ' ') {
248                 d.hint.append (textLine);
249             }
250             d.hint = removeNewline (d.hint);		// Remove final '\n'.
251         }
252     }
253 
254     // //qCDebug(KGOLDRUNNER_LOG) << "Level:" << level << "Layout length:" << d.layout.size();
255     // //qCDebug(KGOLDRUNNER_LOG) << "Name:" << "[" + d.name + "]";
256     // //qCDebug(KGOLDRUNNER_LOG) << "Hint:" << "[" + d.hint + "]";
257 
258     openFile.close();
259     return (result);
260 }
261 
getFilePath(const QString & dir,const QString & prefix,const int level)262 QString KGrGameIO::getFilePath
263         (const QString & dir, const QString & prefix, const int level)
264 {
265     QString filePath = ((level == 0) ? QStringLiteral("ende") : prefix);
266     filePath = dir + QLatin1String("game_") + filePath + QLatin1String(".txt");
267     QFile test (filePath);
268 
269     // See if there is a game-file or "ENDE" screen in KGoldrunner 3 format.
270     if (test.exists()) {
271         return (filePath);
272     }
273 
274     // If not, we are looking for a file in KGoldrunner 2 format.
275     if (level == 0) {
276         // End of game: show the "ENDE" screen.
277         filePath = dir + QStringLiteral("levels/level000.grl");
278     }
279     else {
280         QString num = QString::number (level).rightJustified (3, QLatin1Char('0'));
281         filePath = dir + QLatin1String("levels/") + prefix + num + QLatin1String(".grl");
282     }
283 
284     return (filePath);
285 }
286 
getALine(const bool kgr3,QByteArray & line)287 char KGrGameIO::getALine (const bool kgr3, QByteArray & line)
288 {
289     char c;
290     line = "";
291     while (openFile.getChar (&c)) {
292         line.append (c);
293         if (c == '\n') {
294             break;
295         }
296     }
297 
298     // //qCDebug(KGOLDRUNNER_LOG) << "Raw line:" << line;
299     if (line.size() <= 0) {
300         // Return a '\0' byte if end-of-file.
301         return ('\0');
302     }
303     if (kgr3) {
304         // In KGr 3 format, strip off leading and trailing syntax.
305         if (line.startsWith ("// ")) {
306             line = line.right (line.size() - 3);
307             // //qCDebug(KGOLDRUNNER_LOG) << "Stripped comment is:" << line;
308         }
309         else {
310             if (line.startsWith (" i18n(\"")) {
311                 line = ' ' + line.right (line.size() - 7);
312             }
313             else if (line.startsWith (" NOTi18n(\"")) {
314                 line = ' ' + line.right (line.size() - 10);
315             }
316             else if (line.startsWith (" \"")) {
317                 line = ' ' + line.right (line.size() - 2);
318             }
319             if (line.endsWith ("\");\n")) {
320                 line = line.left (line.size() - 4) + '\n';
321             }
322             else if (line.endsWith ("\\n\"\n")) {
323                 line = line.left (line.size() - 4) + '\n';
324             }
325             else if (line.endsWith ("\"\n")) {
326                 line = line.left (line.size() - 2);
327             }
328             // //qCDebug(KGOLDRUNNER_LOG) << "Stripped syntax is:" << line;
329         }
330         // In Kgr 3 format, return the first byte if not end-of-file.
331         c = line.at (0);
332         line = line.right (line.size() - 1);
333     }
334     else {
335         // In KGr 2 format, return a space if not end-of-file.
336         c = ' ';
337         if (line.startsWith (".")) {	// Line to set an option.
338             c = line.at (0);
339             line = line.right (line.size() - 1);
340         }
341     }
342     return (c);
343 }
344 
removeNewline(const QByteArray & line)345 QByteArray KGrGameIO::removeNewline (const QByteArray & line)
346 {
347     int len = line.size();
348     if ((len > 0) && (line.endsWith ('\n'))) {
349         return (line.left (len -1));
350     }
351     else {
352         return (line);
353     }
354 }
355 
initGameData(Owner o)356 KGrGameData * KGrGameIO::initGameData (Owner o)
357 {
358     KGrGameData * g = new KGrGameData;
359     g->owner    = o;	// Owner of the game: "System" or "User".
360     g->nLevels  = 0;	// Number of levels in the game.
361     g->rules    = 'T';	// Game's rules: KGoldrunner or Traditional.
362     g->digWhileFalling = true;	// The default: allow "dig while falling".
363     g->prefix   = QString();	// Game's filename prefix.
364     g->skill    = 'N';	// Game's skill: Tutorial, Normal or Champion.
365     g->width    = FIELDWIDTH;	// Default width of layout grid (28 cells).
366     g->height   = FIELDHEIGHT;	// Default height of layout grid (20 cells).
367     g->name     = QString();	// Name of the game (translated, if System game).
368     g->about    = "";	// Optional text about the game (untranslated).
369     return (g);
370 }
371 
safeRename(QWidget * theView,const QString & oldName,const QString & newName)372 bool KGrGameIO::safeRename (QWidget * theView, const QString & oldName,
373                             const QString & newName)
374 {
375     QFile newFile (newName);
376     if (newFile.exists()) {
377         // On some file systems we cannot rename if a file with the new name
378         // already exists.  We must delete the existing file, otherwise the
379         // upcoming QFile::rename will fail, according to Qt4 docs.  This
380         // seems to be true with reiserfs at least.
381         if (! newFile.remove()) {
382             KGrMessage::information (theView, i18n ("Rename File"),
383                 i18n ("Cannot delete previous version of file '%1'.", newName));
384             return false;
385         }
386     }
387     QFile oldFile (oldName);
388     if (! oldFile.rename (newName)) {
389         KGrMessage::information (theView, i18n ("Rename File"),
390             i18n ("Cannot rename file '%1' to '%2'.", oldName, newName));
391         return false;
392     }
393     return true;
394 }
395 
396 
397