Merge pull request #6218 from IntelOrca/feature/cache-scenario-repo

Create index file for scenario repository and refactor object repository and track design repository to share the same code for creating index files.
This commit is contained in:
Ted John 2017-09-01 11:29:49 +01:00 committed by GitHub
commit 87c8204602
13 changed files with 821 additions and 581 deletions

View File

@ -15,6 +15,7 @@
- Fix: [#6198] You cannot cancel RCT1 directory selection
- Fix: Infinite loop when removing scenery elements with >127 base height.
- Improved: [#6186] Transparent menu items now draw properly in OpenGL mode.
- Improved: [#6218] Speed up game start up time by saving scenario index to file.
- Improved: Load/save window now refreshes list if native file dialog is closed/cancelled
0.1.1 (2017-08-09)

View File

@ -186,6 +186,7 @@ const char * PlatformEnvironment::FileNames[] =
"hotkeys.dat", // CONFIG_KEYBOARD
"objects.idx", // CACHE_OBJECTS
"tracks.idx", // CACHE_TRACKS
"scenarios.idx", // CACHE_SCENARIOS
"RCTdeluxe_install" PATH_SEPARATOR "Data" PATH_SEPARATOR "mp.dat", // MP_DAT
"groups.json", // NETWORK_GROUPS
"servers.cfg", // NETWORK_SERVERS

View File

@ -55,6 +55,7 @@ namespace OpenRCT2
CONFIG_KEYBOARD, // Keyboard shortcuts. (hotkeys.cfg)
CACHE_OBJECTS, // Object repository cache (objects.idx).
CACHE_TRACKS, // Track repository cache (tracks.idx).
CACHE_SCENARIOS, // Scenario repository cache (scenarios.idx).
MP_DAT, // Mega Park data, Steam RCT1 only (\RCTdeluxe_install\Data\mp.dat)
NETWORK_GROUPS, // Server groups with permissions (groups.json).
NETWORK_SERVERS, // Saved servers (servers.cfg).

View File

@ -23,6 +23,7 @@ extern "C"
#include "../config/Config.h"
#include "../platform/crash.h"
#include "../platform/platform.h"
#include "../localisation/language.h"
}
#include "../core/Console.hpp"
@ -32,6 +33,7 @@ extern "C"
#include "../network/network.h"
#include "../object/ObjectRepository.h"
#include "../OpenRCT2.h"
#include "../PlatformEnvironment.h"
#include "../Version.h"
#include "CommandLine.hpp"
@ -397,9 +399,13 @@ static exitcode_t HandleCommandScanObjects(CommandLineArgEnumerator * enumerator
return result;
}
// IPlatformEnvironment * env = OpenRCT2::SetupEnvironment();
// IObjectRepository * objectRepository = CreateObjectRepository(env);
// objectRepository->Construct();
auto env = OpenRCT2::CreatePlatformEnvironment();
// HACK: set gCurrentLanguage otherwise it be wrong for the index file
gCurrentLanguage = gConfigGeneral.language;
auto objectRepository = CreateObjectRepository(env);
objectRepository->Construct();
return EXITCODE_OK;
}

View File

@ -14,6 +14,13 @@
*****************************************************************************/
#pragma endregion
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <sys/stat.h>
#endif
#include "Console.hpp"
#include "File.h"
#include "FileStream.hpp"
@ -103,6 +110,32 @@ namespace File
Memory::Free(data);
return lines;
}
uint64 GetLastModified(const std::string &path)
{
uint64 lastModified = 0;
#ifdef _WIN32
auto pathW = utf8_to_widechar(path.c_str());
auto hFile = CreateFileW(pathW, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile != INVALID_HANDLE_VALUE)
{
FILETIME ftCreate, ftAccess, ftWrite;
if (GetFileTime(hFile, &ftCreate, &ftAccess, &ftWrite))
{
lastModified = ((uint64)ftWrite.dwHighDateTime << 32ULL) | (uint64)ftWrite.dwLowDateTime;
}
CloseHandle(hFile);
}
free(pathW);
#else
struct stat statInfo;
if (stat(path.c_str(), &statInfo) == 0)
{
lastModified = statInfo.st_mtime;
}
#endif
return lastModified;
}
}
extern "C"

View File

@ -29,4 +29,5 @@ namespace File
void * ReadAllBytes(const std::string &path, size_t * length);
void WriteAllBytes(const std::string &path, const void * buffer, size_t length);
std::vector<std::string> ReadAllLines(const std::string &path);
uint64 GetLastModified(const std::string &path);
}

View File

@ -0,0 +1,290 @@
#pragma region Copyright (c) 2017 OpenRCT2 Developers
/*****************************************************************************
* OpenRCT2, an open source clone of Roller Coaster Tycoon 2.
*
* OpenRCT2 is the work of many authors, a full list can be found in contributors.md
* For more information, visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* A full copy of the GNU General Public License can be found in licence.txt
*****************************************************************************/
#pragma endregion
#pragma once
#include <chrono>
#include <string>
#include <tuple>
#include <vector>
#include "../common.h"
#include "File.h"
#include "FileScanner.h"
#include "FileStream.hpp"
#include "Path.hpp"
template<typename TItem>
class FileIndex
{
private:
struct DirectoryStats
{
uint32 TotalFiles = 0;
uint64 TotalFileSize = 0;
uint32 FileDateModifiedChecksum = 0;
uint32 PathChecksum = 0;
};
struct ScanResult
{
DirectoryStats const Stats;
std::vector<std::string> const Files;
ScanResult(DirectoryStats stats, std::vector<std::string> files)
: Stats(stats),
Files(files)
{
}
};
struct FileIndexHeader
{
uint32 HeaderSize = sizeof(FileIndexHeader);
uint32 MagicNumber = 0;
uint8 VersionA = 0;
uint8 VersionB = 0;
uint16 LanguageId = 0;
DirectoryStats Stats;
uint32 NumItems = 0;
};
// Index file format version which when incremented forces a rebuild
static constexpr uint8 FILE_INDEX_VERSION = 4;
std::string const _name;
uint32 const _magicNumber;
uint8 const _version;
std::string const _indexPath;
std::string const _pattern;
public:
std::vector<std::string> const SearchPaths;
public:
/**
* Creates a new FileIndex.
* @param name Name of the index (used for logging).
* @param magicNumber Magic number for the index (to distinguish between different index files).
* @param version Version of the specialised index, increment this to force a rebuild.
* @param indexPath Full path to read and write the index file to.
* @param pattern The search pattern for indexing files.
* @param paths A list of search directories.
*/
FileIndex(std::string name,
uint32 magicNumber,
uint8 version,
std::string indexPath,
std::string pattern,
std::vector<std::string> paths) :
_name(name),
_magicNumber(magicNumber),
_version(version),
_indexPath(indexPath),
_pattern(pattern),
SearchPaths(paths)
{
}
virtual ~FileIndex() = default;
/**
* Queries and directories and loads the index header. If the index is up to date,
* the items are loaded from the index and returned, otherwise the index is rebuilt.
*/
std::vector<TItem> LoadOrBuild() const
{
std::vector<TItem> items;
auto scanResult = Scan();
auto readIndexResult = ReadIndexFile(scanResult.Stats);
if (std::get<0>(readIndexResult))
{
// Index was loaded
items = std::get<1>(readIndexResult);
}
else
{
// Index was not loaded
items = Build(scanResult);
}
return items;
}
std::vector<TItem> Rebuild() const
{
auto scanResult = Scan();
auto items = Build(scanResult);
return items;
}
protected:
/**
* Loads the given file and creates the item representing the data to store in the index.
* TODO Use std::optional when C++17 is available.
*/
virtual std::tuple<bool, TItem> Create(const std::string &path) const abstract;
/**
* Serialises an index item to the given stream.
*/
virtual void Serialise(IStream * stream, const TItem &item) const abstract;
/**
* Deserialises an index item from the given stream.
*/
virtual TItem Deserialise(IStream * stream) const abstract;
private:
ScanResult Scan() const
{
DirectoryStats stats;
std::vector<std::string> files;
for (const auto directory : SearchPaths)
{
log_verbose("FileIndex:Scanning for %s in '%s'", _pattern.c_str(), directory.c_str());
auto pattern = Path::Combine(directory, _pattern);
auto scanner = Path::ScanDirectory(pattern, true);
while (scanner->Next())
{
auto fileInfo = scanner->GetFileInfo();
auto path = std::string(scanner->GetPath());
files.push_back(path);
stats.TotalFiles++;
stats.TotalFileSize += fileInfo->Size;
stats.FileDateModifiedChecksum ^=
(uint32)(fileInfo->LastModified >> 32) ^
(uint32)(fileInfo->LastModified & 0xFFFFFFFF);
stats.FileDateModifiedChecksum = ror32(stats.FileDateModifiedChecksum, 5);
stats.PathChecksum += GetPathChecksum(path);
}
delete scanner;
}
return ScanResult(stats, files);
}
std::vector<TItem> Build(const ScanResult &scanResult) const
{
std::vector<TItem> items;
Console::WriteLine("Building %s (%zu items)", _name.c_str(), scanResult.Files.size());
auto startTime = std::chrono::high_resolution_clock::now();
for (auto filePath : scanResult.Files)
{
log_verbose("FileIndex:Indexing '%s'", filePath.c_str());
auto item = Create(filePath);
if (std::get<0>(item))
{
items.push_back(std::get<1>(item));
}
}
WriteIndexFile(scanResult.Stats, items);
auto endTime = std::chrono::high_resolution_clock::now();
auto duration = (std::chrono::duration<float>)(endTime - startTime);
Console::WriteLine("Finished building %s in %.2f seconds.", _name.c_str(), duration.count());
return items;
}
std::tuple<bool, std::vector<TItem>> ReadIndexFile(const DirectoryStats &stats) const
{
bool loadedItems = false;
std::vector<TItem> items;
try
{
log_verbose("FileIndex:Loading index: '%s'", _indexPath.c_str());
auto fs = FileStream(_indexPath, FILE_MODE_OPEN);
// Read header, check if we need to re-scan
auto header = fs.ReadValue<FileIndexHeader>();
if (header.HeaderSize == sizeof(FileIndexHeader) &&
header.MagicNumber == _magicNumber &&
header.VersionA == FILE_INDEX_VERSION &&
header.VersionB == _version &&
header.LanguageId == gCurrentLanguage &&
header.Stats.TotalFiles == stats.TotalFiles &&
header.Stats.TotalFileSize == stats.TotalFileSize &&
header.Stats.FileDateModifiedChecksum == stats.FileDateModifiedChecksum &&
header.Stats.PathChecksum == stats.PathChecksum)
{
// Directory is the same, just read the saved items
for (uint32 i = 0; i < header.NumItems; i++)
{
auto item = Deserialise(&fs);
items.push_back(item);
}
loadedItems = true;
}
else
{
Console::WriteLine("%s out of date", _name.c_str());
}
}
catch (const std::exception &e)
{
Console::Error::WriteLine("Unable to load index: '%s'.", _indexPath.c_str());
Console::Error::WriteLine("%s", e.what());
}
return std::make_tuple(loadedItems, items);
}
void WriteIndexFile(const DirectoryStats &stats, const std::vector<TItem> &items) const
{
try
{
log_verbose("FileIndex:Writing index: '%s'", _indexPath.c_str());
auto fs = FileStream(_indexPath, FILE_MODE_WRITE);
// Write header
FileIndexHeader header;
header.MagicNumber = _magicNumber;
header.VersionA = FILE_INDEX_VERSION;
header.VersionB = _version;
header.LanguageId = gCurrentLanguage;
header.Stats = stats;
header.NumItems = (uint32)items.size();
fs.WriteValue(header);
// Write items
for (const auto item : items)
{
Serialise(&fs, item);
}
}
catch (const std::exception &e)
{
Console::Error::WriteLine("Unable to save index: '%s'.", _indexPath.c_str());
Console::Error::WriteLine("%s", e.what());
}
}
static uint32 GetPathChecksum(const std::string &path)
{
uint32 hash = 0xD8430DED;
for (const utf8 * ch = path.c_str(); *ch != '\0'; ch++)
{
hash += (*ch);
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return hash;
}
};

View File

@ -142,6 +142,11 @@ namespace String
}
}
bool StartsWith(const std::string &str, const std::string &match, bool ignoreCase)
{
return StartsWith(str.c_str(), match.c_str(), ignoreCase);
}
size_t IndexOf(const utf8 * str, utf8 match, size_t startIndex)
{
const utf8 * ch = str + startIndex;

View File

@ -36,6 +36,7 @@ namespace String
bool Equals(const std::string &a, const std::string &b, bool ignoreCase = false);
bool Equals(const utf8 * a, const utf8 * b, bool ignoreCase = false);
bool StartsWith(const utf8 * str, const utf8 * match, bool ignoreCase = false);
bool StartsWith(const std::string &str, const std::string &match, bool ignoreCase = false);
size_t IndexOf(const utf8 * str, utf8 match, size_t startIndex = 0);
size_t LastIndexOf(const utf8 * str, utf8 match);

View File

@ -581,7 +581,7 @@ private:
return loadedObject;
}
void ReportMissingObject(const rct_object_entry * entry)
static void ReportMissingObject(const rct_object_entry * entry)
{
utf8 objName[9] = { 0 };
Memory::Copy(objName, entry->name, 8);

View File

@ -15,14 +15,13 @@
#pragma endregion
#include <algorithm>
#include <chrono>
#include <memory>
#include <unordered_map>
#include <vector>
#include "../common.h"
#include "../core/Console.hpp"
#include "../core/FileScanner.h"
#include "../core/FileIndex.hpp"
#include "../core/FileStream.hpp"
#include "../core/Guard.hpp"
#include "../core/IStream.hpp"
@ -54,22 +53,6 @@ extern "C"
using namespace OpenRCT2;
constexpr uint16 OBJECT_REPOSITORY_VERSION = 11;
#pragma pack(push, 1)
struct ObjectRepositoryHeader
{
uint16 Version;
uint16 LanguageId;
uint32 TotalFiles;
uint64 TotalFileSize;
uint32 FileDateModifiedChecksum;
uint32 PathChecksum;
uint32 NumItems;
};
assert_struct_size(ObjectRepositoryHeader, 28);
#pragma pack(pop)
struct ObjectEntryHash
{
size_t operator()(const rct_object_entry &entry) const
@ -95,17 +78,129 @@ using ObjectEntryMap = std::unordered_map<rct_object_entry, size_t, ObjectEntryH
static void ReportMissingObject(const rct_object_entry * entry);
class ObjectRepository final : public IObjectRepository
class ObjectFileIndex final : public FileIndex<ObjectRepositoryItem>
{
const IPlatformEnvironment * _env = nullptr;
std::vector<ObjectRepositoryItem> _items;
QueryDirectoryResult _queryDirectoryResult = { 0 };
ObjectEntryMap _itemMap;
uint16 _languageId = 0;
sint32 _numConflicts = 0;
private:
static constexpr uint32 MAGIC_NUMBER = 0x5844494F; // OIDX
static constexpr uint16 VERSION = 15;
static constexpr auto PATTERN = "*.dat";
public:
ObjectRepository(IPlatformEnvironment * env) : _env(env)
ObjectFileIndex(IPlatformEnvironment * env) :
FileIndex("object index",
MAGIC_NUMBER,
VERSION,
env->GetFilePath(PATHID::CACHE_OBJECTS),
std::string(PATTERN),
std::vector<std::string>({
env->GetDirectoryPath(DIRBASE::RCT2, DIRID::OBJECT),
env->GetDirectoryPath(DIRBASE::USER, DIRID::OBJECT) }))
{
}
public:
std::tuple<bool, ObjectRepositoryItem> Create(const std::string &path) const override
{
auto object = ObjectFactory::CreateObjectFromLegacyFile(path.c_str());
if (object != nullptr)
{
ObjectRepositoryItem item = { 0 };
item.ObjectEntry = *object->GetObjectEntry();
item.Path = String::Duplicate(path);
item.Name = String::Duplicate(object->GetName());
object->SetRepositoryItem(&item);
delete object;
return std::make_tuple(true, item);
}
else
{
return std::make_tuple(true, ObjectRepositoryItem());
}
}
protected:
void Serialise(IStream * stream, const ObjectRepositoryItem &item) const override
{
stream->WriteValue(item.ObjectEntry);
stream->WriteString(item.Path);
stream->WriteString(item.Name);
switch (item.ObjectEntry.flags & 0x0F) {
case OBJECT_TYPE_RIDE:
stream->WriteValue<uint8>(item.RideFlags);
for (sint32 i = 0; i < 2; i++)
{
stream->WriteValue<uint8>(item.RideCategory[i]);
}
for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++)
{
stream->WriteValue<uint8>(item.RideType[i]);
}
stream->WriteValue<uint8>(item.RideGroupIndex);
break;
case OBJECT_TYPE_SCENERY_SETS:
stream->WriteValue<uint16>(item.NumThemeObjects);
for (uint16 i = 0; i < item.NumThemeObjects; i++)
{
stream->WriteValue<rct_object_entry>(item.ThemeObjects[i]);
}
break;
}
}
ObjectRepositoryItem Deserialise(IStream * stream) const override
{
ObjectRepositoryItem item = { 0 };
item.ObjectEntry = stream->ReadValue<rct_object_entry>();
item.Path = stream->ReadString();
item.Name = stream->ReadString();
switch (item.ObjectEntry.flags & 0x0F) {
case OBJECT_TYPE_RIDE:
item.RideFlags = stream->ReadValue<uint8>();
for (sint32 i = 0; i < 2; i++)
{
item.RideCategory[i] = stream->ReadValue<uint8>();
}
for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++)
{
item.RideType[i] = stream->ReadValue<uint8>();
}
item.RideGroupIndex = stream->ReadValue<uint8>();
break;
case OBJECT_TYPE_SCENERY_SETS:
item.NumThemeObjects = stream->ReadValue<uint16>();
item.ThemeObjects = Memory::AllocateArray<rct_object_entry>(item.NumThemeObjects);
for (uint16 i = 0; i < item.NumThemeObjects; i++)
{
item.ThemeObjects[i] = stream->ReadValue<rct_object_entry>();
}
break;
}
return item;
}
private:
bool IsTrackReadOnly(const std::string &path) const
{
return
String::StartsWith(path, SearchPaths[0]) ||
String::StartsWith(path, SearchPaths[1]);
}
};
class ObjectRepository final : public IObjectRepository
{
IPlatformEnvironment * const _env = nullptr;
ObjectFileIndex const _fileIndex;
std::vector<ObjectRepositoryItem> _items;
ObjectEntryMap _itemMap;
public:
ObjectRepository(IPlatformEnvironment * env)
: _env(env),
_fileIndex(env)
{
}
@ -117,24 +212,16 @@ public:
void LoadOrConstruct() override
{
ClearItems();
Query();
if (!Load())
{
_languageId = gCurrentLanguage;
Scan();
Save();
}
// SortItems();
auto items = _fileIndex.LoadOrBuild();
AddItems(items);
SortItems();
}
void Construct() override
{
_languageId = gCurrentLanguage;
Query();
Scan();
Save();
auto items = _fileIndex.Rebuild();
AddItems(items);
SortItems();
}
size_t GetNumObjects() const override
@ -274,146 +361,6 @@ private:
_itemMap.clear();
}
void Query()
{
_queryDirectoryResult = { 0 };
const std::string &rct2Path = _env->GetDirectoryPath(DIRBASE::RCT2, DIRID::OBJECT);
const std::string &openrct2Path = _env->GetDirectoryPath(DIRBASE::USER, DIRID::OBJECT);
QueryDirectory(&_queryDirectoryResult, rct2Path);
QueryDirectory(&_queryDirectoryResult, openrct2Path);
}
void QueryDirectory(QueryDirectoryResult * result, const std::string &directory)
{
utf8 pattern[MAX_PATH];
String::Set(pattern, sizeof(pattern), directory.c_str());
Path::Append(pattern, sizeof(pattern), "*.dat");
Path::QueryDirectory(result, pattern);
}
void Scan()
{
Console::WriteLine("Scanning %lu objects...", _queryDirectoryResult.TotalFiles);
_numConflicts = 0;
auto startTime = std::chrono::high_resolution_clock::now();
const std::string &rct2Path = _env->GetDirectoryPath(DIRBASE::RCT2, DIRID::OBJECT);
const std::string &openrct2Path = _env->GetDirectoryPath(DIRBASE::USER, DIRID::OBJECT);
ScanDirectory(rct2Path);
ScanDirectory(openrct2Path);
auto endTime = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> duration = endTime - startTime;
Console::WriteLine("Scanning complete in %.2f seconds.", duration.count());
if (_numConflicts > 0)
{
Console::WriteLine("%d object conflicts found.", _numConflicts);
}
}
void ScanDirectory(const std::string &directory)
{
utf8 pattern[MAX_PATH];
String::Set(pattern, sizeof(pattern), directory.c_str());
Path::Append(pattern, sizeof(pattern), "*.dat");
IFileScanner * scanner = Path::ScanDirectory(pattern, true);
while (scanner->Next())
{
const utf8 * enumPath = scanner->GetPath();
ScanObject(enumPath);
}
delete scanner;
}
void ScanObject(const utf8 * path)
{
Object * object = ObjectFactory::CreateObjectFromLegacyFile(path);
if (object != nullptr)
{
ObjectRepositoryItem item = { 0 };
item.ObjectEntry = *object->GetObjectEntry();
item.Path = String::Duplicate(path);
item.Name = String::Duplicate(object->GetName());
object->SetRepositoryItem(&item);
AddItem(&item);
delete object;
}
}
bool Load()
{
const std::string &path = _env->GetFilePath(PATHID::CACHE_OBJECTS);
try
{
auto fs = FileStream(path, FILE_MODE_OPEN);
auto header = fs.ReadValue<ObjectRepositoryHeader>();
if (header.Version == OBJECT_REPOSITORY_VERSION &&
header.LanguageId == gCurrentLanguage &&
header.TotalFiles == _queryDirectoryResult.TotalFiles &&
header.TotalFileSize == _queryDirectoryResult.TotalFileSize &&
header.FileDateModifiedChecksum == _queryDirectoryResult.FileDateModifiedChecksum &&
header.PathChecksum == _queryDirectoryResult.PathChecksum)
{
// Header matches, so the index is not out of date
// Buffer the rest of file into memory to speed up item reading
size_t dataSize = (size_t)(fs.GetLength() - fs.GetPosition());
void * data = fs.ReadArray<uint8>(dataSize);
auto ms = MemoryStream(data, dataSize, MEMORY_ACCESS::READ | MEMORY_ACCESS::OWNER);
// Read items
for (uint32 i = 0; i < header.NumItems; i++)
{
ObjectRepositoryItem item = ReadItem(&ms);
AddItem(&item);
}
return true;
}
Console::WriteLine("Object repository is out of date.");
return false;
}
catch (const IOException &)
{
return false;
}
}
void Save() const
{
const std::string &path = _env->GetFilePath(PATHID::CACHE_OBJECTS);
try
{
auto fs = FileStream(path, FILE_MODE_WRITE);
// Write header
ObjectRepositoryHeader header;
header.Version = OBJECT_REPOSITORY_VERSION;
header.LanguageId = _languageId;
header.TotalFiles = _queryDirectoryResult.TotalFiles;
header.TotalFileSize = _queryDirectoryResult.TotalFileSize;
header.FileDateModifiedChecksum = _queryDirectoryResult.FileDateModifiedChecksum;
header.PathChecksum = _queryDirectoryResult.PathChecksum;
header.NumItems = (uint32)_items.size();
fs.WriteValue(header);
// Write items
for (uint32 i = 0; i < header.NumItems; i++)
{
WriteItem(&fs, _items[i]);
}
}
catch (const IOException &)
{
log_error("Unable to write object repository index to '%s'.", path.c_str());
}
}
void SortItems()
{
std::sort(_items.begin(), _items.end(), [](const ObjectRepositoryItem &a,
@ -422,6 +369,12 @@ private:
return strcmp(a.Name, b.Name) < 0;
});
// Fix the IDs
for (size_t i = 0; i < _items.size(); i++)
{
_items[i].Id = i;
}
// Rebuild item map
_itemMap.clear();
for (size_t i = 0; i < _items.size(); i++)
@ -431,85 +384,49 @@ private:
}
}
bool AddItem(ObjectRepositoryItem * item)
void AddItems(const std::vector<ObjectRepositoryItem> &items)
{
const ObjectRepositoryItem * conflict = FindObject(&item->ObjectEntry);
size_t numConflicts = 0;
for (auto item : items)
{
if (!AddItem(item))
{
numConflicts++;
}
}
if (numConflicts > 0)
{
Console::Error::WriteLine("%zu object conflicts found.", numConflicts);
}
}
bool AddItem(const ObjectRepositoryItem &item)
{
auto conflict = FindObject(&item.ObjectEntry);
if (conflict == nullptr)
{
size_t index = _items.size();
item->Id = index;
_items.push_back(*item);
_itemMap[item->ObjectEntry] = index;
auto copy = item;
copy.Id = index;
_items.push_back(copy);
_itemMap[item.ObjectEntry] = index;
return true;
}
else
{
_numConflicts++;
Console::Error::WriteLine("Object conflict: '%s'", conflict->Path);
Console::Error::WriteLine(" : '%s'", item->Path);
Console::Error::WriteLine(" : '%s'", item.Path);
return false;
}
}
static ObjectRepositoryItem ReadItem(IStream * stream)
void ScanObject(const std::string &path)
{
ObjectRepositoryItem item = { 0 };
item.ObjectEntry = stream->ReadValue<rct_object_entry>();
item.Path = stream->ReadString();
item.Name = stream->ReadString();
switch (item.ObjectEntry.flags & 0x0F) {
case OBJECT_TYPE_RIDE:
item.RideFlags = stream->ReadValue<uint8>();
for (sint32 i = 0; i < 2; i++)
{
item.RideCategory[i] = stream->ReadValue<uint8>();
}
for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++)
{
item.RideType[i] = stream->ReadValue<uint8>();
}
item.RideGroupIndex = stream->ReadValue<uint8>();
break;
case OBJECT_TYPE_SCENERY_SETS:
item.NumThemeObjects = stream->ReadValue<uint16>();
item.ThemeObjects = Memory::AllocateArray<rct_object_entry>(item.NumThemeObjects);
for (uint16 i = 0; i < item.NumThemeObjects; i++)
{
item.ThemeObjects[i] = stream->ReadValue<rct_object_entry>();
}
break;
}
return item;
}
static void WriteItem(IStream * stream, const ObjectRepositoryItem &item)
{
stream->WriteValue(item.ObjectEntry);
stream->WriteString(item.Path);
stream->WriteString(item.Name);
switch (item.ObjectEntry.flags & 0x0F) {
case OBJECT_TYPE_RIDE:
stream->WriteValue<uint8>(item.RideFlags);
for (sint32 i = 0; i < 2; i++)
{
stream->WriteValue<uint8>(item.RideCategory[i]);
}
for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++)
{
stream->WriteValue<uint8>(item.RideType[i]);
}
stream->WriteValue<uint8>(item.RideGroupIndex);
break;
case OBJECT_TYPE_SCENERY_SETS:
stream->WriteValue<uint16>(item.NumThemeObjects);
for (uint16 i = 0; i < item.NumThemeObjects; i++)
{
stream->WriteValue<rct_object_entry>(item.ThemeObjects[i]);
}
break;
auto result = _fileIndex.Create(path);
if (std::get<0>(result))
{
auto ori = std::get<1>(result);
AddItem(ori);
}
}

