2016-10-12 13:36:30 +02:00
|
|
|
/*****************************************************************************
|
2018-06-15 14:07:34 +02:00
|
|
|
* Copyright (c) 2014-2018 OpenRCT2 developers
|
2016-10-12 13:36:30 +02:00
|
|
|
*
|
2018-06-15 14:07:34 +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
|
|
|
*
|
2018-06-15 14:07:34 +02:00
|
|
|
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
2016-10-12 13:36:30 +02:00
|
|
|
*****************************************************************************/
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
#include <memory>
|
|
|
|
#include <vector>
|
2018-04-20 23:16:37 +02:00
|
|
|
#include "../Context.h"
|
2016-12-14 13:13:52 +01:00
|
|
|
#include "../core/Console.hpp"
|
2017-07-31 00:06:22 +02:00
|
|
|
#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/Math.hpp"
|
|
|
|
#include "../core/Path.hpp"
|
|
|
|
#include "../core/String.hpp"
|
2016-12-26 23:42:19 +01:00
|
|
|
#include "../core/Util.hpp"
|
2017-01-31 00:08:04 +01:00
|
|
|
#include "../ParkImporter.h"
|
2016-12-14 13:13:52 +01:00
|
|
|
#include "../PlatformEnvironment.h"
|
2017-02-05 13:18:07 +01:00
|
|
|
#include "../rct12/SawyerChunkReader.h"
|
2016-10-12 13:36:30 +02:00
|
|
|
#include "ScenarioRepository.h"
|
|
|
|
#include "ScenarioSources.h"
|
|
|
|
|
2017-09-18 17:05:28 +02:00
|
|
|
#include "../config/Config.h"
|
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"
|
2017-09-18 17:05:28 +02:00
|
|
|
#include "../platform/platform.h"
|
2018-01-02 18:58:43 +01:00
|
|
|
#include "Scenario.h"
|
2018-04-10 21:14:45 +02:00
|
|
|
#include "../Game.h"
|
2016-10-12 13:36:30 +02:00
|
|
|
|
2017-06-11 13:53:37 +02:00
|
|
|
using namespace OpenRCT2;
|
|
|
|
|
2017-01-04 22:17:08 +01:00
|
|
|
static sint32 ScenarioCategoryCompare(sint32 categoryA, sint32 categoryB)
|
2016-10-12 13:36:30 +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;
|
|
|
|
return Math::Sign(categoryA - categoryB);
|
|
|
|
}
|
|
|
|
|
2017-01-04 22:17:08 +01:00
|
|
|
static sint32 scenario_index_entry_CompareByCategory(const scenario_index_entry &entryA,
|
2016-10-12 13:36:30 +02:00
|
|
|
const scenario_index_entry &entryB)
|
|
|
|
{
|
|
|
|
// Order by category
|
|
|
|
if (entryA.category != entryB.category)
|
|
|
|
{
|
|
|
|
return ScenarioCategoryCompare(entryA.category, entryB.category);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then by source game / name
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-04 22:17:08 +01:00
|
|
|
static sint32 scenario_index_entry_CompareByIndex(const scenario_index_entry &entryA,
|
2016-10-12 13:36:30 +02:00
|
|
|
const scenario_index_entry &entryB)
|
|
|
|
{
|
|
|
|
// Order by source game
|
|
|
|
if (entryA.source_game != entryB.source_game)
|
|
|
|
{
|
|
|
|
return entryA.source_game - entryB.source_game;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then by index / category / name
|
|
|
|
uint8 sourceGame = entryA.source_game;
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
else if (entryB.source_index == -1)
|
|
|
|
{
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return entryA.source_index - entryB.source_index;
|
|
|
|
}
|
|
|
|
case SCENARIO_SOURCE_REAL:
|
|
|
|
return scenario_index_entry_CompareByCategory(entryA, entryB);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scenario_highscore_free(scenario_highscore_entry * highscore)
|
|
|
|
{
|
|
|
|
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:
|
2017-08-30 22:01:07 +02:00
|
|
|
static constexpr uint32 MAGIC_NUMBER = 0x58444953; // SIDX
|
2017-12-31 12:42:40 +01:00
|
|
|
static constexpr uint16 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-04-20 20:58:59 +02:00
|
|
|
explicit ScenarioFileIndex(const IPlatformEnvironment& env) :
|
2017-08-30 20:27:25 +02:00
|
|
|
FileIndex("scenario index",
|
|
|
|
MAGIC_NUMBER,
|
2017-08-30 00:25:52 +02:00
|
|
|
VERSION,
|
2018-04-20 20:58:59 +02:00
|
|
|
env.GetFilePath(PATHID::CACHE_SCENARIOS),
|
2017-08-30 00:25:52 +02:00
|
|
|
std::string(PATTERN),
|
|
|
|
std::vector<std::string>({
|
2018-04-20 20:58:59 +02:00
|
|
|
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-04-27 19:23:39 +02:00
|
|
|
std::tuple<bool, scenario_index_entry> Create(sint32, const std::string &path) const override
|
2017-08-30 00:25:52 +02:00
|
|
|
{
|
|
|
|
scenario_index_entry entry;
|
|
|
|
auto timestamp = File::GetLastModified(path);
|
2017-08-30 20:27:25 +02:00
|
|
|
if (GetScenarioInfo(path, timestamp, &entry))
|
2017-08-30 00:25:52 +02:00
|
|
|
{
|
2017-08-30 20:27:25 +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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Serialise(IStream * stream, const scenario_index_entry &item) const override
|
|
|
|
{
|
2017-10-13 10:00:42 +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);
|
|
|
|
|
2017-12-31 12:42:40 +01:00
|
|
|
stream->Write(item.internal_name, sizeof(item.internal_name));
|
2017-10-13 10:00:42 +02:00
|
|
|
stream->Write(item.name, sizeof(item.name));
|
|
|
|
stream->Write(item.details, sizeof(item.details));
|
2017-08-30 00:25:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
scenario_index_entry Deserialise(IStream * stream) const override
|
|
|
|
{
|
2017-10-13 10:00:42 +02:00
|
|
|
scenario_index_entry item;
|
|
|
|
|
|
|
|
stream->Read(item.path, sizeof(item.path));
|
|
|
|
item.timestamp = stream->ReadValue<uint64>();
|
|
|
|
|
|
|
|
item.category = stream->ReadValue<uint8>();
|
|
|
|
item.source_game = stream->ReadValue<uint8>();
|
|
|
|
item.source_index = stream->ReadValue<sint16>();
|
|
|
|
item.sc_id = stream->ReadValue<uint16>();
|
|
|
|
|
|
|
|
item.objective_type = stream->ReadValue<uint8>();
|
|
|
|
item.objective_arg_1 = stream->ReadValue<uint8>();
|
|
|
|
item.objective_arg_2 = stream->ReadValue<sint32>();
|
|
|
|
item.objective_arg_3 = stream->ReadValue<sint16>();
|
|
|
|
item.highscore = nullptr;
|
|
|
|
|
2017-12-31 12:42:40 +01:00
|
|
|
stream->Read(item.internal_name, sizeof(item.internal_name));
|
2017-10-13 10:00:42 +02:00
|
|
|
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.
|
|
|
|
*/
|
|
|
|
static bool GetScenarioInfo(const std::string &path, uint64 timestamp, scenario_index_entry * entry)
|
|
|
|
{
|
|
|
|
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
|
|
|
|
{
|
2018-04-20 23:16:37 +02:00
|
|
|
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-01-02 20:23:22 +01: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>();
|
2018-04-10 21:14:45 +02:00
|
|
|
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-01-02 20:23:22 +01: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;
|
|
|
|
}
|
|
|
|
|
|
|
|
static scenario_index_entry CreateNewScenarioEntry(const std::string &path, uint64 timestamp, rct_s6_info * s6Info)
|
|
|
|
{
|
2018-06-04 19:50:46 +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);
|
|
|
|
}
|
|
|
|
|
2017-12-31 12:42:40 +01: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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scenario_translate(&entry, &s6Info->entry);
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-10-12 13:36:30 +02:00
|
|
|
class ScenarioRepository final : public IScenarioRepository
|
|
|
|
{
|
|
|
|
private:
|
|
|
|
static constexpr uint32 HighscoreFileVersion = 1;
|
|
|
|
|
2018-04-20 20:58:59 +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)
|
2017-08-30 20:11:39 +02:00
|
|
|
: _env(env),
|
2018-04-20 20:58:59 +02:00
|
|
|
_fileIndex(*env)
|
2016-12-10 20:20:32 +01:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2016-10-12 13:36:30 +02:00
|
|
|
virtual ~ScenarioRepository()
|
|
|
|
{
|
|
|
|
ClearHighscores();
|
|
|
|
}
|
|
|
|
|
2018-04-27 00:48:25 +02:00
|
|
|
void Scan(sint32 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-26 19:19:30 +02:00
|
|
|
{
|
2017-08-30 00:25:52 +02:00
|
|
|
AddScenario(scenario);
|
2017-08-26 19:19:30 +02:00
|
|
|
}
|
2017-07-31 00:06:22 +02:00
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
const scenario_index_entry * GetByIndex(size_t index) const override
|
|
|
|
{
|
|
|
|
const scenario_index_entry * result = nullptr;
|
|
|
|
if (index < _scenarios.size())
|
|
|
|
{
|
|
|
|
result = &_scenarios[index];
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const scenario_index_entry * GetByFilename(const utf8 * filename) const override
|
|
|
|
{
|
2017-12-06 00:12:36 +01:00
|
|
|
for (const auto &scenario : _scenarios)
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2017-12-06 00:12:36 +01: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;
|
|
|
|
}
|
|
|
|
|
2017-12-31 12:42:40 +01: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-01-28 21:01:28 +01:00
|
|
|
if (scenario->source_game == SCENARIO_SOURCE_OTHER && scenario->sc_id == SC_UNIDENTIFIED)
|
2017-12-31 12:42:40 +01:00
|
|
|
continue;
|
|
|
|
|
|
|
|
// Note: this is always case insensitive search for cross platform consistency
|
|
|
|
if (String::Equals(name, scenario->internal_name, true)) {
|
|
|
|
return &_scenarios[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2016-10-12 13:36:30 +02:00
|
|
|
const scenario_index_entry * GetByPath(const utf8 * path) const override
|
|
|
|
{
|
2017-12-06 00:12:36 +01: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-04-27 00:48:25 +02:00
|
|
|
bool TryRecordHighscore(sint32 language, const utf8 * scenarioFileName, money32 companyValue, const utf8 * name) override
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2017-01-02 00:31:24 +01: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);
|
2017-01-02 00:31:24 +01:00
|
|
|
|
2016-10-14 01:01:34 +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
|
|
|
|
scenario_highscore_entry * highscore = scenario->highscore;
|
|
|
|
if (highscore == nullptr || companyValue > highscore->company_value ||
|
2017-01-02 01:28:14 +01:00
|
|
|
(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
|
|
|
|
{
|
2017-01-02 01:28:14 +01:00
|
|
|
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:
|
|
|
|
scenario_index_entry * GetByFilename(const utf8 * filename)
|
|
|
|
{
|
|
|
|
const ScenarioRepository * repo = this;
|
|
|
|
return (scenario_index_entry *)repo->GetByFilename(filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
scenario_index_entry * GetByPath(const utf8 * path)
|
|
|
|
{
|
|
|
|
const ScenarioRepository * repo = this;
|
|
|
|
return (scenario_index_entry *)repo->GetByPath(path);
|
|
|
|
}
|
|
|
|
|
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-07-31 00:06:22 +02:00
|
|
|
{
|
2017-08-30 20:11:39 +02:00
|
|
|
auto mpdatPath = _env->GetFilePath(PATHID::MP_DAT);
|
|
|
|
auto scenarioDirectory = _env->GetDirectoryPath(DIRBASE::USER, DIRID::SCENARIO);
|
|
|
|
auto sc21Path = Path::Combine(scenarioDirectory, "sc21.sc4");
|
|
|
|
if (File::Exists(mpdatPath) && !File::Exists(sc21Path))
|
2017-07-31 00:06:22 +02:00
|
|
|
{
|
2017-08-30 20:11:39 +02:00
|
|
|
ConvertMegaPark(mpdatPath, sc21Path);
|
|
|
|
}
|
|
|
|
}
|
2017-07-31 00:06:22 +02:00
|
|
|
|
2017-08-30 20:11:39 +02:00
|
|
|
/**
|
|
|
|
* Converts Mega Park to normalised file location (mp.dat to sc21.sc4)
|
2018-06-06 22:36:04 +02:00
|
|
|
* @param srcPath Full path to mp.dat
|
|
|
|
* @param dstPath Full path to sc21.dat
|
2017-08-30 20:11:39 +02:00
|
|
|
*/
|
|
|
|
void ConvertMegaPark(const std::string &srcPath, const std::string &dstPath)
|
|
|
|
{
|
|
|
|
auto directory = Path::GetDirectory(dstPath);
|
|
|
|
platform_ensure_directory_exists(directory.c_str());
|
2017-07-31 00:06:22 +02:00
|
|
|
|
2018-05-02 14:15:26 +02:00
|
|
|
auto mpdat = File::ReadAllBytes(srcPath);
|
2017-07-31 00:06:22 +02:00
|
|
|
|
2017-08-30 20:11:39 +02:00
|
|
|
// Rotate each byte of mp.dat left by 4 bits to convert
|
2018-05-02 14:15:26 +02:00
|
|
|
for (size_t i = 0; i < mpdat.size(); i++)
|
2017-08-30 20:11:39 +02:00
|
|
|
{
|
|
|
|
mpdat[i] = rol8(mpdat[i], 4);
|
2017-07-31 00:06:22 +02:00
|
|
|
}
|
2017-08-30 20:11:39 +02:00
|
|
|
|
2018-05-02 14:15:26 +02:00
|
|
|
File::WriteAllBytes(dstPath, mpdat.data(), mpdat.size());
|
2017-07-31 00:06:22 +02:00
|
|
|
}
|
|
|
|
|
2017-08-30 00:25:52 +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);
|
2017-10-02 23:05:08 +02:00
|
|
|
|
|
|
|
if (!String::Equals(filename, ""))
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2017-10-02 23:05:08 +02:00
|
|
|
auto existingEntry = GetByFilename(filename);
|
|
|
|
if (existingEntry != nullptr)
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2017-10-02 23:05:08 +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
|
|
|
|
2017-10-02 23:05:08 +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
|
|
|
|
{
|
2017-10-02 23:05:08 +02:00
|
|
|
_scenarios.push_back(entry);
|
2016-10-12 13:36:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2017-10-02 23:05:08 +02:00
|
|
|
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)
|
|
|
|
{
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void LoadScores()
|
|
|
|
{
|
2016-12-10 20:20:32 +01:00
|
|
|
std::string path = _env->GetFilePath(PATHID::SCORES);
|
|
|
|
if (!platform_file_exists(path.c_str()))
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
2016-12-10 20:20:32 +01:00
|
|
|
auto fs = FileStream(path, FILE_MODE_OPEN);
|
2016-10-12 13:36:30 +02:00
|
|
|
uint32 fileVersion = fs.ReadValue<uint32>();
|
|
|
|
if (fileVersion != 1)
|
|
|
|
{
|
|
|
|
Console::Error::WriteLine("Invalid or incompatible highscores file.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ClearHighscores();
|
|
|
|
|
|
|
|
uint32 numHighscores = fs.ReadValue<uint32>();
|
2016-10-13 00:54:06 +02:00
|
|
|
for (uint32 i = 0; i < numHighscores; i++)
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
|
|
|
scenario_highscore_entry * highscore = InsertHighscore();
|
|
|
|
highscore->fileName = fs.ReadString();
|
|
|
|
highscore->name = fs.ReadString();
|
|
|
|
highscore->company_value = fs.ReadValue<money32>();
|
|
|
|
highscore->timestamp = fs.ReadValue<datetime64>();
|
|
|
|
}
|
|
|
|
}
|
2018-01-02 20:23:22 +01: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()
|
|
|
|
{
|
2016-12-10 20:20:32 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2016-12-10 20:20:32 +01:00
|
|
|
void LoadLegacyScores(const std::string &path)
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2016-12-10 20:20:32 +01: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 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;
|
2017-12-06 00:12:36 +01: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)
|
|
|
|
{
|
|
|
|
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-01-02 20:23:22 +01:00
|
|
|
catch (const std::exception &)
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2016-12-14 00:33:16 +01: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();
|
|
|
|
}
|
|
|
|
|
|
|
|
scenario_highscore_entry * InsertHighscore()
|
|
|
|
{
|
|
|
|
auto highscore = new scenario_highscore_entry();
|
|
|
|
memset(highscore, 0, sizeof(scenario_highscore_entry));
|
|
|
|
_highscores.push_back(highscore);
|
|
|
|
return highscore;
|
|
|
|
}
|
|
|
|
|
|
|
|
void AttachHighscores()
|
|
|
|
{
|
2017-12-06 00:12:36 +01:00
|
|
|
for (auto &highscore : _highscores)
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2016-10-14 01:01:34 +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()
|
|
|
|
{
|
2016-12-10 20:20:32 +01:00
|
|
|
std::string path = _env->GetFilePath(PATHID::SCORES);
|
2016-10-12 13:36:30 +02:00
|
|
|
try
|
|
|
|
{
|
2016-12-10 20:20:32 +01:00
|
|
|
auto fs = FileStream(path, FILE_MODE_WRITE);
|
2016-10-12 13:36:30 +02:00
|
|
|
fs.WriteValue<uint32>(HighscoreFileVersion);
|
|
|
|
fs.WriteValue<uint32>((uint32)_highscores.size());
|
|
|
|
for (size_t i = 0; i < _highscores.size(); i++)
|
|
|
|
{
|
|
|
|
const scenario_highscore_entry * highscore = _highscores[i];
|
|
|
|
fs.WriteString(highscore->fileName);
|
|
|
|
fs.WriteString(highscore->name);
|
|
|
|
fs.WriteValue(highscore->company_value);
|
|
|
|
fs.WriteValue(highscore->timestamp);
|
|
|
|
}
|
|
|
|
}
|
2018-01-02 20:23:22 +01:00
|
|
|
catch (const std::exception &)
|
2016-10-12 13:36:30 +02:00
|
|
|
{
|
2016-12-14 00:33:16 +01:00
|
|
|
Console::Error::WriteLine("Unable to save highscores to '%s'", path.c_str());
|
2016-10-12 13:36:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-04-20 23:16:37 +02:00
|
|
|
std::unique_ptr<IScenarioRepository> CreateScenarioRepository(const std::shared_ptr<IPlatformEnvironment>& env)
|
2016-12-10 20:20:32 +01:00
|
|
|
{
|
2018-04-20 23:16:37 +02:00
|
|
|
return std::make_unique<ScenarioRepository>(env);
|
2016-12-10 20:20:32 +01:00
|
|
|
}
|
|
|
|
|
2016-10-12 13:36:30 +02:00
|
|
|
IScenarioRepository * GetScenarioRepository()
|
|
|
|
{
|
2018-04-20 23:16:37 +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-02-01 18:49:14 +01: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()
|
|
|
|
{
|
|
|
|
IScenarioRepository * repo = GetScenarioRepository();
|
|
|
|
return repo->GetCount();
|
|
|
|
}
|
2016-10-12 13:36:30 +02:00
|
|
|
|
2018-02-01 18:49:14 +01:00
|
|
|
const scenario_index_entry *scenario_repository_get_by_index(size_t index)
|
|
|
|
{
|
|
|
|
IScenarioRepository * repo = GetScenarioRepository();
|
|
|
|
return repo->GetByIndex(index);
|
|
|
|
}
|
2016-10-12 13:36:30 +02:00
|
|
|
|
2018-02-01 18:49:14 +01:00
|
|
|
bool scenario_repository_try_record_highscore(const utf8 * scenarioFileName, money32 companyValue, const utf8 * name)
|
|
|
|
{
|
|
|
|
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
|
|
|
}
|
2018-02-01 18:49:14 +01:00
|
|
|
|