1 /*****************************************************************************
2 * Copyright (c) 2014-2020 OpenRCT2 developers
3 *
4 * For a complete list of all authors, please refer to contributors.md
5 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
6 *
7 * OpenRCT2 is licensed under the GNU General Public License version 3.
8 *****************************************************************************/
9
10 #include "ScenarioRepository.h"
11
12 #include "../Context.h"
13 #include "../Game.h"
14 #include "../ParkImporter.h"
15 #include "../PlatformEnvironment.h"
16 #include "../config/Config.h"
17 #include "../core/Console.hpp"
18 #include "../core/File.h"
19 #include "../core/FileIndex.hpp"
20 #include "../core/FileStream.h"
21 #include "../core/MemoryStream.h"
22 #include "../core/Numerics.hpp"
23 #include "../core/Path.hpp"
24 #include "../core/String.hpp"
25 #include "../localisation/Language.h"
26 #include "../localisation/Localisation.h"
27 #include "../localisation/LocalisationService.h"
28 #include "../platform/Platform2.h"
29 #include "../rct12/RCT12.h"
30 #include "../rct12/SawyerChunkReader.h"
31 #include "Scenario.h"
32 #include "ScenarioSources.h"
33
34 #include <algorithm>
35 #include <memory>
36 #include <vector>
37
38 using namespace OpenRCT2;
39
ScenarioCategoryCompare(int32_t categoryA,int32_t categoryB)40 static int32_t ScenarioCategoryCompare(int32_t categoryA, int32_t categoryB)
41 {
42 if (categoryA == categoryB)
43 return 0;
44 if (categoryA == SCENARIO_CATEGORY_DLC)
45 return -1;
46 if (categoryB == SCENARIO_CATEGORY_DLC)
47 return 1;
48 if (categoryA == SCENARIO_CATEGORY_BUILD_YOUR_OWN)
49 return -1;
50 if (categoryB == SCENARIO_CATEGORY_BUILD_YOUR_OWN)
51 return 1;
52 if (categoryA < categoryB)
53 return -1;
54 return 1;
55 }
56
scenario_index_entry_CompareByCategory(const scenario_index_entry & entryA,const scenario_index_entry & entryB)57 static int32_t scenario_index_entry_CompareByCategory(const scenario_index_entry& entryA, const scenario_index_entry& entryB)
58 {
59 // Order by category
60 if (entryA.category != entryB.category)
61 {
62 return ScenarioCategoryCompare(entryA.category, entryB.category);
63 }
64
65 // Then by source game / name
66 switch (entryA.category)
67 {
68 default:
69 if (entryA.source_game != entryB.source_game)
70 {
71 return static_cast<int32_t>(entryA.source_game) - static_cast<int32_t>(entryB.source_game);
72 }
73 return strcmp(entryA.name, entryB.name);
74 case SCENARIO_CATEGORY_REAL:
75 case SCENARIO_CATEGORY_OTHER:
76 return strcmp(entryA.name, entryB.name);
77 }
78 }
79
scenario_index_entry_CompareByIndex(const scenario_index_entry & entryA,const scenario_index_entry & entryB)80 static int32_t scenario_index_entry_CompareByIndex(const scenario_index_entry& entryA, const scenario_index_entry& entryB)
81 {
82 // Order by source game
83 if (entryA.source_game != entryB.source_game)
84 {
85 return static_cast<int32_t>(entryA.source_game) - static_cast<int32_t>(entryB.source_game);
86 }
87
88 // Then by index / category / name
89 ScenarioSource sourceGame = ScenarioSource{ entryA.source_game };
90 switch (sourceGame)
91 {
92 default:
93 if (entryA.source_index == -1 && entryB.source_index == -1)
94 {
95 if (entryA.category == entryB.category)
96 {
97 return scenario_index_entry_CompareByCategory(entryA, entryB);
98 }
99
100 return ScenarioCategoryCompare(entryA.category, entryB.category);
101 }
102 if (entryA.source_index == -1)
103 {
104 return 1;
105 }
106 if (entryB.source_index == -1)
107 {
108 return -1;
109 }
110 return entryA.source_index - entryB.source_index;
111
112 case ScenarioSource::Real:
113 return scenario_index_entry_CompareByCategory(entryA, entryB);
114 }
115 }
116
scenario_highscore_free(scenario_highscore_entry * highscore)117 static void scenario_highscore_free(scenario_highscore_entry* highscore)
118 {
119 SafeFree(highscore->fileName);
120 SafeFree(highscore->name);
121 SafeDelete(highscore);
122 }
123
124 class ScenarioFileIndex final : public FileIndex<scenario_index_entry>
125 {
126 private:
127 static constexpr uint32_t MAGIC_NUMBER = 0x58444953; // SIDX
128 static constexpr uint16_t VERSION = 5;
129 static constexpr auto PATTERN = "*.sc4;*.sc6;*.sea";
130
131 public:
ScenarioFileIndex(const IPlatformEnvironment & env)132 explicit ScenarioFileIndex(const IPlatformEnvironment& env)
133 : FileIndex(
134 "scenario index", MAGIC_NUMBER, VERSION, env.GetFilePath(PATHID::CACHE_SCENARIOS), std::string(PATTERN),
135 std::vector<std::string>({
136 env.GetDirectoryPath(DIRBASE::RCT1, DIRID::SCENARIO),
137 env.GetDirectoryPath(DIRBASE::RCT2, DIRID::SCENARIO),
138 env.GetDirectoryPath(DIRBASE::USER, DIRID::SCENARIO),
139 }))
140 {
141 }
142
143 protected:
Create(int32_t,const std::string & path) const144 std::tuple<bool, scenario_index_entry> Create(int32_t, const std::string& path) const override
145 {
146 scenario_index_entry entry;
147 auto timestamp = File::GetLastModified(path);
148 if (GetScenarioInfo(path, timestamp, &entry))
149 {
150 return std::make_tuple(true, entry);
151 }
152
153 return std::make_tuple(true, scenario_index_entry());
154 }
155
Serialise(DataSerialiser & ds,scenario_index_entry & item) const156 void Serialise(DataSerialiser& ds, scenario_index_entry& item) const override
157 {
158 ds << item.path;
159 ds << item.timestamp;
160 ds << item.category;
161 ds << item.source_game;
162 ds << item.source_index;
163 ds << item.sc_id;
164 ds << item.objective_type;
165 ds << item.objective_arg_1;
166 ds << item.objective_arg_2;
167 ds << item.objective_arg_3;
168
169 ds << item.internal_name;
170 ds << item.name;
171 ds << item.details;
172 }
173
174 private:
GetStreamFromRCT2Scenario(const std::string & path)175 static std::unique_ptr<IStream> GetStreamFromRCT2Scenario(const std::string& path)
176 {
177 if (String::Equals(Path::GetExtension(path), ".sea", true))
178 {
179 auto data = DecryptSea(fs::u8path(path));
180 auto ms = std::make_unique<MemoryStream>();
181 // Need to copy the data into MemoryStream as the overload will borrow instead of copy.
182 ms->Write(data.data(), data.size());
183 ms->SetPosition(0);
184 return ms;
185 }
186
187 auto fs = std::make_unique<FileStream>(path, FILE_MODE_OPEN);
188 return fs;
189 }
190
191 /**
192 * Reads basic information from a scenario file.
193 */
GetScenarioInfo(const std::string & path,uint64_t timestamp,scenario_index_entry * entry)194 static bool GetScenarioInfo(const std::string& path, uint64_t timestamp, scenario_index_entry* entry)
195 {
196 log_verbose("GetScenarioInfo(%s, %d, ...)", path.c_str(), timestamp);
197 try
198 {
199 std::string extension = Path::GetExtension(path);
200 if (String::Equals(extension, ".sc4", true))
201 {
202 // RCT1 scenario
203 bool result = false;
204 try
205 {
206 auto s4Importer = ParkImporter::CreateS4();
207 s4Importer->LoadScenario(path.c_str(), true);
208 if (s4Importer->GetDetails(entry))
209 {
210 String::Set(entry->path, sizeof(entry->path), path.c_str());
211 entry->timestamp = timestamp;
212 result = true;
213 }
214 }
215 catch (const std::exception&)
216 {
217 }
218 return result;
219 }
220
221 // RCT2 or RCTC scenario
222 auto stream = GetStreamFromRCT2Scenario(path);
223 auto chunkReader = SawyerChunkReader(stream.get());
224
225 rct_s6_header header = chunkReader.ReadChunkAs<rct_s6_header>();
226 if (header.type == S6_TYPE_SCENARIO)
227 {
228 rct_s6_info info = chunkReader.ReadChunkAs<rct_s6_info>();
229 // If the name or the details contain a colour code, they might be in UTF-8 already.
230 // This is caused by a bug that was in OpenRCT2 for 3 years.
231 if (!IsLikelyUTF8(info.name) && !IsLikelyUTF8(info.details))
232 {
233 rct2_to_utf8_self(info.name, sizeof(info.name));
234 rct2_to_utf8_self(info.details, sizeof(info.details));
235 }
236
237 *entry = CreateNewScenarioEntry(path, timestamp, &info);
238 return true;
239 }
240
241 log_verbose("%s is not a scenario", path.c_str());
242 }
243 catch (const std::exception&)
244 {
245 Console::Error::WriteLine("Unable to read scenario: '%s'", path.c_str());
246 }
247 return false;
248 }
249
CreateNewScenarioEntry(const std::string & path,uint64_t timestamp,rct_s6_info * s6Info)250 static scenario_index_entry CreateNewScenarioEntry(const std::string& path, uint64_t timestamp, rct_s6_info* s6Info)
251 {
252 scenario_index_entry entry = {};
253
254 // Set new entry
255 String::Set(entry.path, sizeof(entry.path), path.c_str());
256 entry.timestamp = timestamp;
257 entry.category = s6Info->category;
258 entry.objective_type = s6Info->objective_type;
259 entry.objective_arg_1 = s6Info->objective_arg_1;
260 entry.objective_arg_2 = s6Info->objective_arg_2;
261 entry.objective_arg_3 = s6Info->objective_arg_3;
262 entry.highscore = nullptr;
263 if (String::IsNullOrEmpty(s6Info->name))
264 {
265 // If the scenario doesn't have a name, set it to the filename
266 String::Set(entry.name, sizeof(entry.name), Path::GetFileNameWithoutExtension(entry.path));
267 }
268 else
269 {
270 String::Set(entry.name, sizeof(entry.name), s6Info->name);
271 // Normalise the name to make the scenario as recognisable as possible.
272 ScenarioSources::NormaliseName(entry.name, sizeof(entry.name), entry.name);
273 }
274
275 // entry.name will be translated later so keep the untranslated name here
276 String::Set(entry.internal_name, sizeof(entry.internal_name), entry.name);
277
278 String::Set(entry.details, sizeof(entry.details), s6Info->details);
279
280 // Look up and store information regarding the origins of this scenario.
281 source_desc desc;
282 if (ScenarioSources::TryGetByName(entry.name, &desc))
283 {
284 entry.sc_id = desc.id;
285 entry.source_index = desc.index;
286 entry.source_game = ScenarioSource{ desc.source };
287 entry.category = desc.category;
288 }
289 else
290 {
291 entry.sc_id = SC_UNIDENTIFIED;
292 entry.source_index = -1;
293 if (entry.category == SCENARIO_CATEGORY_REAL)
294 {
295 entry.source_game = ScenarioSource::Real;
296 }
297 else
298 {
299 entry.source_game = ScenarioSource::Other;
300 }
301 }
302
303 scenario_translate(&entry);
304 return entry;
305 }
306 };
307
308 class ScenarioRepository final : public IScenarioRepository
309 {
310 private:
311 static constexpr uint32_t HighscoreFileVersion = 2;
312
313 std::shared_ptr<IPlatformEnvironment> const _env;
314 ScenarioFileIndex const _fileIndex;
315 std::vector<scenario_index_entry> _scenarios;
316 std::vector<scenario_highscore_entry*> _highscores;
317
318 public:
ScenarioRepository(const std::shared_ptr<IPlatformEnvironment> & env)319 explicit ScenarioRepository(const std::shared_ptr<IPlatformEnvironment>& env)
320 : _env(env)
321 , _fileIndex(*env)
322 {
323 }
324
~ScenarioRepository()325 virtual ~ScenarioRepository()
326 {
327 ClearHighscores();
328 }
329
Scan(int32_t language)330 void Scan(int32_t language) override
331 {
332 ImportMegaPark();
333
334 // Reload scenarios from index
335 _scenarios.clear();
336 auto scenarios = _fileIndex.LoadOrBuild(language);
337 for (const auto& scenario : scenarios)
338 {
339 AddScenario(scenario);
340 }
341
342 // Sort the scenarios and load the highscores
343 Sort();
344 LoadScores();
345 LoadLegacyScores();
346 AttachHighscores();
347 }
348
GetCount() const349 size_t GetCount() const override
350 {
351 return _scenarios.size();
352 }
353
GetByIndex(size_t index) const354 const scenario_index_entry* GetByIndex(size_t index) const override
355 {
356 const scenario_index_entry* result = nullptr;
357 if (index < _scenarios.size())
358 {
359 result = &_scenarios[index];
360 }
361 return result;
362 }
363
GetByFilename(const utf8 * filename) const364 const scenario_index_entry* GetByFilename(const utf8* filename) const override
365 {
366 for (const auto& scenario : _scenarios)
367 {
368 const utf8* scenarioFilename = Path::GetFileName(scenario.path);
369
370 // Note: this is always case insensitive search for cross platform consistency
371 if (String::Equals(filename, scenarioFilename, true))
372 {
373 return &scenario;
374 }
375 }
376 return nullptr;
377 }
378
GetByInternalName(const utf8 * name) const379 const scenario_index_entry* GetByInternalName(const utf8* name) const override
380 {
381 for (size_t i = 0; i < _scenarios.size(); i++)
382 {
383 const scenario_index_entry* scenario = &_scenarios[i];
384
385 if (scenario->source_game == ScenarioSource::Other && scenario->sc_id == SC_UNIDENTIFIED)
386 continue;
387
388 // Note: this is always case insensitive search for cross platform consistency
389 if (String::Equals(name, scenario->internal_name, true))
390 {
391 return &_scenarios[i];
392 }
393 }
394 return nullptr;
395 }
396
GetByPath(const utf8 * path) const397 const scenario_index_entry* GetByPath(const utf8* path) const override
398 {
399 for (const auto& scenario : _scenarios)
400 {
401 if (Path::Equals(path, scenario.path))
402 {
403 return &scenario;
404 }
405 }
406 return nullptr;
407 }
408
TryRecordHighscore(int32_t language,const utf8 * scenarioFileName,money64 companyValue,const utf8 * name)409 bool TryRecordHighscore(int32_t language, const utf8* scenarioFileName, money64 companyValue, const utf8* name) override
410 {
411 // Scan the scenarios so we have a fresh list to query. This is to prevent the issue of scenario completions
412 // not getting recorded, see #4951.
413 Scan(language);
414
415 scenario_index_entry* scenario = GetByFilename(scenarioFileName);
416
417 // Check if this is an RCTC scenario that corresponds to a known RCT1/2 scenario or vice versa, see #12626
418 if (scenario == nullptr)
419 {
420 const std::string scenarioBaseName = String::ToStd(Path::GetFileNameWithoutExtension(scenarioFileName));
421 const std::string scenarioExtension = String::ToStd(Path::GetExtension(scenarioFileName));
422
423 if (String::Equals(scenarioExtension, ".sea", true))
424 {
425 // Get scenario using RCT2 style name of RCTC scenario
426 scenario = GetByFilename((scenarioBaseName + ".sc6").c_str());
427 }
428 else if (String::Equals(scenarioExtension, ".sc6", true))
429 {
430 // Get scenario using RCTC style name of RCT2 scenario
431 scenario = GetByFilename((scenarioBaseName + ".sea").c_str());
432 }
433 }
434
435 if (scenario != nullptr)
436 {
437 // Check if record company value has been broken or the highscore is the same but no name is registered
438 scenario_highscore_entry* highscore = scenario->highscore;
439 if (highscore == nullptr || companyValue > highscore->company_value
440 || (String::IsNullOrEmpty(highscore->name) && companyValue == highscore->company_value))
441 {
442 if (highscore == nullptr)
443 {
444 highscore = InsertHighscore();
445 highscore->timestamp = platform_get_datetime_now_utc();
446 scenario->highscore = highscore;
447 }
448 else
449 {
450 if (!String::IsNullOrEmpty(highscore->name))
451 {
452 highscore->timestamp = platform_get_datetime_now_utc();
453 }
454 SafeFree(highscore->fileName);
455 SafeFree(highscore->name);
456 }
457 highscore->fileName = String::Duplicate(Path::GetFileName(scenario->path));
458 highscore->name = String::Duplicate(name);
459 highscore->company_value = companyValue;
460 SaveHighscores();
461 return true;
462 }
463 }
464 return false;
465 }
466
467 private:
GetByFilename(const utf8 * filename)468 scenario_index_entry* GetByFilename(const utf8* filename)
469 {
470 const ScenarioRepository* repo = this;
471 return const_cast<scenario_index_entry*>(repo->GetByFilename(filename));
472 }
473
GetByPath(const utf8 * path)474 scenario_index_entry* GetByPath(const utf8* path)
475 {
476 const ScenarioRepository* repo = this;
477 return const_cast<scenario_index_entry*>(repo->GetByPath(path));
478 }
479
480 /**
481 * Mega Park from RollerCoaster Tycoon 1 is stored in an encrypted hidden file: mp.dat.
482 * Decrypt the file and save it as sc21.sc4 in the user's scenario directory.
483 */
ImportMegaPark()484 void ImportMegaPark()
485 {
486 auto mpdatPath = _env->GetFilePath(PATHID::MP_DAT);
487 auto scenarioDirectory = _env->GetDirectoryPath(DIRBASE::USER, DIRID::SCENARIO);
488 auto expectedSc21Path = Path::Combine(scenarioDirectory, "sc21.sc4");
489 auto sc21Path = Path::ResolveCasing(expectedSc21Path);
490
491 // If the user has a Steam installation.
492 if (!File::Exists(mpdatPath))
493 {
494 mpdatPath = Path::ResolveCasing(
495 Path::Combine(_env->GetDirectoryPath(DIRBASE::RCT1), "RCTdeluxe_install", "Data", "mp.dat"));
496 }
497
498 if (File::Exists(mpdatPath) && !File::Exists(sc21Path))
499 {
500 ConvertMegaPark(mpdatPath, expectedSc21Path);
501 }
502 }
503
504 /**
505 * Converts Mega Park to normalised file location (mp.dat to sc21.sc4)
506 * @param srcPath Full path to mp.dat
507 * @param dstPath Full path to sc21.dat
508 */
ConvertMegaPark(const std::string & srcPath,const std::string & dstPath)509 void ConvertMegaPark(const std::string& srcPath, const std::string& dstPath)
510 {
511 auto directory = Path::GetDirectory(dstPath);
512 platform_ensure_directory_exists(directory.c_str());
513
514 auto mpdat = File::ReadAllBytes(srcPath);
515
516 // Rotate each byte of mp.dat left by 4 bits to convert
517 for (size_t i = 0; i < mpdat.size(); i++)
518 {
519 mpdat[i] = Numerics::rol8(mpdat[i], 4);
520 }
521
522 File::WriteAllBytes(dstPath, mpdat.data(), mpdat.size());
523 }
524
AddScenario(const scenario_index_entry & entry)525 void AddScenario(const scenario_index_entry& entry)
526 {
527 auto filename = Path::GetFileName(entry.path);
528
529 if (!String::Equals(filename, ""))
530 {
531 auto existingEntry = GetByFilename(filename);
532 if (existingEntry != nullptr)
533 {
534 std::string conflictPath;
535 if (existingEntry->timestamp > entry.timestamp)
536 {
537 // Existing entry is more recent
538 conflictPath = String::ToStd(existingEntry->path);
539
540 // Overwrite existing entry with this one
541 *existingEntry = entry;
542 }
543 else
544 {
545 // This entry is more recent
546 conflictPath = entry.path;
547 }
548 Console::WriteLine("Scenario conflict: '%s' ignored because it is newer.", conflictPath.c_str());
549 }
550 else
551 {
552 _scenarios.push_back(entry);
553 }
554 }
555 else
556 {
557 log_error("Tried to add scenario with an empty filename!");
558 }
559 }
560
Sort()561 void Sort()
562 {
563 if (gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_ORIGIN)
564 {
565 std::sort(
566 _scenarios.begin(), _scenarios.end(), [](const scenario_index_entry& a, const scenario_index_entry& b) -> bool {
567 return scenario_index_entry_CompareByIndex(a, b) < 0;
568 });
569 }
570 else
571 {
572 std::sort(
573 _scenarios.begin(), _scenarios.end(), [](const scenario_index_entry& a, const scenario_index_entry& b) -> bool {
574 return scenario_index_entry_CompareByCategory(a, b) < 0;
575 });
576 }
577 }
578
LoadScores()579 void LoadScores()
580 {
581 std::string path = _env->GetFilePath(PATHID::SCORES);
582 if (!Platform::FileExists(path))
583 {
584 return;
585 }
586
587 try
588 {
589 auto fs = FileStream(path, FILE_MODE_OPEN);
590 uint32_t fileVersion = fs.ReadValue<uint32_t>();
591 if (fileVersion != 1 && fileVersion != 2)
592 {
593 Console::Error::WriteLine("Invalid or incompatible highscores file.");
594 return;
595 }
596
597 ClearHighscores();
598
599 uint32_t numHighscores = fs.ReadValue<uint32_t>();
600 for (uint32_t i = 0; i < numHighscores; i++)
601 {
602 scenario_highscore_entry* highscore = InsertHighscore();
603 highscore->fileName = fs.ReadString();
604 highscore->name = fs.ReadString();
605 highscore->company_value = fileVersion == 1 ? fs.ReadValue<money32>() : fs.ReadValue<money64>();
606 highscore->timestamp = fs.ReadValue<datetime64>();
607 }
608 }
609 catch (const std::exception&)
610 {
611 Console::Error::WriteLine("Error reading highscores.");
612 }
613 }
614
615 /**
616 * Loads the original scores.dat file and replaces any highscores that
617 * are better for matching scenarios.
618 */
LoadLegacyScores()619 void LoadLegacyScores()
620 {
621 std::string rct2Path = _env->GetFilePath(PATHID::SCORES_RCT2);
622 std::string legacyPath = _env->GetFilePath(PATHID::SCORES_LEGACY);
623 LoadLegacyScores(legacyPath);
624 LoadLegacyScores(rct2Path);
625 }
626
LoadLegacyScores(const std::string & path)627 void LoadLegacyScores(const std::string& path)
628 {
629 if (!Platform::FileExists(path))
630 {
631 return;
632 }
633
634 bool highscoresDirty = false;
635 try
636 {
637 auto fs = FileStream(path, FILE_MODE_OPEN);
638 if (fs.GetLength() <= 4)
639 {
640 // Initial value of scores for RCT2, just ignore
641 return;
642 }
643
644 // Load header
645 auto header = fs.ReadValue<rct_scores_header>();
646 for (uint32_t i = 0; i < header.ScenarioCount; i++)
647 {
648 // Read legacy entry
649 auto scBasic = fs.ReadValue<rct_scores_entry>();
650
651 // Ignore non-completed scenarios
652 if (scBasic.Flags & SCENARIO_FLAGS_COMPLETED)
653 {
654 bool notFound = true;
655 for (auto& highscore : _highscores)
656 {
657 if (String::Equals(scBasic.Path, highscore->fileName, true))
658 {
659 notFound = false;
660
661 // Check if legacy highscore is better
662 if (scBasic.CompanyValue > highscore->company_value)
663 {
664 SafeFree(highscore->name);
665 std::string name = rct2_to_utf8(scBasic.CompletedBy, RCT2LanguageId::EnglishUK);
666 highscore->name = String::Duplicate(name.c_str());
667 highscore->company_value = scBasic.CompanyValue;
668 highscore->timestamp = DATETIME64_MIN;
669 break;
670 }
671 }
672 }
673 if (notFound)
674 {
675 scenario_highscore_entry* highscore = InsertHighscore();
676 highscore->fileName = String::Duplicate(scBasic.Path);
677 std::string name = rct2_to_utf8(scBasic.CompletedBy, RCT2LanguageId::EnglishUK);
678 highscore->name = String::Duplicate(name.c_str());
679 highscore->company_value = scBasic.CompanyValue;
680 highscore->timestamp = DATETIME64_MIN;
681 }
682 }
683 }
684 }
685 catch (const std::exception&)
686 {
687 Console::Error::WriteLine("Error reading legacy scenario scores file: '%s'", path.c_str());
688 }
689
690 if (highscoresDirty)
691 {
692 SaveHighscores();
693 }
694 }
695
ClearHighscores()696 void ClearHighscores()
697 {
698 for (auto highscore : _highscores)
699 {
700 scenario_highscore_free(highscore);
701 }
702 _highscores.clear();
703 }
704
InsertHighscore()705 scenario_highscore_entry* InsertHighscore()
706 {
707 auto highscore = new scenario_highscore_entry();
708 std::memset(highscore, 0, sizeof(scenario_highscore_entry));
709 _highscores.push_back(highscore);
710 return highscore;
711 }
712
AttachHighscores()713 void AttachHighscores()
714 {
715 for (auto& highscore : _highscores)
716 {
717 scenario_index_entry* scenario = GetByFilename(highscore->fileName);
718 if (scenario != nullptr)
719 {
720 scenario->highscore = highscore;
721 }
722 }
723 }
724
SaveHighscores()725 void SaveHighscores()
726 {
727 std::string path = _env->GetFilePath(PATHID::SCORES);
728 try
729 {
730 auto fs = FileStream(path, FILE_MODE_WRITE);
731 fs.WriteValue<uint32_t>(HighscoreFileVersion);
732 fs.WriteValue<uint32_t>(static_cast<uint32_t>(_highscores.size()));
733 for (size_t i = 0; i < _highscores.size(); i++)
734 {
735 const scenario_highscore_entry* highscore = _highscores[i];
736 fs.WriteString(highscore->fileName);
737 fs.WriteString(highscore->name);
738 fs.WriteValue(highscore->company_value);
739 fs.WriteValue(highscore->timestamp);
740 }
741 }
742 catch (const std::exception&)
743 {
744 Console::Error::WriteLine("Unable to save highscores to '%s'", path.c_str());
745 }
746 }
747 };
748
CreateScenarioRepository(const std::shared_ptr<IPlatformEnvironment> & env)749 std::unique_ptr<IScenarioRepository> CreateScenarioRepository(const std::shared_ptr<IPlatformEnvironment>& env)
750 {
751 return std::make_unique<ScenarioRepository>(env);
752 }
753
GetScenarioRepository()754 IScenarioRepository* GetScenarioRepository()
755 {
756 return GetContext()->GetScenarioRepository();
757 }
758
scenario_repository_scan()759 void scenario_repository_scan()
760 {
761 IScenarioRepository* repo = GetScenarioRepository();
762 repo->Scan(LocalisationService_GetCurrentLanguage());
763 }
764
scenario_repository_get_count()765 size_t scenario_repository_get_count()
766 {
767 IScenarioRepository* repo = GetScenarioRepository();
768 return repo->GetCount();
769 }
770
scenario_repository_get_by_index(size_t index)771 const scenario_index_entry* scenario_repository_get_by_index(size_t index)
772 {
773 IScenarioRepository* repo = GetScenarioRepository();
774 return repo->GetByIndex(index);
775 }
776
scenario_repository_try_record_highscore(const utf8 * scenarioFileName,money64 companyValue,const utf8 * name)777 bool scenario_repository_try_record_highscore(const utf8* scenarioFileName, money64 companyValue, const utf8* name)
778 {
779 IScenarioRepository* repo = GetScenarioRepository();
780 return repo->TryRecordHighscore(LocalisationService_GetCurrentLanguage(), scenarioFileName, companyValue, name);
781 }
782