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