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