/***************************************************************************** * Copyright (c) 2014-2024 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "ObjectFactory.h" #include "../Context.h" #include "../OpenRCT2.h" #include "../PlatformEnvironment.h" #include "../audio/audio.h" #include "../core/Console.hpp" #include "../core/File.h" #include "../core/FileStream.h" #include "../core/Json.hpp" #include "../core/Memory.hpp" #include "../core/MemoryStream.h" #include "../core/Path.hpp" #include "../core/String.hpp" #include "../core/Zip.h" #include "../rct12/SawyerChunkReader.h" #include "AudioObject.h" #include "BannerObject.h" #include "EntranceObject.h" #include "FootpathObject.h" #include "FootpathRailingsObject.h" #include "FootpathSurfaceObject.h" #include "LargeSceneryObject.h" #include "MusicObject.h" #include "Object.h" #include "ObjectLimits.h" #include "ObjectList.h" #include "PathAdditionObject.h" #include "RideObject.h" #include "SceneryGroupObject.h" #include "SmallSceneryObject.h" #include "StationObject.h" #include "TerrainEdgeObject.h" #include "TerrainSurfaceObject.h" #include "WallObject.h" #include "WaterObject.h" #include #include #include struct IFileDataRetriever { virtual ~IFileDataRetriever() = default; virtual std::vector GetData(std::string_view path) const abstract; virtual ObjectAsset GetAsset(std::string_view path) const abstract; }; class FileSystemDataRetriever : public IFileDataRetriever { private: std::string _basePath; public: FileSystemDataRetriever(std::string_view basePath) : _basePath(basePath) { } std::vector GetData(std::string_view path) const override { auto absolutePath = Path::Combine(_basePath, path); return File::ReadAllBytes(absolutePath); } ObjectAsset GetAsset(std::string_view path) const override { if (Path::IsAbsolute(path)) { return ObjectAsset(path); } else { auto absolutePath = Path::Combine(_basePath, path); return ObjectAsset(absolutePath); } } }; class ZipDataRetriever : public IFileDataRetriever { private: const std::string _path; const IZipArchive& _zipArchive; public: ZipDataRetriever(std::string_view path, const IZipArchive& zipArchive) : _path(path) , _zipArchive(zipArchive) { } std::vector GetData(std::string_view path) const override { return _zipArchive.GetFileData(path); } ObjectAsset GetAsset(std::string_view path) const override { return ObjectAsset(_path, path); } }; class ReadObjectContext : public IReadObjectContext { private: IObjectRepository& _objectRepository; const IFileDataRetriever* _fileDataRetriever; std::string _identifier; bool _loadImages; std::string _basePath; bool _wasVerbose = false; bool _wasWarning = false; bool _wasError = false; public: bool WasVerbose() const { return _wasVerbose; } bool WasWarning() const { return _wasWarning; } bool WasError() const { return _wasError; } ReadObjectContext( IObjectRepository& objectRepository, const std::string& identifier, bool loadImages, const IFileDataRetriever* fileDataRetriever) : _objectRepository(objectRepository) , _fileDataRetriever(fileDataRetriever) , _identifier(identifier) , _loadImages(loadImages) { } std::string_view GetObjectIdentifier() override { return _identifier; } IObjectRepository& GetObjectRepository() override { return _objectRepository; } bool ShouldLoadImages() override { return _loadImages; } std::vector GetData(std::string_view path) override { if (_fileDataRetriever != nullptr) { return _fileDataRetriever->GetData(path); } return {}; } ObjectAsset GetAsset(std::string_view path) override { if (_fileDataRetriever != nullptr) { return _fileDataRetriever->GetAsset(path); } return {}; } void LogVerbose(ObjectError code, const utf8* text) override { _wasVerbose = true; if (!String::IsNullOrEmpty(text)) { LOG_VERBOSE("[%s] Info (%d): %s", _identifier.c_str(), code, text); } } void LogWarning(ObjectError code, const utf8* text) override { _wasWarning = true; if (!String::IsNullOrEmpty(text)) { Console::Error::WriteLine("[%s] Warning (%d): %s", _identifier.c_str(), code, text); } } void LogError(ObjectError code, const utf8* text) override { _wasError = true; if (!String::IsNullOrEmpty(text)) { Console::Error::WriteLine("[%s] Error (%d): %s", _identifier.c_str(), code, text); } } }; namespace ObjectFactory { /** * @param jRoot Must be JSON node of type object * @note jRoot is deliberately left non-const: json_t behaviour changes when const */ static std::unique_ptr CreateObjectFromJson( IObjectRepository& objectRepository, json_t& jRoot, const IFileDataRetriever* fileRetriever, bool loadImageTable); static ObjectSourceGame ParseSourceGame(const std::string& s) { static const std::unordered_map LookupTable{ { "rct1", ObjectSourceGame::RCT1 }, { "rct1aa", ObjectSourceGame::AddedAttractions }, { "rct1ll", ObjectSourceGame::LoopyLandscapes }, { "rct2", ObjectSourceGame::RCT2 }, { "rct2ww", ObjectSourceGame::WackyWorlds }, { "rct2tt", ObjectSourceGame::TimeTwister }, { "official", ObjectSourceGame::OpenRCT2Official }, { "custom", ObjectSourceGame::Custom }, }; auto result = LookupTable.find(s); return (result != LookupTable.end()) ? result->second : ObjectSourceGame::Custom; } static void ReadObjectLegacy(Object& object, IReadObjectContext* context, OpenRCT2::IStream* stream) { try { object.ReadLegacy(context, stream); } catch (const IOException&) { // TODO check that ex is really EOF and not some other error context->LogError(ObjectError::UnexpectedEOF, "Unexpectedly reached end of file."); } catch (const std::exception&) { context->LogError(ObjectError::Unknown, nullptr); } } std::unique_ptr CreateObjectFromLegacyFile(IObjectRepository& objectRepository, const utf8* path, bool loadImages) { LOG_VERBOSE("CreateObjectFromLegacyFile(..., \"%s\")", path); std::unique_ptr result; try { auto fs = OpenRCT2::FileStream(path, OpenRCT2::FILE_MODE_OPEN); auto chunkReader = SawyerChunkReader(&fs); RCTObjectEntry entry = fs.ReadValue(); if (entry.GetType() != ObjectType::ScenarioText) { result = CreateObject(entry.GetType()); result->SetDescriptor(ObjectEntryDescriptor(entry)); utf8 objectName[DAT_NAME_LENGTH + 1] = { 0 }; ObjectEntryGetNameFixed(objectName, sizeof(objectName), &entry); LOG_VERBOSE(" entry: { 0x%08X, \"%s\", 0x%08X }", entry.flags, objectName, entry.checksum); auto chunk = chunkReader.ReadChunk(); LOG_VERBOSE(" size: %zu", chunk->GetLength()); auto chunkStream = OpenRCT2::MemoryStream(chunk->GetData(), chunk->GetLength()); auto readContext = ReadObjectContext(objectRepository, objectName, loadImages, nullptr); ReadObjectLegacy(*result, &readContext, &chunkStream); if (readContext.WasError()) { throw std::runtime_error("Object has errors"); } result->SetSourceGames({ entry.GetSourceGame() }); } } catch (const std::exception& e) { LOG_ERROR("Error: %s when processing object %s", e.what(), path); } return result; } std::unique_ptr CreateObjectFromLegacyData( IObjectRepository& objectRepository, const RCTObjectEntry* entry, const void* data, size_t dataSize) { Guard::ArgumentNotNull(entry, GUARD_LINE); Guard::ArgumentNotNull(data, GUARD_LINE); auto result = CreateObject(entry->GetType()); if (result != nullptr) { result->SetDescriptor(ObjectEntryDescriptor(*entry)); utf8 objectName[DAT_NAME_LENGTH + 1]; ObjectEntryGetNameFixed(objectName, sizeof(objectName), entry); auto readContext = ReadObjectContext(objectRepository, objectName, !gOpenRCT2NoGraphics, nullptr); auto chunkStream = OpenRCT2::MemoryStream(data, dataSize); ReadObjectLegacy(*result, &readContext, &chunkStream); if (readContext.WasError()) { LOG_ERROR("Error when processing object."); } else { result->SetSourceGames({ entry->GetSourceGame() }); } } return result; } std::unique_ptr CreateObject(ObjectType type) { std::unique_ptr result; switch (type) { case ObjectType::Ride: result = std::make_unique(); break; case ObjectType::SmallScenery: result = std::make_unique(); break; case ObjectType::LargeScenery: result = std::make_unique(); break; case ObjectType::Walls: result = std::make_unique(); break; case ObjectType::Banners: result = std::make_unique(); break; case ObjectType::Paths: result = std::make_unique(); break; case ObjectType::PathAdditions: result = std::make_unique(); break; case ObjectType::SceneryGroup: result = std::make_unique(); break; case ObjectType::ParkEntrance: result = std::make_unique(); break; case ObjectType::Water: result = std::make_unique(); break; case ObjectType::ScenarioText: break; case ObjectType::TerrainSurface: result = std::make_unique(); break; case ObjectType::TerrainEdge: result = std::make_unique(); break; case ObjectType::Station: result = std::make_unique(); break; case ObjectType::Music: result = std::make_unique(); break; case ObjectType::FootpathSurface: result = std::make_unique(); break; case ObjectType::FootpathRailings: result = std::make_unique(); break; case ObjectType::Audio: result = std::make_unique(); break; default: throw std::runtime_error("Invalid object type"); } return result; } static ObjectType ParseObjectType(const std::string& s) { if (s == "ride") return ObjectType::Ride; if (s == "footpath_banner") return ObjectType::Banners; if (s == "footpath_item") return ObjectType::PathAdditions; if (s == "scenery_small") return ObjectType::SmallScenery; if (s == "scenery_large") return ObjectType::LargeScenery; if (s == "scenery_wall") return ObjectType::Walls; if (s == "scenery_group") return ObjectType::SceneryGroup; if (s == "park_entrance") return ObjectType::ParkEntrance; if (s == "water") return ObjectType::Water; if (s == "terrain_surface") return ObjectType::TerrainSurface; if (s == "terrain_edge") return ObjectType::TerrainEdge; if (s == "station") return ObjectType::Station; if (s == "music") return ObjectType::Music; if (s == "footpath_surface") return ObjectType::FootpathSurface; if (s == "footpath_railings") return ObjectType::FootpathRailings; if (s == "audio") return ObjectType::Audio; return ObjectType::None; } std::unique_ptr CreateObjectFromZipFile(IObjectRepository& objectRepository, std::string_view path, bool loadImages) { try { auto archive = Zip::Open(path, ZIP_ACCESS::READ); auto jsonBytes = archive->GetFileData("object.json"); if (jsonBytes.empty()) { throw std::runtime_error("Unable to open object.json."); } json_t jRoot = Json::FromVector(jsonBytes); if (jRoot.is_object()) { auto fileDataRetriever = ZipDataRetriever(path, *archive); return CreateObjectFromJson(objectRepository, jRoot, &fileDataRetriever, loadImages); } } catch (const std::exception& e) { Console::Error::WriteLine("Unable to open or read '%s': %s", std::string(path).c_str(), e.what()); } return nullptr; } std::unique_ptr CreateObjectFromJsonFile( IObjectRepository& objectRepository, const std::string& path, bool loadImages) { LOG_VERBOSE("CreateObjectFromJsonFile(\"%s\")", path.c_str()); try { json_t jRoot = Json::ReadFromFile(path.c_str()); auto fileDataRetriever = FileSystemDataRetriever(Path::GetDirectory(path)); return CreateObjectFromJson(objectRepository, jRoot, &fileDataRetriever, loadImages); } catch (const std::runtime_error& err) { Console::Error::WriteLine("Unable to open or read '%s': %s", path.c_str(), err.what()); } return nullptr; } static void ExtractSourceGames(const std::string& id, json_t& jRoot, Object& result) { auto sourceGames = jRoot["sourceGame"]; if (sourceGames.is_array() || sourceGames.is_string()) { std::vector sourceGameVector; for (const auto& jSourceGame : sourceGames) { sourceGameVector.push_back(ParseSourceGame(Json::GetString(jSourceGame))); } if (!sourceGameVector.empty()) { result.SetSourceGames(sourceGameVector); } else { LOG_ERROR("Object %s has an incorrect sourceGame parameter.", id.c_str()); result.SetSourceGames({ ObjectSourceGame::Custom }); } } // >90% of objects are custom, so allow omitting the parameter without displaying an error. else if (sourceGames.is_null()) { result.SetSourceGames({ ObjectSourceGame::Custom }); } else { LOG_ERROR("Object %s has an incorrect sourceGame parameter.", id.c_str()); result.SetSourceGames({ ObjectSourceGame::Custom }); } } static bool isUsingClassic() { auto env = OpenRCT2::GetContext()->GetPlatformEnvironment(); return env->IsUsingClassic(); } std::unique_ptr CreateObjectFromJson( IObjectRepository& objectRepository, json_t& jRoot, const IFileDataRetriever* fileRetriever, bool loadImageTable) { if (!jRoot.is_object()) { throw std::runtime_error("Object JSON root was not an object"); } LOG_VERBOSE("CreateObjectFromJson(...)"); std::unique_ptr result; auto objectType = ParseObjectType(Json::GetString(jRoot["objectType"])); if (objectType != ObjectType::None) { auto id = Json::GetString(jRoot["id"]); // Base audio files are renamed to a common, virtual name so asset packs can override it correctly. const bool isRCT2BaseAudio = id == OpenRCT2::Audio::AudioObjectIdentifiers::kRCT2Base && !isUsingClassic(); const bool isRCTCBaseAudio = id == OpenRCT2::Audio::AudioObjectIdentifiers::kRCTCBase && isUsingClassic(); if (isRCT2BaseAudio || isRCTCBaseAudio) { id = OpenRCT2::Audio::AudioObjectIdentifiers::kRCT2; } auto version = VersionTuple(Json::GetString(jRoot["version"])); ObjectEntryDescriptor descriptor; auto originalId = Json::GetString(jRoot["originalId"]); if (originalId.length() == 8 + 1 + 8 + 1 + 8) { auto originalName = originalId.substr(9, 8); RCTObjectEntry entry = {}; entry.flags = std::stoul(originalId.substr(0, 8), nullptr, 16); entry.checksum = std::stoul(originalId.substr(18, 8), nullptr, 16); auto minLength = std::min(8, originalName.length()); std::memcpy(entry.name, originalName.c_str(), minLength); // Some bad objects try to override different types if (entry.GetType() != objectType) { LOG_ERROR( "Object \"%s\" has invalid originalId set \"%s\". Ignoring override.", id.c_str(), originalId.c_str()); descriptor = ObjectEntryDescriptor(objectType, id); } else { descriptor = ObjectEntryDescriptor(entry); } } else { descriptor = ObjectEntryDescriptor(objectType, id); } descriptor.Version = version; result = CreateObject(objectType); result->SetVersion(version); result->SetIdentifier(id); result->SetDescriptor(descriptor); result->MarkAsJsonObject(); auto readContext = ReadObjectContext(objectRepository, id, loadImageTable, fileRetriever); result->ReadJson(&readContext, jRoot); if (readContext.WasError()) { throw std::runtime_error("Object has errors"); } auto jAuthors = jRoot["authors"]; std::vector authorVector; for (const auto& jAuthor : jAuthors) { if (jAuthor.is_string()) { authorVector.emplace_back(Json::GetString(jAuthor)); } } result->SetAuthors(std::move(authorVector)); result->SetIsCompatibilityObject(Json::GetBoolean(jRoot["isCompatibilityObject"])); ExtractSourceGames(id, jRoot, *result); } return result; } } // namespace ObjectFactory