1 //
2 // Copyright (C) 2016 Andrei Bondor, ab396356@users.sourceforge.net
3 //
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License
6 // as published by the Free Software Foundation; either version 2
7 // of the License, or (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program; if not, write to the Free Software
16 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 //
18 
19 #pragma once
20 
21 #include <algorithm>
22 #include <functional>
23 #include <iomanip>
24 #include <istream>
25 #include <limits>
26 #include <ostream>
27 #include <regex>
28 #include <sstream>
29 #include <string>
30 #include <unordered_map>
31 #include <unordered_set>
32 #include <vector>
33 #include <physfs.h>
34 
35 #include "physfs_utils.h"
36 
37 #define GETLINE_SKIP_EMPTY_LINES(InputStream, String)   if (true) { \
38     while (std::getline(InputStream, String)) {                     \
39         if (!String.empty())                                        \
40             break;                                                  \
41     }                                                               \
42     if (String.empty())                                             \
43         return InputStream;                                         \
44 } else (void)0
45 
46 
47 // TODO: remove duplicate code
48 #define GETLINE_SKIP_EMPTY_LINES_B(InputStream, String) if (true) { \
49     while (std::getline(InputStream, String)) {                     \
50         if (!String.empty())                                        \
51             break;                                                  \
52     }                                                               \
53     if (String.empty())                                             \
54         return static_cast<bool> (InputStream);                     \
55 } else (void)0
56 
57 ///
58 /// @brief Basic structure to load and save race results.
59 ///
60 struct RaceData
61 {
62     std::string playername;     ///< e.g. "Andrei"
63     std::string mapname;        ///< e.g. "/maps/jumpy/jumpy.level"
64     std::string carname;        ///< e.g. "FMC Fox"
65     std::string carclass;       ///< e.g. "Super500"
66     float totaltime;            ///< e.g. "102.2"
67     float maxspeed;             ///< e.g. "212.0"
68 
69     RaceData() = default;
70 
71     ///
72     /// @note This exists because the player's name isn't read from file.
73     ///
RaceDataRaceData74     explicit RaceData(const std::string &playername):
75         playername(playername)
76     {
77     }
78 
RaceDataRaceData79     RaceData(
80         const std::string &playername,
81         const std::string &mapname,
82         const std::string &carname,
83         const std::string &carclass,
84         float totaltime,
85         float maxspeed
86         ):
87         playername(playername),
88         mapname(mapname),
89         carname(carname),
90         carclass(carclass),
91         totaltime(totaltime),
92         maxspeed(maxspeed)
93     {
94     }
95 };
96 
97 ///
98 /// @brief Reads a RaceData object from an input stream.
99 /// @note The player name is intentionally omitted.
100 /// @todo Each call to `std::getline()` should be checked for success.
101 /// @param [in,out] is      Input stream.
102 /// @param [out] rd         Race data to be read.
103 /// @returns The input stream.
104 ///
105 inline std::istream & operator >> (std::istream &is, RaceData &rd)
106 {
107     std::string ts; // Temporary String
108 
109     GETLINE_SKIP_EMPTY_LINES(is, rd.mapname);
110     GETLINE_SKIP_EMPTY_LINES(is, rd.carname);
111     GETLINE_SKIP_EMPTY_LINES(is, rd.carclass);
112     GETLINE_SKIP_EMPTY_LINES(is, ts);
113     rd.totaltime = std::stof(ts);
114     GETLINE_SKIP_EMPTY_LINES(is, ts);
115     rd.maxspeed = std::stof(ts);
116     return is;
117 }
118 
119 ///
120 /// @brief Writes a RaceData object to an output stream.
121 /// @todo Each call to `std::operator<<()` should be checked for success.
122 /// @todo Decide if should call `std::ostream::flush()`.
123 /// @param [in,out] os      Output stream.
124 /// @param [in] rd          Race data to be written.
125 /// @returns The output stream.
126 ///
127 inline std::ostream & operator << (std::ostream &os, const RaceData &rd)
128 {
129     os << rd.mapname << '\n';
130     os << rd.carname << '\n';
131     os << rd.carclass << '\n';
132     os << rd.totaltime << '\n';
133     os << rd.maxspeed << "\n\n";
134     return os;
135 }
136 
137 enum class HISCORE1_SORT
138 {
139     BY_TOTALTIME_ASC,
140     BY_TOTALTIME_DESC,
141     BY_MAXSPEED_ASC,
142     BY_MAXSPEED_DESC,
143     BY_PLAYERNAME_ASC,
144     BY_PLAYERNAME_DESC,
145     BY_CARNAME_ASC,
146     BY_CARNAME_DESC,
147     BY_CARCLASS_ASC,
148     BY_CARCLASS_DESC
149 };
150 
151 using UnlockData = std::unordered_set<std::string>;
152 
153 ///
154 /// @brief Used to display the best times information.
155 ///
156 struct TimeEntry
157 {
158     unsigned long int place = 0;    ///< True place, depending on time.
159     RaceData rd;                    ///< Race data.
160     bool highlighted = false;       ///< Highlight flag.
161 
TimeEntryTimeEntry162     TimeEntry(
163         unsigned long int place,
164         const RaceData &rd,
165         bool highlighted
166         ):
167         place(place),
168         rd(rd),
169         highlighted(highlighted)
170     {
171     }
172 };
173 
174 ///
175 /// @brief Loads and saves the player's best times.
176 /// @details As can be seen from the name, this isn't supposed to a
177 ///  final version. Also it doesn't concern itself as much with "score"
178 ///  as it does with best times.
179 ///
180 class HiScore1
181 {
182 public:
183 
184     HiScore1() = delete;
185 
186     ///
187     /// @brief Constructs a high score object.
188     /// @param [in] searchdir       Directory where to scan for score data (.PLAYER files).
189     /// @param [in] playername      Name of the player such that the correct .PLAYER file is updated.
190     /// @todo Don't use magic numbers for preemptive storage reservation.
191     ///
192     explicit HiScore1(const std::string &searchdir, const std::string &playername = "Player"):
searchdir(searchdir)193         searchdir(searchdir),
194         playername(playername)
195     {
196         currenttimes.reserve(16);
197     }
198 
199     ///
200     /// @brief Writes to the selected player's file.
201     ///
~HiScore1()202     ~HiScore1()
203     {
204         writePlayerData(playername);
205     }
206 
207     ///
208     /// @brief Sets the `playername`.
209     /// @param [in] pname           New player name to use.
210     ///
setPlayerName(const std::string & pname)211     void setPlayerName(const std::string &pname)
212     {
213         playername = pname;
214     }
215 
216     ///
217     /// @brief Loads all times from .PLAYER files.
218     ///
loadAllTimes()219     void loadAllTimes()
220     {
221         if (PHYSFS_isInit() == 0)
222             return;
223 
224         char **rc = PHYSFS_enumerateFiles(searchdir.c_str());
225 
226         for (char **fname = rc; *fname != nullptr; ++fname)
227         {
228             // remove the extension from the filename
229             std::smatch mr; // Match Results
230             std::regex pat(R"(^([\s\w]+)(\.player)$)"); // Pattern
231             std::string fn(*fname); // Filename
232 
233             if (!std::regex_search(fn, mr, pat))
234                 continue;
235 
236             std::string pname = mr[1]; // Player Name
237             PHYSFS_File *pfile = PHYSFS_openRead((searchdir + '/' + *fname).c_str()); // Player File
238             std::string pdata(PHYSFS_fileLength(pfile), '\0'); // Player Data
239 
240             physfs_read(pfile, &pdata.front(), sizeof(char), pdata.size());
241             readPlayerData(pname, pdata);
242             PHYSFS_close(pfile);
243         }
244 
245         PHYSFS_freeList(rc);
246     }
247 
248     ///
249     /// @brief Adds new race data.
250     /// @param [in] rd      Race data.
251     ///
addNewTime(const RaceData & rd)252     void addNewTime(const RaceData &rd)
253     {
254         alltimes.insert({rd.mapname, rd});
255     }
256 
257     ///
258     /// @brief Sets how many saves to skip.
259     /// @param sk       New value for skipped saves.
260     ///
setSkipSaves(unsigned long int sk)261     void setSkipSaves(unsigned long int sk)
262     {
263         skipSaves = sk;
264         sc = 0; // reset Skip Counter
265     }
266 
267     ///
268     /// @brief Saves the player's race data.
269     ///
savePlayer()270     void savePlayer() const
271     {
272         writePlayerData(playername);
273     }
274 
275     ///
276     /// @brief Saves the player's race data with possible skipping.
277     /// @details The purpose of this function is to cut down on the expensive
278     ///  file output operations by skipping calls to `writePlayerData()`.
279     ///
skipSavePlayer()280     void skipSavePlayer() const
281     {
282         if (skipSaves <= -1) // save only by destructor
283             return;
284 
285         if (sc++ == skipSaves)
286         {
287             writePlayerData(playername);
288             sc = 0;
289         }
290     }
291 
292     ///
293     /// @brief Adds new unlock data for the current player.
294     /// @param [in] udata       Unlock data.
295     ///
addNewUnlock(const std::string & udata)296     void addNewUnlock(const std::string &udata)
297     {
298         addNewUnlock(playername, udata);
299     }
300 
301     ///
302     /// @brief Adds new unlock data for the player.
303     /// @param [in] pname       Player name.
304     /// @param [in] udata       Unlock data.
305     ///
addNewUnlock(const std::string & pname,const std::string & udata)306     void addNewUnlock(const std::string &pname, const std::string &udata)
307     {
308         allunlocks[pname].insert(udata);
309     }
310 
311     ///
312     /// @brief Retrieves unlock data for the current player.
313     /// @returns Unlock data.
314     ///
getUnlockData()315     UnlockData getUnlockData() const
316     {
317         return getUnlockData(playername);
318     }
319 
320     ///
321     /// @brief Retrieves unlock data for a player.
322     /// @param [in] pname       Player name.
323     /// @returns Unlock data.
324     ///
getUnlockData(const std::string & pname)325     UnlockData getUnlockData(const std::string &pname) const
326     {
327         if (allunlocks.count(pname) == 0)
328             return UnlockData {};
329 
330         return allunlocks.at(pname);
331     }
332 
333     ///
334     /// @brief Retrieves the best time for `mapname`, if available.
335     /// @param [in] mapname         Map for which to get the best time.
336     /// @returns The best time.
337     /// @retval -1.0f               If no best time is available.
338     /// @note Check for above like `(x < 0)` instead of `(x == -1)`.
339     /// @todo Use `auto` parameters for lambda after C++17.
340     ///
getBestTime(const std::string & mapname)341     float getBestTime(const std::string &mapname)
342     {
343         if (alltimes.count(mapname) == 0)
344             return -1.0f;
345 
346         const auto range = alltimes.equal_range(mapname);
347 
348         const auto rdi = std::min_element(range.first, range.second,
349             [](decltype (*range.first) a, decltype (*range.first) b) -> bool
350             {
351                 return a.second.totaltime < b.second.totaltime;
352             });
353 
354         return rdi->second.totaltime;
355     }
356 
357     ///
358     /// @brief Retrieves the best class time for `mapname`, if available.
359     /// @param [in] mapname         Map for which to get the best class time.
360     /// @param [in] carclass        Car class for which to retrieve the time.
361     /// @returns The best class time.
362     /// @retval -1.0f               If no best class time is available.
363     /// @note Check for above like `(x < 0)` instead of `(x == -1)`.
364     ///
getBestClassTime(const std::string & mapname,const std::string & carclass)365     float getBestClassTime(const std::string &mapname, const std::string &carclass)
366     {
367         if (alltimes.count(mapname) == 0)
368             return -1.0f;
369 
370         const auto range = alltimes.equal_range(mapname);
371 
372         bool found_a_time = false;
373         float bct; // Best Class Time
374 
375         if (std::numeric_limits<float>::has_infinity) // "usually true"
376             bct = std::numeric_limits<float>::infinity();
377         else // maximum is good enough
378             bct = std::numeric_limits<float>::max();
379 
380         for (auto i = range.first; i != range.second; ++i)
381             if (i->second.carclass == carclass)
382             {
383                 bct = std::min(bct, i->second.totaltime);
384                 found_a_time = true;
385             }
386 
387         if (!found_a_time)
388             return -1.0f;
389 
390         return bct;
391     }
392 
393     ///
394     /// @brief Retrieves best times list for `mapname`, sorted by `sortmethod`.
395     /// @param [in] mapname         Map for which to get the times.
396     /// @param sortmethod           How to sort the times.
397     /// @see `HISCORE1_SORT` enum.
398     /// @note For `mapname == ""` the current results list will be re-sorted.
399     /// @returns Sorted list of results.
400     ///
getCurrentTimes(const std::string & mapname,HISCORE1_SORT sortmethod)401     const std::vector<TimeEntry> & getCurrentTimes(const std::string &mapname, HISCORE1_SORT sortmethod)
402     {
403         if (!mapname.empty())
404         {
405             const auto range = alltimes.equal_range(mapname);
406 
407             currenttimes.clear();
408 
409             for (auto i = range.first; i != range.second; ++i)
410                 currenttimes.push_back({0, i->second, false});
411 
412             sortAndUpdatePlaces();
413         }
414 
415         sortCurrentTimes(sortmethod);
416         return currenttimes;
417     }
418 
419     ///
420     /// @brief Sorts and retrieves current highlighted times.
421     /// @param sortmethod           How to sort the times.
422     /// @see `HISCORE1_SORT` enum.
423     /// @returns Sorted list of highlighted results.
424     ///
getCurrentTimesHL(HISCORE1_SORT sortmethod)425     const std::vector<TimeEntry> & getCurrentTimesHL(HISCORE1_SORT sortmethod)
426     {
427         sortCurrentTimes(sortmethod);
428         return currenttimes;
429     }
430 
431     ///
432     /// @brief Inserts race data and retrieves the updated highlighted times.
433     /// @remarks Sorting method is essentially `HISCORE1_SORT::BY_TOTALTIME_ASC`.
434     /// @remarks Target map is deduced from `rd.mapname`.
435     /// @param [in] rd              Race data to be inserted and highlighted.
436     /// @returns Sorted list of highlighted results.
437     ///
insertAndGetCurrentTimesHL(const RaceData & rd)438     const std::vector<TimeEntry> & insertAndGetCurrentTimesHL(const RaceData &rd)
439     {
440         // get old times before inserting newest one
441         const auto range = alltimes.equal_range(rd.mapname);
442 
443         currenttimes.clear();
444 
445         for (auto i = range.first; i != range.second; ++i)
446             currenttimes.push_back({0, i->second, false});
447 
448         currenttimes.push_back({0, rd, true}); // the newest, highlighted time
449         sortAndUpdatePlaces();
450         alltimes.insert({rd.mapname, rd});
451         return currenttimes;
452     }
453 
454 #ifndef NDEBUG
455 
456     ///
457     /// @brief Debug printing of current times.
458     /// @param [in,out] os      Output stream to print to.
459     ///
printCurrentTimes(std::ostream & os)460     void printCurrentTimes(std::ostream &os) const
461     {
462         for (const TimeEntry &te: currenttimes)
463         {
464             if (te.highlighted)
465                 os << std::setw(5) << "> " + std::to_string(te.place) << ' ';
466             else
467                 os << std::setw(5) << te.place << ' ';
468 
469             os << std::setw(12) << te.rd.playername << ' ';
470             os << std::setw(12) << te.rd.carname << ' ';
471             os << std::setw(12) << te.rd.carclass << ' ';
472             os << std::setw(6) << te.rd.maxspeed << " SU ";
473             os << std::setw(6) << te.rd.totaltime;
474 
475             if (te.highlighted)
476                 os << " <\n";
477             else
478                 os << '\n';
479         }
480 
481         os << "***" << std::endl;
482     }
483 
484     ///
485     /// @brief Debug printing of current unlocks for all players.
486     /// @param [in,out] os      Output stream to print to.
487     ///
printCurrentUnlocks(std::ostream & os)488     void printCurrentUnlocks(std::ostream &os) const
489     {
490         for (const auto &p: allunlocks)
491         {
492             os << p.first << ":\n";
493 
494             for (const std::string &s: p.second)
495                 os << '\t' << s << '\n';
496         }
497 
498         os << "***" << std::endl;
499     }
500 
501 #endif
502 
503 private:
504 
505     ///
506     /// @brief Sorts the current times list by time and updates places data.
507     ///
sortAndUpdatePlaces()508     void sortAndUpdatePlaces()
509     {
510         sortCurrentTimes(HISCORE1_SORT::BY_TOTALTIME_ASC);
511 
512         unsigned long int p = 1; // Place
513 
514         for (TimeEntry &te: currenttimes)
515             te.place = p++;
516     }
517 
518     ///
519     /// @brief Sorts the current times list.
520     /// @remarks Helper function for internal use.
521     /// @param sortmethod           How to sort the times.
522     /// @see `HISCORE1_SORT` enum.
523     ///
sortCurrentTimes(HISCORE1_SORT sortmethod)524     void sortCurrentTimes(HISCORE1_SORT sortmethod)
525     {
526         std::function<bool (const TimeEntry &, const TimeEntry &)> cmpfunc; // Comparison Function
527 
528         switch (sortmethod)
529         {
530             // case HISCORE1_SORT::BY_TOTALTIME_ASC: // later, this is the default
531 
532             case HISCORE1_SORT::BY_TOTALTIME_DESC:
533 
534                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
535                 {
536                     return a.rd.totaltime > b.rd.totaltime;
537                 };
538 
539                 break;
540 
541             case HISCORE1_SORT::BY_MAXSPEED_ASC:
542 
543                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
544                 {
545                     return a.rd.maxspeed > b.rd.maxspeed;
546                 };
547 
548                 break;
549 
550             case HISCORE1_SORT::BY_MAXSPEED_DESC:
551 
552                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
553                 {
554                     return a.rd.maxspeed < b.rd.maxspeed;
555                 };
556 
557                 break;
558 
559             case HISCORE1_SORT::BY_PLAYERNAME_ASC:
560 
561                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
562                 {
563                     if (a.rd.playername == b.rd.playername)
564                         return a.rd.totaltime < b.rd.totaltime;
565 
566                     return a.rd.playername < b.rd.playername;
567                 };
568 
569                 break;
570 
571             case HISCORE1_SORT::BY_PLAYERNAME_DESC:
572 
573                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
574                 {
575                     if (a.rd.playername == b.rd.playername)
576                         return a.rd.totaltime < b.rd.totaltime;
577 
578                     return a.rd.playername > b.rd.playername;
579                 };
580 
581                 break;
582 
583             case HISCORE1_SORT::BY_CARNAME_ASC:
584 
585                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
586                 {
587                     if (a.rd.carname == b.rd.carname)
588                         return a.rd.totaltime < b.rd.totaltime;
589 
590                     return a.rd.carname < b.rd.carname;
591                 };
592 
593                 break;
594 
595             case HISCORE1_SORT::BY_CARNAME_DESC:
596 
597                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
598                 {
599                     if (a.rd.carname == b.rd.carname)
600                         return a.rd.totaltime < b.rd.totaltime;
601 
602                     return a.rd.carname > b.rd.carname;
603                 };
604 
605                 break;
606 
607             case HISCORE1_SORT::BY_CARCLASS_ASC:
608 
609                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
610                 {
611                     if (a.rd.carclass == b.rd.carclass)
612                         return a.rd.totaltime < b.rd.totaltime;
613 
614                     return a.rd.carclass < b.rd.carclass;
615                 };
616 
617                 break;
618 
619             case HISCORE1_SORT::BY_CARCLASS_DESC:
620 
621                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
622                 {
623                     if (a.rd.carclass == b.rd.carclass)
624                         return a.rd.totaltime < b.rd.totaltime;
625 
626                     return a.rd.carclass > b.rd.carclass;
627                 };
628 
629                 break;
630 
631             case HISCORE1_SORT::BY_TOTALTIME_ASC:
632             default:
633 
634                 cmpfunc = [](const TimeEntry &a, const TimeEntry &b) -> bool
635                 {
636                     return a.rd.totaltime < b.rd.totaltime;
637                 };
638 
639                 break;
640         }
641 
642         std::sort(currenttimes.begin(), currenttimes.end(), cmpfunc);
643     }
644 
645     ///
646     /// @brief Encryption and decryption key, or rather pad.
647     /// @warning Changing this invalidates all highscore files of previous versions!
648     ///
649     const std::vector<unsigned char> edkey {
650         0x02, 0x43, 0x5E, 0xAC, 0x2E, 0x40, 0xD2, 0x7F, 0x84, 0xFB, 0xA0, 0x53, 0x52, 0x05, 0x4E, 0xEC,
651         0x1A, 0xAB, 0x58, 0x8D, 0x2E, 0xFA, 0xC6, 0x2F, 0x65, 0x99, 0x69, 0x3D, 0xBC, 0x38, 0x0E, 0x64,
652         0x45, 0x4B, 0xD9, 0x4B, 0xE5, 0x51, 0x73, 0xB3, 0x8A, 0x4E, 0x1B, 0xC1, 0x80, 0x11, 0x73, 0x16,
653         0xE6, 0x66, 0x63, 0x09, 0x3A, 0x29, 0x90, 0x7F, 0xEC, 0xF6, 0x6B, 0xA5, 0x23, 0x2E, 0x77, 0xEC,
654         0xDF, 0xA0, 0x92, 0x12, 0xB9, 0x7F, 0x3E, 0x63, 0x44, 0x9A, 0x53, 0x59, 0x97, 0xE0, 0x91, 0xE2,
655         0x48, 0x20, 0xAA, 0x5C, 0x68, 0x4C, 0x09, 0x20, 0x63, 0xA6, 0x0A, 0xED, 0x80, 0x21, 0x12, 0xF0,
656         0xE3, 0x4A, 0x74, 0xCA, 0x8C, 0xE0, 0x88, 0xDE, 0xC8, 0x47, 0xC8, 0xB2, 0x5B, 0x3C, 0x58, 0xFB,
657         0x93, 0xC1, 0x1F, 0xFE, 0xEE, 0x16, 0x7D, 0xC7, 0x32, 0x00, 0x09, 0xE5, 0x32, 0x60, 0x5F, 0x31,
658         0x98, 0x12, 0x30, 0x4D, 0x5A, 0xC8, 0x72, 0xF7, 0x83, 0xFE, 0x9B, 0xF1, 0x49, 0x6B, 0x83, 0x79,
659         0xD4, 0xD1, 0x99, 0x1D, 0xB2, 0x1A, 0xC4, 0xFB, 0xB4, 0x6F, 0x8F, 0xE7, 0xE8, 0x0C, 0xB6, 0x14,
660         0x84, 0x70, 0x37, 0xBE, 0x18, 0x84, 0xC9, 0x8B, 0xD9, 0x3D, 0xDD, 0x25, 0x1C, 0x17, 0x45, 0x20,
661         0xED, 0x78, 0xC6, 0x40, 0xCA, 0x55, 0xF2, 0x2A, 0x4A, 0x28, 0x62, 0x3F, 0x94, 0xEB, 0xC9, 0x62,
662         0x3F, 0xCF, 0x16, 0x9D, 0x6A, 0x53, 0x04, 0xEE, 0xFC, 0x2E, 0x10, 0xFE, 0xB6, 0xA7, 0x5B, 0x27,
663         0x4C, 0x22, 0x15, 0xF9, 0x00, 0x73, 0x10, 0x3A, 0x29, 0x3B, 0x30, 0xCC, 0x41, 0x86, 0x15, 0x35,
664         0xF1, 0x22, 0x22, 0x67, 0xC0, 0xEB, 0xA1, 0xD9, 0x9A, 0x12, 0x3B, 0x98, 0x70, 0x22, 0x3D, 0x6E,
665         0x08, 0xF7, 0xF4, 0x98, 0xFE, 0x5A, 0xD3, 0x80, 0xC8, 0xC3, 0x78, 0x8F, 0xBB, 0xAD, 0x50, 0xF0,
666         0xF3, 0x8A, 0xDB, 0x9B, 0xD1, 0xBD, 0xB3, 0x57, 0x67, 0xC4, 0x7B, 0xB2, 0xF1, 0x1E, 0x0B, 0xF7,
667         0xF8, 0xC0, 0xEF, 0x31, 0x25, 0x3A, 0x4A, 0xE3, 0xC9, 0xDC, 0xAC, 0x52, 0x19, 0xC4, 0xC9, 0xBE,
668         0x83, 0xC3, 0xDC, 0x53, 0xEC, 0xD7, 0xD1, 0x64, 0xF8, 0x39, 0x57, 0xBA, 0x84, 0x62, 0xF1, 0xEA,
669         0x5E, 0x12, 0x9D, 0xF8, 0x59, 0x3D, 0xAB, 0x07, 0xBC, 0x62, 0x6F, 0x86, 0x4E, 0x41, 0x54, 0x23,
670         0xB4, 0xFE, 0x3A, 0xB7, 0x1C, 0xFC, 0x86, 0x24, 0x69, 0xB8, 0x5E, 0xB7, 0x17, 0xA6, 0xA8, 0x0B,
671         0xD8, 0x5C, 0x8B, 0x6E, 0x74, 0x70, 0xD9, 0x35, 0xBB, 0xEF, 0xAF, 0xBA, 0xD5, 0xCB, 0x6D, 0x21,
672         0x38, 0x75, 0xC1, 0x77, 0x58, 0xC1, 0x76, 0xA6, 0x3D, 0xE7, 0xB7, 0x0A, 0x08, 0x55, 0x9D, 0xDA,
673         0x2B, 0x12, 0xC1, 0xAE, 0xDE, 0x27, 0xB0, 0x5D, 0x9B, 0x49, 0xDD, 0x76, 0xAC, 0xD0, 0xAE, 0x55,
674         0x61, 0x7C, 0x36, 0xE4, 0x2A, 0x0B, 0xC7, 0x7F, 0xA4, 0x8C, 0x86, 0xDE, 0x39, 0x79, 0x5C, 0xE6,
675         0x5B, 0xE7, 0xFF, 0x80, 0x45, 0xD7, 0xD9, 0xDE, 0xF9, 0xC2, 0xAC, 0x50, 0x84, 0xA7, 0xD9, 0x13,
676         0x95, 0xC9, 0xEB, 0x6B, 0x7D, 0x66, 0x1E, 0x88, 0xFE, 0xA4, 0xE4, 0xC9, 0x8F, 0x00, 0xF1, 0x9F,
677         0x3F, 0x8C, 0x04, 0x5F, 0x30, 0xDF, 0x43, 0x7A, 0x73, 0x27, 0xAD, 0x1D, 0x90, 0x79, 0x36, 0x95,
678         0x1F, 0xCE, 0x4D, 0xBA, 0xED, 0x28, 0x93, 0xD5, 0x08, 0xA4, 0x0B, 0x5A, 0xCA, 0x42, 0x9D, 0x84,
679         0x66, 0x85, 0x8B, 0xCF, 0x25, 0xED, 0xB8, 0x91, 0x88, 0x04, 0x4F, 0x87, 0xE6, 0xBC, 0xA8, 0x6D,
680         0xAE, 0xA4, 0x8F, 0x5E, 0x30, 0xB6, 0x39, 0x45, 0xDD, 0x78, 0x49, 0x08, 0xC5, 0x78, 0x72, 0x02,
681         0x13, 0xB3, 0xA2, 0x90, 0x17, 0x1D, 0xA3, 0xC6, 0xD1, 0xD1, 0x77, 0x20, 0x0C, 0x54, 0x05, 0x15,
682         0xB3, 0x76, 0x53, 0x33, 0x50, 0x9B, 0xF8, 0xDD, 0x28, 0x62, 0x27, 0x02, 0x97, 0xEF, 0xE7, 0x21,
683         0x0A, 0x70, 0x5D, 0x84, 0x44, 0xAA, 0x38, 0x0E, 0xB4, 0xDE, 0xCA, 0xFA, 0x22, 0x98, 0x96, 0xF5,
684         0x8F, 0x4B, 0xA5, 0xF9, 0xAF, 0xDE, 0x87, 0xCD, 0x70, 0x68, 0x2B, 0xCB, 0x28, 0xA1, 0x89, 0x2E,
685         0x6D, 0xB3, 0x68, 0xA0, 0xB6, 0xD9, 0x64, 0xDA, 0xF9, 0xD9, 0xCB, 0xE7, 0x04, 0x33, 0xF2, 0xB8,
686         0xCA, 0xDC, 0x61, 0xFC, 0x63, 0x7E, 0xDA, 0xD2, 0x27, 0x36, 0x44, 0xC1, 0x6D, 0xA0, 0xDB, 0xBD,
687         0xB3, 0x0F, 0xD6, 0xF1, 0x0D, 0x18, 0xA6, 0x6F, 0x5B, 0xD7, 0x4F, 0xE5, 0xCA, 0xEE, 0xA5, 0xCE,
688         0x5C, 0xB1, 0x52, 0x2F, 0xB5, 0x0F, 0xBF, 0xD3, 0x19, 0x5A, 0x65, 0x6E, 0x4B, 0xE5, 0xC8, 0x37,
689         0x27, 0xF8, 0x7A, 0x4D, 0xA3, 0x3E, 0x33, 0x37, 0xDE, 0x16, 0x03, 0x1A, 0xC6, 0x2E, 0x87, 0x01,
690         0xAE, 0x6B, 0xB7, 0x39, 0xBD, 0xE8, 0x17, 0x9B, 0x58, 0x4B, 0x01, 0x82, 0xD6, 0x09, 0x50, 0xBE,
691         0xF3, 0x78, 0x2D, 0xB2, 0xB8, 0x8B, 0x17, 0x50, 0x02, 0x03, 0xFE, 0x1F, 0x45, 0x76, 0xF7, 0xD6,
692         0x63, 0xCA, 0x85, 0x10, 0x3A, 0x61, 0x6D, 0xD2, 0x69, 0x96, 0x5E, 0x64, 0x09, 0xE4, 0x80, 0xC2,
693         0x23, 0x63, 0x2E, 0x46, 0xF2, 0x3D, 0x4C, 0xE1, 0x11, 0xD5, 0x8F, 0x33, 0xBE, 0x10, 0x25, 0x8F,
694         0x11, 0x7D, 0x90, 0xCC, 0x3A, 0xA0, 0x47, 0x09, 0xD7, 0xA4, 0x3B, 0x77, 0x96, 0x61, 0xFE, 0x8D,
695         0xDB, 0x0A, 0x1F, 0x1B, 0xCC, 0x44, 0x32, 0x65, 0x2B, 0xB9, 0x7F, 0x3C, 0x75, 0x58, 0x52, 0x82,
696         0x48, 0x50, 0xE5, 0xE7, 0x34, 0x53, 0xFD, 0x7A, 0x17, 0xF8, 0xE1, 0x91, 0x73, 0x65, 0x82, 0xAD,
697         0xDB, 0x1F, 0xA3, 0xA5, 0x19, 0x90, 0x38, 0xDF, 0x0A, 0x0D, 0x96, 0x69, 0x0D, 0xB9, 0xA6, 0x88,
698         0x3C, 0xC0, 0x02, 0xEB, 0x0A, 0xBF, 0x03, 0x09, 0x9D, 0x2F, 0x39, 0xBC, 0x73, 0x97, 0x65, 0xB3,
699         0x79, 0x5B, 0x69, 0xE4, 0xAE, 0xF9, 0x6F, 0x32, 0xC8, 0x47, 0xBF, 0x14, 0x8F, 0x6E, 0x78, 0xDE
700     };
701 
702     ///
703     /// @brief Encrypts or decrypts the player data.
704     /// @details The idea behind encrypting player data is not about stopping serious
705     ///  cheaters from doing their thing. It's about discouraging otherwise honest
706     ///  players from editing a very tempting text file.
707     /// @param pdata        Encrypted/decrypted player data.
708     /// @returns Decrypted/encrypted player data.
709     ///
xorcrypt(std::string pdata)710     std::string xorcrypt(std::string pdata) const
711     {
712         auto ki = edkey.cbegin(); // Key Iterator
713 
714         for (char &c: pdata)
715         {
716             if (ki == edkey.cend())
717                 ki = edkey.cbegin();
718 
719             c ^= *ki++;
720         }
721 
722         return pdata;
723     }
724 
725     ///
726     /// @brief Reads the player name and race data into the `alltimes` collection.
727     /// @param [in] pname       Player name.
728     /// @param [in] pdata       Player data.
729     ///
readPlayerData(const std::string & pname,const std::string & pdata)730     bool readPlayerData(const std::string &pname, const std::string &pdata)
731     {
732         unsigned long int nu = 0; // Number of Unlocks
733         std::string ts; // Temporary String
734         RaceData rd(pname);
735 
736 #define decrypt xorcrypt
737         std::istringstream sspdata(decrypt(pdata));
738 #undef decrypt
739 
740         GETLINE_SKIP_EMPTY_LINES_B(sspdata, ts);
741         nu = std::stoul(ts);
742 
743         while (nu-- != 0)
744         {
745             GETLINE_SKIP_EMPTY_LINES_B(sspdata, ts);
746             allunlocks[pname].insert(ts);
747         }
748 
749         while (sspdata >> rd)
750             alltimes.insert({rd.mapname, rd});
751 
752         return static_cast<bool> (sspdata);
753     }
754 
755     ///
756     /// @brief Writes the player's race data to his .PLAYER file.
757     /// @note If `pname` is an empty string, no data is saved.
758     /// @todo Should return `bool` and check for stream errors.
759     /// @param [in] pname       Player name.
760     ///
writePlayerData(const std::string & pname)761     void writePlayerData(const std::string &pname) const
762     {
763         if (pname.empty())
764             return;
765 
766         if (PHYSFS_isInit() == 0)
767             return;
768 
769         std::string pfname = searchdir + '/' + pname + ".player"; // Player Filename
770         std::ostringstream sspdata;
771 
772         // save unlock data
773         if (allunlocks.count(pname) != 0)
774         {
775             sspdata << allunlocks.at(pname).size() << '\n';
776 
777             for (const std::string &s: allunlocks.at(pname))
778                 sspdata << s << '\n';
779         }
780         else
781             sspdata << 0 << '\n';
782 
783         sspdata << '\n';
784 
785         // save race data
786         for (const auto &p: alltimes)
787             if (p.second.playername == pname)
788                 sspdata << p.second;
789 
790 #define encrypt xorcrypt
791         sspdata.str(encrypt(sspdata.str()));
792 #undef encrypt
793 
794         PHYSFS_File *pfile = PHYSFS_openWrite(pfname.c_str());
795 
796 #ifndef NDEBUG
797         if (pfile == nullptr)
798         {
799             std::clog << "pfname is \"" << pfname << "\"\n";
800             std::clog << "PhysFS error: " << physfs_getErrorString() << std::endl;
801             return;
802         }
803 #endif
804 
805         physfs_write(pfile, sspdata.str().data(), sizeof(char), sspdata.str().size());
806         PHYSFS_close(pfile);
807     }
808 
809     std::unordered_multimap<std::string, RaceData> alltimes;    ///< All times for all maps.
810     std::unordered_map<std::string, UnlockData> allunlocks;     ///< All unlock data for all players.
811     std::vector<TimeEntry> currenttimes;                        ///< Selected times, for current map.
812     std::string searchdir;                                      ///< Directory where player profiles are.
813     std::string playername;                                     ///< Name of the current player.
814     long int skipSaves = 5;                                     ///< Number of saves to skip, -1 means "save all by dtor".
815     mutable long int sc = 0;                                    ///< Skip counter.
816 };
817 
818 #undef GETLINE_SKIP_EMPTY_LINES
819 #undef GETLINE_SKIP_EMPTY_LINES_B
820