View File

@ -21,14 +21,14 @@
#include "../core/Collections.hpp"
#include "../core/Console.hpp"
#include "../core/File.h"
#include "../core/FileScanner.h"
#include "../core/FileIndex.hpp"
#include "../core/FileStream.hpp"
#include "../core/Path.hpp"
#include "RideGroupManager.h"
#include "../core/String.hpp"
#include "../object/ObjectRepository.h"
#include "../object/RideObject.h"
#include "../PlatformEnvironment.h"
#include "RideGroupManager.h"
#include "TrackDesignRepository.h"
extern "C"
@ -38,52 +38,117 @@ extern "C"
using namespace OpenRCT2;
#pragma pack(push, 1)
struct TrackRepositoryHeader
{
uint32 MagicNumber;
uint16 Version;
uint32 TotalFiles;
uint64 TotalFileSize;
uint32 FileDateModifiedChecksum;
uint32 PathChecksum;
uint32 NumItems;
};
#pragma pack(pop)
struct TrackRepositoryItem
{
std::string Name;
std::string Path;
uint8 RideType = 0;
std::string ObjectEntry;
uint32 Flags;
uint32 Flags = 0;
};
constexpr uint32 TRACK_REPOSITORY_MAGIC_NUMBER = 0x58444954;
constexpr uint16 TRACK_REPOSITORY_VERSION = 1;
enum TRACK_REPO_ITEM_FLAGS
{
TRIF_READ_ONLY = (1 << 0),
};
static std::string GetNameFromTrackPath(const std::string &path)
{
std::string name = Path::GetFileNameWithoutExtension(path);
//The track name should be the file name until the first instance of a dot
name = name.substr(0, name.find_first_of("."));
return name;
}
class TrackDesignFileIndex final : public FileIndex<TrackRepositoryItem>
{
private:
static constexpr uint32 MAGIC_NUMBER = 0x58444954; // TIDX
static constexpr uint16 VERSION = 1;
static constexpr auto PATTERN = "*.td4;*.td6";
public:
TrackDesignFileIndex(IPlatformEnvironment * env) :
FileIndex("track design index",
MAGIC_NUMBER,
VERSION,
env->GetFilePath(PATHID::CACHE_TRACKS),
std::string(PATTERN),
std::vector<std::string>({
env->GetDirectoryPath(DIRBASE::RCT1, DIRID::TRACK),
env->GetDirectoryPath(DIRBASE::RCT2, DIRID::TRACK),
env->GetDirectoryPath(DIRBASE::USER, DIRID::TRACK) }))
{
}
public:
std::tuple<bool, TrackRepositoryItem> Create(const std::string &path) const override
{
auto td6 = track_design_open(path.c_str());
if (td6 != nullptr)
{
TrackRepositoryItem item;
item.Name = GetNameFromTrackPath(path);
item.Path = path;
item.RideType = td6->type;
item.ObjectEntry = std::string(td6->vehicle_object.name, 8);
item.Flags = 0;
if (IsTrackReadOnly(path))
{
item.Flags |= TRIF_READ_ONLY;
}
track_design_dispose(td6);
return std::make_tuple(true, item);
}
else
{
return std::make_tuple(true, TrackRepositoryItem());
}
}
protected:
void Serialise(IStream * stream, const TrackRepositoryItem &item) const override
{
stream->WriteString(item.Name);
stream->WriteString(item.Path);
stream->WriteValue(item.RideType);
stream->WriteString(item.ObjectEntry);
stream->WriteValue(item.Flags);
}
TrackRepositoryItem Deserialise(IStream * stream) const override
{
TrackRepositoryItem item;
item.Name = stream->ReadStdString();
item.Path = stream->ReadStdString();
item.RideType = stream->ReadValue<uint8>();
item.ObjectEntry = stream->ReadStdString();
item.Flags = stream->ReadValue<uint32>();
return item;
}
private:
bool IsTrackReadOnly(const std::string &path) const
{
return
String::StartsWith(path, SearchPaths[0]) ||
String::StartsWith(path, SearchPaths[1]);
}
};
class TrackDesignRepository final : public ITrackDesignRepository
{
private:
static constexpr const utf8 * TD_FILE_PATTERN = "*.td4;*.td6";
IPlatformEnvironment * _env;
IPlatformEnvironment * const _env;
TrackDesignFileIndex const _fileIndex;
std::vector<TrackRepositoryItem> _items;
QueryDirectoryResult _directoryQueryResult = { 0 };
public:
TrackDesignRepository(IPlatformEnvironment * env)
: _env(env),
_fileIndex(env)
{
Guard::ArgumentNotNull(env);
_env = env;
}
virtual ~TrackDesignRepository() final
@ -227,21 +292,14 @@ public:
void Scan() override
{
std::string rct2Directory = _env->GetDirectoryPath(DIRBASE::RCT2, DIRID::TRACK);
std::string userDirectory = _env->GetDirectoryPath(DIRBASE::USER, DIRID::TRACK);
_items.clear();
_directoryQueryResult = { 0 };
Query(rct2Directory);
Query(userDirectory);
if (!Load())
auto trackDesigns = _fileIndex.LoadOrBuild();
for (auto td : trackDesigns)
{
Scan(rct2Directory, TRIF_READ_ONLY);
Scan(userDirectory);
SortItems();
Save();
_items.push_back(td);
}
SortItems();
}
bool Delete(const std::string &path) override
@ -295,48 +353,18 @@ public:
std::string newPath = Path::Combine(installDir, fileName);
if (File::Copy(path, newPath, false))
{
AddTrack(path);
SortItems();
result = path;
auto td = _fileIndex.Create(path);
if (std::get<0>(td))
{
_items.push_back(std::get<1>(td));
SortItems();
result = path;
}
}
return result;
}
private:
void Query(const std::string &directory)
{
std::string pattern = Path::Combine(directory, TD_FILE_PATTERN);
Path::QueryDirectory(&_directoryQueryResult, pattern);
}
void Scan(const std::string &directory, uint32 flags = 0)
{
std::string pattern = Path::Combine(directory, TD_FILE_PATTERN);
IFileScanner * scanner = Path::ScanDirectory(pattern, true);
while (scanner->Next())
{
const utf8 * path = scanner->GetPath();
AddTrack(path, flags);
}
delete scanner;
}
void AddTrack(const std::string path, uint32 flags = 0)
{
rct_track_td6 * td6 = track_design_open(path.c_str());
if (td6 != nullptr)
{
TrackRepositoryItem item;
item.Name = GetNameFromTrackPath(path);
item.Path = path;
item.RideType = td6->type;
item.ObjectEntry = std::string(td6->vehicle_object.name, 8);
item.Flags = flags;
_items.push_back(item);
track_design_dispose(td6);
}
}
void SortItems()
{
std::sort(_items.begin(), _items.end(), [](const TrackRepositoryItem &a,
@ -350,78 +378,6 @@ private:
});
}
bool Load()
{
std::string path = _env->GetFilePath(PATHID::CACHE_TRACKS);
bool result = false;
try
{
auto fs = FileStream(path, FILE_MODE_OPEN);
// Read header, check if we need to re-scan
auto header = fs.ReadValue<TrackRepositoryHeader>();
if (header.MagicNumber == TRACK_REPOSITORY_MAGIC_NUMBER &&
header.Version == TRACK_REPOSITORY_VERSION &&
header.TotalFiles == _directoryQueryResult.TotalFiles &&
header.TotalFileSize == _directoryQueryResult.TotalFileSize &&
header.FileDateModifiedChecksum == _directoryQueryResult.FileDateModifiedChecksum &&
header.PathChecksum == _directoryQueryResult.PathChecksum)
{
// Directory is the same, just read the saved items
for (uint32 i = 0; i < header.NumItems; i++)
{
TrackRepositoryItem item;
item.Name = fs.ReadStdString();
item.Path = fs.ReadStdString();
item.RideType = fs.ReadValue<uint8>();
item.ObjectEntry = fs.ReadStdString();
item.Flags = fs.ReadValue<uint32>();
_items.push_back(item);
}
result = true;
}
}
catch (const Exception &)
{
Console::Error::WriteLine("Unable to write object repository index.");
}
return result;
}
void Save() const
{
std::string path = _env->GetFilePath(PATHID::CACHE_TRACKS);
try
{
auto fs = FileStream(path, FILE_MODE_WRITE);
// Write header
TrackRepositoryHeader header = { 0 };
header.MagicNumber = TRACK_REPOSITORY_MAGIC_NUMBER;
header.Version = TRACK_REPOSITORY_VERSION;
header.TotalFiles = _directoryQueryResult.TotalFiles;
header.TotalFileSize = _directoryQueryResult.TotalFileSize;
header.FileDateModifiedChecksum = _directoryQueryResult.FileDateModifiedChecksum;
header.PathChecksum = _directoryQueryResult.PathChecksum;
header.NumItems = (uint32)_items.size();
fs.WriteValue(header);
// Write items
for (const auto item : _items)
{
fs.WriteString(item.Name);
fs.WriteString(item.Path);
fs.WriteValue(item.RideType);
fs.WriteString(item.ObjectEntry);
fs.WriteValue(item.Flags);
}
}
catch (const Exception &)
{
Console::Error::WriteLine("Unable to write object repository index.");
}
}
size_t GetTrackIndex(const std::string &path) const
{
for (size_t i = 0; i < _items.size(); i++)
@ -444,15 +400,6 @@ private:
}
return result;
}
public:
static std::string GetNameFromTrackPath(const std::string &path)
{
std::string name = Path::GetFileNameWithoutExtension(path);
//The track name should be the file name until the first instance of a dot
name = name.substr(0, name.find_first_of("."));
return name;
}
};
static TrackDesignRepository * _trackDesignRepository = nullptr;
@ -522,6 +469,6 @@ extern "C"
utf8 * track_repository_get_name_from_path(const utf8 * path)
{
return String::Duplicate(TrackDesignRepository::GetNameFromTrackPath(path));
return String::Duplicate(GetNameFromTrackPath(path));
}
}

