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 index d206c32394..05766786ce 100644 --- a/src/openrct2/core/FileIndex.hpp +++ b/src/openrct2/core/FileIndex.hpp @@ -17,7 +17,13 @@ #pragma once #include +#include +#include #include "../common.h" +#include "File.h" +#include "FileScanner.h" +#include "FileStream.hpp" +#include "Path.hpp" template class FileIndex @@ -29,7 +35,7 @@ private: uint64 TotalFileSize = 0; uint32 FileDateModifiedChecksum = 0; uint32 PathChecksum = 0; - } + }; struct ScanResult { @@ -49,17 +55,17 @@ private: uint8 VersionA = 0; uint8 VersionB = 0; uint16 LanguageId = 0; - DirectoryStats DirectoryStats; + DirectoryStats Stats; uint32 NumItems = 0; }; - constexpr uint8 FILE_INDEX_VERSION = 4; + static constexpr uint8 FILE_INDEX_VERSION = 4; - uint32 _magicNumber; - uint8 _version; - std::string _indexPath; - std::string _pattern; - std::vector _paths; + uint32 const _magicNumber; + uint8 const _version; + std::string const _indexPath; + std::string const _pattern; + std::vector const _paths; public: FileIndex(uint32 magicNumber, @@ -75,11 +81,13 @@ public: { } + 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() + std::vector LoadOrBuild() const { std::vector items; auto scanResult = Scan(); @@ -97,30 +105,32 @@ public: auto item = Create(filePath); items.push_back(item); } - WriteIndexFile(items); + WriteIndexFile(scanResult.Stats, items); } return items; } +protected: /** * Loads the given file and creates the item representing the data to store in the index. */ - virtual TItem Create(const std::string &path) abstract; + virtual TItem Create(const std::string &path) const abstract; /** * Serialises an index item to the given stream. */ - virtual void Serialise(IStream * stream, const TItem item); + virtual void Serialise(IStream * stream, const TItem &item) const abstract; /** * Deserialises an index item from the given stream. */ - virtual TItem Deserialise(IStream * stream) abstract; + virtual TItem Deserialise(IStream * stream) const abstract; private: - ScanResult Scan() + ScanResult Scan() const { - ScanResult scanResult; + DirectoryStats stats; + std::vector files; for (const auto directory : _paths) { auto pattern = Path::Combine(directory, _pattern); @@ -130,42 +140,43 @@ private: auto fileInfo = scanner->GetFileInfo(); auto path = std::string(scanner->GetPath()); - scanResult.Files.push(path); + files.push_back(path); - scanResult.TotalFiles++; - scanResult.TotalFileSize += fileInfo->Size; - scanResult.FileDateModifiedChecksum ^= + stats.TotalFiles++; + stats.TotalFileSize += fileInfo->Size; + stats.FileDateModifiedChecksum ^= (uint32)(fileInfo->LastModified >> 32) ^ (uint32)(fileInfo->LastModified & 0xFFFFFFFF); - scanResult.FileDateModifiedChecksum = ror32(result->FileDateModifiedChecksum, 5); - scanResult.PathChecksum += GetPathChecksum(path); + stats.FileDateModifiedChecksum = ror32(stats.FileDateModifiedChecksum, 5); + stats.PathChecksum += GetPathChecksum(path); } delete scanner; } + return ScanResult(stats, files); } - std::tuple> ReadIndexFile(const DirectoryStats &stats) + std::tuple> ReadIndexFile(const DirectoryStats &stats) const { bool loadedItems = false; std::vector items; try { - auto fs = FileStream(path, FILE_MODE_OPEN); + auto fs = FileStream(_indexPath, FILE_MODE_OPEN); // Read header, check if we need to re-scan auto header = fs.ReadValue(); if (header.MagicNumber == _magicNumber && header.VersionA == FILE_INDEX_VERSION && header.VersionB == _version && - header.TotalFiles == scanResult.TotalFiles && - header.TotalFileSize == scanResult.TotalFileSize && - header.FileDateModifiedChecksum == scanResult.FileDateModifiedChecksum && - header.PathChecksum == scanResult.PathChecksum) + 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); + auto item = Deserialise(&fs); items.push_back(item); } loadedItems = true; @@ -178,7 +189,7 @@ private: return std::make_tuple(loadedItems, items); } - void WriteIndexFile(const DirectoryStats &stats, const std::vector &items) + void WriteIndexFile(const DirectoryStats &stats, const std::vector &items) const { try { @@ -187,23 +198,37 @@ private: // Write header FileIndexHeader header = { 0 }; header.MagicNumber = _magicNumber; - header.Version = SCENARIO_REPOSITORY_VERSION; + header.VersionA = FILE_INDEX_VERSION; + header.VersionB = _version; header.LanguageId = gCurrentLanguage; - header.DirectoryStats = stats; - header.NumItems = items.size(); + header.Stats = stats; + header.NumItems = (uint32)items.size(); fs.WriteValue(header); // Write items for (const auto item : items) { - Serialise(fs, item); + Serialise(&fs, item); } - return true; } catch (const Exception &) { Console::Error::WriteLine("Unable to save index."); - return false; } } + + 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/scenario/ScenarioRepository.cpp b/src/openrct2/scenario/ScenarioRepository.cpp index 9ce8368134..af3b5e5c67 100644 --- a/src/openrct2/scenario/ScenarioRepository.cpp +++ b/src/openrct2/scenario/ScenarioRepository.cpp @@ -19,6 +19,7 @@ #include #include "../core/Console.hpp" #include "../core/File.h" +#include "../core/FileIndex.hpp" #include "../core/FileScanner.h" #include "../core/FileStream.hpp" #include "../core/Math.hpp" @@ -136,6 +137,165 @@ static void scenario_highscore_free(scenario_highscore_entry * highscore) SafeDelete(highscore); } +class ScenarioFileIndex final : public FileIndex +{ +private: + static constexpr uint32 MAGIC_NUMBER = 0x58444953; + static constexpr uint16 VERSION = 1; + static constexpr auto PATTERN = "*.sc4;*.sc6"; + +public: + ScenarioFileIndex(IPlatformEnvironment * env) : + FileIndex(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: + scenario_index_entry Create(const std::string &path) const override + { + scenario_index_entry entry; + auto timestamp = File::GetLastModified(path); + if (!GetScenarioInfo(path, timestamp, &entry)) + { + // TODO + } + return 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: @@ -143,12 +303,13 @@ private: static constexpr uint32 HighscoreFileVersion = 1; IPlatformEnvironment * _env; + ScenarioFileIndex const _fileIndex; std::vector _scenarios; std::vector _highscores; - QueryDirectoryResult _directoryQueryResult = { 0 }; public: ScenarioRepository(IPlatformEnvironment * env) + : _fileIndex(env) { _env = env; } @@ -162,29 +323,16 @@ public: { _scenarios.clear(); - // 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); - - _directoryQueryResult = { 0 }; - Query(rct1dir); - Query(rct2dir); - Query(openrct2dir); - - if (!Load()) + auto scenarios = _fileIndex.LoadOrBuild(); + for (auto scenario : scenarios) { - Scan(rct1dir); - Scan(rct2dir); - Scan(openrct2dir); - Save(); + AddScenario(scenario); } - - ConvertMegaPark(mpdatdir, openrct2dir); + + // std::string mpdatdir = _env->GetFilePath(PATHID::MP_DAT); + // ConvertMegaPark(mpdatdir, openrct2dir); Sort(); - LoadScores(); LoadLegacyScores(); AttachHighscores(); @@ -286,28 +434,6 @@ private: return (scenario_index_entry *)repo->GetByPath(path); } - void Query(const std::string &directory) - { - std::string pattern = Path::Combine(directory, SC_FILE_PATTERN); - Path::QueryDirectory(&_directoryQueryResult, pattern); - } - - void Scan(const std::string &directory) - { - utf8 pattern[MAX_PATH]; - String::Set(pattern, sizeof(pattern), directory.c_str()); - Path::Append(pattern, sizeof(pattern), SC_FILE_PATTERN); - - IFileScanner * scanner = Path::ScanDirectory(pattern, true); - while (scanner->Next()) - { - 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) @@ -340,20 +466,14 @@ private: } } - void AddScenario(const std::string &path, uint64 timestamp) + void AddScenario(const scenario_index_entry &entry) { - scenario_index_entry entry; - if (!GetScenarioInfo(path, timestamp, &entry)) - { - return; - } - - const std::string filename = Path::GetFileName(path); - scenario_index_entry * existingEntry = GetByFilename(filename.c_str()); + 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); @@ -364,7 +484,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()); } @@ -374,115 +494,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) @@ -503,69 +514,6 @@ private: } } - bool Load() - { - std::string path = _env->GetFilePath(PATHID::CACHE_SCENARIOS); - 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 == SCENARIO_REPOSITORY_MAGIC_NUMBER && - header.Version == SCENARIO_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++) - { - auto scenario = fs.ReadValue(); - _scenarios.push_back(scenario); - } - result = true; - } - } - catch (const Exception &) - { - Console::Error::WriteLine("Unable to load scenario repository index."); - } - return result; - } - - void Save() const - { - std::string path = _env->GetFilePath(PATHID::CACHE_SCENARIOS); - try - { - auto fs = FileStream(path, FILE_MODE_WRITE); - - // Write header - ScenarioRepositoryHeader header = { 0 }; - header.MagicNumber = SCENARIO_REPOSITORY_MAGIC_NUMBER; - header.Version = SCENARIO_REPOSITORY_VERSION; - header.TotalFiles = _directoryQueryResult.TotalFiles; - header.TotalFileSize = _directoryQueryResult.TotalFileSize; - header.FileDateModifiedChecksum = _directoryQueryResult.FileDateModifiedChecksum; - header.PathChecksum = _directoryQueryResult.PathChecksum; - header.NumItems = (uint32)_scenarios.size(); - fs.WriteValue(header); - - // Write items - for (const auto scenario : _scenarios) - { - fs.WriteValue(scenario); - } - } - catch (const Exception &) - { - Console::Error::WriteLine("Unable to write scenario repository index."); - } - } - void LoadScores() { std::string path = _env->GetFilePath(PATHID::SCORES);