OpenRCT2/src/openrct2/scenario/ScenarioRepository.cpp

782 lines
26 KiB
C++
Raw Normal View History

2016-10-12 13:36:30 +02:00
/*****************************************************************************
* Copyright (c) 2014-2019 OpenRCT2 developers
2016-10-12 13:36:30 +02:00
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
2016-10-12 13:36:30 +02:00
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
2016-10-12 13:36:30 +02:00
*****************************************************************************/
2018-06-22 23:14:50 +02:00
#include "ScenarioRepository.h"
#include "../Context.h"
2018-06-22 23:14:50 +02:00
#include "../Game.h"
#include "../ParkImporter.h"
#include "../PlatformEnvironment.h"
#include "../config/Config.h"
2016-12-14 13:13:52 +01:00
#include "../core/Console.hpp"
#include "../core/File.h"
2017-08-30 00:25:52 +02:00
#include "../core/FileIndex.hpp"
2016-12-14 13:13:52 +01:00
#include "../core/FileStream.hpp"
#include "../core/Path.hpp"
#include "../core/String.hpp"
2018-05-14 22:16:25 +02:00
#include "../localisation/Language.h"
2018-01-06 18:32:25 +01:00
#include "../localisation/Localisation.h"
2018-04-27 00:48:25 +02:00
#include "../localisation/LocalisationService.h"
#include "../platform/platform.h"
2018-06-22 23:14:50 +02:00
#include "../rct12/SawyerChunkReader.h"
2018-01-02 18:58:43 +01:00
#include "Scenario.h"
2018-06-22 23:14:50 +02:00
#include "ScenarioSources.h"
#include <algorithm>
#include <memory>
#include <vector>
2016-10-12 13:36:30 +02:00
2017-06-11 13:53:37 +02:00
using namespace OpenRCT2;
static int32_t ScenarioCategoryCompare(int32_t categoryA, int32_t categoryB)
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
if (categoryA == categoryB)
return 0;
if (categoryA == SCENARIO_CATEGORY_DLC)
return -1;
if (categoryB == SCENARIO_CATEGORY_DLC)
return 1;
if (categoryA == SCENARIO_CATEGORY_BUILD_YOUR_OWN)
return -1;
if (categoryB == SCENARIO_CATEGORY_BUILD_YOUR_OWN)
return 1;
2018-08-12 14:04:00 +02:00
if (categoryA < categoryB)
return -1;
else
return 1;
2016-10-12 13:36:30 +02:00
}
2018-06-22 23:14:50 +02:00
static int32_t scenario_index_entry_CompareByCategory(const scenario_index_entry& entryA, const scenario_index_entry& entryB)
2016-10-12 13:36:30 +02:00
{
// Order by category
if (entryA.category != entryB.category)
{
return ScenarioCategoryCompare(entryA.category, entryB.category);
}
// Then by source game / name
2018-06-22 23:14:50 +02:00
switch (entryA.category)
{
default:
if (entryA.source_game != entryB.source_game)
{
return entryA.source_game - entryB.source_game;
}
return strcmp(entryA.name, entryB.name);
case SCENARIO_CATEGORY_REAL:
case SCENARIO_CATEGORY_OTHER:
return strcmp(entryA.name, entryB.name);
2016-10-12 13:36:30 +02:00
}
}
2018-06-22 23:14:50 +02:00
static int32_t scenario_index_entry_CompareByIndex(const scenario_index_entry& entryA, const scenario_index_entry& entryB)
2016-10-12 13:36:30 +02:00
{
// Order by source game
if (entryA.source_game != entryB.source_game)
{
return entryA.source_game - entryB.source_game;
}
// Then by index / category / name
uint8_t sourceGame = entryA.source_game;
2018-06-22 23:14:50 +02:00
switch (sourceGame)
{
default:
if (entryA.source_index == -1 && entryB.source_index == -1)
{
if (entryA.category == entryB.category)
{
return scenario_index_entry_CompareByCategory(entryA, entryB);
}
else
{
return ScenarioCategoryCompare(entryA.category, entryB.category);
}
}
else if (entryA.source_index == -1)
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
return 1;
}
else if (entryB.source_index == -1)
{
return -1;
2016-10-12 13:36:30 +02:00
}
else
{
2018-06-22 23:14:50 +02:00
return entryA.source_index - entryB.source_index;
2016-10-12 13:36:30 +02:00
}
2018-06-22 23:14:50 +02:00
case SCENARIO_SOURCE_REAL:
return scenario_index_entry_CompareByCategory(entryA, entryB);
2016-10-12 13:36:30 +02:00
}
}
2018-06-22 23:14:50 +02:00
static void scenario_highscore_free(scenario_highscore_entry* highscore)
2016-10-12 13:36:30 +02:00
{
SafeFree(highscore->fileName);
SafeFree(highscore->name);
SafeDelete(highscore);
}
2017-08-30 00:25:52 +02:00
class ScenarioFileIndex final : public FileIndex<scenario_index_entry>
{
private:
static constexpr uint32_t MAGIC_NUMBER = 0x58444953; // SIDX
static constexpr uint16_t VERSION = 3;
2017-08-30 00:25:52 +02:00
static constexpr auto PATTERN = "*.sc4;*.sc6";
2018-05-14 22:16:25 +02:00
2017-08-30 00:25:52 +02:00
public:
2018-06-22 23:14:50 +02:00
explicit ScenarioFileIndex(const IPlatformEnvironment& env)
: FileIndex(
2019-11-04 14:02:30 +01:00
"scenario index", MAGIC_NUMBER, VERSION, env.GetFilePath(PATHID::CACHE_SCENARIOS), std::string(PATTERN),
std::vector<std::string>({
env.GetDirectoryPath(DIRBASE::RCT1, DIRID::SCENARIO),
env.GetDirectoryPath(DIRBASE::RCT2, DIRID::SCENARIO),
env.GetDirectoryPath(DIRBASE::USER, DIRID::SCENARIO),
}))
2017-08-30 00:25:52 +02:00
{
}
protected:
2018-06-22 23:14:50 +02:00
std::tuple<bool, scenario_index_entry> Create(int32_t, const std::string& path) const override
2017-08-30 00:25:52 +02:00
{
scenario_index_entry entry;
auto timestamp = File::GetLastModified(path);
if (GetScenarioInfo(path, timestamp, &entry))
2017-08-30 00:25:52 +02:00
{
return std::make_tuple(true, entry);
}
else
{
return std::make_tuple(true, scenario_index_entry());
2017-08-30 00:25:52 +02:00
}
}
2018-06-22 23:14:50 +02:00
void Serialise(IStream* stream, const scenario_index_entry& item) const override
2017-08-30 00:25:52 +02:00
{
stream->Write(item.path, sizeof(item.path));
stream->WriteValue(item.timestamp);
stream->WriteValue(item.category);
stream->WriteValue(item.source_game);
stream->WriteValue(item.source_index);
stream->WriteValue(item.sc_id);
stream->WriteValue(item.objective_type);
stream->WriteValue(item.objective_arg_1);
stream->WriteValue(item.objective_arg_2);
stream->WriteValue(item.objective_arg_3);
stream->Write(item.internal_name, sizeof(item.internal_name));
stream->Write(item.name, sizeof(item.name));
stream->Write(item.details, sizeof(item.details));
2017-08-30 00:25:52 +02:00
}
2018-06-22 23:14:50 +02:00
scenario_index_entry Deserialise(IStream* stream) const override
2017-08-30 00:25:52 +02:00
{
scenario_index_entry item;
stream->Read(item.path, sizeof(item.path));
item.timestamp = stream->ReadValue<uint64_t>();
item.category = stream->ReadValue<uint8_t>();
item.source_game = stream->ReadValue<uint8_t>();
item.source_index = stream->ReadValue<int16_t>();
item.sc_id = stream->ReadValue<uint16_t>();
item.objective_type = stream->ReadValue<uint8_t>();
item.objective_arg_1 = stream->ReadValue<uint8_t>();
item.objective_arg_2 = stream->ReadValue<int32_t>();
item.objective_arg_3 = stream->ReadValue<int16_t>();
item.highscore = nullptr;
stream->Read(item.internal_name, sizeof(item.internal_name));
stream->Read(item.name, sizeof(item.name));
stream->Read(item.details, sizeof(item.details));
return item;
2017-08-30 00:25:52 +02:00
}
private:
/**
* Reads basic information from a scenario file.
*/
2018-06-22 23:14:50 +02:00
static bool GetScenarioInfo(const std::string& path, uint64_t timestamp, scenario_index_entry* entry)
2017-08-30 00:25:52 +02:00
{
log_verbose("GetScenarioInfo(%s, %d, ...)", path.c_str(), timestamp);
try
{
std::string extension = Path::GetExtension(path);
if (String::Equals(extension, ".sc4", true))
{
// RCT1 scenario
bool result = false;
try
{
auto s4Importer = ParkImporter::CreateS4();
2017-08-30 00:25:52 +02:00
s4Importer->LoadScenario(path.c_str(), true);
if (s4Importer->GetDetails(entry))
{
String::Set(entry->path, sizeof(entry->path), path.c_str());
entry->timestamp = timestamp;
result = true;
}
}
2018-06-22 23:14:50 +02:00
catch (const std::exception&)
2017-08-30 00:25:52 +02:00
{
}
return result;
}
else
{
// RCT2 scenario
auto fs = FileStream(path, FILE_MODE_OPEN);
auto chunkReader = SawyerChunkReader(&fs);
2018-05-14 22:16:25 +02:00
2017-08-30 00:25:52 +02:00
rct_s6_header header = chunkReader.ReadChunkAs<rct_s6_header>();
if (header.type == S6_TYPE_SCENARIO)
{
rct_s6_info info = chunkReader.ReadChunkAs<rct_s6_info>();
// If the name or the details contain a colour code, they might be in UTF-8 already.
// This is caused by a bug that was in OpenRCT2 for 3 years.
if (!String::ContainsColourCode(info.name) && !String::ContainsColourCode(info.details))
{
rct2_to_utf8_self(info.name, sizeof(info.name));
rct2_to_utf8_self(info.details, sizeof(info.details));
}
2017-08-30 00:25:52 +02:00
*entry = CreateNewScenarioEntry(path, timestamp, &info);
return true;
}
else
{
log_verbose("%s is not a scenario", path.c_str());
}
}
}
2018-06-22 23:14:50 +02:00
catch (const std::exception&)
2017-08-30 00:25:52 +02:00
{
Console::Error::WriteLine("Unable to read scenario: '%s'", path.c_str());
}
return false;
}
2018-06-22 23:14:50 +02:00
static scenario_index_entry CreateNewScenarioEntry(const std::string& path, uint64_t timestamp, rct_s6_info* s6Info)
2017-08-30 00:25:52 +02:00
{
scenario_index_entry entry = {};
2017-08-30 00:25:52 +02:00
// Set new entry
String::Set(entry.path, sizeof(entry.path), path.c_str());
entry.timestamp = timestamp;
entry.category = s6Info->category;
entry.objective_type = s6Info->objective_type;
entry.objective_arg_1 = s6Info->objective_arg_1;
entry.objective_arg_2 = s6Info->objective_arg_2;
entry.objective_arg_3 = s6Info->objective_arg_3;
entry.highscore = nullptr;
if (String::IsNullOrEmpty(s6Info->name))
{
// If the scenario doesn't have a name, set it to the filename
String::Set(entry.name, sizeof(entry.name), Path::GetFileNameWithoutExtension(entry.path));
}
else
{
String::Set(entry.name, sizeof(entry.name), s6Info->name);
// Normalise the name to make the scenario as recognisable as possible.
ScenarioSources::NormaliseName(entry.name, sizeof(entry.name), entry.name);
}
2018-06-22 23:14:50 +02:00
// entry.name will be translated later so keep the untranslated name here
String::Set(entry.internal_name, sizeof(entry.internal_name), entry.name);
2017-08-30 00:25:52 +02:00
String::Set(entry.details, sizeof(entry.details), s6Info->details);
// Look up and store information regarding the origins of this scenario.
source_desc desc;
if (ScenarioSources::TryGetByName(entry.name, &desc))
{
entry.sc_id = desc.id;
entry.source_index = desc.index;
entry.source_game = desc.source;
entry.category = desc.category;
}
else
{
entry.sc_id = SC_UNIDENTIFIED;
entry.source_index = -1;
if (entry.category == SCENARIO_CATEGORY_REAL)
{
entry.source_game = SCENARIO_SOURCE_REAL;
}
else
{
entry.source_game = SCENARIO_SOURCE_OTHER;
}
}
2018-08-29 23:29:45 +02:00
scenario_translate(&entry);
2017-08-30 00:25:52 +02:00
return entry;
}
};
2016-10-12 13:36:30 +02:00
class ScenarioRepository final : public IScenarioRepository
{
private:
static constexpr uint32_t HighscoreFileVersion = 1;
2016-10-12 13:36:30 +02:00
std::shared_ptr<IPlatformEnvironment> const _env;
2017-08-30 00:25:52 +02:00
ScenarioFileIndex const _fileIndex;
2016-10-12 13:36:30 +02:00
std::vector<scenario_index_entry> _scenarios;
std::vector<scenario_highscore_entry*> _highscores;
public:
2018-04-27 19:47:57 +02:00
explicit ScenarioRepository(const std::shared_ptr<IPlatformEnvironment>& env)
2018-06-22 23:14:50 +02:00
: _env(env)
, _fileIndex(*env)
{
}
2016-10-12 13:36:30 +02:00
virtual ~ScenarioRepository()
{
ClearHighscores();
}
void Scan(int32_t language) override
2016-10-12 13:36:30 +02:00
{
2017-08-30 20:11:39 +02:00
ImportMegaPark();
2016-10-12 13:36:30 +02:00
2017-08-30 20:11:39 +02:00
// Reload scenarios from index
_scenarios.clear();
2018-04-27 00:48:25 +02:00
auto scenarios = _fileIndex.LoadOrBuild(language);
2017-08-30 00:25:52 +02:00
for (auto scenario : scenarios)
{
2017-08-30 00:25:52 +02:00
AddScenario(scenario);
}
2017-08-30 20:11:39 +02:00
// Sort the scenarios and load the highscores
2016-10-12 13:36:30 +02:00
Sort();
LoadScores();
LoadLegacyScores();
AttachHighscores();
}
size_t GetCount() const override
{
return _scenarios.size();
}
2018-06-22 23:14:50 +02:00
const scenario_index_entry* GetByIndex(size_t index) const override
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
const scenario_index_entry* result = nullptr;
2016-10-12 13:36:30 +02:00
if (index < _scenarios.size())
{
result = &_scenarios[index];
}
return result;
}
2018-06-22 23:14:50 +02:00
const scenario_index_entry* GetByFilename(const utf8* filename) const override
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
for (const auto& scenario : _scenarios)
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
const utf8* scenarioFilename = Path::GetFileName(scenario.path);
2016-10-12 13:36:30 +02:00
// Note: this is always case insensitive search for cross platform consistency
if (String::Equals(filename, scenarioFilename, true))
{
2017-12-06 00:12:36 +01:00
return &scenario;
2016-10-12 13:36:30 +02:00
}
}
return nullptr;
}
2018-06-22 23:14:50 +02:00
const scenario_index_entry* GetByInternalName(const utf8* name) const override
{
for (size_t i = 0; i < _scenarios.size(); i++)
{
const scenario_index_entry* scenario = &_scenarios[i];
2018-06-22 23:14:50 +02:00
if (scenario->source_game == SCENARIO_SOURCE_OTHER && scenario->sc_id == SC_UNIDENTIFIED)
continue;
2018-06-22 23:14:50 +02:00
// Note: this is always case insensitive search for cross platform consistency
if (String::Equals(name, scenario->internal_name, true))
{
return &_scenarios[i];
}
}
return nullptr;
}
2018-06-22 23:14:50 +02:00
const scenario_index_entry* GetByPath(const utf8* path) const override
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
for (const auto& scenario : _scenarios)
2016-10-12 13:36:30 +02:00
{
2017-12-06 00:12:36 +01:00
if (Path::Equals(path, scenario.path))
2016-10-12 13:36:30 +02:00
{
2017-12-06 00:12:36 +01:00
return &scenario;
2016-10-12 13:36:30 +02:00
}
}
return nullptr;
}
2018-06-22 23:14:50 +02:00
bool TryRecordHighscore(int32_t language, const utf8* scenarioFileName, money32 companyValue, const utf8* name) override
2016-10-12 13:36:30 +02:00
{
// Scan the scenarios so we have a fresh list to query. This is to prevent the issue of scenario completions
// not getting recorded, see #4951.
2018-04-27 00:48:25 +02:00
Scan(language);
2018-06-22 23:14:50 +02:00
scenario_index_entry* scenario = GetByFilename(scenarioFileName);
2016-10-12 13:36:30 +02:00
if (scenario != nullptr)
{
// Check if record company value has been broken or the highscore is the same but no name is registered
2018-06-22 23:14:50 +02:00
scenario_highscore_entry* highscore = scenario->highscore;
if (highscore == nullptr || companyValue > highscore->company_value
|| (String::IsNullOrEmpty(highscore->name) && companyValue == highscore->company_value))
2016-10-12 13:36:30 +02:00
{
if (highscore == nullptr)
{
highscore = InsertHighscore();
2016-10-14 01:01:34 +02:00
highscore->timestamp = platform_get_datetime_now_utc();
scenario->highscore = highscore;
2016-10-12 13:36:30 +02:00
}
else
{
if (!String::IsNullOrEmpty(highscore->name))
2016-10-12 13:36:30 +02:00
{
2016-10-14 01:01:34 +02:00
highscore->timestamp = platform_get_datetime_now_utc();
2016-10-12 13:36:30 +02:00
}
SafeFree(highscore->fileName);
SafeFree(highscore->name);
}
2016-10-14 01:01:34 +02:00
highscore->fileName = String::Duplicate(Path::GetFileName(scenario->path));
highscore->name = String::Duplicate(name);
highscore->company_value = companyValue;
2016-10-12 13:36:30 +02:00
SaveHighscores();
return true;
}
}
return false;
}
private:
2018-06-22 23:14:50 +02:00
scenario_index_entry* GetByFilename(const utf8* filename)
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
const ScenarioRepository* repo = this;
return (scenario_index_entry*)repo->GetByFilename(filename);
2016-10-12 13:36:30 +02:00
}
2018-06-22 23:14:50 +02:00
scenario_index_entry* GetByPath(const utf8* path)
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
const ScenarioRepository* repo = this;
return (scenario_index_entry*)repo->GetByPath(path);
2016-10-12 13:36:30 +02:00
}
2017-08-30 20:11:39 +02:00
/**
* Mega Park from RollerCoaster Tycoon 1 is stored in an encrypted hidden file: mp.dat.
* Decrypt the file and save it as sc21.sc4 in the user's scenario directory.
*/
void ImportMegaPark()
{
2017-08-30 20:11:39 +02:00
auto mpdatPath = _env->GetFilePath(PATHID::MP_DAT);
auto scenarioDirectory = _env->GetDirectoryPath(DIRBASE::USER, DIRID::SCENARIO);
auto expectedSc21Path = Path::Combine(scenarioDirectory, "sc21.sc4");
auto sc21Path = Path::ResolveCasing(expectedSc21Path);
// If the user has a Steam installation.
if (!File::Exists(mpdatPath))
{
mpdatPath = Path::ResolveCasing(
Path::Combine(_env->GetDirectoryPath(DIRBASE::RCT1), "RCTdeluxe_install", "Data", "mp.dat"));
}
2017-08-30 20:11:39 +02:00
if (File::Exists(mpdatPath) && !File::Exists(sc21Path))
{
ConvertMegaPark(mpdatPath, expectedSc21Path);
2017-08-30 20:11:39 +02:00
}
}
2017-08-30 20:11:39 +02:00
/**
* Converts Mega Park to normalised file location (mp.dat to sc21.sc4)
* @param srcPath Full path to mp.dat
* @param dstPath Full path to sc21.dat
2017-08-30 20:11:39 +02:00
*/
2018-06-22 23:14:50 +02:00
void ConvertMegaPark(const std::string& srcPath, const std::string& dstPath)
2017-08-30 20:11:39 +02:00
{
auto directory = Path::GetDirectory(dstPath);
platform_ensure_directory_exists(directory.c_str());
auto mpdat = File::ReadAllBytes(srcPath);
2017-08-30 20:11:39 +02:00
// Rotate each byte of mp.dat left by 4 bits to convert
for (size_t i = 0; i < mpdat.size(); i++)
2017-08-30 20:11:39 +02:00
{
mpdat[i] = rol8(mpdat[i], 4);
}
2017-08-30 20:11:39 +02:00
File::WriteAllBytes(dstPath, mpdat.data(), mpdat.size());
}
2018-06-22 23:14:50 +02:00
void AddScenario(const scenario_index_entry& entry)
2016-10-12 13:36:30 +02:00
{
2017-08-30 00:25:52 +02:00
auto filename = Path::GetFileName(entry.path);
if (!String::Equals(filename, ""))
2016-10-12 13:36:30 +02:00
{
auto existingEntry = GetByFilename(filename);
if (existingEntry != nullptr)
2016-10-12 13:36:30 +02:00
{
std::string conflictPath;
if (existingEntry->timestamp > entry.timestamp)
{
// Existing entry is more recent
conflictPath = String::ToStd(existingEntry->path);
2016-10-12 13:36:30 +02:00
// Overwrite existing entry with this one
*existingEntry = entry;
}
else
{
// This entry is more recent
conflictPath = entry.path;
}
Console::WriteLine("Scenario conflict: '%s' ignored because it is newer.", conflictPath.c_str());
2016-10-12 13:36:30 +02:00
}
else
{
_scenarios.push_back(entry);
2016-10-12 13:36:30 +02:00
}
}
else
{
log_error("Tried to add scenario with an empty filename!");
2016-10-12 13:36:30 +02:00
}
}
void Sort()
{
if (gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_ORIGIN)
{
2018-06-22 23:14:50 +02:00
std::sort(
_scenarios.begin(), _scenarios.end(), [](const scenario_index_entry& a, const scenario_index_entry& b) -> bool {
return scenario_index_entry_CompareByIndex(a, b) < 0;
});
2016-10-12 13:36:30 +02:00
}
else
{
2018-06-22 23:14:50 +02:00
std::sort(
_scenarios.begin(), _scenarios.end(), [](const scenario_index_entry& a, const scenario_index_entry& b) -> bool {
return scenario_index_entry_CompareByCategory(a, b) < 0;
});
2016-10-12 13:36:30 +02:00
}
}
void LoadScores()
{
std::string path = _env->GetFilePath(PATHID::SCORES);
if (!platform_file_exists(path.c_str()))
2016-10-12 13:36:30 +02:00
{
return;
}
try
{
auto fs = FileStream(path, FILE_MODE_OPEN);
uint32_t fileVersion = fs.ReadValue<uint32_t>();
2016-10-12 13:36:30 +02:00
if (fileVersion != 1)
{
Console::Error::WriteLine("Invalid or incompatible highscores file.");
return;
}
ClearHighscores();
uint32_t numHighscores = fs.ReadValue<uint32_t>();
for (uint32_t i = 0; i < numHighscores; i++)
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
scenario_highscore_entry* highscore = InsertHighscore();
2016-10-12 13:36:30 +02:00
highscore->fileName = fs.ReadString();
highscore->name = fs.ReadString();
highscore->company_value = fs.ReadValue<money32>();
highscore->timestamp = fs.ReadValue<datetime64>();
}
}
2018-06-22 23:14:50 +02:00
catch (const std::exception&)
2016-10-12 13:36:30 +02:00
{
Console::Error::WriteLine("Error reading highscores.");
}
}
/**
* Loads the original scores.dat file and replaces any highscores that
* are better for matching scenarios.
*/
void LoadLegacyScores()
{
std::string rct2Path = _env->GetFilePath(PATHID::SCORES_RCT2);
std::string legacyPath = _env->GetFilePath(PATHID::SCORES_LEGACY);
LoadLegacyScores(legacyPath);
LoadLegacyScores(rct2Path);
2016-10-12 13:36:30 +02:00
}
2018-06-22 23:14:50 +02:00
void LoadLegacyScores(const std::string& path)
2016-10-12 13:36:30 +02:00
{
if (!platform_file_exists(path.c_str()))
2016-10-12 13:36:30 +02:00
{
return;
}
bool highscoresDirty = false;
try
{
auto fs = FileStream(path, FILE_MODE_OPEN);
if (fs.GetLength() <= 4)
{
// Initial value of scores for RCT2, just ignore
return;
}
// Load header
2017-01-17 13:21:57 +01:00
auto header = fs.ReadValue<rct_scores_header>();
for (uint32_t i = 0; i < header.ScenarioCount; i++)
2016-10-12 13:36:30 +02:00
{
// Read legacy entry
2017-01-17 13:21:57 +01:00
auto scBasic = fs.ReadValue<rct_scores_entry>();
2016-10-12 13:36:30 +02:00
// Ignore non-completed scenarios
2017-01-17 13:21:57 +01:00
if (scBasic.Flags & SCENARIO_FLAGS_COMPLETED)
2016-10-12 13:36:30 +02:00
{
bool notFound = true;
2018-06-22 23:14:50 +02:00
for (auto& highscore : _highscores)
2016-10-12 13:36:30 +02:00
{
2017-01-17 13:21:57 +01:00
if (String::Equals(scBasic.Path, highscore->fileName, true))
2016-10-12 13:36:30 +02:00
{
notFound = false;
// Check if legacy highscore is better
2017-01-17 13:21:57 +01:00
if (scBasic.CompanyValue > highscore->company_value)
2016-10-12 13:36:30 +02:00
{
SafeFree(highscore->name);
2018-05-14 22:16:25 +02:00
std::string name = rct2_to_utf8(scBasic.CompletedBy, RCT2_LANGUAGE_ID_ENGLISH_UK);
highscore->name = String::Duplicate(name.c_str());
2017-01-17 13:21:57 +01:00
highscore->company_value = scBasic.CompanyValue;
2016-10-12 13:36:30 +02:00
highscore->timestamp = DATETIME64_MIN;
break;
}
}
}
if (notFound)
{
2018-06-22 23:14:50 +02:00
scenario_highscore_entry* highscore = InsertHighscore();
2017-01-17 13:21:57 +01:00
highscore->fileName = String::Duplicate(scBasic.Path);
2018-05-14 22:16:25 +02:00
std::string name = rct2_to_utf8(scBasic.CompletedBy, RCT2_LANGUAGE_ID_ENGLISH_UK);
highscore->name = String::Duplicate(name.c_str());
2017-01-17 13:21:57 +01:00
highscore->company_value = scBasic.CompanyValue;
2016-10-12 13:36:30 +02:00
highscore->timestamp = DATETIME64_MIN;
}
}
}
}
2018-06-22 23:14:50 +02:00
catch (const std::exception&)
2016-10-12 13:36:30 +02:00
{
Console::Error::WriteLine("Error reading legacy scenario scores file: '%s'", path.c_str());
2016-10-12 13:36:30 +02:00
}
if (highscoresDirty)
{
SaveHighscores();
}
}
void ClearHighscores()
{
for (auto highscore : _highscores)
{
scenario_highscore_free(highscore);
}
_highscores.clear();
}
2018-06-22 23:14:50 +02:00
scenario_highscore_entry* InsertHighscore()
2016-10-12 13:36:30 +02:00
{
auto highscore = new scenario_highscore_entry();
std::memset(highscore, 0, sizeof(scenario_highscore_entry));
2016-10-12 13:36:30 +02:00
_highscores.push_back(highscore);
return highscore;
}
void AttachHighscores()
{
2018-06-22 23:14:50 +02:00
for (auto& highscore : _highscores)
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
scenario_index_entry* scenerio = GetByFilename(highscore->fileName);
2016-10-12 13:36:30 +02:00
if (scenerio != nullptr)
{
scenerio->highscore = highscore;
}
}
}
void SaveHighscores()
{
std::string path = _env->GetFilePath(PATHID::SCORES);
2016-10-12 13:36:30 +02:00
try
{
auto fs = FileStream(path, FILE_MODE_WRITE);
fs.WriteValue<uint32_t>(HighscoreFileVersion);
fs.WriteValue<uint32_t>((uint32_t)_highscores.size());
2016-10-12 13:36:30 +02:00
for (size_t i = 0; i < _highscores.size(); i++)
{
2018-06-22 23:14:50 +02:00
const scenario_highscore_entry* highscore = _highscores[i];
2016-10-12 13:36:30 +02:00
fs.WriteString(highscore->fileName);
fs.WriteString(highscore->name);
fs.WriteValue(highscore->company_value);
fs.WriteValue(highscore->timestamp);
}
}
2018-06-22 23:14:50 +02:00
catch (const std::exception&)
2016-10-12 13:36:30 +02:00
{
Console::Error::WriteLine("Unable to save highscores to '%s'", path.c_str());
2016-10-12 13:36:30 +02:00
}
}
};
std::unique_ptr<IScenarioRepository> CreateScenarioRepository(const std::shared_ptr<IPlatformEnvironment>& env)
{
return std::make_unique<ScenarioRepository>(env);
}
2018-06-22 23:14:50 +02:00
IScenarioRepository* GetScenarioRepository()
2016-10-12 13:36:30 +02:00
{
return GetContext()->GetScenarioRepository();
2016-10-12 13:36:30 +02:00
}
2018-02-01 18:49:14 +01:00
void scenario_repository_scan()
2016-10-12 13:36:30 +02:00
{
2018-06-22 23:14:50 +02:00
IScenarioRepository* repo = GetScenarioRepository();
2018-04-27 00:48:25 +02:00
repo->Scan(LocalisationService_GetCurrentLanguage());
2018-02-01 18:49:14 +01:00
}
2016-10-12 13:36:30 +02:00
2018-02-01 18:49:14 +01:00
size_t scenario_repository_get_count()
{
2018-06-22 23:14:50 +02:00
IScenarioRepository* repo = GetScenarioRepository();
2018-02-01 18:49:14 +01:00
return repo->GetCount();
}
2016-10-12 13:36:30 +02:00
2018-06-22 23:14:50 +02:00
const scenario_index_entry* scenario_repository_get_by_index(size_t index)
2018-02-01 18:49:14 +01:00
{
2018-06-22 23:14:50 +02:00
IScenarioRepository* repo = GetScenarioRepository();
2018-02-01 18:49:14 +01:00
return repo->GetByIndex(index);
}
2016-10-12 13:36:30 +02:00
2018-06-22 23:14:50 +02:00
bool scenario_repository_try_record_highscore(const utf8* scenarioFileName, money32 companyValue, const utf8* name)
2018-02-01 18:49:14 +01:00
{
2018-06-22 23:14:50 +02:00
IScenarioRepository* repo = GetScenarioRepository();
2018-04-27 00:48:25 +02:00
return repo->TryRecordHighscore(LocalisationService_GetCurrentLanguage(), scenarioFileName, companyValue, name);
2016-10-12 13:36:30 +02:00
}