diff --git a/distribution/changelog.txt b/distribution/changelog.txt index fadb065552..7a447ba347 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -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) diff --git a/src/openrct2/PlatformEnvironment.cpp b/src/openrct2/PlatformEnvironment.cpp index b79eb7c749..abb711a938 100644 --- a/src/openrct2/PlatformEnvironment.cpp +++ b/src/openrct2/PlatformEnvironment.cpp @@ -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 diff --git a/src/openrct2/PlatformEnvironment.h b/src/openrct2/PlatformEnvironment.h index 1996d5efa1..d0eeeae1fb 100644 --- a/src/openrct2/PlatformEnvironment.h +++ b/src/openrct2/PlatformEnvironment.h @@ -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). diff --git a/src/openrct2/cmdline/RootCommands.cpp b/src/openrct2/cmdline/RootCommands.cpp index 829240b60c..c7cf5498dd 100644 --- a/src/openrct2/cmdline/RootCommands.cpp +++ b/src/openrct2/cmdline/RootCommands.cpp @@ -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; } diff --git a/src/openrct2/core/File.cpp b/src/openrct2/core/File.cpp index b33650b124..ed43400c85 100644 --- a/src/openrct2/core/File.cpp +++ b/src/openrct2/core/File.cpp @@ -14,6 +14,13 @@ *****************************************************************************/ #pragma endregion +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #include +#else + #include +#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" diff --git a/src/openrct2/core/File.h b/src/openrct2/core/File.h index d15481b3b3..2084be8608 100644 --- a/src/openrct2/core/File.h +++ b/src/openrct2/core/File.h @@ -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 ReadAllLines(const std::string &path); + uint64 GetLastModified(const std::string &path); } diff --git a/src/openrct2/core/FileIndex.hpp b/src/openrct2/core/FileIndex.hpp new file mode 100644 index 0000000000..5e9d527f45 --- /dev/null +++ b/src/openrct2/core/FileIndex.hpp @@ -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 +#include +#include +#include +#include "../common.h" +#include "File.h" +#include "FileScanner.h" +#include "FileStream.hpp" +#include "Path.hpp" + +template +class FileIndex +{ +private: + struct DirectoryStats + { + uint32 TotalFiles = 0; + uint64 TotalFileSize = 0; + uint32 FileDateModifiedChecksum = 0; + uint32 PathChecksum = 0; + }; + + struct ScanResult + { + DirectoryStats const Stats; + std::vector const Files; + + ScanResult(DirectoryStats stats, std::vector 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 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 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 LoadOrBuild() const + { + std::vector 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 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 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 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 Build(const ScanResult &scanResult) const + { + std::vector 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)(endTime - startTime); + Console::WriteLine("Finished building %s in %.2f seconds.", _name.c_str(), duration.count()); + return items; + } + + std::tuple> ReadIndexFile(const DirectoryStats &stats) const + { + bool loadedItems = false; + std::vector 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(); + 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 &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; + } +}; diff --git a/src/openrct2/core/String.cpp b/src/openrct2/core/String.cpp index 650bbfca28..febc475c10 100644 --- a/src/openrct2/core/String.cpp +++ b/src/openrct2/core/String.cpp @@ -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; diff --git a/src/openrct2/core/String.hpp b/src/openrct2/core/String.hpp index 97d43173df..74613b02a9 100644 --- a/src/openrct2/core/String.hpp +++ b/src/openrct2/core/String.hpp @@ -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); diff --git a/src/openrct2/object/ObjectManager.cpp b/src/openrct2/object/ObjectManager.cpp index 0995316594..f4885539bd 100644 --- a/src/openrct2/object/ObjectManager.cpp +++ b/src/openrct2/object/ObjectManager.cpp @@ -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); diff --git a/src/openrct2/object/ObjectRepository.cpp b/src/openrct2/object/ObjectRepository.cpp index ce262302fe..3bc47ff295 100644 --- a/src/openrct2/object/ObjectRepository.cpp +++ b/src/openrct2/object/ObjectRepository.cpp @@ -15,14 +15,13 @@ #pragma endregion #include -#include #include #include #include #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 { - const IPlatformEnvironment * _env = nullptr; - std::vector _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({ + env->GetDirectoryPath(DIRBASE::RCT2, DIRID::OBJECT), + env->GetDirectoryPath(DIRBASE::USER, DIRID::OBJECT) })) + { + } + +public: + std::tuple 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(item.RideFlags); + for (sint32 i = 0; i < 2; i++) + { + stream->WriteValue(item.RideCategory[i]); + } + for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++) + { + stream->WriteValue(item.RideType[i]); + } + stream->WriteValue(item.RideGroupIndex); + break; + case OBJECT_TYPE_SCENERY_SETS: + stream->WriteValue(item.NumThemeObjects); + for (uint16 i = 0; i < item.NumThemeObjects; i++) + { + stream->WriteValue(item.ThemeObjects[i]); + } + break; + } + } + + ObjectRepositoryItem Deserialise(IStream * stream) const override + { + ObjectRepositoryItem item = { 0 }; + + item.ObjectEntry = stream->ReadValue(); + item.Path = stream->ReadString(); + item.Name = stream->ReadString(); + + switch (item.ObjectEntry.flags & 0x0F) { + case OBJECT_TYPE_RIDE: + item.RideFlags = stream->ReadValue(); + for (sint32 i = 0; i < 2; i++) + { + item.RideCategory[i] = stream->ReadValue(); + } + for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++) + { + item.RideType[i] = stream->ReadValue(); + } + item.RideGroupIndex = stream->ReadValue(); + break; + case OBJECT_TYPE_SCENERY_SETS: + item.NumThemeObjects = stream->ReadValue(); + item.ThemeObjects = Memory::AllocateArray(item.NumThemeObjects); + for (uint16 i = 0; i < item.NumThemeObjects; i++) + { + item.ThemeObjects[i] = stream->ReadValue(); + } + 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 _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 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(); - - 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(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 &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(); - item.Path = stream->ReadString(); - item.Name = stream->ReadString(); - - switch (item.ObjectEntry.flags & 0x0F) { - case OBJECT_TYPE_RIDE: - item.RideFlags = stream->ReadValue(); - for (sint32 i = 0; i < 2; i++) - { - item.RideCategory[i] = stream->ReadValue(); - } - for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++) - { - item.RideType[i] = stream->ReadValue(); - } - item.RideGroupIndex = stream->ReadValue(); - break; - case OBJECT_TYPE_SCENERY_SETS: - item.NumThemeObjects = stream->ReadValue(); - item.ThemeObjects = Memory::AllocateArray(item.NumThemeObjects); - for (uint16 i = 0; i < item.NumThemeObjects; i++) - { - item.ThemeObjects[i] = stream->ReadValue(); - } - 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(item.RideFlags); - for (sint32 i = 0; i < 2; i++) - { - stream->WriteValue(item.RideCategory[i]); - } - for (sint32 i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++) - { - stream->WriteValue(item.RideType[i]); - } - stream->WriteValue(item.RideGroupIndex); - break; - case OBJECT_TYPE_SCENERY_SETS: - stream->WriteValue(item.NumThemeObjects); - for (uint16 i = 0; i < item.NumThemeObjects; i++) - { - stream->WriteValue(item.ThemeObjects[i]); - } - break; + auto result = _fileIndex.Create(path); + if (std::get<0>(result)) + { + auto ori = std::get<1>(result); + AddItem(ori); } } diff --git a/src/openrct2/ride/TrackDesignRepository.cpp b/src/openrct2/ride/TrackDesignRepository.cpp index 944559417e..0ea0433bb0 100644 --- a/src/openrct2/ride/TrackDesignRepository.cpp +++ b/src/openrct2/ride/TrackDesignRepository.cpp @@ -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 +{ +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({ + env->GetDirectoryPath(DIRBASE::RCT1, DIRID::TRACK), + env->GetDirectoryPath(DIRBASE::RCT2, DIRID::TRACK), + env->GetDirectoryPath(DIRBASE::USER, DIRID::TRACK) })) + { + } + +public: + std::tuple 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(); + item.ObjectEntry = stream->ReadStdString(); + item.Flags = stream->ReadValue(); + 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 _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(); - 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(); - item.ObjectEntry = fs.ReadStdString(); - item.Flags = fs.ReadValue(); - _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)); } } diff --git a/src/openrct2/scenario/ScenarioRepository.cpp b/src/openrct2/scenario/ScenarioRepository.cpp index f25ba11c4f..dc52dd8e6e 100644 --- a/src/openrct2/scenario/ScenarioRepository.cpp +++ b/src/openrct2/scenario/ScenarioRepository.cpp @@ -19,7 +19,7 @@ #include #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 +{ +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({ + env->GetDirectoryPath(DIRBASE::RCT1, DIRID::SCENARIO), + env->GetDirectoryPath(DIRBASE::RCT2, DIRID::SCENARIO), + env->GetDirectoryPath(DIRBASE::USER, DIRID::SCENARIO) })) + { + } + +protected: + std::tuple 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(); + // 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(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(); + if (header.type == S6_TYPE_SCENARIO) + { + rct_s6_info info = chunkReader.ReadChunkAs(); + *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 _scenarios; std::vector _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(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(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(); - if (header.type == S6_TYPE_SCENARIO) - { - rct_s6_info info = chunkReader.ReadChunkAs(); - *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)