mirror of https://github.com/OpenRCT2/OpenRCT2.git
3232 lines
138 KiB
C++
3232 lines
138 KiB
C++
/*****************************************************************************
|
||
* 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/PeepData.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 <algorithm>
|
||
#include <iterator>
|
||
#include <memory>
|
||
#include <vector>
|
||
|
||
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<MAX_RIDE_OBJECTS> _researchRideEntryUsed{};
|
||
BitSet<EnumValue(RideType::Count)> _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<S4> ReadAndDecodeS4(IStream* stream, bool isScenario)
|
||
{
|
||
auto s4 = std::make_unique<S4>();
|
||
size_t dataSize = stream->GetLength() - stream->GetPosition();
|
||
auto data = stream->ReadArray<uint8_t>(dataSize);
|
||
auto decodedData = std::make_unique<uint8_t[]>(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<EnumValue(RideType::Count)> 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<RideType>(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<RideType>(researchItem->RelatedRide), static_cast<VehicleType>(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<size_t>(getObjectEntryGroupCount(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<BannerType>(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<RideStatus>(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<RideMode>(src->OperatingMode);
|
||
}
|
||
|
||
SetRideColourScheme(dst, src);
|
||
|
||
// Maintenance
|
||
dst->build_date = static_cast<int32_t>(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<RideMeasurement>();
|
||
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<typename T> 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<PeepActionType>(src->Action);
|
||
dst->SpecialSprite = src->SpecialSprite;
|
||
dst->NextActionSpriteType = static_cast<PeepActionSpriteType>(src->NextActionSpriteType);
|
||
dst->ActionSpriteImageOffset = src->ActionSpriteImageOffset;
|
||
dst->WalkingFrameNum = src->NoActionFrameNum;
|
||
dst->ActionSpriteType = static_cast<PeepActionSpriteType>(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<PeepState>(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<uint8_t>::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<std::string>& 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<std::string>({ "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<RCT12TileElement>(
|
||
Limits::MaxMapSize, _s4.TileElements, std::size(_s4.TileElements));
|
||
|
||
std::vector<TileElement> 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<RCT1::ResearchItem> 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<RideType>(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<RideType>(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<EnumValue(RideType::Count)> GetRideTypesPresentInResearchList(
|
||
const RCT1::ResearchItem* researchList, size_t researchListCount)
|
||
{
|
||
BitSet<EnumValue(RideType::Count)> 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<StringId>(_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<AwardType>(src.Type) });
|
||
}
|
||
}
|
||
|
||
// Number of guests history
|
||
std::fill(
|
||
std::begin(gameState.GuestsInParkHistory), std::end(gameState.GuestsInParkHistory),
|
||
std::numeric_limits<uint32_t>::max());
|
||
for (size_t i = 0; i < std::size(_s4.GuestsInParkHistory); i++)
|
||
{
|
||
if (_s4.GuestsInParkHistory[i] != std::numeric_limits<uint8_t>::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<News::ItemType>(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<WeatherLevel>(_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<WeatherLevel>(_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<int8_t>(_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<Guest>())
|
||
{
|
||
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<TileElement*>(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<Guest>())
|
||
{
|
||
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<RideType>(researchList[i].Item), static_cast<VehicleType>(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<const RCT1::Peep&>(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<const RCT1::Vehicle*>(&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<uint8_t>(::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<Guest>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<Guest>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT1::Peep*>(&srcBase);
|
||
ImportPeep(dst, src);
|
||
|
||
dst->OutsideOfPark = static_cast<bool>(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<IntensityRange>(src->Intensity);
|
||
dst->NauseaTolerance = static_cast<PeepNauseaTolerance>(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<PeepThoughtType>(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<Staff>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<Staff>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT1::Peep*>(&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<Litter>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<Litter>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityLitter*>(&srcBase);
|
||
ImportEntityCommonProperties(dst, src);
|
||
|
||
dst->SubType = Litter::Type(src->Type);
|
||
dst->creationTick = src->CreationTick;
|
||
}
|
||
|
||
template<> void S4Importer::ImportEntity<SteamParticle>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<SteamParticle>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntitySteamParticle*>(&srcBase);
|
||
|
||
ImportEntityCommonProperties(dst, src);
|
||
dst->frame = src->Frame;
|
||
dst->time_to_move = src->TimeToMove;
|
||
}
|
||
|
||
template<> void S4Importer::ImportEntity<MoneyEffect>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<MoneyEffect>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityMoneyEffect*>(&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<VehicleCrashParticle>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<VehicleCrashParticle>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityCrashedVehicleParticle*>(&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<ExplosionCloud>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<ExplosionCloud>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityParticle*>(&srcBase);
|
||
ImportEntityCommonProperties(dst, src);
|
||
dst->frame = src->Frame;
|
||
}
|
||
|
||
template<> void S4Importer::ImportEntity<ExplosionFlare>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<ExplosionFlare>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityParticle*>(&srcBase);
|
||
ImportEntityCommonProperties(dst, src);
|
||
dst->frame = src->Frame;
|
||
}
|
||
|
||
template<> void S4Importer::ImportEntity<CrashSplashParticle>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<CrashSplashParticle>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityParticle*>(&srcBase);
|
||
ImportEntityCommonProperties(dst, src);
|
||
dst->frame = src->Frame;
|
||
}
|
||
|
||
template<> void S4Importer::ImportEntity<JumpingFountain>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<JumpingFountain>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityJumpingFountain*>(&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<Balloon>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<Balloon>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityBalloon*>(&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<Duck>(const RCT12EntityBase& srcBase)
|
||
{
|
||
auto* dst = CreateEntityAt<Duck>(EntityId::FromUnderlying(srcBase.EntityIndex));
|
||
auto* src = static_cast<const RCT12EntityDuck*>(&srcBase);
|
||
|
||
ImportEntityCommonProperties(dst, src);
|
||
dst->frame = src->Frame;
|
||
dst->target_x = src->TargetX;
|
||
dst->target_y = src->TargetY;
|
||
dst->state = static_cast<Duck::DuckState>(src->State);
|
||
}
|
||
|
||
void S4Importer::ImportEntity(const RCT12EntityBase& src)
|
||
{
|
||
switch (GetEntityTypeFromRCT1Sprite(src))
|
||
{
|
||
case EntityType::Vehicle:
|
||
ImportEntity<::Vehicle>(src);
|
||
break;
|
||
case EntityType::Guest:
|
||
ImportEntity<Guest>(src);
|
||
break;
|
||
case EntityType::Staff:
|
||
ImportEntity<Staff>(src);
|
||
break;
|
||
case EntityType::SteamParticle:
|
||
ImportEntity<SteamParticle>(src);
|
||
break;
|
||
case EntityType::MoneyEffect:
|
||
ImportEntity<MoneyEffect>(src);
|
||
break;
|
||
case EntityType::CrashedVehicleParticle:
|
||
ImportEntity<VehicleCrashParticle>(src);
|
||
break;
|
||
case EntityType::ExplosionCloud:
|
||
ImportEntity<ExplosionCloud>(src);
|
||
break;
|
||
case EntityType::ExplosionFlare:
|
||
ImportEntity<ExplosionFlare>(src);
|
||
break;
|
||
case EntityType::CrashSplash:
|
||
ImportEntity<CrashSplashParticle>(src);
|
||
break;
|
||
case EntityType::JumpingFountain:
|
||
ImportEntity<JumpingFountain>(src);
|
||
break;
|
||
case EntityType::Balloon:
|
||
ImportEntity<Balloon>(src);
|
||
break;
|
||
case EntityType::Duck:
|
||
ImportEntity<Duck>(src);
|
||
break;
|
||
case EntityType::Litter:
|
||
ImportEntity<Litter>(src);
|
||
break;
|
||
default:
|
||
// Null elements do not need imported
|
||
break;
|
||
}
|
||
}
|
||
} // namespace RCT1
|
||
|
||
std::unique_ptr<IParkImporter> ParkImporter::CreateS4()
|
||
{
|
||
return std::make_unique<RCT1::S4Importer>();
|
||
}
|