View File

@ -19,7 +19,7 @@
#include <vector>
#include "../core/Console.hpp"
#include "../core/File.h"
#include "../core/FileScanner.h"
#include "../core/FileIndex.hpp"
#include "../core/FileStream.hpp"
#include "../core/Math.hpp"
#include "../core/Path.hpp"
@ -122,19 +122,184 @@ static void scenario_highscore_free(scenario_highscore_entry * highscore)
SafeDelete(highscore);
}
class ScenarioFileIndex final : public FileIndex<scenario_index_entry>
{
private:
static constexpr uint32 MAGIC_NUMBER = 0x58444953; // SIDX
static constexpr uint16 VERSION = 1;
static constexpr auto PATTERN = "*.sc4;*.sc6";
public:
ScenarioFileIndex(IPlatformEnvironment * env) :
FileIndex("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) }))
{
}
protected:
std::tuple<bool, scenario_index_entry> Create(const std::string &path) const override
{
scenario_index_entry entry;
auto timestamp = File::GetLastModified(path);
if (GetScenarioInfo(path, timestamp, &entry))
{
return std::make_tuple(true, entry);
}
else
{
return std::make_tuple(true, scenario_index_entry());
}
}
void Serialise(IStream * stream, const scenario_index_entry &item) const override
{
// HACK: Zero highscore pointer
auto copy = item;
copy.highscore = nullptr;
stream->WriteValue(copy);
}
scenario_index_entry Deserialise(IStream * stream) const override
{
auto result = stream->ReadValue<scenario_index_entry>();
// HACK: Zero highscore pointer
result.highscore = nullptr;
return result;
}
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
{
auto s4Importer = std::unique_ptr<IParkImporter>(ParkImporter::CreateS4());
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;
}
}
catch (Exception)
{
}
return result;
}
else
{
// RCT2 scenario
auto fs = FileStream(path, FILE_MODE_OPEN);
auto chunkReader = SawyerChunkReader(&fs);
rct_s6_header header = chunkReader.ReadChunkAs<rct_s6_header>();
if (header.type == S6_TYPE_SCENARIO)
{
rct_s6_info info = chunkReader.ReadChunkAs<rct_s6_info>();
*entry = CreateNewScenarioEntry(path, timestamp, &info);
return true;
}
else
{
log_verbose("%s is not a scenario", path.c_str());
}
}
}
catch (Exception)
{
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)
{
scenario_index_entry entry = { 0 };
// 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);
}
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;
}
};
class ScenarioRepository final : public IScenarioRepository
{
private:
static constexpr uint32 HighscoreFileVersion = 1;
IPlatformEnvironment * _env;
IPlatformEnvironment * const _env;
ScenarioFileIndex const _fileIndex;
std::vector<scenario_index_entry> _scenarios;
std::vector<scenario_highscore_entry*> _highscores;
public:
ScenarioRepository(IPlatformEnvironment * env)
: _env(env),
_fileIndex(env)
{
_env = env;
}
virtual ~ScenarioRepository()
@ -144,20 +309,17 @@ public:
void Scan() override
{
ImportMegaPark();
// Reload scenarios from index
_scenarios.clear();
auto scenarios = _fileIndex.LoadOrBuild();
for (auto scenario : scenarios)
{
AddScenario(scenario);
}
// Scan RCT2 directory
std::string rct1dir = _env->GetDirectoryPath(DIRBASE::RCT1, DIRID::SCENARIO);
std::string rct2dir = _env->GetDirectoryPath(DIRBASE::RCT2, DIRID::SCENARIO);
std::string openrct2dir = _env->GetDirectoryPath(DIRBASE::USER, DIRID::SCENARIO);
std::string mpdatdir = _env->GetFilePath(PATHID::MP_DAT);
Scan(rct1dir);
Scan(rct2dir);
Scan(openrct2dir);
ConvertMegaPark(mpdatdir, openrct2dir);
// Sort the scenarios and load the highscores
Sort();
LoadScores();
LoadLegacyScores();
@ -260,68 +422,52 @@ private:
return (scenario_index_entry *)repo->GetByPath(path);
}
void Scan(const std::string &directory)
/**
* 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()
{
utf8 pattern[MAX_PATH];
String::Set(pattern, sizeof(pattern), directory.c_str());
Path::Append(pattern, sizeof(pattern), "*.sc4;*.sc6");
IFileScanner * scanner = Path::ScanDirectory(pattern, true);
while (scanner->Next())
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))
{
auto path = scanner->GetPath();
auto fileInfo = scanner->GetFileInfo();
AddScenario(path, fileInfo->LastModified);
}
delete scanner;
}
void ConvertMegaPark(std::string &mpdatDir, std::string &scenarioDir)
{
//Convert mp.dat from RCT1 Data directory into SC21.SC4 (Mega Park)
utf8 mpdatPath[MAX_PATH];
utf8 sc21Path[MAX_PATH];
String::Set(mpdatPath, sizeof(mpdatPath), mpdatDir.c_str());
if (platform_file_exists(mpdatPath))
{
//Make sure the scenario directory exists, and that SC21.SC4 hasn't already been created
String::Set(sc21Path, sizeof(sc21Path), scenarioDir.c_str());
platform_ensure_directory_exists(sc21Path);
Path::Append(sc21Path, sizeof(sc21Path), "SC21.SC4");
if (!platform_file_exists(sc21Path)) {
size_t length;
auto mpdat = (uint8 *)(File::ReadAllBytes(mpdatPath, &length));
auto outFS = FileStream(sc21Path, FILE_MODE_WRITE);
for (uint32 i = 0; i < (uint32)length; i++)
{
//Rotate each byte of mp.dat left by 4 bits to convert
mpdat[i] = rol8(mpdat[i], 4);
}
outFS.WriteArray<uint8>(mpdat, length);
Memory::FreeArray(mpdat, length);
}
ConvertMegaPark(mpdatPath, sc21Path);
}
}
void AddScenario(const std::string &path, uint64 timestamp)
/**
* Converts Mega Park to normalised file location (mp.dat to sc21.sc4)
* @param Full path to mp.dat
* @param Full path to sc21.dat
*/
void ConvertMegaPark(const std::string &srcPath, const std::string &dstPath)
{
scenario_index_entry entry;
if (!GetScenarioInfo(path, timestamp, &entry))
auto directory = Path::GetDirectory(dstPath);
platform_ensure_directory_exists(directory.c_str());
size_t length;
auto mpdat = (uint8 *)(File::ReadAllBytes(srcPath, &length));
// Rotate each byte of mp.dat left by 4 bits to convert
for (size_t i = 0; i < length; i++)
{
return;
mpdat[i] = rol8(mpdat[i], 4);
}
const std::string filename = Path::GetFileName(path);
scenario_index_entry * existingEntry = GetByFilename(filename.c_str());
File::WriteAllBytes(dstPath, mpdat, length);
Memory::FreeArray(mpdat, length);
}
void AddScenario(const scenario_index_entry &entry)
{
auto filename = Path::GetFileName(entry.path);
auto existingEntry = GetByFilename(filename);
if (existingEntry != nullptr)
{
std::string conflictPath;
if (existingEntry->timestamp > timestamp)
if (existingEntry->timestamp > entry.timestamp)
{
// Existing entry is more recent
conflictPath = String::ToStd(existingEntry->path);
@ -332,7 +478,7 @@ private:
else
{
// This entry is more recent
conflictPath = path;
conflictPath = entry.path;
}
Console::WriteLine("Scenario conflict: '%s' ignored because it is newer.", conflictPath.c_str());
}
@ -342,115 +488,6 @@ private:
}
}
/**
* Reads basic information from a scenario file.
*/
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
{
auto s4Importer = std::unique_ptr<IParkImporter>(ParkImporter::CreateS4());
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;
}
}
catch (Exception)
{
}
return result;
}
else
{
// RCT2 scenario
auto fs = FileStream(path, FILE_MODE_OPEN);
auto chunkReader = SawyerChunkReader(&fs);
rct_s6_header header = chunkReader.ReadChunkAs<rct_s6_header>();
if (header.type == S6_TYPE_SCENARIO)
{
rct_s6_info info = chunkReader.ReadChunkAs<rct_s6_info>();
*entry = CreateNewScenarioEntry(path, timestamp, &info);
return true;
}
else
{
log_verbose("%s is not a scenario", path.c_str());
}
}
}
catch (Exception)
{
Console::Error::WriteLine("Unable to read scenario: '%s'", path.c_str());
}
return false;
}
scenario_index_entry CreateNewScenarioEntry(const std::string &path, uint64 timestamp, rct_s6_info * s6Info)
{
scenario_index_entry entry = { 0 };
// 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);
}
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;
}
void Sort()
{
if (gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_ORIGIN)