/***************************************************************************** * 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 "../Cheats.h" #include "../Context.h" #include "../Editor.h" #include "../Game.h" #include "../GameState.h" #include "../ParkImporter.h" #include "../actions/WallPlaceAction.h" #include "../audio/audio.h" #include "../core/BitSet.hpp" #include "../core/Collections.hpp" #include "../core/Console.hpp" #include "../core/FileStream.h" #include "../core/Guard.hpp" #include "../core/IStream.hpp" #include "../core/Memory.hpp" #include "../core/Path.hpp" #include "../core/String.hpp" #include "../entity/Balloon.h" #include "../entity/Duck.h" #include "../entity/EntityList.h" #include "../entity/EntityRegistry.h" #include "../entity/Fountain.h" #include "../entity/Litter.h" #include "../entity/MoneyEffect.h" #include "../entity/Particle.h" #include "../entity/PatrolArea.h" #include "../entity/Peep.h" #include "../entity/Staff.h" #include "../interface/Window.h" #include "../localisation/Date.h" #include "../localisation/Localisation.h" #include "../management/Award.h" #include "../management/Finance.h" #include "../management/Marketing.h" #include "../management/NewsItem.h" #include "../object/Object.h" #include "../object/ObjectList.h" #include "../object/ObjectManager.h" #include "../object/ObjectRepository.h" #include "../peep/RideUseSystem.h" #include "../rct12/EntryList.h" #include "../ride/RideData.h" #include "../ride/Station.h" #include "../ride/Track.h" #include "../ride/TrainManager.h" #include "../ride/Vehicle.h" #include "../scenario/Scenario.h" #include "../scenario/ScenarioRepository.h" #include "../scenario/ScenarioSources.h" #include "../util/SawyerCoding.h" #include "../util/Util.h" #include "../world/Climate.h" #include "../world/Entrance.h" #include "../world/Footpath.h" #include "../world/MapAnimation.h" #include "../world/Park.h" #include "../world/Scenery.h" #include "../world/Surface.h" #include "../world/TilePointerIndex.hpp" #include "../world/Wall.h" #include "RCT1.h" #include "Tables.h" #include #include #include #include using namespace OpenRCT2; static constexpr ObjectEntryIndex ObjectEntryIndexIgnore = 254; namespace RCT1 { class S4Importer final : public IParkImporter { private: std::string _s4Path; S4 _s4 = {}; uint8_t _gameVersion = 0; uint8_t _parkValueConversionFactor = 0; bool _isScenario = false; // Lists of dynamic object entries RCT12::EntryList _rideEntries; RCT12::EntryList _smallSceneryEntries; RCT12::EntryList _largeSceneryEntries; RCT12::EntryList _wallEntries; RCT12::EntryList _bannerEntries; RCT12::EntryList _pathEntries; RCT12::EntryList _pathAdditionEntries; RCT12::EntryList _sceneryGroupEntries; RCT12::EntryList _waterEntry; RCT12::EntryList _terrainSurfaceEntries; RCT12::EntryList _terrainEdgeEntries; RCT12::EntryList _footpathSurfaceEntries; RCT12::EntryList _footpathRailingsEntries; // Lookup tables for converting from RCT1 hard coded types to the new dynamic object entries ObjectEntryIndex _rideTypeToRideEntryMap[EnumValue(RideType::Count)]{}; ObjectEntryIndex _vehicleTypeToRideEntryMap[EnumValue(VehicleType::Count)]{}; ObjectEntryIndex _smallSceneryTypeToEntryMap[256]{}; ObjectEntryIndex _largeSceneryTypeToEntryMap[256]{}; ObjectEntryIndex _wallTypeToEntryMap[256]{}; ObjectEntryIndex _bannerTypeToEntryMap[9]{}; ObjectEntryIndex _pathTypeToEntryMap[24]{}; ObjectEntryIndex _pathAdditionTypeToEntryMap[16]{}; ObjectEntryIndex _sceneryThemeTypeToEntryMap[24]{}; ObjectEntryIndex _terrainSurfaceTypeToEntryMap[16]{}; ObjectEntryIndex _terrainEdgeTypeToEntryMap[16]{}; ObjectEntryIndex _footpathSurfaceTypeToEntryMap[32]{}; ObjectEntryIndex _footpathRailingsTypeToEntryMap[4]{}; // Research BitSet _researchRideEntryUsed{}; BitSet _researchRideTypeUsed{}; // Scenario repository - used for determining scenario name IScenarioRepository* _scenarioRepository = GetScenarioRepository(); public: ParkLoadResult Load(const u8string& path) override { const auto extension = Path::GetExtension(path); if (String::IEquals(extension, ".sc4")) { return LoadScenario(path); } if (String::IEquals(extension, ".sv4")) { return LoadSavedGame(path); } throw std::runtime_error("Invalid RCT1 park extension."); } ParkLoadResult LoadSavedGame(const u8string& path, bool skipObjectCheck = false) override { auto fs = FileStream(path, FILE_MODE_OPEN); auto result = LoadFromStream(&fs, false, skipObjectCheck, path); return result; } ParkLoadResult LoadScenario(const u8string& path, bool skipObjectCheck = false) override { auto fs = FileStream(path, FILE_MODE_OPEN); auto result = LoadFromStream(&fs, true, skipObjectCheck, path); return result; } ParkLoadResult LoadFromStream( IStream* stream, bool isScenario, [[maybe_unused]] bool skipObjectCheck, const u8string& path) override { _s4 = *ReadAndDecodeS4(stream, isScenario); _s4Path = path; _isScenario = isScenario; _gameVersion = SawyerCodingDetectRCT1Version(_s4.GameVersion) & FILE_VERSION_MASK; // Only determine what objects we required to import this saved game InitialiseEntryMaps(); CreateAvailableObjectMappings(); return ParkLoadResult(GetRequiredObjects()); } void Import(GameState_t& gameState) override { Initialise(gameState); CreateAvailableObjectMappings(); ImportRides(); ImportRideMeasurements(); ImportEntities(); ImportTileElements(); ImportPeepSpawns(); ImportFinance(gameState); ImportResearch(gameState); ImportParkName(); ImportParkFlags(gameState); ImportClimate(gameState); ImportScenarioNameDetails(gameState); ImportScenarioObjective(gameState); ImportSavedView(); FixLandOwnership(); FixUrbanPark(); FixNextGuestNumber(gameState); CountBlockSections(); SetDefaultNames(); DetermineRideEntranceAndExitLocations(); ResearchDetermineFirstOfType(); CheatsReset(); ClearRestrictedScenery(); RestrictAllMiscScenery(); } bool GetDetails(ScenarioIndexEntry* dst) override { *dst = {}; SourceDescriptor desc; // If no entry is found, this is a custom scenario. bool isOfficial = ScenarioSources::TryGetById(_s4.ScenarioSlotIndex, &desc); dst->Category = desc.category; dst->SourceGame = ScenarioSource{ desc.source }; dst->SourceIndex = desc.index; dst->ScenarioId = desc.id; dst->ObjectiveType = _s4.ScenarioObjectiveType; dst->ObjectiveArg1 = _s4.ScenarioObjectiveYears; // RCT1 used another way of calculating park value. if (_s4.ScenarioObjectiveType == OBJECTIVE_PARK_VALUE_BY) dst->ObjectiveArg2 = CorrectRCT1ParkValue(_s4.ScenarioObjectiveCurrency); else dst->ObjectiveArg2 = _s4.ScenarioObjectiveCurrency; dst->ObjectiveArg3 = _s4.ScenarioObjectiveNumGuests; // This does not seem to be saved in the objective arguments, so look up the ID from the available rides instead. if (_s4.ScenarioObjectiveType == OBJECTIVE_BUILD_THE_BEST) { dst->ObjectiveArg3 = GetBuildTheBestRideId(); } auto name = RCT2StringToUTF8(_s4.ScenarioName, RCT2LanguageId::EnglishUK); std::string details; // TryGetById won't set this property if the scenario is not recognised, // but localisation needs it. if (!isOfficial) { desc.title = name.c_str(); } String::Set(dst->InternalName, sizeof(dst->InternalName), desc.title); StringId localisedStringIds[3]; if (LanguageGetLocalisedScenarioStrings(desc.title, localisedStringIds)) { if (localisedStringIds[0] != STR_NONE) { name = String::ToStd(LanguageGetString(localisedStringIds[0])); } if (localisedStringIds[2] != STR_NONE) { details = String::ToStd(LanguageGetString(localisedStringIds[2])); } } String::Set(dst->Name, sizeof(dst->Name), name.c_str()); String::Set(dst->Details, sizeof(dst->Details), details.c_str()); return true; } money64 CorrectRCT1ParkValue(money32 oldParkValue) { if (oldParkValue == kMoney32Undefined) { return kMoney64Undefined; } if (_parkValueConversionFactor == 0) { if (_s4.ParkValue != 0) { // Use the ratio between the old and new park value to calcute the ratio to // use for the park value history and the goal. _parkValueConversionFactor = (Park::CalculateParkValue() * 10) / _s4.ParkValue; } else { // In new games, the park value isn't set. _parkValueConversionFactor = 100; } } return (oldParkValue * _parkValueConversionFactor) / 10; } private: std::unique_ptr ReadAndDecodeS4(IStream* stream, bool isScenario) { auto s4 = std::make_unique(); size_t dataSize = stream->GetLength() - stream->GetPosition(); auto data = stream->ReadArray(dataSize); auto decodedData = std::make_unique(sizeof(S4)); size_t decodedSize; int32_t fileType = SawyerCodingDetectFileType(data.get(), dataSize); if (isScenario && (fileType & FILE_VERSION_MASK) != FILE_VERSION_RCT1) { decodedSize = SawyerCodingDecodeSC4(data.get(), decodedData.get(), dataSize, sizeof(S4)); } else { decodedSize = SawyerCodingDecodeSV4(data.get(), decodedData.get(), dataSize, sizeof(S4)); } if (decodedSize == sizeof(S4)) { std::memcpy(s4.get(), decodedData.get(), sizeof(S4)); return s4; } throw std::runtime_error("Unable to decode park."); } void Initialise(GameState_t& gameState) { // Avoid reusing the value used for last import _parkValueConversionFactor = 0; uint16_t mapSize = _s4.MapSize == 0 ? Limits::MaxMapSize : _s4.MapSize; gScenarioFileName = GetRCT1ScenarioName(); // Do map initialisation, same kind of stuff done when loading scenario editor gameStateInitAll(gameState, { mapSize, mapSize }); gameState.EditorStep = EditorStep::ObjectSelection; gameState.Park.Flags |= PARK_FLAGS_SHOW_REAL_GUEST_NAMES; gameState.ScenarioCategory = SCENARIO_CATEGORY_OTHER; } std::string GetRCT1ScenarioName() { const ScenarioIndexEntry* scenarioEntry = _scenarioRepository->GetByInternalName(_s4.ScenarioName); if (scenarioEntry == nullptr) { return ""; } return Path::GetFileName(scenarioEntry->Path); } void InitialiseEntryMaps() { std::fill(std::begin(_rideTypeToRideEntryMap), std::end(_rideTypeToRideEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_vehicleTypeToRideEntryMap), std::end(_vehicleTypeToRideEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_smallSceneryTypeToEntryMap), std::end(_smallSceneryTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_largeSceneryTypeToEntryMap), std::end(_largeSceneryTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_wallTypeToEntryMap), std::end(_wallTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_bannerTypeToEntryMap), std::end(_bannerTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_pathTypeToEntryMap), std::end(_pathTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_pathAdditionTypeToEntryMap), std::end(_pathAdditionTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_sceneryThemeTypeToEntryMap), std::end(_sceneryThemeTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill( std::begin(_terrainSurfaceTypeToEntryMap), std::end(_terrainSurfaceTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill(std::begin(_terrainEdgeTypeToEntryMap), std::end(_terrainEdgeTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill( std::begin(_footpathSurfaceTypeToEntryMap), std::end(_footpathSurfaceTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); std::fill( std::begin(_footpathRailingsTypeToEntryMap), std::end(_footpathRailingsTypeToEntryMap), OBJECT_ENTRY_INDEX_NULL); } /** * Scans the map and research list for all the object types used and builds lists and * lookup tables for converting from hard coded RCT1 object types to dynamic object entries. */ void CreateAvailableObjectMappings() { AddDefaultEntries(); AddAvailableEntriesFromResearchList(); AddAvailableEntriesFromMap(); AddAvailableEntriesFromRides(); AddAvailableEntriesFromSceneryGroups(); AddAvailableEntriesFromBannerList(); AddEntryForWater(); } void AddDefaultEntries() { // Add default scenery groups _sceneryGroupEntries.AddRange({ "rct2.scenery_group.scgtrees", "rct2.scenery_group.scgshrub", "rct2.scenery_group.scggardn", "rct2.scenery_group.scgfence", "rct2.scenery_group.scgwalls", "rct2.scenery_group.scgpathx", }); // Add default footpaths _footpathSurfaceEntries.AddRange( { "rct1.footpath_surface.tarmac", "rct1.footpath_surface.dirt", "rct1.footpath_surface.crazy_paving", "rct1.footpath_surface.tiles_brown", "rct1aa.footpath_surface.ash", "rct1aa.footpath_surface.tarmac_green", "rct1aa.footpath_surface.tarmac_brown", "rct1aa.footpath_surface.tiles_grey", "rct1aa.footpath_surface.tarmac_red", "rct1ll.footpath_surface.tiles_green", "rct1ll.footpath_surface.tiles_red", "rct1.footpath_surface.queue_blue", "rct1aa.footpath_surface.queue_red", "rct1aa.footpath_surface.queue_yellow", "rct1aa.footpath_surface.queue_green" }); _footpathRailingsEntries.AddRange({ "rct2.footpath_railings.wood", "rct1ll.footpath_railings.space", "rct1ll.footpath_railings.bamboo", "rct2.footpath_railings.concrete" }); // Add default surfaces _terrainSurfaceEntries.AddRange( { "rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.dirt", "rct2.terrain_surface.rock", "rct2.terrain_surface.martian", "rct2.terrain_surface.chequerboard", "rct2.terrain_surface.grass_clumps", "rct2.terrain_surface.ice", "rct2.terrain_surface.grid_red", "rct2.terrain_surface.grid_yellow", "rct2.terrain_surface.grid_purple", "rct2.terrain_surface.grid_green", "rct2.terrain_surface.sand_red", "rct2.terrain_surface.sand_brown", "rct1aa.terrain_surface.roof_red", "rct1ll.terrain_surface.roof_grey", "rct1ll.terrain_surface.rust", "rct1ll.terrain_surface.wood" }); // Add default edges _terrainEdgeEntries.AddRange({ "rct2.terrain_edge.rock", "rct2.terrain_edge.wood_red", "rct2.terrain_edge.wood_black", "rct2.terrain_edge.ice", "rct1.terrain_edge.brick", "rct1.terrain_edge.iron", "rct1aa.terrain_edge.grey", "rct1aa.terrain_edge.yellow", "rct1aa.terrain_edge.red", "rct1ll.terrain_edge.purple", "rct1ll.terrain_edge.green", "rct1ll.terrain_edge.stone_brown", "rct1ll.terrain_edge.stone_grey", "rct1ll.terrain_edge.skyscraper_a", "rct1ll.terrain_edge.skyscraper_b" }); } void AddAvailableEntriesFromResearchList() { size_t researchListCount; const ResearchItem* researchList = GetResearchList(&researchListCount); BitSet rideTypeInResearch = GetRideTypesPresentInResearchList( researchList, researchListCount); for (size_t i = 0; i < researchListCount; i++) { const ResearchItem* researchItem = &researchList[i]; if (researchItem->Flags == RCT1ResearchFlagsSeparator) { if (researchItem->Item == RCT1_RESEARCH_END) { break; } if (researchItem->Item == RCT1_RESEARCH_END_AVAILABLE || researchItem->Item == RCT1_RESEARCH_END_RESEARCHABLE) { continue; } } switch (researchItem->Type) { case RCT1_RESEARCH_TYPE_THEME: AddEntriesForSceneryTheme(researchItem->Item); break; case RCT1_RESEARCH_TYPE_RIDE: AddEntryForRideType(static_cast(researchItem->Item)); break; case RCT1_RESEARCH_TYPE_VEHICLE: // For some bizarre reason, RCT1 research lists contain vehicles that aren't actually researched. if (rideTypeInResearch[researchItem->RelatedRide]) { AddEntryForVehicleType( static_cast(researchItem->RelatedRide), static_cast(researchItem->Item)); } break; } } } void AddAvailableEntriesFromMap() { size_t maxTiles = Limits::MaxMapSize * Limits::MaxMapSize; size_t tileIndex = 0; RCT12TileElement* tileElement = _s4.TileElements; while (tileIndex < maxTiles) { switch (tileElement->GetType()) { case RCT12TileElementType::Surface: { auto surfaceEl = tileElement->AsSurface(); auto surfaceStyle = surfaceEl->GetSurfaceStyle(); auto edgeStyle = surfaceEl->GetEdgeStyle(); AddEntryForTerrainSurface(surfaceStyle); AddEntryForTerrainEdge(edgeStyle); break; } case RCT12TileElementType::Path: { uint8_t pathType = tileElement->AsPath()->GetRCT1PathType(); uint8_t pathAdditionsType = tileElement->AsPath()->GetAddition(); uint8_t footpathRailingsType = RCT1_PATH_SUPPORT_TYPE_TRUSS; if (_gameVersion == FILE_VERSION_RCT1_LL) { footpathRailingsType = tileElement->AsPath()->GetRCT1SupportType(); } AddEntryForPathAddition(pathAdditionsType); AddEntryForPathSurface(pathType); AddEntryForFootpathRailings(footpathRailingsType); break; } case RCT12TileElementType::SmallScenery: AddEntryForSmallScenery(tileElement->AsSmallScenery()->GetEntryIndex()); break; case RCT12TileElementType::LargeScenery: AddEntryForLargeScenery(tileElement->AsLargeScenery()->GetEntryIndex()); break; case RCT12TileElementType::Wall: { for (int32_t edge = 0; edge < 4; edge++) { int32_t type = tileElement->AsWall()->GetRCT1WallType(edge); if (type != -1) { AddEntryForWall(type); } } break; } default: break; } if ((tileElement++)->IsLastForTile()) { tileIndex++; } } } void AddAvailableEntriesFromRides() { for (size_t i = 0; i < std::size(_s4.Rides); i++) { auto ride = &_s4.Rides[i]; if (ride->Type != RideType::Null) { if (RCT1::RideTypeUsesVehicles(ride->Type)) AddEntryForVehicleType(ride->Type, ride->VehicleType); else AddEntryForRideType(ride->Type); } } } void AddAvailableEntriesFromSceneryGroups() { for (int32_t sceneryTheme = 0; sceneryTheme <= RCT1_SCENERY_THEME_PAGODA; sceneryTheme++) { if (sceneryTheme != 0 && _sceneryThemeTypeToEntryMap[sceneryTheme] == OBJECT_ENTRY_INDEX_NULL) continue; auto objects = RCT1::GetSceneryObjects(sceneryTheme); for (auto objectName : objects) { auto& objectRepository = OpenRCT2::GetContext()->GetObjectRepository(); auto foundObject = objectRepository.FindObject(objectName); if (foundObject != nullptr) { auto objectType = foundObject->Type; switch (objectType) { case ObjectType::SmallScenery: case ObjectType::LargeScenery: case ObjectType::Walls: case ObjectType::Banners: case ObjectType::PathAdditions: { RCT12::EntryList* entries = GetEntryList(objectType); // Check if there are spare entries available size_t maxEntries = static_cast(object_entry_group_counts[EnumValue(objectType)]); if (entries != nullptr && entries->GetCount() < maxEntries) { entries->GetOrAddEntry(objectName); } break; } default: // This switch processes only ObjectTypes valid for scenery break; } } else { LOG_ERROR("Cannot find object %s", objectName); } } } } void AddAvailableEntriesFromBannerList() { for (size_t i = 0; i < std::size(_s4.Banners); i++) { auto& banner = _s4.Banners[i]; auto type = static_cast(banner.Type); if (type == BannerType::Null) continue; AddEntryForBanner(type); } } void AddEntryForWater() { std::string_view entryName; if (_gameVersion < FILE_VERSION_RCT1_LL) { entryName = RCT1::GetWaterObject(RCT1_WATER_CYAN); } else { entryName = RCT1::GetWaterObject(_s4.WaterColour); } _waterEntry.GetOrAddEntry(entryName); } void AddEntryForRideType(RideType rideType) { Guard::Assert(EnumValue(rideType) < std::size(_rideTypeToRideEntryMap)); if (_rideTypeToRideEntryMap[EnumValue(rideType)] == OBJECT_ENTRY_INDEX_NULL) { auto entryName = RCT1::GetRideTypeObject(rideType, _gameVersion == FILE_VERSION_RCT1_LL); if (!entryName.empty()) { auto entryIndex = _rideEntries.GetOrAddEntry(entryName); _rideTypeToRideEntryMap[EnumValue(rideType)] = entryIndex; } } } void AddEntryForVehicleType(RideType rideType, VehicleType vehicleType) { Guard::Assert(EnumValue(rideType) < std::size(_rideTypeToRideEntryMap)); if (_vehicleTypeToRideEntryMap[EnumValue(vehicleType)] == OBJECT_ENTRY_INDEX_NULL) { auto entryName = RCT1::GetVehicleObject(vehicleType); if (!entryName.empty()) { auto entryIndex = _rideEntries.GetOrAddEntry(entryName); _vehicleTypeToRideEntryMap[EnumValue(vehicleType)] = entryIndex; if (rideType != RideType::Null) AddEntryForRideType(rideType); } } } void AddEntryForSmallScenery(ObjectEntryIndex smallSceneryType) { assert(smallSceneryType < std::size(_smallSceneryTypeToEntryMap)); if (_smallSceneryTypeToEntryMap[smallSceneryType] == OBJECT_ENTRY_INDEX_NULL) { auto entryName = RCT1::GetSmallSceneryObject(smallSceneryType); auto entryIndex = _smallSceneryEntries.GetOrAddEntry(entryName); _smallSceneryTypeToEntryMap[smallSceneryType] = entryIndex; } } void AddEntryForLargeScenery(ObjectEntryIndex largeSceneryType) { assert(largeSceneryType < std::size(_largeSceneryTypeToEntryMap)); if (_largeSceneryTypeToEntryMap[largeSceneryType] == OBJECT_ENTRY_INDEX_NULL) { auto entryName = RCT1::GetLargeSceneryObject(largeSceneryType); auto entryIndex = _largeSceneryEntries.GetOrAddEntry(entryName); _largeSceneryTypeToEntryMap[largeSceneryType] = entryIndex; } } void AddEntryForWall(ObjectEntryIndex wallType) { assert(wallType < std::size(_wallTypeToEntryMap)); if (_wallTypeToEntryMap[wallType] == OBJECT_ENTRY_INDEX_NULL) { auto entryName = RCT1::GetWallObject(wallType); auto entryIndex = _wallEntries.GetOrAddEntry(entryName); _wallTypeToEntryMap[wallType] = entryIndex; } } void AddEntryForBanner(BannerType bannerType) { assert(EnumValue(bannerType) < std::size(_bannerTypeToEntryMap)); if (_bannerTypeToEntryMap[EnumValue(bannerType)] == OBJECT_ENTRY_INDEX_NULL) { auto entryName = RCT1::GetBannerObject(bannerType); auto entryIndex = _bannerEntries.GetOrAddEntry(entryName); _bannerTypeToEntryMap[EnumValue(bannerType)] = entryIndex; } } void AddEntryForPathSurface(ObjectEntryIndex pathType) { assert(pathType < std::size(_footpathSurfaceTypeToEntryMap)); if (_footpathSurfaceTypeToEntryMap[pathType] == OBJECT_ENTRY_INDEX_NULL) { auto identifier = RCT1::GetPathSurfaceObject(pathType); if (!identifier.empty()) { auto entryIndex = _footpathSurfaceEntries.GetOrAddEntry(identifier); _footpathSurfaceTypeToEntryMap[pathType] = entryIndex; } } } void AddEntryForPathAddition(ObjectEntryIndex pathAdditionType) { if (pathAdditionType == RCT1_PATH_ADDITION_NONE) return; if (_pathAdditionTypeToEntryMap[pathAdditionType] == OBJECT_ENTRY_INDEX_NULL) { uint8_t normalisedPathAdditionType = RCT1::NormalisePathAddition(pathAdditionType); if (_pathAdditionTypeToEntryMap[normalisedPathAdditionType] == OBJECT_ENTRY_INDEX_NULL) { auto entryName = RCT1::GetPathAddtionObject(normalisedPathAdditionType); auto entryIndex = _pathAdditionEntries.GetOrAddEntry(entryName); _pathAdditionTypeToEntryMap[normalisedPathAdditionType] = entryIndex; } _pathAdditionTypeToEntryMap[pathAdditionType] = _pathAdditionTypeToEntryMap[normalisedPathAdditionType]; } } void AddEntriesForSceneryTheme(ObjectEntryIndex sceneryThemeType) { if (sceneryThemeType == RCT1_SCENERY_THEME_GENERAL || sceneryThemeType == RCT1_SCENERY_THEME_JUMPING_FOUNTAINS || sceneryThemeType == RCT1_SCENERY_THEME_GARDEN_CLOCK) { _sceneryThemeTypeToEntryMap[sceneryThemeType] = ObjectEntryIndexIgnore; } else { auto entryName = RCT1::GetSceneryGroupObject(sceneryThemeType); if (_sceneryGroupEntries.GetCount() >= MAX_SCENERY_GROUP_OBJECTS) { Console::WriteLine("Warning: More than %d (max scenery groups) in RCT1 park.", MAX_SCENERY_GROUP_OBJECTS); std::string entryNameString = std::string(entryName); Console::WriteLine(" [%s] scenery group not added.", entryNameString.c_str()); } else { auto entryIndex = _sceneryGroupEntries.GetOrAddEntry(entryName); _sceneryThemeTypeToEntryMap[sceneryThemeType] = entryIndex; } } } void AddEntryForTerrainSurface(ObjectEntryIndex terrainSurfaceType) { assert(terrainSurfaceType < std::size(_terrainSurfaceTypeToEntryMap)); if (_terrainSurfaceTypeToEntryMap[terrainSurfaceType] == OBJECT_ENTRY_INDEX_NULL) { auto identifier = RCT1::GetTerrainSurfaceObject(terrainSurfaceType); if (!identifier.empty()) { auto entryIndex = _terrainSurfaceEntries.GetOrAddEntry(identifier); _terrainSurfaceTypeToEntryMap[terrainSurfaceType] = entryIndex; } } } void AddEntryForTerrainEdge(ObjectEntryIndex terrainEdgeType) { assert(terrainEdgeType < std::size(_terrainEdgeTypeToEntryMap)); if (_terrainEdgeTypeToEntryMap[terrainEdgeType] == OBJECT_ENTRY_INDEX_NULL) { auto identifier = RCT1::GetTerrainEdgeObject(terrainEdgeType); if (!identifier.empty()) { auto entryIndex = _terrainEdgeEntries.GetOrAddEntry(identifier); _terrainEdgeTypeToEntryMap[terrainEdgeType] = entryIndex; } } } void AddEntryForFootpathRailings(ObjectEntryIndex railingsType) { assert(railingsType < std::size(_footpathRailingsTypeToEntryMap)); if (_footpathRailingsTypeToEntryMap[railingsType] == OBJECT_ENTRY_INDEX_NULL) { auto identifier = RCT1::GetFootpathRailingsObject(railingsType); if (!identifier.empty()) { auto entryIndex = _footpathRailingsEntries.GetOrAddEntry(identifier); _footpathRailingsTypeToEntryMap[railingsType] = entryIndex; } } } void ImportRides() { for (int32_t i = 0; i < Limits::MaxRidesInPark; i++) { if (_s4.Rides[i].Type != RideType::Null) { const auto rideId = RideId::FromUnderlying(i); ImportRide(RideAllocateAtIndex(rideId), &_s4.Rides[i], rideId); } } } void ImportRide(::Ride* dst, RCT1::Ride* src, RideId rideIndex) { *dst = {}; dst->id = rideIndex; // This is a peculiarity of this exact version number, which only Heide-Park seems to use. if (_s4.GameVersion == 110018 && src->Type == RideType::InvertedRollerCoaster) { dst->type = RIDE_TYPE_COMPACT_INVERTED_COASTER; } else { dst->type = RCT1::GetRideType(src->Type, src->VehicleType); } if (RCT1::RideTypeUsesVehicles(src->Type)) { dst->subtype = _vehicleTypeToRideEntryMap[EnumValue(src->VehicleType)]; } else { dst->subtype = _rideTypeToRideEntryMap[EnumValue(src->Type)]; } const auto* rideEntry = GetRideEntryByIndex(dst->subtype); // This can happen with hacked parks if (rideEntry == nullptr) { LOG_WARNING("Discarding ride with invalid ride entry"); dst->type = RIDE_TYPE_NULL; return; } // Ride name if (IsUserStringID(src->Name)) { dst->custom_name = GetUserString(src->Name); } dst->status = static_cast(src->Status); // Flags dst->lifecycle_flags = src->LifecycleFlags; // These flags were not in the base game if (_gameVersion == FILE_VERSION_RCT1) { dst->lifecycle_flags &= ~RIDE_LIFECYCLE_MUSIC; dst->lifecycle_flags &= ~RIDE_LIFECYCLE_INDESTRUCTIBLE; dst->lifecycle_flags &= ~RIDE_LIFECYCLE_INDESTRUCTIBLE_TRACK; } if (VehicleTypeIsReversed(src->VehicleType)) { dst->lifecycle_flags |= RIDE_LIFECYCLE_REVERSED_TRAINS; } // Station if (src->OverallView.IsNull()) { dst->overall_view.SetNull(); } else { dst->overall_view = TileCoordsXY{ src->OverallView.x, src->OverallView.y }.ToCoordsXY(); } for (StationIndex::UnderlyingType i = 0; i < Limits::MaxStationsPerRide; i++) { auto& dstStation = dst->GetStation(StationIndex::FromUnderlying(i)); if (src->StationStarts[i].IsNull()) { dstStation.Start.SetNull(); } else { auto tileStartLoc = TileCoordsXY{ src->StationStarts[i].x, src->StationStarts[i].y }; dstStation.Start = tileStartLoc.ToCoordsXY(); } dstStation.SetBaseZ(src->StationHeights[i] * Limits::CoordsZStep); dstStation.Length = src->StationLengths[i]; dstStation.Depart = src->StationLights[i]; dstStation.TrainAtStation = src->StationDeparts[i]; // Direction is fixed later. if (src->Entrances[i].IsNull()) dstStation.Entrance.SetNull(); else dstStation.Entrance = { src->Entrances[i].x, src->Entrances[i].y, src->StationHeights[i] / 2, 0 }; if (src->Exits[i].IsNull()) dstStation.Exit.SetNull(); else dstStation.Exit = { src->Exits[i].x, src->Exits[i].y, src->StationHeights[i] / 2, 0 }; dstStation.QueueTime = src->QueueTime[i]; dstStation.LastPeepInQueue = EntityId::FromUnderlying(src->LastPeepInQueue[i]); dstStation.QueueLength = src->NumPeepsInQueue[i]; dstStation.SegmentTime = src->Time[i]; dstStation.SegmentLength = src->Length[i]; } // All other values take 0 as their default. Since they're already memset to that, no need to do it again. for (int32_t i = Limits::MaxStationsPerRide; i < OpenRCT2::Limits::MaxStationsPerRide; i++) { auto& dstStation = dst->GetStation(StationIndex::FromUnderlying(i)); dstStation.Start.SetNull(); dstStation.TrainAtStation = RideStation::kNoTrain; dstStation.Entrance.SetNull(); dstStation.Exit.SetNull(); dstStation.LastPeepInQueue = EntityId::GetNull(); } dst->num_stations = src->NumStations; // Vehicle links (indexes converted later) for (int32_t i = 0; i < Limits::MaxTrainsPerRide; i++) { dst->vehicles[i] = EntityId::FromUnderlying(src->Vehicles[i]); } for (int32_t i = Limits::MaxTrainsPerRide; i <= OpenRCT2::Limits::MaxTrainsPerRide; i++) { dst->vehicles[i] = EntityId::GetNull(); } dst->NumTrains = src->NumTrains; dst->num_cars_per_train = src->NumCarsPerTrain + rideEntry->zero_cars; dst->ProposedNumTrains = src->NumTrains; dst->max_trains = src->MaxTrains; dst->proposed_num_cars_per_train = src->NumCarsPerTrain + rideEntry->zero_cars; dst->special_track_elements = src->SpecialTrackElements; dst->num_sheltered_sections = src->NumShelteredSections; dst->sheltered_length = src->ShelteredLength; // Operation dst->depart_flags = src->DepartFlags; dst->min_waiting_time = src->MinWaitingTime; dst->max_waiting_time = src->MaxWaitingTime; dst->operation_option = src->OperationOption; dst->num_circuits = 1; dst->MinCarsPerTrain = rideEntry->min_cars_in_train; dst->MaxCarsPerTrain = rideEntry->max_cars_in_train; // RCT1 used 5mph / 8 km/h for every lift hill dst->lift_hill_speed = 5; dst->music = OBJECT_ENTRY_INDEX_NULL; if (GetRideTypeDescriptor(dst->type).HasFlag(RIDE_TYPE_FLAG_ALLOW_MUSIC)) { if (_gameVersion == FILE_VERSION_RCT1) { // Original RCT had no music settings, take default style auto style = GetStyleFromMusicIdentifier(GetRideTypeDescriptor(dst->type).DefaultMusic); if (style.has_value()) { dst->music = style.value(); } // Only merry-go-round and dodgems had music and used // the same flag as synchronise stations for the option to enable it if (src->Type == RideType::MerryGoRound || src->Type == RideType::Dodgems) { if (src->DepartFlags & RCT1_RIDE_DEPART_PLAY_MUSIC) { dst->depart_flags &= ~RCT1_RIDE_DEPART_PLAY_MUSIC; dst->lifecycle_flags |= RIDE_LIFECYCLE_MUSIC; } } } else { dst->music = src->Music; } } if (src->OperatingMode == RCT1_RIDE_MODE_POWERED_LAUNCH) { // Launched rides never passed through the station in RCT1. dst->mode = RideMode::PoweredLaunch; } else { dst->mode = static_cast(src->OperatingMode); } SetRideColourScheme(dst, src); // Maintenance dst->build_date = static_cast(src->BuildDate); dst->inspection_interval = src->InspectionInterval; dst->last_inspection = src->LastInspection; dst->reliability = src->Reliability; dst->unreliability_factor = src->UnreliabilityFactor; dst->downtime = src->Downtime; dst->breakdown_reason = src->BreakdownReason; dst->mechanic_status = src->MechanicStatus; dst->mechanic = EntityId::FromUnderlying(src->Mechanic); dst->breakdown_reason_pending = src->BreakdownReasonPending; dst->inspection_station = StationIndex::FromUnderlying(src->InspectionStation); dst->broken_car = src->BrokenCar; dst->broken_vehicle = src->BrokenVehicle; // Measurement data dst->excitement = src->Excitement; dst->intensity = src->Intensity; dst->nausea = src->Nausea; dst->max_speed = src->MaxSpeed; dst->average_speed = src->AverageSpeed; dst->max_positive_vertical_g = src->MaxPositiveVerticalG; dst->max_negative_vertical_g = src->MaxNegativeVerticalG; dst->max_lateral_g = src->MaxLateralG; dst->previous_lateral_g = src->PreviousLateralG; dst->previous_vertical_g = src->PreviousVerticalG; dst->turn_count_banked = src->TurnCountBanked; dst->turn_count_default = src->TurnCountDefault; dst->turn_count_sloped = src->TurnCountSloped; dst->drops = src->NumDrops; dst->start_drop_height = src->StartDropHeight / 2; dst->highest_drop_height = src->HighestDropHeight / 2; if (dst->type == RIDE_TYPE_MINI_GOLF) dst->holes = src->NumInversions & 0x1F; else dst->inversions = src->NumInversions & 0x1F; dst->sheltered_eighths = src->NumInversions >> 5; dst->boat_hire_return_direction = src->BoatHireReturnDirection; dst->boat_hire_return_position = { src->BoatHireReturnPosition.x, src->BoatHireReturnPosition.y }; dst->chairlift_bullwheel_rotation = src->ChairliftBullwheelRotation; for (int i = 0; i < 2; i++) { dst->ChairliftBullwheelLocation[i] = { src->ChairliftBullwheelLocation[i].x, src->ChairliftBullwheelLocation[i].y, src->ChairliftBullwheelZ[i] / 2 }; } if (src->CurTestTrackLocation.IsNull()) { dst->CurTestTrackLocation.SetNull(); } else { dst->CurTestTrackLocation = { src->CurTestTrackLocation.x, src->CurTestTrackLocation.y, src->CurTestTrackZ / 2 }; } dst->testing_flags = src->TestingFlags; dst->current_test_segment = src->CurrentTestSegment; dst->current_test_station = StationIndex::GetNull(); dst->average_speed_test_timeout = src->AverageSpeedTestTimeout; dst->slide_in_use = src->SlideInUse; dst->slide_peep_t_shirt_colour = RCT1::GetColour(src->SlidePeepTshirtColour); dst->spiral_slide_progress = src->SpiralSlideProgress; // Doubles as slide_peep dst->maze_tiles = src->MazeTiles; // Finance / customers dst->upkeep_cost = ToMoney64(src->UpkeepCost); dst->price[0] = src->Price; dst->price[1] = src->PriceSecondary; dst->income_per_hour = ToMoney64(src->IncomePerHour); dst->total_customers = src->TotalCustomers; dst->profit = ToMoney64(src->Profit); dst->total_profit = ToMoney64(src->TotalProfit); dst->value = ToMoney64(src->Value); for (size_t i = 0; i < std::size(src->NumCustomers); i++) { dst->num_customers[i] = src->NumCustomers[i]; } dst->satisfaction = src->Satisfaction; dst->satisfaction_time_out = src->SatisfactionTimeOut; dst->satisfaction_next = src->SatisfactionNext; dst->popularity = src->Popularity; dst->popularity_next = src->PopularityNext; dst->popularity_time_out = src->PopularityTimeOut; dst->num_riders = src->NumRiders; dst->music_tune_id = TUNE_ID_NULL; } void SetRideColourScheme(::Ride* dst, RCT1::Ride* src) { // Colours dst->colour_scheme_type = src->ColourScheme; if (_gameVersion == FILE_VERSION_RCT1) { dst->track_colour[0].main = RCT1::GetColour(src->TrackPrimaryColour); dst->track_colour[0].additional = RCT1::GetColour(src->TrackSecondaryColour); dst->track_colour[0].supports = RCT1::GetColour(src->TrackSupportColour); // Balloons were always blue in the original RCT. if (src->Type == RideType::BalloonStall) { dst->track_colour[0].main = COLOUR_LIGHT_BLUE; } else if (src->Type == RideType::RiverRapids) { dst->track_colour[0].main = COLOUR_WHITE; } } else { for (int i = 0; i < Limits::NumColourSchemes; i++) { dst->track_colour[i].main = RCT1::GetColour(src->TrackColourMain[i]); dst->track_colour[i].additional = RCT1::GetColour(src->TrackColourAdditional[i]); dst->track_colour[i].supports = RCT1::GetColour(src->TrackColourSupports[i]); } } dst->entrance_style = OBJECT_ENTRY_INDEX_NULL; if (dst->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_ENTRANCE_EXIT)) { // Entrance styles were introduced with AA. They correspond directly with those in RCT2. if (_gameVersion == FILE_VERSION_RCT1) { dst->entrance_style = 0; // plain entrance } else { dst->entrance_style = src->EntranceStyle; } } if (_gameVersion < FILE_VERSION_RCT1_LL && dst->type == RIDE_TYPE_MERRY_GO_ROUND) { // The merry-go-round in pre-LL versions was always yellow with red dst->vehicle_colours[0].Body = COLOUR_YELLOW; dst->vehicle_colours[0].Trim = COLOUR_BRIGHT_RED; } else { for (int i = 0; i < Limits::MaxTrainsPerRide; i++) { // RCT1 had no third colour const auto colourSchemeCopyDescriptor = GetColourSchemeCopyDescriptor(src->VehicleType); if (colourSchemeCopyDescriptor.colour1 == COPY_COLOUR_1) { dst->vehicle_colours[i].Body = RCT1::GetColour(src->VehicleColours[i].Body); } else if (colourSchemeCopyDescriptor.colour1 == COPY_COLOUR_2) { dst->vehicle_colours[i].Body = RCT1::GetColour(src->VehicleColours[i].Trim); } else { dst->vehicle_colours[i].Body = colourSchemeCopyDescriptor.colour1; } if (colourSchemeCopyDescriptor.colour2 == COPY_COLOUR_1) { dst->vehicle_colours[i].Trim = RCT1::GetColour(src->VehicleColours[i].Body); } else if (colourSchemeCopyDescriptor.colour2 == COPY_COLOUR_2) { dst->vehicle_colours[i].Trim = RCT1::GetColour(src->VehicleColours[i].Trim); } else { dst->vehicle_colours[i].Trim = colourSchemeCopyDescriptor.colour2; } if (colourSchemeCopyDescriptor.colour3 == COPY_COLOUR_1) { dst->vehicle_colours[i].Tertiary = RCT1::GetColour(src->VehicleColours[i].Body); } else if (colourSchemeCopyDescriptor.colour3 == COPY_COLOUR_2) { dst->vehicle_colours[i].Tertiary = RCT1::GetColour(src->VehicleColours[i].Trim); } else { dst->vehicle_colours[i].Tertiary = colourSchemeCopyDescriptor.colour3; } } } // In RCT1 and AA, the maze was always hedges. // LL has 4 types, like RCT2. For LL, only guard against invalid values. if (src->Type == RideType::HedgeMaze) { if (_gameVersion < FILE_VERSION_RCT1_LL || src->TrackColourSupports[0] > 3) dst->track_colour[0].supports = MAZE_WALL_TYPE_HEDGE; else dst->track_colour[0].supports = src->TrackColourSupports[0]; } } void ImportRideMeasurements() { for (const auto& src : _s4.RideMeasurements) { if (src.RideIndex != RCT12_RIDE_ID_NULL) { auto ride = GetRide(RCT12RideIdToOpenRCT2RideId(src.RideIndex)); if (ride != nullptr) { ride->measurement = std::make_unique(); ImportRideMeasurement(*ride->measurement, src); } } } } void ImportRideMeasurement(RideMeasurement& dst, const RCT12RideMeasurement& src) { dst.flags = src.Flags; dst.last_use_tick = src.LastUseTick; dst.num_items = src.NumItems; dst.current_item = src.CurrentItem; dst.vehicle_index = src.VehicleIndex; dst.current_station = StationIndex::FromUnderlying(src.CurrentStation); for (size_t i = 0; i < std::size(src.Velocity); i++) { dst.velocity[i] = src.Velocity[i] / 2; dst.altitude[i] = src.Altitude[i] / 2; dst.vertical[i] = src.Vertical[i] / 2; dst.lateral[i] = src.Lateral[i] / 2; } } void ImportEntity(const RCT12EntityBase& src); template void ImportEntity(const RCT12EntityBase& src); void ImportEntities() { for (int i = 0; i < Limits::MaxEntities; i++) { ImportEntity(_s4.Entities[i].Unknown); } } void SetVehicleColours(::Vehicle* dst, const RCT1::Vehicle* src) { const auto& srcRide = _s4.Rides[src->Ride]; RCT1::VehicleColourSchemeCopyDescriptor colourSchemeCopyDescriptor = RCT1::GetColourSchemeCopyDescriptor( srcRide.VehicleType); // RCT1 had no third colour if (colourSchemeCopyDescriptor.colour1 == COPY_COLOUR_1) { dst->colours.Body = RCT1::GetColour(src->Colours.BodyColour); } else if (colourSchemeCopyDescriptor.colour1 == COPY_COLOUR_2) { dst->colours.Body = RCT1::GetColour(src->Colours.TrimColour); } else { dst->colours.Body = colourSchemeCopyDescriptor.colour1; } if (colourSchemeCopyDescriptor.colour2 == COPY_COLOUR_1) { dst->colours.Trim = RCT1::GetColour(src->Colours.BodyColour); } else if (colourSchemeCopyDescriptor.colour2 == COPY_COLOUR_2) { dst->colours.Trim = RCT1::GetColour(src->Colours.TrimColour); } else { dst->colours.Trim = colourSchemeCopyDescriptor.colour2; } if (colourSchemeCopyDescriptor.colour3 == COPY_COLOUR_1) { dst->colours.Tertiary = RCT1::GetColour(src->Colours.BodyColour); } else if (colourSchemeCopyDescriptor.colour3 == COPY_COLOUR_2) { dst->colours.Tertiary = RCT1::GetColour(src->Colours.TrimColour); } else { dst->colours.Tertiary = colourSchemeCopyDescriptor.colour3; } } void FixImportStaff() { // Only the individual patrol areas have been converted, so generate the combined patrol areas of each staff type UpdateConsolidatedPatrolAreas(); } void ImportPeep(::Peep* dst, const RCT1::Peep* src) { // Peep vs. staff (including which kind) dst->SpriteType = RCT1::GetPeepSpriteType(src->SpriteType); dst->Action = static_cast(src->Action); dst->SpecialSprite = src->SpecialSprite; dst->NextActionSpriteType = static_cast(src->NextActionSpriteType); dst->ActionSpriteImageOffset = src->ActionSpriteImageOffset; dst->WalkingFrameNum = src->NoActionFrameNum; dst->ActionSpriteType = static_cast(src->ActionSpriteType); dst->ActionFrame = src->ActionFrame; const SpriteBounds* spriteBounds = &GetSpriteBounds(dst->SpriteType, dst->ActionSpriteType); dst->SpriteData.Width = spriteBounds->sprite_width; dst->SpriteData.HeightMin = spriteBounds->sprite_height_negative; dst->SpriteData.HeightMax = spriteBounds->sprite_height_positive; dst->MoveTo({ src->x, src->y, src->z }); dst->Orientation = src->EntityDirection; // Peep name if (IsUserStringID(src->NameStringID)) { dst->SetName(GetUserString(src->NameStringID)); } dst->State = static_cast(src->State); dst->SubState = src->SubState; dst->NextLoc = { src->NextX, src->NextY, src->NextZ * Limits::CoordsZStep }; dst->NextFlags = src->NextFlags; dst->Var37 = src->Var37; dst->StepProgress = src->StepProgress; dst->TshirtColour = RCT1::GetColour(src->TshirtColour); dst->TrousersColour = RCT1::GetColour(src->TrousersColour); dst->DestinationX = src->DestinationX; dst->DestinationY = src->DestinationY; dst->DestinationTolerance = src->DestinationTolerance; dst->PeepDirection = src->Direction; dst->Energy = src->Energy; dst->EnergyTarget = src->EnergyTarget; dst->Mass = src->Mass; dst->WindowInvalidateFlags = 0; dst->CurrentRide = RCT12RideIdToOpenRCT2RideId(src->CurrentRide); dst->CurrentRideStation = StationIndex::FromUnderlying(src->CurrentRideStation); dst->CurrentTrain = src->CurrentTrain; dst->CurrentCar = src->CurrentCar; dst->CurrentSeat = src->CurrentSeat; dst->InteractionRideIndex = RCT12RideIdToOpenRCT2RideId(src->InteractionRideIndex); dst->PeepId = src->ID; dst->PathCheckOptimisation = 0; dst->PeepFlags = 0; dst->PathfindGoal.x = 0xFF; dst->PathfindGoal.y = 0xFF; dst->PathfindGoal.z = 0xFF; dst->PathfindGoal.direction = INVALID_DIRECTION; } void ImportStaffPatrolArea(Staff* staffmember, uint8_t staffId) { // TODO: It is likely that S4 files should have a staffmode check before setting // patrol areas. See S6 importer. // The patrol areas in RCT1 are encoded as follows, for coordinates x and y, separately for every staff member: // - Chop off the 7 lowest bits of the x and y coordinates, which leaves 5 bits per coordinate. // This step also "produces" the 4x4 patrol squares. // - Append the two bitstrings to a 10-bit value like so: yyyyyxxxxx // - Use this 10-bit value as an index into an 8-bit array. The array is sized such that every 4x4 square // used for the patrols on the map has a bit in that array. If a bit is 1, that square is part of the patrol. // The correct bit position in that array is found like this: yyyyyxx|xxx // index in the array ----^ ^--- bit position in the 8-bit value // We do the opposite in this function to recover the x and y values. int32_t peepOffset = staffId * Limits::PatrolAreaSize; for (int32_t i = 0; i < Limits::PatrolAreaSize; i++) { if (_s4.PatrolAreas[peepOffset + i] == 0) { // No patrol for this area continue; } // Loop over the bits of the uint8_t for (int32_t j = 0; j < 8; j++) { int8_t bit = (_s4.PatrolAreas[peepOffset + i] >> j) & 1; if (bit == 0) { // No patrol for this area continue; } // val contains the 5 highest bits of both the x and y coordinates int32_t val = j | (i << 3); int32_t x = val & 0x1F; x <<= 7; int32_t y = val & 0x3E0; y <<= 2; staffmember->SetPatrolArea( MapRange(x, y, x + (4 * COORDS_XY_STEP) - 1, y + (4 * COORDS_XY_STEP) - 1), true); } } } void ImportEntityCommonProperties(EntityBase* dst, const RCT12EntityBase* src) { dst->Orientation = src->EntityDirection; dst->SpriteData.Width = src->SpriteWidth; dst->SpriteData.HeightMin = src->SpriteHeightNegative; dst->SpriteData.HeightMax = src->SpriteHeightPositive; dst->x = src->x; dst->y = src->y; dst->z = src->z; } void ImportPeepSpawns() { auto& gameState = GetGameState(); gameState.PeepSpawns.clear(); for (size_t i = 0; i < Limits::MaxPeepSpawns; i++) { if (_s4.PeepSpawn[i].x != RCT12_PEEP_SPAWN_UNDEFINED) { PeepSpawn spawn = { _s4.PeepSpawn[i].x, _s4.PeepSpawn[i].y, _s4.PeepSpawn[i].z * 16, _s4.PeepSpawn[i].direction }; gameState.PeepSpawns.push_back(spawn); } } } void ImportFinance(GameState_t& gameState) { gameState.Park.EntranceFee = _s4.ParkEntranceFee; gameState.LandPrice = ToMoney64(_s4.LandPrice); gameState.ConstructionRightsPrice = ToMoney64(_s4.ConstructionRightsPrice); gameState.Cash = ToMoney64(_s4.Cash); gameState.BankLoan = ToMoney64(_s4.Loan); gameState.MaxBankLoan = ToMoney64(_s4.MaxLoan); // It's more like 1.33%, but we can only use integers. Can be fixed once we have our own save format. gameState.BankLoanInterestRate = 1; gameState.InitialCash = ToMoney64(_s4.Cash); gameState.CompanyValue = ToMoney64(_s4.CompanyValue); gameState.Park.Value = CorrectRCT1ParkValue(_s4.ParkValue); gameState.CurrentProfit = ToMoney64(_s4.Profit); for (size_t i = 0; i < Limits::FinanceGraphSize; i++) { gameState.CashHistory[i] = ToMoney64(_s4.CashHistory[i]); gameState.Park.ValueHistory[i] = CorrectRCT1ParkValue(_s4.ParkValueHistory[i]); gameState.WeeklyProfitHistory[i] = ToMoney64(_s4.WeeklyProfitHistory[i]); } for (size_t i = 0; i < Limits::ExpenditureTableMonthCount; i++) { for (size_t j = 0; j < Limits::ExpenditureTypeCount; j++) { gameState.ExpenditureTable[i][j] = ToMoney64(_s4.Expenditure[i][j]); } } gameState.CurrentExpenditure = ToMoney64(_s4.TotalExpenditure); gameState.ScenarioCompletedCompanyValue = RCT12CompletedCompanyValueToOpenRCT2(_s4.CompletedCompanyValue); gameState.TotalAdmissions = _s4.NumAdmissions; gameState.TotalIncomeFromAdmissions = ToMoney64(_s4.AdmissionTotalIncome); // TODO marketing campaigns not working static_assert( std::numeric_limits::max() > ADVERTISING_CAMPAIGN_COUNT, "Advertising enum bigger than capacity of iterator"); for (uint8_t i = 0; i < ADVERTISING_CAMPAIGN_COUNT; i++) { if (_s4.MarketingStatus[i] & CAMPAIGN_ACTIVE_FLAG) { MarketingCampaign campaign; campaign.Type = i; campaign.WeeksLeft = _s4.MarketingStatus[i] & ~CAMPAIGN_ACTIVE_FLAG; if (campaign.Type == ADVERTISING_CAMPAIGN_RIDE_FREE || campaign.Type == ADVERTISING_CAMPAIGN_RIDE) { campaign.RideId = RCT12RideIdToOpenRCT2RideId(_s4.MarketingAssoc[i]); } else if (campaign.Type == ADVERTISING_CAMPAIGN_FOOD_OR_DRINK_FREE) { campaign.ShopItemType = ShopItem(_s4.MarketingAssoc[i]); } gMarketingCampaigns.push_back(campaign); } } } void AppendRequiredObjects(ObjectList& objectList, ObjectType objectType, const RCT12::EntryList& entryList) { AppendRequiredObjects(objectList, objectType, entryList.GetEntries()); } void AppendRequiredObjects(ObjectList& objectList, ObjectType objectType, const std::vector& objectNames) { for (const auto& objectName : objectNames) { auto descriptor = ObjectEntryDescriptor(objectName); descriptor.Type = objectType; objectList.Add(descriptor); } } ObjectList GetRequiredObjects() { ObjectList result; AppendRequiredObjects(result, ObjectType::Ride, _rideEntries); AppendRequiredObjects(result, ObjectType::SmallScenery, _smallSceneryEntries); AppendRequiredObjects(result, ObjectType::LargeScenery, _largeSceneryEntries); AppendRequiredObjects(result, ObjectType::Walls, _wallEntries); AppendRequiredObjects(result, ObjectType::Paths, _pathEntries); AppendRequiredObjects(result, ObjectType::PathAdditions, _pathAdditionEntries); AppendRequiredObjects(result, ObjectType::SceneryGroup, _sceneryGroupEntries); AppendRequiredObjects(result, ObjectType::Banners, _bannerEntries); AppendRequiredObjects(result, ObjectType::ParkEntrance, std::vector({ "rct2.park_entrance.pkent1" })); AppendRequiredObjects(result, ObjectType::Water, _waterEntry); AppendRequiredObjects(result, ObjectType::TerrainSurface, _terrainSurfaceEntries); AppendRequiredObjects(result, ObjectType::TerrainEdge, _terrainEdgeEntries); AppendRequiredObjects(result, ObjectType::FootpathSurface, _footpathSurfaceEntries); AppendRequiredObjects(result, ObjectType::FootpathRailings, _footpathRailingsEntries); RCT12AddDefaultObjects(result); return result; } void ImportTileElements() { // Build tile pointer cache (needed to get the first element at a certain location) auto tilePointerIndex = TilePointerIndex( Limits::MaxMapSize, _s4.TileElements, std::size(_s4.TileElements)); std::vector tileElements; const auto maxSize = _s4.MapSize == 0 ? Limits::MaxMapSize : _s4.MapSize; for (TileCoordsXY coords = { 0, 0 }; coords.y < kMaximumMapSizeTechnical; coords.y++) { for (coords.x = 0; coords.x < kMaximumMapSizeTechnical; coords.x++) { auto tileAdded = false; if (coords.x < maxSize && coords.y < maxSize) { // This is the equivalent of MapGetFirstElementAt(x, y), but on S4 data. RCT12TileElement* srcElement = tilePointerIndex.GetFirstElementAt(coords); do { if (srcElement->BaseHeight == Limits::MaxElementHeight) continue; // Reserve 8 elements for import auto originalSize = tileElements.size(); tileElements.resize(originalSize + 16); auto dstElement = tileElements.data() + originalSize; auto numAddedElements = ImportTileElement(dstElement, srcElement); tileElements.resize(originalSize + numAddedElements); tileAdded = true; } while (!(srcElement++)->IsLastForTile()); } if (!tileAdded) { // Add a default surface element, we always need at least one element per tile auto& dstElement = tileElements.emplace_back(); dstElement.ClearAs(TileElementType::Surface); dstElement.SetLastForTile(true); } // Set last element flag in case the original last element was never added if (tileElements.size() > 0) { tileElements.back().SetLastForTile(true); } } } SetTileElements(std::move(tileElements)); FixEntrancePositions(); } size_t ImportTileElement(TileElement* dst, const RCT12TileElement* src) { const auto rct12type = src->GetType(); const auto tileElementType = ToOpenRCT2TileElementType(rct12type); dst->ClearAs(tileElementType); dst->SetDirection(src->GetDirection()); // All saved in "flags" dst->SetOccupiedQuadrants(src->GetOccupiedQuadrants()); // Skipping IsGhost, which appears to use a different flag in RCT1. // This flag will be set by the caller. dst->SetLastForTile(false); dst->SetBaseZ(src->BaseHeight * Limits::CoordsZStep); dst->SetClearanceZ(src->ClearanceHeight * Limits::CoordsZStep); switch (tileElementType) { case TileElementType::Surface: { auto dst2 = dst->AsSurface(); auto src2 = src->AsSurface(); auto surfaceStyle = _terrainSurfaceTypeToEntryMap[src2->GetSurfaceStyle()]; auto edgeStyle = _terrainEdgeTypeToEntryMap[src2->GetEdgeStyle()]; dst2->SetSlope(src2->GetSlope()); dst2->SetSurfaceObjectIndex(surfaceStyle); dst2->SetEdgeObjectIndex(edgeStyle); dst2->SetGrassLength(src2->GetGrassLength()); dst2->SetOwnership(src2->GetOwnership()); dst2->SetParkFences(src2->GetParkFences()); dst2->SetWaterHeight(src2->GetWaterHeight()); dst2->SetHasTrackThatNeedsWater(src2->HasTrackThatNeedsWater()); return 1; } case TileElementType::Path: { auto dst2 = dst->AsPath(); auto src2 = src->AsPath(); dst2->SetQueueBannerDirection(src2->GetQueueBannerDirection()); dst2->SetSloped(src2->IsSloped()); dst2->SetSlopeDirection(src2->GetSlopeDirection()); dst2->SetRideIndex(RCT12RideIdToOpenRCT2RideId(src2->GetRideIndex())); dst2->SetStationIndex(StationIndex::FromUnderlying(src2->GetStationIndex())); dst2->SetWide(src2->IsWide()); dst2->SetHasQueueBanner(src2->HasQueueBanner()); dst2->SetEdges(src2->GetEdges()); dst2->SetCorners(src2->GetCorners()); dst2->SetAddition(0); dst2->SetAdditionIsGhost(false); dst2->SetAdditionStatus(src2->GetAdditionStatus()); // Type uint8_t pathType = src2->GetRCT1PathType(); auto entryIndex = _footpathSurfaceTypeToEntryMap[pathType]; dst2->SetDirection(0); dst2->SetIsBroken(false); dst2->SetIsBlockedByVehicle(false); dst2->SetLegacyPathEntryIndex(entryIndex); dst2->SetShouldDrawPathOverSupports(true); if (RCT1::PathIsQueue(pathType)) { dst2->SetIsQueue(true); } uint8_t railingsType = RCT1_PATH_SUPPORT_TYPE_TRUSS; if (_gameVersion == FILE_VERSION_RCT1_LL) { railingsType = src2->GetRCT1SupportType(); } auto railingsEntryIndex = _footpathRailingsTypeToEntryMap[railingsType]; dst2->SetRailingsEntryIndex(railingsEntryIndex); // Additions ObjectEntryIndex additionType = src2->GetAddition(); if (additionType != RCT1_PATH_ADDITION_NONE) { ObjectEntryIndex normalisedType = RCT1::NormalisePathAddition(additionType); entryIndex = _pathAdditionTypeToEntryMap[normalisedType]; if (additionType != normalisedType) { dst2->SetIsBroken(true); } dst2->SetAdditionEntryIndex(entryIndex); } return 1; } case TileElementType::Track: { auto dst2 = dst->AsTrack(); auto src2 = src->AsTrack(); const auto* ride = GetRide(RCT12RideIdToOpenRCT2RideId(src2->GetRideIndex())); auto rideType = (ride != nullptr) ? ride->type : RIDE_TYPE_NULL; dst2->SetTrackType(RCT1TrackTypeToOpenRCT2(src2->GetTrackType(), rideType)); dst2->SetRideType(rideType); dst2->SetSequenceIndex(src2->GetSequenceIndex()); dst2->SetRideIndex(RCT12RideIdToOpenRCT2RideId(src2->GetRideIndex())); dst2->SetColourScheme(src2->GetColourScheme()); dst2->SetHasChain(src2->HasChain()); dst2->SetHasCableLift(false); dst2->SetInverted(src2->IsInverted()); dst2->SetStationIndex(StationIndex::FromUnderlying(src2->GetStationIndex())); dst2->SetHasGreenLight(src2->HasGreenLight()); dst2->SetIsIndestructible(src2->IsIndestructible()); if (rideType == RIDE_TYPE_GHOST_TRAIN) { dst2->SetDoorAState(src2->GetDoorAState()); dst2->SetDoorBState(src2->GetDoorBState()); } else { dst2->SetSeatRotation(DEFAULT_SEAT_ROTATION); } // Skipping IsHighlighted() auto trackType = dst2->GetTrackType(); // Brakes import as closed to preserve legacy behaviour dst2->SetBrakeClosed(trackType == TrackElemType::Brakes); if (TrackTypeHasSpeedSetting(trackType)) { dst2->SetBrakeBoosterSpeed(src2->GetBrakeBoosterSpeed()); } else if (trackType == TrackElemType::OnRidePhoto) { dst2->SetPhotoTimeout(src2->GetPhotoTimeout()); } // This has to be done last, since the maze entry shares fields with the colour and sequence fields. const auto& rtd = GetRideTypeDescriptor(rideType); if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE)) { dst2->SetMazeEntry(src2->GetMazeEntry()); } if (TrackTypeMustBeMadeInvisible(rideType, trackType)) { dst->SetInvisible(true); } return 1; } case TileElementType::SmallScenery: { auto dst2 = dst->AsSmallScenery(); auto src2 = src->AsSmallScenery(); auto entryIndex = _smallSceneryTypeToEntryMap[src2->GetEntryIndex()]; dst2->SetEntryIndex(entryIndex); dst2->SetAge(src2->GetAge()); dst2->SetSceneryQuadrant(src2->GetSceneryQuadrant()); dst2->SetPrimaryColour(RCT1::GetColour(src2->GetPrimaryColour())); if (src2->NeedsSupports()) dst2->SetNeedsSupports(); // Copied from [rct2: 0x006A2956] switch (src2->GetEntryIndex()) { case RCT1_SCENERY_GEOMETRIC_SCULPTURE_1: case RCT1_SCENERY_GEOMETRIC_SCULPTURE_2: case RCT1_SCENERY_GEOMETRIC_SCULPTURE_3: case RCT1_SCENERY_GEOMETRIC_SCULPTURE_4: case RCT1_SCENERY_GEOMETRIC_SCULPTURE_5: dst2->SetSecondaryColour(COLOUR_WHITE); break; case RCT1_SCENERY_TULIPS_1: case RCT1_SCENERY_TULIPS_2: dst2->SetPrimaryColour(COLOUR_BRIGHT_RED); dst2->SetSecondaryColour(COLOUR_YELLOW); break; case RCT1_SCENERY_SMALL_RED_GARDENS: dst2->SetPrimaryColour(COLOUR_BRIGHT_RED); break; } return 1; } case TileElementType::Entrance: { auto dst2 = dst->AsEntrance(); auto src2 = src->AsEntrance(); dst2->SetEntranceType(src2->GetEntranceType()); dst2->SetRideIndex(RCT12RideIdToOpenRCT2RideId(src2->GetRideIndex())); dst2->SetStationIndex(StationIndex::FromUnderlying(src2->GetStationIndex())); dst2->SetSequenceIndex(src2->GetSequenceIndex()); if (src2->GetEntranceType() == ENTRANCE_TYPE_PARK_ENTRANCE) { auto pathType = src2->GetPathType(); if (pathType == 0) { pathType = RCT1_FOOTPATH_TYPE_TARMAC_GREY; } auto entryIndex = _footpathSurfaceTypeToEntryMap[pathType]; dst2->SetSurfaceEntryIndex(entryIndex); } return 1; } case TileElementType::Wall: { auto src2 = src->AsWall(); auto slope = src2->GetRCT1Slope(); size_t numAddedElements = 0; for (int32_t edge = 0; edge < 4; edge++) { int32_t type = src2->GetRCT1WallType(edge); if (type == -1) continue; colour_t colourA = RCT1::GetColour(src2->GetRCT1WallColour()); colour_t colourB = COLOUR_BLACK; colour_t colourC = COLOUR_BLACK; ConvertWall(type, &colourA, &colourB); type = _wallTypeToEntryMap[type]; auto baseZ = src->BaseHeight * Limits::CoordsZStep; auto clearanceZ = src->ClearanceHeight * Limits::CoordsZStep; auto edgeSlope = GetWallSlopeFromEdgeSlope(slope, edge & 3); if (edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS)) { clearanceZ += LAND_HEIGHT_STEP; } if (edgeSlope & EDGE_SLOPE_ELEVATED) { edgeSlope &= ~EDGE_SLOPE_ELEVATED; baseZ += LAND_HEIGHT_STEP; clearanceZ += LAND_HEIGHT_STEP; } dst->SetType(TileElementType::Wall); dst->SetDirection(edge); dst->SetBaseZ(baseZ); dst->SetClearanceZ(clearanceZ); // Will be set later. dst->SetLastForTile(false); auto* wallElement = dst->AsWall(); wallElement->SetEntryIndex(type); wallElement->SetPrimaryColour(colourA); wallElement->SetSecondaryColour(colourB); wallElement->SetTertiaryColour(colourC); wallElement->SetBannerIndex(BannerIndex::GetNull()); wallElement->SetAcrossTrack(false); wallElement->SetAnimationIsBackwards(false); wallElement->SetSlope(edgeSlope); dst++; numAddedElements++; } return numAddedElements; } case TileElementType::LargeScenery: { auto dst2 = dst->AsLargeScenery(); auto src2 = src->AsLargeScenery(); auto type = src2->GetEntryIndex(); dst2->SetEntryIndex(_largeSceneryTypeToEntryMap[type]); dst2->SetSequenceIndex(src2->GetSequenceIndex()); dst2->SetPrimaryColour(RCT1::GetColour(src2->GetPrimaryColour())); dst2->SetSecondaryColour(RCT1::GetColour(src2->GetSecondaryColour())); return 1; } case TileElementType::Banner: { auto dst2 = dst->AsBanner(); auto src2 = src->AsBanner(); dst2->SetPosition(src2->GetPosition()); dst2->SetAllowedEdges(src2->GetAllowedEdges()); auto index = src2->GetIndex(); if (index < std::size(_s4.Banners)) { auto srcBanner = &_s4.Banners[index]; auto dstBanner = GetOrCreateBanner(BannerIndex::FromUnderlying(index)); if (dstBanner == nullptr) { dst2->SetIndex(BannerIndex::GetNull()); } else { ImportBanner(dstBanner, srcBanner); dst2->SetIndex(BannerIndex::FromUnderlying(index)); } } else { dst2->SetIndex(BannerIndex::GetNull()); } return 1; } default: assert(false); } return 0; } void ImportResearch(GameState_t& gameState) { // All available objects must be loaded before this method is called as it // requires them to correctly insert objects into the research list ResearchResetItems(gameState); size_t researchListCount; const RCT1::ResearchItem* researchList = GetResearchList(&researchListCount); // Initialise the "seen" tables _researchRideEntryUsed.reset(); _researchRideTypeUsed.reset(); // The first six scenery groups are always available for (uint8_t i = 0; i < 6; i++) { ResearchInsertSceneryGroupEntry(i, true); } bool researched = true; auto rideTypeInResearch = GetRideTypesPresentInResearchList(researchList, researchListCount); std::vector vehiclesWithMissingRideTypes; for (size_t i = 0; i < researchListCount; i++) { const auto& researchItem = researchList[i]; if (researchItem.Flags == RCT1ResearchFlagsSeparator) { if (researchItem.Item == RCT1_RESEARCH_END_AVAILABLE) { researched = false; continue; } // We don't import the random items yet. else if (researchItem.Item == RCT1_RESEARCH_END_RESEARCHABLE || researchItem.Item == RCT1_RESEARCH_END) { break; } } switch (researchItem.Type) { case RCT1_RESEARCH_TYPE_THEME: { uint8_t rct1SceneryTheme = researchItem.Item; auto sceneryGroupEntryIndex = _sceneryThemeTypeToEntryMap[rct1SceneryTheme]; if (sceneryGroupEntryIndex != ObjectEntryIndexIgnore && sceneryGroupEntryIndex != OBJECT_ENTRY_INDEX_NULL) { ResearchInsertSceneryGroupEntry(sceneryGroupEntryIndex, researched); } break; } case RCT1_RESEARCH_TYPE_RIDE: { const auto rct1RideType = static_cast(researchItem.Item); _researchRideTypeUsed[EnumValue(rct1RideType)] = true; auto ownRideEntryIndex = _rideTypeToRideEntryMap[EnumValue(rct1RideType)]; Guard::Assert( ownRideEntryIndex != OBJECT_ENTRY_INDEX_NULL, "ownRideEntryIndex was OBJECT_ENTRY_INDEX_NULL"); bool foundOwnType = false; // If the ride type does not use vehicles, no point looking for them in the research list. if (RCT1::RideTypeUsesVehicles(rct1RideType)) { // Add all vehicles for this ride type that are researched or before this research item for (size_t j = 0; j < researchListCount; j++) { const auto& researchItem2 = researchList[j]; if (researchItem2.Flags == RCT1ResearchFlagsSeparator) { if (researchItem2.Item == RCT1_RESEARCH_END_RESEARCHABLE || researchItem2.Item == RCT1_RESEARCH_END) { break; } continue; } if (researchItem2.Type == RCT1_RESEARCH_TYPE_VEHICLE && static_cast(researchItem2.RelatedRide) == rct1RideType) { auto rideEntryIndex2 = _vehicleTypeToRideEntryMap[researchItem2.Item]; bool isOwnType = (ownRideEntryIndex == rideEntryIndex2); if (isOwnType) { foundOwnType = true; } // Only add the vehicles that were listed before this ride, otherwise we might // change the research order if (j < i && (researched || isOwnType)) { InsertResearchVehicle(researchItem2, researched); } } } } if (!foundOwnType) { if (!_researchRideEntryUsed[ownRideEntryIndex]) { _researchRideEntryUsed[ownRideEntryIndex] = true; ResearchInsertRideEntry(ownRideEntryIndex, researched); } } break; } case RCT1_RESEARCH_TYPE_VEHICLE: { // Only add vehicle if the related ride has been seen, this to make sure that vehicles // are researched only after the ride has been researched. Otherwise, remove them from the research // list, so that they are automatically co-invented when their master ride is invented. if (_researchRideTypeUsed[researchItem.RelatedRide]) { InsertResearchVehicle(researchItem, researched); } else if (!rideTypeInResearch[researchItem.RelatedRide] && _gameVersion == FILE_VERSION_RCT1_LL) { vehiclesWithMissingRideTypes.push_back(researchItem); } break; } case RCT1_RESEARCH_TYPE_SPECIAL: // Not supported break; } } for (const auto& researchItem : vehiclesWithMissingRideTypes) { InsertResearchVehicle(researchItem, false); } // Research funding / priority uint8_t activeResearchTypes = 0; if (_s4.ResearchPriority & RCT1_RESEARCH_CATEGORY_ROLLERCOASTERS) { activeResearchTypes |= EnumToFlag(ResearchCategory::Rollercoaster); } if (_s4.ResearchPriority & RCT1_RESEARCH_CATEGORY_THRILL_RIDES) { activeResearchTypes |= EnumToFlag(ResearchCategory::Thrill); activeResearchTypes |= EnumToFlag(ResearchCategory::Water); } if (_s4.ResearchPriority & RCT1_RESEARCH_CATEGORY_GENTLE_TRANSPORT_RIDES) { activeResearchTypes |= EnumToFlag(ResearchCategory::Gentle); activeResearchTypes |= EnumToFlag(ResearchCategory::Transport); } if (_s4.ResearchPriority & RCT1_RESEARCH_CATEGORY_SHOPS) { activeResearchTypes |= EnumToFlag(ResearchCategory::Shop); } if (_s4.ResearchPriority & RCT1_RESEARCH_CATEGORY_SCENERY_THEMING) { activeResearchTypes |= EnumToFlag(ResearchCategory::SceneryGroup); } gameState.ResearchPriorities = activeResearchTypes; gameState.ResearchFundingLevel = _s4.ResearchLevel; // This will mark items as researched/unresearched according to the research list. // This needs to be called before importing progress, as it will reset it. ResearchResetCurrentItem(); // Research history gameState.ResearchProgress = _s4.ResearchProgress; gameState.ResearchProgressStage = _s4.ResearchProgressStage; gameState.ResearchExpectedDay = _s4.NextResearchExpectedDay; gameState.ResearchExpectedMonth = _s4.NextResearchExpectedMonth; if (_s4.LastResearchFlags == 0xFF) { gameState.ResearchLastItem = std::nullopt; } else { ::ResearchItem researchItem = {}; ConvertResearchEntry(&researchItem, _s4.LastResearchItem, _s4.LastResearchType); gameState.ResearchLastItem = researchItem; } if (_s4.NextResearchFlags == 0xFF) { gameState.ResearchNextItem = std::nullopt; gameState.ResearchProgressStage = RESEARCH_STAGE_INITIAL_RESEARCH; gameState.ResearchProgress = 0; } else { ::ResearchItem researchItem = {}; ConvertResearchEntry(&researchItem, _s4.NextResearchItem, _s4.NextResearchType); gameState.ResearchNextItem = researchItem; } } static BitSet GetRideTypesPresentInResearchList( const RCT1::ResearchItem* researchList, size_t researchListCount) { BitSet ret = {}; for (size_t i = 0; i < researchListCount; i++) { const auto& researchItem = researchList[i]; if (researchItem.Flags == RCT1ResearchFlagsSeparator) { if (researchItem.Item == RCT1_RESEARCH_END_AVAILABLE || researchItem.Item == RCT1_RESEARCH_END_RESEARCHABLE) { continue; } if (researchItem.Item == RCT1_RESEARCH_END) { break; } } if (researchItem.Type == RCT1_RESEARCH_TYPE_RIDE) { ret[researchItem.Item] = true; } } return ret; } void InsertResearchVehicle(const ResearchItem& researchItem, bool researched) { uint8_t vehicle = researchItem.Item; // RCT1 research sometimes contain vehicles that aren’t actually researched. // In such cases, `_vehicleTypeToRideEntryMap` will return OBJECT_ENTRY_INDEX_NULL. This is expected. auto rideEntryIndex = _vehicleTypeToRideEntryMap[vehicle]; if (rideEntryIndex < std::size(_researchRideEntryUsed) && !_researchRideEntryUsed[rideEntryIndex]) { _researchRideEntryUsed[rideEntryIndex] = true; ResearchInsertRideEntry(rideEntryIndex, researched); } } void ImportParkName() { std::string parkName = std::string(_s4.ScenarioName); if (IsUserStringID(static_cast(_s4.ParkNameStringIndex))) { std::string userString = GetUserString(_s4.ParkNameStringIndex); if (!userString.empty()) { parkName = std::move(userString); } } auto& park = GetGameState().Park; park.Name = std::move(parkName); } void ImportParkFlags(GameState_t& gameState) { // Date and srand gameState.CurrentTicks = _s4.Ticks; ScenarioRandSeed(_s4.RandomA, _s4.RandomB); gameState.Date = Date{ _s4.Month, _s4.Day }; // Park rating gameState.Park.Rating = _s4.ParkRating; Park::ResetHistories(gameState); std::copy(std::begin(_s4.ParkRatingHistory), std::end(_s4.ParkRatingHistory), gameState.Park.RatingHistory); for (size_t i = 0; i < std::size(_s4.GuestsInParkHistory); i++) { if (_s4.GuestsInParkHistory[i] != RCT12ParkHistoryUndefined) { gameState.GuestsInParkHistory[i] = _s4.GuestsInParkHistory[i] * RCT12GuestsInParkHistoryFactor; } } // Awards auto& currentAwards = gameState.CurrentAwards; for (auto& src : _s4.Awards) { if (src.Time != 0) { currentAwards.push_back(Award{ src.Time, static_cast(src.Type) }); } } // Number of guests history std::fill( std::begin(gameState.GuestsInParkHistory), std::end(gameState.GuestsInParkHistory), std::numeric_limits::max()); for (size_t i = 0; i < std::size(_s4.GuestsInParkHistory); i++) { if (_s4.GuestsInParkHistory[i] != std::numeric_limits::max()) { gameState.GuestsInParkHistory[i] = _s4.GuestsInParkHistory[i] * 20; } } // News items for (size_t i = 0; i < Limits::MaxNewsItems; i++) { const RCT12NewsItem* src = &_s4.Messages[i]; News::Item* dst = &gameState.NewsItems[i]; dst->Type = static_cast(src->Type); dst->Flags = src->Flags; dst->Ticks = src->Ticks; dst->MonthYear = src->MonthYear; dst->Day = src->Day; dst->Text = ConvertFormattedStringToOpenRCT2(std::string_view(src->Text, sizeof(src->Text))); if (dst->Type == News::ItemType::Research) { uint8_t researchItem = src->Assoc & 0x000000FF; uint8_t researchType = (src->Assoc & 0x00FF0000) >> 16; ::ResearchItem tmpResearchItem = {}; ConvertResearchEntry(&tmpResearchItem, researchItem, researchType); dst->Assoc = tmpResearchItem.rawValue; } else { dst->Assoc = src->Assoc; } } // Initial guest status gameState.GuestInitialCash = ToMoney64(_s4.GuestInitialCash); gameState.GuestInitialHunger = _s4.GuestInitialHunger; gameState.GuestInitialThirst = _s4.GuestInitialThirst; gameState.GuestInitialHappiness = _s4.GuestInitialHappiness; gameState.GuestGenerationProbability = _s4.GuestGenerationProbability; // Staff colours gameState.StaffHandymanColour = RCT1::GetColour(_s4.HandymanColour); gameState.StaffMechanicColour = RCT1::GetColour(_s4.MechanicColour); gameState.StaffSecurityColour = RCT1::GetColour(_s4.SecurityGuardColour); // Flags gameState.Park.Flags = _s4.ParkFlags; gameState.Park.Flags &= ~PARK_FLAGS_ANTI_CHEAT_DEPRECATED; gameState.Park.Flags |= PARK_FLAGS_RCT1_INTEREST; // Loopy Landscape parks can set a flag to lock the entry price to free. // If this flag is not set, the player can ask money for both rides and entry. if (!(_s4.ParkFlags & RCT1_PARK_FLAGS_PARK_ENTRY_LOCKED_AT_FREE)) { gameState.Park.Flags |= PARK_FLAGS_UNLOCK_ALL_PRICES; } gameState.Park.Size = _s4.ParkSize; gameState.TotalRideValueForMoney = _s4.TotalRideValueForMoney; gameState.SamePriceThroughoutPark = 0; if (_gameVersion == FILE_VERSION_RCT1_LL) { gameState.SamePriceThroughoutPark = _s4.SamePriceThroughout; } } void ConvertResearchEntry(::ResearchItem* dst, uint8_t srcItem, uint8_t srcType) { dst->SetNull(); if (srcType == RCT1_RESEARCH_TYPE_RIDE) { auto entryIndex = _rideTypeToRideEntryMap[srcItem]; if (entryIndex != OBJECT_ENTRY_INDEX_NULL) { const auto* rideEntry = GetRideEntryByIndex(entryIndex); if (rideEntry != nullptr) { auto rideType = rideEntry->GetFirstNonNullRideType(); dst->entryIndex = entryIndex; dst->baseRideType = rideType; dst->type = Research::EntryType::Ride; dst->flags = 0; dst->category = GetRideTypeDescriptor(rideType).GetResearchCategory(); } } } else if (srcType == RCT1_RESEARCH_TYPE_VEHICLE) { auto entryIndex = _vehicleTypeToRideEntryMap[srcItem]; if (entryIndex != OBJECT_ENTRY_INDEX_NULL) { const auto* rideEntry = GetRideEntryByIndex(entryIndex); if (rideEntry != nullptr) { auto rideType = rideEntry->GetFirstNonNullRideType(); dst->entryIndex = entryIndex; dst->baseRideType = rideType; dst->type = Research::EntryType::Ride; dst->flags = 0; dst->category = GetRideTypeDescriptor(rideType).GetResearchCategory(); } } } else if (srcType == RCT1_RESEARCH_TYPE_THEME) { auto entryIndex = _sceneryThemeTypeToEntryMap[srcItem]; if (entryIndex != ObjectEntryIndexIgnore && entryIndex != OBJECT_ENTRY_INDEX_NULL) { dst->entryIndex = entryIndex; dst->type = Research::EntryType::Scenery; dst->category = ResearchCategory::SceneryGroup; dst->baseRideType = 0; dst->flags = 0; } } } void ImportClimate(GameState_t& gameState) { gameState.Climate = ClimateType{ _s4.Climate }; gameState.ClimateUpdateTimer = _s4.ClimateTimer; gameState.ClimateCurrent.Temperature = _s4.Temperature; gameState.ClimateCurrent.Weather = WeatherType{ _s4.Weather }; gameState.ClimateCurrent.WeatherEffect = WeatherEffectType::None; gameState.ClimateCurrent.WeatherGloom = _s4.WeatherGloom; gameState.ClimateCurrent.Level = static_cast(_s4.Rain); gameState.ClimateNext.Temperature = _s4.TargetTemperature; gameState.ClimateNext.Weather = WeatherType{ _s4.TargetWeather }; gameState.ClimateNext.WeatherEffect = WeatherEffectType::None; gameState.ClimateNext.WeatherGloom = _s4.TargetWeatherGloom; gameState.ClimateNext.Level = static_cast(_s4.TargetRain); } void ImportScenarioNameDetails(GameState_t& gameState) { std::string name = String::ToStd(_s4.ScenarioName); std::string parkName; std::string details; int32_t scNumber = _s4.ScenarioSlotIndex; if (scNumber != -1) { SourceDescriptor sourceDesc; if (ScenarioSources::TryGetById(scNumber, &sourceDesc)) { StringId localisedStringIds[3]; if (LanguageGetLocalisedScenarioStrings(sourceDesc.title, localisedStringIds)) { if (localisedStringIds[0] != STR_NONE) { name = String::ToStd(LanguageGetString(localisedStringIds[0])); } if (localisedStringIds[1] != STR_NONE) { parkName = String::ToStd(LanguageGetString(localisedStringIds[1])); } if (localisedStringIds[2] != STR_NONE) { details = String::ToStd(LanguageGetString(localisedStringIds[2])); } } } } gameState.ScenarioName = std::move(name); gameState.ScenarioDetails = std::move(details); if (_isScenario && !parkName.empty()) { auto& park = GetGameState().Park; park.Name = std::move(parkName); } } void ImportScenarioObjective(GameState_t& gameState) { gameState.ScenarioObjective.Type = _s4.ScenarioObjectiveType; gameState.ScenarioObjective.Year = _s4.ScenarioObjectiveYears; gameState.ScenarioObjective.NumGuests = _s4.ScenarioObjectiveNumGuests; // RCT1 used a different way of calculating the park value. // This is corrected here, but since scenario_objective_currency doubles as minimum excitement rating, // we need to check the goal to avoid affecting scenarios like Volcania. if (_s4.ScenarioObjectiveType == OBJECTIVE_PARK_VALUE_BY) gameState.ScenarioObjective.Currency = CorrectRCT1ParkValue(_s4.ScenarioObjectiveCurrency); else gameState.ScenarioObjective.Currency = ToMoney64(_s4.ScenarioObjectiveCurrency); // This does not seem to be saved in the objective arguments, so look up the ID from the available rides instead. if (_s4.ScenarioObjectiveType == OBJECTIVE_BUILD_THE_BEST) gameState.ScenarioObjective.RideId = GetBuildTheBestRideId(); } void ImportSavedView() { auto& gameState = GetGameState(); gameState.SavedView = ScreenCoordsXY{ _s4.ViewX, _s4.ViewY }; gameState.SavedViewZoom = ZoomLevel{ static_cast(_s4.ViewZoom) }; gameState.SavedViewRotation = _s4.ViewRotation; } void ConvertWall(const int32_t& type, colour_t* colourA, colour_t* colourB) { switch (type) { case RCT1_WALL_TYPE_WOODEN_PANEL_FENCE: *colourA = COLOUR_DARK_BROWN; break; case RCT1_WALL_TYPE_WHITE_WOODEN_PANEL_FENCE: *colourA = COLOUR_WHITE; break; case RCT1_WALL_TYPE_RED_WOODEN_PANEL_FENCE: *colourA = COLOUR_SALMON_PINK; break; case RCT1_WALL_TYPE_WOODEN_PANEL_FENCE_WITH_SNOW: *colourA = COLOUR_DARK_BROWN; break; case RCT1_WALL_TYPE_GLASS_SMOOTH: case RCT1_WALL_TYPE_GLASS_PANELS: *colourB = COLOUR_WHITE; break; case RCT1_WALL_TYPE_SMALL_GREY_CASTLE: case RCT1_WALL_TYPE_LARGE_GREY_CASTLE: case RCT1_WALL_TYPE_LARGE_GREY_CASTLE_CROSS: case RCT1_WALL_TYPE_LARGE_GREY_CASTLE_GATE: case RCT1_WALL_TYPE_LARGE_GREY_CASTLE_WINDOW: case RCT1_WALL_TYPE_MEDIUM_GREY_CASTLE: *colourA = COLOUR_GREY; break; } } void ImportBanner(Banner* dst, const RCT12Banner* src) { auto id = dst->id; *dst = {}; dst->id = id; auto type = RCTEntryIndexToOpenRCT2EntryIndex(src->Type); if (type < std::size(_bannerTypeToEntryMap)) type = _bannerTypeToEntryMap[type]; else type = OBJECT_ENTRY_INDEX_NULL; dst->type = type; dst->flags = 0; if (src->Flags & BANNER_FLAG_NO_ENTRY) { dst->flags |= BANNER_FLAG_NO_ENTRY; } if (IsUserStringID(src->StringID)) { dst->text = GetUserString(src->StringID); } dst->colour = RCT1::GetColour(src->Colour); dst->text_colour = src->TextColour; dst->position.x = src->x; dst->position.y = src->y; } void FixEntrancePositions() { auto& gameState = GetGameState(); gameState.Park.Entrances.clear(); TileElementIterator it; TileElementIteratorBegin(&it); while (TileElementIteratorNext(&it) && gameState.Park.Entrances.size() < Limits::MaxParkEntrances) { TileElement* element = it.element; if (element->GetType() != TileElementType::Entrance) continue; if (element->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_PARK_ENTRANCE) continue; if ((element->AsEntrance()->GetSequenceIndex()) != 0) continue; CoordsXYZD entrance = { TileCoordsXY(it.x, it.y).ToCoordsXY(), element->GetBaseZ(), element->GetDirection() }; gameState.Park.Entrances.push_back(entrance); } } RCT12::EntryList* GetEntryList(ObjectType objectType) { switch (objectType) { case ObjectType::Ride: return &_rideEntries; case ObjectType::SmallScenery: return &_smallSceneryEntries; case ObjectType::LargeScenery: return &_largeSceneryEntries; case ObjectType::Walls: return &_wallEntries; case ObjectType::Banners: return &_bannerEntries; case ObjectType::Paths: return &_pathEntries; case ObjectType::PathAdditions: return &_pathAdditionEntries; case ObjectType::SceneryGroup: return &_sceneryGroupEntries; case ObjectType::Water: return &_waterEntry; default: // This switch processes only ObjectType for for Entries break; } return nullptr; } const RCT1::ResearchItem* GetResearchList(size_t* count) { // Loopy Landscapes stores research items in a different place if (_gameVersion == FILE_VERSION_RCT1_LL) { *count = std::size(_s4.ResearchItemsLL); return _s4.ResearchItemsLL; } *count = std::size(_s4.ResearchItems); return _s4.ResearchItems; } std::string GetUserString(StringId stringId) { const auto originalString = _s4.StringTable[stringId % 1024]; auto originalStringView = std::string_view( originalString, RCT12::GetRCTStringBufferLen(originalString, USER_STRING_MAX_LENGTH)); auto asUtf8 = RCT2StringToUTF8(originalStringView, RCT2LanguageId::EnglishUK); auto justText = RCT12RemoveFormattingUTF8(asUtf8); return justText.data(); } void FixLandOwnership() { switch (_s4.ScenarioSlotIndex) { case SC_DYNAMITE_DUNES: FixLandOwnershipTiles({ { 97, 18 }, { 99, 19 }, { 83, 34 } }); break; case SC_LEAFY_LAKE: FixLandOwnershipTiles({ { 49, 66 }, { 74, 96 } }); break; case SC_TRINITY_ISLANDS: FixLandOwnershipTilesWithOwnership({ { 80, 60 } }, OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED); break; case SC_KATIES_DREAMLAND: FixLandOwnershipTiles({ { 74, 70 }, { 75, 70 }, { 76, 70 }, { 77, 73 }, { 80, 77 } }); FixLandOwnershipTilesWithOwnership( { { 115, 63 }, { 105, 66 }, { 109, 66 }, { 118, 67 } }, OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED); FixLandOwnershipTilesWithOwnership({ { 45, 69 }, { 59, 74 } }, OWNERSHIP_OWNED); break; case SC_POKEY_PARK: FixLandOwnershipTiles({ { 84, 71 }, { 64, 102 } }); break; case SC_WHITE_WATER_PARK: FixLandOwnershipTilesWithOwnership({ { 42, 85 }, { 89, 42 } }, OWNERSHIP_OWNED); break; case SC_MELS_WORLD: FixLandOwnershipTilesWithOwnership({ { 93, 76 }, { 93, 77 } }, OWNERSHIP_OWNED); break; case SC_MYSTIC_MOUNTAIN: FixLandOwnershipTiles({ { 98, 69 }, { 98, 70 }, { 103, 64 }, { 53, 79 }, { 86, 93 }, { 87, 93 } }); break; case SC_PACIFIC_PYRAMIDS: FixLandOwnershipTiles({ { 93, 105 }, { 63, 34 }, { 76, 25 }, { 85, 31 }, { 96, 47 }, { 96, 48 } }); break; case SC_THREE_MONKEYS_PARK: FixLandOwnershipTilesWithOwnership({ { 89, 92 } }, OWNERSHIP_UNOWNED); FixLandOwnershipTilesWithOwnership({ { 46, 22 } }, OWNERSHIP_OWNED); break; case SC_HAUNTED_HARBOUR: FixLandOwnershipTiles({ { 49, 42 } }); break; case SC_COASTER_CANYON: FixLandOwnershipTilesWithOwnership({ { 21, 55 } }, OWNERSHIP_OWNED); break; case SC_UTOPIA_PARK: FixLandOwnershipTiles({ { 85, 73 }, { 71, 75 }, { 90, 73 } }); break; case SC_ROTTING_HEIGHTS: FixLandOwnershipTilesWithOwnership({ { 35, 20 } }, OWNERSHIP_OWNED); break; case SC_URBAN_PARK: FixLandOwnershipTiles({ { 64, 77 }, { 61, 66 }, { 61, 67 }, { 39, 20 } }); FixLandOwnershipTilesWithOwnership({ { 46, 47 } }, OWNERSHIP_CONSTRUCTION_RIGHTS_AVAILABLE); break; case SC_GRAND_GLACIER: FixLandOwnershipTilesWithOwnership({ { 99, 58 } }, OWNERSHIP_OWNED); break; case SC_WOODWORM_PARK: FixLandOwnershipTilesWithOwnership({ { 62, 105 }, { 101, 55 } }, OWNERSHIP_OWNED); break; case SC_PLEASURE_ISLAND: FixLandOwnershipTilesWithOwnership({ { 37, 66 } }, OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED); break; case SC_NEVERMORE_PARK: FixLandOwnershipTilesWithOwnership({ { 9, 74 } }, OWNERSHIP_OWNED); break; case SC_ALTON_TOWERS: FixLandOwnershipTilesWithOwnership({ { 11, 31 }, { 68, 112 }, { 72, 118 } }, OWNERSHIP_OWNED); break; case SC_BLACKPOOL_PLEASURE_BEACH: FixLandOwnershipTilesWithOwnership( { { 93, 23 }, { 94, 23 }, { 95, 23 }, { 95, 24 }, { 96, 24 }, { 96, 25 }, { 97, 25 }, { 97, 26 }, { 97, 27 }, { 96, 28 } }, OWNERSHIP_OWNED); FixLandOwnershipTilesWithOwnership({ { 94, 24 }, { 95, 25 } }, OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED); break; case SC_FORT_ANACHRONISM: FixLandOwnershipTiles({ { 36, 87 }, { 54, 29 }, { 53, 88 } }); break; } } /** * In Urban Park, the entrance and exit of the merry-go-round are the wrong way round. This code fixes that. * To avoid messing up saves (in which this problem is most likely solved by the user), only carry out this * fix when loading from a scenario. */ void FixUrbanPark() { if (_s4.ScenarioSlotIndex == SC_URBAN_PARK && _isScenario) { const auto merryGoRoundId = RideId::FromUnderlying(0); // First, make the queuing peep exit for (auto peep : EntityList()) { if (peep->State == PeepState::QueuingFront && peep->CurrentRide == merryGoRoundId) { peep->RemoveFromQueue(); peep->SetState(PeepState::Falling); break; } } // Now, swap the entrance and exit. auto ride = GetRide(merryGoRoundId); if (ride != nullptr) { auto& station = ride->GetStation(); auto entranceCoords = station.Exit; auto exitCoords = station.Entrance; station.Entrance = entranceCoords; station.Exit = exitCoords; auto entranceElement = MapGetRideExitElementAt(entranceCoords.ToCoordsXYZD(), false); entranceElement->SetEntranceType(ENTRANCE_TYPE_RIDE_ENTRANCE); auto exitElement = MapGetRideEntranceElementAt(exitCoords.ToCoordsXYZD(), false); exitElement->SetEntranceType(ENTRANCE_TYPE_RIDE_EXIT); // Trigger footpath update FootpathQueueChainReset(); FootpathConnectEdges( entranceCoords.ToCoordsXY(), reinterpret_cast(entranceElement), GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED); FootpathUpdateQueueChains(); } } } void FixNextGuestNumber(GameState_t& gameState) { // In RCT1, the next guest number is not saved, so we have to calculate it. // This is done by finding the highest guest number in the park, and adding 1. uint32_t nextGuestNumber = 0; // TODO: Entities are currently read from the global state, change this once entities are stored // in the passed gameState. for (auto peep : EntityList()) { nextGuestNumber = std::max(nextGuestNumber, peep->PeepId); } gameState.NextGuestNumber = nextGuestNumber + 1; } /** * Counts the block sections. The reason this iterates over the map is to avoid getting into infinite loops, * which can happen with hacked parks. */ void CountBlockSections() { for (int32_t x = 0; x < Limits::MaxMapSize; x++) { for (int32_t y = 0; y < Limits::MaxMapSize; y++) { TileElement* tileElement = MapGetFirstElementAt(TileCoordsXY{ x, y }); if (tileElement == nullptr) continue; do { if (tileElement->GetType() == TileElementType::Track) { // Lift hill tops are the only pieces present in RCT1 that can count as a block brake. if (!tileElement->AsTrack()->HasChain()) continue; auto trackType = tileElement->AsTrack()->GetTrackType(); switch (trackType) { case TrackElemType::Up25ToFlat: case TrackElemType::Up60ToFlat: case TrackElemType::DiagUp25ToFlat: case TrackElemType::DiagUp60ToFlat: break; default: continue; } RideId rideIndex = tileElement->AsTrack()->GetRideIndex(); auto ride = GetRide(rideIndex); if (ride != nullptr) { ride->num_block_brakes++; } } } while (!(tileElement++)->IsLastForTile()); } } } /** * This has to be done after importing tile elements, because it needs those to detect if a pre-existing ride * name should be considered reserved. */ void SetDefaultNames() { for (auto& ride : GetRideManager()) { if (ride.custom_name.empty()) { ride.SetNameToDefault(); } } } ObjectEntryIndex GetBuildTheBestRideId() { size_t researchListCount; const RCT1::ResearchItem* researchList = GetResearchList(&researchListCount); for (size_t i = 0; i < researchListCount; i++) { if (researchList[i].Flags == 0xFF) { break; } if (researchList[i].Type == RCT1_RESEARCH_TYPE_RIDE) { return RCT1::GetRideType(static_cast(researchList[i].Item), static_cast(0)); } } return RIDE_TYPE_NULL; } }; // Very similar but not the same as S6Importer version (due to peeps) constexpr EntityType GetEntityTypeFromRCT1Sprite(const RCT12EntityBase& src) { EntityType output = EntityType::Null; switch (src.EntityIdentifier) { case RCT12EntityIdentifier::Vehicle: output = EntityType::Vehicle; break; case RCT12EntityIdentifier::Peep: { const auto& peep = static_cast(src); if (peep.PeepType == RCT12PeepType::Guest) { output = EntityType::Guest; } else { output = EntityType::Staff; } break; } case RCT12EntityIdentifier::Misc: switch (RCT12MiscEntityType(src.Type)) { case RCT12MiscEntityType::SteamParticle: output = EntityType::SteamParticle; break; case RCT12MiscEntityType::MoneyEffect: output = EntityType::MoneyEffect; break; case RCT12MiscEntityType::CrashedVehicleParticle: output = EntityType::CrashedVehicleParticle; break; case RCT12MiscEntityType::ExplosionCloud: output = EntityType::ExplosionCloud; break; case RCT12MiscEntityType::CrashSplash: output = EntityType::CrashSplash; break; case RCT12MiscEntityType::ExplosionFlare: output = EntityType::ExplosionFlare; break; case RCT12MiscEntityType::JumpingFountainWater: case RCT12MiscEntityType::JumpingFountainSnow: output = EntityType::JumpingFountain; break; case RCT12MiscEntityType::Balloon: output = EntityType::Balloon; break; case RCT12MiscEntityType::Duck: output = EntityType::Duck; break; default: break; } break; case RCT12EntityIdentifier::Litter: output = EntityType::Litter; break; default: break; } return output; } template<> void S4Importer::ImportEntity<::Vehicle>(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt<::Vehicle>(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); const auto* ride = GetRide(RideId::FromUnderlying(src->Ride)); if (ride == nullptr) return; const auto& rct1Ride = _s4.Rides[src->Ride]; uint8_t vehicleEntryIndex = RCT1::GetVehicleSubEntryIndex(rct1Ride.VehicleType, src->CarType); dst->ride = RideId::FromUnderlying(src->Ride); dst->ride_subtype = RCTEntryIndexToOpenRCT2EntryIndex(ride->subtype); dst->vehicle_type = vehicleEntryIndex; dst->SubType = ::Vehicle::Type(src->Type); dst->var_44 = src->Var44; dst->remaining_distance = src->RemainingDistance; // Properties from vehicle entry dst->SpriteData.Width = src->SpriteWidth; dst->SpriteData.HeightMin = src->SpriteHeightNegative; dst->SpriteData.HeightMax = src->SpriteHeightPositive; dst->Orientation = src->EntityDirection; dst->SpriteData.SpriteRect = ScreenRect(src->SpriteLeft, src->SpriteTop, src->SpriteRight, src->SpriteBottom); dst->mass = src->Mass; dst->num_seats = src->NumSeats; dst->speed = src->Speed; dst->powered_acceleration = src->PoweredAcceleration; dst->brake_speed = src->BrakeSpeed; dst->velocity = src->Velocity; dst->acceleration = src->Acceleration; dst->SwingSprite = src->SwingSprite; dst->SwingPosition = src->SwingPosition; dst->SwingSpeed = src->SwingSpeed; dst->restraints_position = src->RestraintsPosition; dst->spin_sprite = src->SpinSprite; dst->sound_vector_factor = src->SoundVectorFactor; dst->spin_speed = src->SpinSpeed; dst->sound2_flags = src->Sound2Flags; dst->sound1_id = OpenRCT2::Audio::SoundId::Null; dst->sound2_id = OpenRCT2::Audio::SoundId::Null; dst->var_C0 = src->VarC0; dst->CollisionDetectionTimer = src->CollisionDetectionTimer; dst->animation_frame = src->AnimationFrame; dst->animationState = src->AnimationState; dst->NumLaps = src->NumLaps; dst->var_D3 = src->VarD3; dst->scream_sound_id = OpenRCT2::Audio::SoundId::Null; dst->Pitch = src->Pitch; dst->bank_rotation = src->BankRotation; // Seat rotation was not in RCT1 dst->target_seat_rotation = DEFAULT_SEAT_ROTATION; dst->seat_rotation = DEFAULT_SEAT_ROTATION; // Vehicle links (indexes converted later) dst->prev_vehicle_on_ride = EntityId::FromUnderlying(src->PrevVehicleOnRide); dst->next_vehicle_on_ride = EntityId::FromUnderlying(src->NextVehicleOnRide); dst->next_vehicle_on_train = EntityId::FromUnderlying(src->NextVehicleOnTrain); // Guests (indexes converted later) for (int i = 0; i < 32; i++) { const auto spriteIndex = EntityId::FromUnderlying(src->Peep[i]); dst->peep[i] = spriteIndex; if (!spriteIndex.IsNull()) { dst->peep_tshirt_colours[i] = RCT1::GetColour(src->PeepTshirtColours[i]); } } ::Vehicle::Status statusSrc = ::Vehicle::Status::MovingToEndOfStation; if (src->Status <= static_cast(::Vehicle::Status::StoppedByBlockBrakes)) { statusSrc = static_cast<::Vehicle::Status>(src->Status); } dst->status = statusSrc; dst->TrackSubposition = VehicleTrackSubposition{ src->TrackSubposition }; dst->TrackLocation = { src->TrackX, src->TrackY, src->TrackZ }; dst->current_station = StationIndex::FromUnderlying(src->CurrentStation); if (src->BoatLocation.IsNull() || ride->mode != RideMode::BoatHire || statusSrc != ::Vehicle::Status::TravellingBoat) { dst->BoatLocation.SetNull(); dst->SetTrackDirection(src->GetTrackDirection()); dst->SetTrackType(RCT1TrackTypeToOpenRCT2(src->GetTrackType(), ride->type)); } else { dst->BoatLocation = TileCoordsXY{ src->BoatLocation.x, src->BoatLocation.y }.ToCoordsXY(); dst->SetTrackDirection(0); dst->SetTrackType(0); } dst->track_progress = src->TrackProgress; dst->vertical_drop_countdown = src->VerticalDropCountdown; dst->sub_state = src->SubState; dst->Flags = src->UpdateFlags; SetVehicleColours(dst, src); dst->mini_golf_current_animation = MiniGolfAnimation(src->MiniGolfCurrentAnimation); dst->mini_golf_flags = src->MiniGolfFlags; dst->MoveTo({ src->x, src->y, src->z }); dst->num_peeps = src->NumPeeps; dst->next_free_seat = src->NextFreeSeat; if (src->Flags & RCT12_ENTITY_FLAGS_IS_CRASHED_VEHICLE_ENTITY) { dst->SetFlag(VehicleFlags::Crashed); } dst->BlockBrakeSpeed = kRCT2DefaultBlockBrakeSpeed; if (VehicleTypeIsReversed(rct1Ride.VehicleType)) { dst->SetFlag(VehicleFlags::CarIsReversed); } } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportPeep(dst, src); dst->OutsideOfPark = static_cast(src->OutsideOfPark); dst->TimeToConsume = src->TimeToConsume; dst->VandalismSeen = src->VandalismSeen; dst->UmbrellaColour = RCT1::GetColour(src->UmbrellaColour); dst->HatColour = RCT1::GetColour(src->HatColour); // Balloons were always blue in RCT1 without AA/LL if (_gameVersion == FILE_VERSION_RCT1) { dst->BalloonColour = COLOUR_LIGHT_BLUE; } else { dst->BalloonColour = RCT1::GetColour(src->BalloonColour); } dst->Happiness = src->Happiness; dst->HappinessTarget = src->HappinessTarget; dst->Nausea = src->Nausea; dst->NauseaTarget = src->NauseaTarget; dst->Hunger = src->Hunger; dst->Thirst = src->Thirst; dst->Toilet = src->Toilet; dst->LitterCount = src->LitterCount; dst->DisgustingCount = src->DisgustingCount; dst->Intensity = static_cast(src->Intensity); dst->NauseaTolerance = static_cast(src->NauseaTolerance); dst->GuestTimeOnRide = src->TimeOnRide; dst->DaysInQueue = src->DaysInQueue; dst->CashInPocket = src->CashInPocket; dst->CashSpent = src->CashSpent; dst->ParkEntryTime = src->ParkEntryTime; dst->GuestNumRides = src->NumRides; dst->AmountOfDrinks = src->NumDrinks; dst->AmountOfFood = src->NumFood; dst->AmountOfSouvenirs = src->NumSouvenirs; dst->PaidToEnter = src->PaidToEnter; dst->PaidOnRides = src->PaidOnRides; dst->PaidOnDrink = src->PaidOnDrink; dst->PaidOnFood = src->PaidOnFood; dst->PaidOnSouvenirs = src->PaidOnSouvenirs; dst->VoucherRideId = RCT12RideIdToOpenRCT2RideId(src->VoucherArguments); dst->VoucherType = src->VoucherType; dst->SurroundingsThoughtTimeout = src->SurroundingsThoughtTimeout; dst->Angriness = src->Angriness; dst->TimeLost = src->TimeLost; OpenRCT2::RideUse::GetHistory().Set(dst->Id, RCT12GetRidesBeenOn(src)); OpenRCT2::RideUse::GetTypeHistory().Set(dst->Id, RCT12GetRideTypesBeenOn(src)); dst->Photo1RideRef = RCT12RideIdToOpenRCT2RideId(src->Photo1RideRef); for (size_t i = 0; i < std::size(src->Thoughts); i++) { auto srcThought = &src->Thoughts[i]; auto dstThought = &dst->Thoughts[i]; dstThought->type = static_cast(srcThought->Type); if (srcThought->Item == RCT12PeepThoughtItemNone) dstThought->item = PeepThoughtItemNone; else dstThought->item = srcThought->Item; dstThought->freshness = srcThought->Freshness; dstThought->fresh_timeout = srcThought->FreshTimeout; } dst->PreviousRide = RCT12RideIdToOpenRCT2RideId(src->PreviousRide); dst->PreviousRideTimeOut = src->PreviousRideTimeOut; dst->GuestHeadingToRideId = RCT12RideIdToOpenRCT2RideId(src->GuestHeadingToRideID); dst->GuestIsLostCountdown = src->PeepIsLostCountdown; dst->GuestNextInQueue = EntityId::FromUnderlying(src->NextInQueue); // Guests' favourite ride was only saved in LL. // Set it to N/A if the save comes from the original or AA. if (_gameVersion == FILE_VERSION_RCT1_LL) { dst->FavouriteRide = RCT12RideIdToOpenRCT2RideId(src->FavouriteRide); dst->FavouriteRideRating = src->FavouriteRideRating; } else { dst->FavouriteRide = RideId::GetNull(); dst->FavouriteRideRating = 0; } dst->SetItemFlags(src->GetItemFlags()); } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportPeep(dst, src); dst->AssignedStaffType = StaffType(src->StaffType); dst->MechanicTimeSinceCall = src->MechanicTimeSinceCall; dst->HireDate = src->ParkEntryTime; dst->StaffOrders = src->StaffOrders; dst->StaffMowingTimeout = src->StaffMowingTimeout; dst->StaffLawnsMown = src->PaidToEnter; dst->StaffGardensWatered = src->PaidOnRides; dst->StaffLitterSwept = src->PaidOnFood; dst->StaffBinsEmptied = src->PaidOnSouvenirs; ImportStaffPatrolArea(dst, src->StaffID); } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->SubType = Litter::Type(src->Type); dst->creationTick = src->CreationTick; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; dst->time_to_move = src->TimeToMove; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->MoveDelay = src->MoveDelay; dst->NumMovements = src->NumMovements; dst->GuestPurchase = src->Vertical; dst->Value = src->Value; dst->OffsetX = src->OffsetX; dst->Wiggle = src->Wiggle; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; dst->time_to_live = src->TimeToLive; dst->colour[0] = RCT1::GetColour(src->Colour[0]); dst->colour[1] = RCT1::GetColour(src->Colour[1]); dst->crashed_sprite_base = src->CrashedEntityBase; dst->velocity_x = src->VelocityX; dst->velocity_y = src->VelocityY; dst->velocity_z = src->VelocityZ; dst->acceleration_x = src->AccelerationX; dst->acceleration_y = src->AccelerationY; dst->acceleration_z = src->AccelerationZ; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); auto fountainType = JumpingFountainType::Water; if (RCT12MiscEntityType(src->Type) == RCT12MiscEntityType::JumpingFountainSnow) fountainType = JumpingFountainType::Snow; ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; dst->FountainType = fountainType; dst->NumTicksAlive = src->NumTicksAlive; dst->FountainFlags = src->FountainFlags; dst->TargetX = src->TargetX; dst->TargetY = src->TargetY; dst->Iteration = src->Iteration; } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; dst->popped = src->Popped; dst->time_to_move = src->TimeToMove; // Balloons were always blue in RCT1 without AA/LL if (_gameVersion == FILE_VERSION_RCT1) { dst->colour = COLOUR_LIGHT_BLUE; } else { dst->colour = RCT1::GetColour(src->Colour); } } template<> void S4Importer::ImportEntity(const RCT12EntityBase& srcBase) { auto* dst = CreateEntityAt(EntityId::FromUnderlying(srcBase.EntityIndex)); auto* src = static_cast(&srcBase); ImportEntityCommonProperties(dst, src); dst->frame = src->Frame; dst->target_x = src->TargetX; dst->target_y = src->TargetY; dst->state = static_cast(src->State); } void S4Importer::ImportEntity(const RCT12EntityBase& src) { switch (GetEntityTypeFromRCT1Sprite(src)) { case EntityType::Vehicle: ImportEntity<::Vehicle>(src); break; case EntityType::Guest: ImportEntity(src); break; case EntityType::Staff: ImportEntity(src); break; case EntityType::SteamParticle: ImportEntity(src); break; case EntityType::MoneyEffect: ImportEntity(src); break; case EntityType::CrashedVehicleParticle: ImportEntity(src); break; case EntityType::ExplosionCloud: ImportEntity(src); break; case EntityType::ExplosionFlare: ImportEntity(src); break; case EntityType::CrashSplash: ImportEntity(src); break; case EntityType::JumpingFountain: ImportEntity(src); break; case EntityType::Balloon: ImportEntity(src); break; case EntityType::Duck: ImportEntity(src); break; case EntityType::Litter: ImportEntity(src); break; default: // Null elements do not need imported break; } } } // namespace RCT1 std::unique_ptr ParkImporter::CreateS4() { return std::make_unique(); }