mirror of https://github.com/OpenRCT2/OpenRCT2.git
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:
commit
87c8204602
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue