OpenRCT2/src/openrct2/management/Research.cpp

1111 lines
33 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 "Research.h"
#include "../Date.h"
#include "../Game.h"
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../actions/ParkSetResearchFundingAction.h"
#include "../config/Config.h"
#include "../core/BitSet.hpp"
#include "../core/Guard.hpp"
#include "../core/Memory.hpp"
#include "../interface/Window.h"
#include "../localisation/Date.h"
#include "../localisation/Formatter.h"
#include "../localisation/Localisation.h"
#include "../localisation/StringIds.h"
#include "../object/ObjectEntryManager.h"
#include "../object/ObjectList.h"
#include "../object/RideObject.h"
#include "../object/SceneryGroupEntry.h"
#include "../profiling/Profiling.h"
#include "../ride/Ride.h"
#include "../ride/RideData.h"
#include "../ride/RideEntry.h"
#include "../ride/TrackData.h"
#include "../scenario/Scenario.h"
#include "../util/Util.h"
#include "../world/Park.h"
#include "../world/Scenery.h"
#include "Finance.h"
#include "NewsItem.h"
#include <algorithm>
#include <iterator>
using namespace OpenRCT2;
static constexpr int32_t _researchRate[] = {
0,
160,
250,
400,
};
static bool _researchedRideTypes[RIDE_TYPE_COUNT];
static bool _researchedRideEntries[MAX_RIDE_OBJECTS];
static bool _researchedSceneryItems[SCENERY_TYPE_COUNT][UINT16_MAX];
bool gSilentResearch = false;
/**
*
* rct2: 0x006671AD, part of 0x00667132
*/
void ResearchResetItems(GameState_t& gameState)
{
gameState.ResearchItemsUninvented.clear();
gameState.ResearchItemsInvented.clear();
}
/**
*
* rct2: 0x00684BAE
*/
void ResearchUpdateUncompletedTypes()
{
auto& gameState = GetGameState();
int32_t uncompletedResearchTypes = 0;
for (auto const& researchItem : gameState.ResearchItemsUninvented)
{
uncompletedResearchTypes |= EnumToFlag(researchItem.category);
}
gameState.ResearchUncompletedCategories = uncompletedResearchTypes;
}
/**
*
* rct2: 0x00684D2A
*/
static void ResearchCalculateExpectedDate()
{
auto& gameState = GetGameState();
if (gameState.ResearchProgressStage == RESEARCH_STAGE_INITIAL_RESEARCH
|| gameState.ResearchFundingLevel == RESEARCH_FUNDING_NONE)
{
gameState.ResearchExpectedDay = 255;
}
else
{
auto& date = GetDate();
int32_t progressRemaining = gameState.ResearchProgressStage == RESEARCH_STAGE_COMPLETING_DESIGN ? 0x10000 : 0x20000;
progressRemaining -= gameState.ResearchProgress;
int32_t daysRemaining = (progressRemaining / _researchRate[gameState.ResearchFundingLevel]) * 128;
int32_t expectedDay = date.GetMonthTicks() + (daysRemaining & 0xFFFF);
int32_t dayQuotient = expectedDay / 0x10000;
int32_t dayRemainder = expectedDay % 0x10000;
int32_t expectedMonth = DateGetMonth(date.GetMonthsElapsed() + dayQuotient + (daysRemaining >> 16));
expectedDay = (dayRemainder * Date::GetDaysInMonth(expectedMonth)) >> 16;
gameState.ResearchExpectedDay = expectedDay;
gameState.ResearchExpectedMonth = expectedMonth;
}
}
static void ResearchInvalidateRelatedWindows()
{
WindowInvalidateByClass(WindowClass::ConstructRide);
WindowInvalidateByClass(WindowClass::Research);
}
static void ResearchMarkAsFullyCompleted()
{
auto& gameState = GetGameState();
gameState.ResearchProgress = 0;
gameState.ResearchProgressStage = RESEARCH_STAGE_FINISHED_ALL;
ResearchInvalidateRelatedWindows();
// Reset funding to 0 if no more rides.
auto gameAction = ParkSetResearchFundingAction(gameState.ResearchPriorities, 0);
GameActions::Execute(&gameAction);
}
/**
*
* rct2: 0x00684BE5
*/
static void ResearchNextDesign()
{
auto& gameState = GetGameState();
if (gameState.ResearchItemsUninvented.empty())
{
ResearchMarkAsFullyCompleted();
return;
}
// Try to find a research item of a matching type, if none found, use any first item
auto it = std::find_if(
gameState.ResearchItemsUninvented.begin(), gameState.ResearchItemsUninvented.end(),
[&gameState](const auto& e) { return (gameState.ResearchPriorities & EnumToFlag(e.category)) != 0; });
if (it == gameState.ResearchItemsUninvented.end())
{
it = gameState.ResearchItemsUninvented.begin();
}
gameState.ResearchNextItem = *it;
gameState.ResearchProgress = 0;
gameState.ResearchProgressStage = RESEARCH_STAGE_DESIGNING;
ResearchInvalidateRelatedWindows();
}
static void MarkResearchItemInvented(const ResearchItem& researchItem)
{
auto& gameState = GetGameState();
gameState.ResearchItemsUninvented.erase(
std::remove(gameState.ResearchItemsUninvented.begin(), gameState.ResearchItemsUninvented.end(), researchItem),
gameState.ResearchItemsUninvented.end());
if (std::find(gameState.ResearchItemsInvented.begin(), gameState.ResearchItemsInvented.end(), researchItem)
== gameState.ResearchItemsInvented.end())
{
gameState.ResearchItemsInvented.push_back(researchItem);
}
}
/**
*
* rct2: 0x006848D4
*/
void ResearchFinishItem(const ResearchItem& researchItem)
{
auto& gameState = GetGameState();
gameState.ResearchLastItem = researchItem;
ResearchInvalidateRelatedWindows();
if (researchItem.type == Research::EntryType::Ride)
{
// Ride
auto base_ride_type = researchItem.baseRideType;
ObjectEntryIndex rideEntryIndex = researchItem.entryIndex;
const auto* rideEntry = GetRideEntryByIndex(rideEntryIndex);
if (rideEntry != nullptr && base_ride_type != RIDE_TYPE_NULL)
{
if (!RideTypeIsValid(base_ride_type))
{
LOG_WARNING("Invalid ride type: %d", base_ride_type);
base_ride_type = rideEntry->GetFirstNonNullRideType();
}
StringId availabilityString;
RideTypeSetInvented(base_ride_type);
RideEntrySetInvented(rideEntryIndex);
bool seenRideEntry[MAX_RIDE_OBJECTS]{};
for (auto const& researchItem3 : gameState.ResearchItemsUninvented)
{
ObjectEntryIndex index = researchItem3.entryIndex;
seenRideEntry[index] = true;
}
// RCT2 made non-separated vehicles available at once, by removing all but one from research.
// To ensure old files keep working, look for ride entries not in research, and make them available as well.
for (int32_t i = 0; i < MAX_RIDE_OBJECTS; i++)
{
if (!seenRideEntry[i])
{
const auto* rideEntry2 = GetRideEntryByIndex(i);
if (rideEntry2 != nullptr)
{
for (uint8_t j = 0; j < RCT2::ObjectLimits::MaxRideTypesPerRideEntry; j++)
{
if (rideEntry2->ride_type[j] == base_ride_type)
{
RideEntrySetInvented(i);
ResearchInsertRideEntry(i, true);
break;
}
}
}
}
}
Formatter ft;
// If a vehicle is the first to be invented for its ride type, show the ride type/group name.
// Independently listed vehicles (like all flat rides and shops) should always be announced as such.
if (GetRideTypeDescriptor(base_ride_type).HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY)
|| researchItem.flags & RESEARCH_ENTRY_FLAG_FIRST_OF_TYPE)
{
RideNaming naming = GetRideNaming(base_ride_type, *rideEntry);
availabilityString = STR_NEWS_ITEM_RESEARCH_NEW_RIDE_AVAILABLE;
ft.Add<StringId>(naming.Name);
}
// If the vehicle should not be listed separately and it isn't the first to be invented for its ride group,
// report it as a new vehicle for the existing ride group.
else
{
availabilityString = STR_NEWS_ITEM_RESEARCH_NEW_VEHICLE_AVAILABLE;
RideNaming baseRideNaming = GetRideNaming(base_ride_type, *rideEntry);
ft.Add<StringId>(baseRideNaming.Name);
ft.Add<StringId>(rideEntry->naming.Name);
}
if (!gSilentResearch)
{
if (gConfigNotifications.RideResearched)
{
News::AddItemToQueue(News::ItemType::Research, availabilityString, researchItem.rawValue, ft);
}
}
ResearchInvalidateRelatedWindows();
}
}
else
{
// Scenery
const auto* sceneryGroupEntry = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(researchItem.entryIndex);
if (sceneryGroupEntry != nullptr)
{
SceneryGroupSetInvented(researchItem.entryIndex);
Formatter ft;
ft.Add<StringId>(sceneryGroupEntry->name);
if (!gSilentResearch)
{
if (gConfigNotifications.RideResearched)
{
News::AddItemToQueue(
News::ItemType::Research, STR_NEWS_ITEM_RESEARCH_NEW_SCENERY_SET_AVAILABLE, researchItem.rawValue, ft);
}
}
ResearchInvalidateRelatedWindows();
SceneryInit();
}
}
}
/**
*
* rct2: 0x00684C7A
*/
void ResearchUpdate()
{
PROFILED_FUNCTION();
int32_t editorScreenFlags, researchLevel, currentResearchProgress;
editorScreenFlags = SCREEN_FLAGS_SCENARIO_EDITOR | SCREEN_FLAGS_TRACK_DESIGNER | SCREEN_FLAGS_TRACK_MANAGER;
if (gScreenFlags & editorScreenFlags)
{
return;
}
auto& gameState = GetGameState();
if (gameState.CurrentTicks % 32 != 0)
{
return;
}
if ((gameState.Park.Flags & PARK_FLAGS_NO_MONEY) && gameState.ResearchFundingLevel == RESEARCH_FUNDING_NONE)
{
researchLevel = RESEARCH_FUNDING_NORMAL;
}
else
{
researchLevel = gameState.ResearchFundingLevel;
}
currentResearchProgress = gameState.ResearchProgress;
currentResearchProgress += _researchRate[researchLevel];
if (currentResearchProgress <= 0xFFFF)
{
gameState.ResearchProgress = currentResearchProgress;
}
else
{
switch (gameState.ResearchProgressStage)
{
case RESEARCH_STAGE_INITIAL_RESEARCH:
ResearchNextDesign();
ResearchCalculateExpectedDate();
break;
case RESEARCH_STAGE_DESIGNING:
gameState.ResearchProgress = 0;
gameState.ResearchProgressStage = RESEARCH_STAGE_COMPLETING_DESIGN;
ResearchCalculateExpectedDate();
ResearchInvalidateRelatedWindows();
break;
case RESEARCH_STAGE_COMPLETING_DESIGN:
MarkResearchItemInvented(*gameState.ResearchNextItem);
ResearchFinishItem(*gameState.ResearchNextItem);
gameState.ResearchProgress = 0;
gameState.ResearchProgressStage = RESEARCH_STAGE_INITIAL_RESEARCH;
ResearchCalculateExpectedDate();
ResearchUpdateUncompletedTypes();
ResearchInvalidateRelatedWindows();
break;
case RESEARCH_STAGE_FINISHED_ALL:
gameState.ResearchFundingLevel = RESEARCH_FUNDING_NONE;
break;
}
}
}
/**
*
* rct2: 0x00684AC3
*/
void ResearchResetCurrentItem()
{
auto& gameState = GetGameState();
SetEveryRideTypeNotInvented();
SetEveryRideEntryNotInvented();
// The following two instructions together make all items not tied to a scenery group available.
SetAllSceneryItemsInvented();
SetAllSceneryGroupsNotInvented();
for (const auto& researchItem : gameState.ResearchItemsInvented)
{
ResearchFinishItem(researchItem);
}
gameState.ResearchLastItem = std::nullopt;
gameState.ResearchProgressStage = RESEARCH_STAGE_INITIAL_RESEARCH;
gameState.ResearchProgress = 0;
}
/**
*
* rct2: 0x006857FA
*/
static void ResearchInsertUnresearched(ResearchItem&& item)
{
auto& gameState = GetGameState();
// First check to make sure that entry is not already accounted for
if (item.Exists())
{
return;
}
gameState.ResearchItemsUninvented.push_back(std::move(item));
}
/**
*
* rct2: 0x00685826
*/
static void ResearchInsertResearched(ResearchItem&& item)
{
auto& gameState = GetGameState();
// First check to make sure that entry is not already accounted for
if (item.Exists())
{
return;
}
gameState.ResearchItemsInvented.push_back(std::move(item));
}
/**
*
* rct2: 0x006857CF
*/
void ResearchRemove(const ResearchItem& researchItem)
{
auto& gameState = GetGameState();
gameState.ResearchItemsUninvented.erase(
std::remove(gameState.ResearchItemsUninvented.begin(), gameState.ResearchItemsUninvented.end(), researchItem),
gameState.ResearchItemsUninvented.end());
gameState.ResearchItemsInvented.erase(
std::remove(gameState.ResearchItemsInvented.begin(), gameState.ResearchItemsInvented.end(), researchItem),
gameState.ResearchItemsInvented.end());
}
void ResearchInsert(ResearchItem&& item, bool researched)
{
if (researched)
{
ResearchInsertResearched(std::move(item));
}
else
{
ResearchInsertUnresearched(std::move(item));
}
}
/**
*
* rct2: 0x00685675
*/
void ResearchPopulateListRandom()
{
auto& gameState = GetGameState();
ResearchResetItems(gameState);
// Rides
for (int32_t i = 0; i < MAX_RIDE_OBJECTS; i++)
{
const auto* rideEntry = GetRideEntryByIndex(i);
if (rideEntry == nullptr)
{
continue;
}
int32_t researched = (ScenarioRand() & 0xFF) > 128;
for (auto rideType : rideEntry->ride_type)
{
if (rideType != RIDE_TYPE_NULL)
{
ResearchCategory category = GetRideTypeDescriptor(rideType).GetResearchCategory();
ResearchInsertRideEntry(rideType, i, category, researched);
}
}
}
// Scenery
for (uint32_t i = 0; i < MAX_SCENERY_GROUP_OBJECTS; i++)
{
const auto* sceneryGroupEntry = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(i);
if (sceneryGroupEntry == nullptr)
{
continue;
}
int32_t researched = (ScenarioRand() & 0xFF) > 85;
ResearchInsertSceneryGroupEntry(i, researched);
}
}
bool ResearchInsertRideEntry(ride_type_t rideType, ObjectEntryIndex entryIndex, ResearchCategory category, bool researched)
{
if (rideType != RIDE_TYPE_NULL && entryIndex != OBJECT_ENTRY_INDEX_NULL)
{
auto tmpItem = ResearchItem(Research::EntryType::Ride, entryIndex, rideType, category, 0);
ResearchInsert(std::move(tmpItem), researched);
return true;
}
return false;
}
void ResearchInsertRideEntry(ObjectEntryIndex entryIndex, bool researched)
{
const auto* rideEntry = GetRideEntryByIndex(entryIndex);
if (rideEntry == nullptr)
return;
for (auto rideType : rideEntry->ride_type)
{
if (rideType != RIDE_TYPE_NULL)
{
ResearchCategory category = GetRideTypeDescriptor(rideType).GetResearchCategory();
ResearchInsertRideEntry(rideType, entryIndex, category, researched);
}
}
}
bool ResearchInsertSceneryGroupEntry(ObjectEntryIndex entryIndex, bool researched)
{
if (entryIndex != OBJECT_ENTRY_INDEX_NULL)
{
auto tmpItem = ResearchItem(Research::EntryType::Scenery, entryIndex, 0, ResearchCategory::SceneryGroup, 0);
ResearchInsert(std::move(tmpItem), researched);
return true;
}
return false;
}
bool ResearchIsInvented(ObjectType objectType, ObjectEntryIndex index)
{
switch (objectType)
{
case ObjectType::Ride:
return RideEntryIsInvented(index);
case ObjectType::SceneryGroup:
return SceneryGroupIsInvented(index);
case ObjectType::SmallScenery:
return SceneryIsInvented({ SCENERY_TYPE_SMALL, index });
case ObjectType::LargeScenery:
return SceneryIsInvented({ SCENERY_TYPE_LARGE, index });
case ObjectType::Walls:
return SceneryIsInvented({ SCENERY_TYPE_WALL, index });
case ObjectType::Banners:
return SceneryIsInvented({ SCENERY_TYPE_BANNER, index });
case ObjectType::PathAdditions:
return SceneryIsInvented({ SCENERY_TYPE_PATH_ITEM, index });
default:
return true;
}
}
bool RideTypeIsInvented(uint32_t rideType)
{
return RideTypeIsValid(rideType) ? _researchedRideTypes[rideType] : false;
}
bool RideEntryIsInvented(ObjectEntryIndex rideEntryIndex)
{
if (rideEntryIndex >= std::size(_researchedRideEntries))
return false;
return _researchedRideEntries[rideEntryIndex];
}
void RideTypeSetInvented(uint32_t rideType)
{
if (RideTypeIsValid(rideType))
{
_researchedRideTypes[rideType] = true;
}
}
void RideEntrySetInvented(ObjectEntryIndex rideEntryIndex)
{
if (rideEntryIndex >= std::size(_researchedRideEntries))
LOG_ERROR("Tried setting ride entry %u as invented", rideEntryIndex);
else
_researchedRideEntries[rideEntryIndex] = true;
}
bool SceneryIsInvented(const ScenerySelection& sceneryItem)
{
if (sceneryItem.SceneryType < SCENERY_TYPE_COUNT)
{
return _researchedSceneryItems[sceneryItem.SceneryType][sceneryItem.EntryIndex];
}
LOG_WARNING("Invalid Scenery Type");
return false;
}
void ScenerySetInvented(const ScenerySelection& sceneryItem)
{
if (sceneryItem.SceneryType < SCENERY_TYPE_COUNT)
{
_researchedSceneryItems[sceneryItem.SceneryType][sceneryItem.EntryIndex] = true;
}
else
{
LOG_WARNING("Invalid Scenery Type");
}
}
void ScenerySetNotInvented(const ScenerySelection& sceneryItem)
{
if (sceneryItem.SceneryType < SCENERY_TYPE_COUNT)
{
_researchedSceneryItems[sceneryItem.SceneryType][sceneryItem.EntryIndex] = false;
}
else
{
LOG_WARNING("Invalid Scenery Type");
}
}
bool SceneryGroupIsInvented(int32_t sgIndex)
{
auto& gameState = GetGameState();
const auto sgEntry = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(sgIndex);
if (sgEntry == nullptr || sgEntry->SceneryEntries.empty())
{
return false;
}
// All scenery is temporarily invented when in the scenario editor
if (gScreenFlags & SCREEN_FLAGS_EDITOR)
{
return true;
}
if (GetGameState().Cheats.IgnoreResearchStatus)
{
return true;
}
return std::none_of(
std::begin(gameState.ResearchItemsUninvented), std::end(gameState.ResearchItemsUninvented),
[sgIndex](const ResearchItem& item) {
return item.type == Research::EntryType::Scenery && item.entryIndex == sgIndex;
});
}
void SceneryGroupSetInvented(int32_t sgIndex)
{
const auto sgEntry = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(sgIndex);
if (sgEntry != nullptr)
{
for (const auto& entry : sgEntry->SceneryEntries)
{
ScenerySetInvented(entry);
}
}
}
void SetAllSceneryGroupsNotInvented()
{
for (int32_t i = 0; i < MAX_SCENERY_GROUP_OBJECTS; ++i)
{
const auto* scenery_set = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(i);
if (scenery_set == nullptr)
{
continue;
}
for (const auto& sceneryEntry : scenery_set->SceneryEntries)
{
ScenerySetNotInvented(sceneryEntry);
}
}
}
void SetAllSceneryItemsInvented()
{
for (auto sceneryType = 0; sceneryType < SCENERY_TYPE_COUNT; sceneryType++)
{
std::fill(std::begin(_researchedSceneryItems[sceneryType]), std::end(_researchedSceneryItems[sceneryType]), true);
}
}
void SetAllSceneryItemsNotInvented()
{
for (auto sceneryType = 0; sceneryType < SCENERY_TYPE_COUNT; sceneryType++)
{
std::fill(std::begin(_researchedSceneryItems[sceneryType]), std::end(_researchedSceneryItems[sceneryType]), false);
}
}
void SetEveryRideTypeInvented()
{
std::fill(std::begin(_researchedRideTypes), std::end(_researchedRideTypes), true);
}
void SetEveryRideTypeNotInvented()
{
std::fill(std::begin(_researchedRideTypes), std::end(_researchedRideTypes), false);
}
void SetEveryRideEntryInvented()
{
std::fill(std::begin(_researchedRideEntries), std::end(_researchedRideEntries), true);
}
void SetEveryRideEntryNotInvented()
{
std::fill(std::begin(_researchedRideEntries), std::end(_researchedRideEntries), false);
}
/**
*
* rct2: 0x0068563D
*/
StringId ResearchItem::GetName() const
{
if (type == Research::EntryType::Ride)
{
const auto* rideEntry = GetRideEntryByIndex(entryIndex);
if (rideEntry == nullptr)
{
return STR_EMPTY;
}
return rideEntry->naming.Name;
}
const auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(entryIndex);
if (sceneryEntry == nullptr)
{
return STR_EMPTY;
}
return sceneryEntry->name;
}
/**
*
* rct2: 0x00685A79
* Do not use the research list outside of the inventions list window with the flags
* Clears flags like "always researched".
*/
void ResearchRemoveFlags()
{
auto& gameState = GetGameState();
for (auto& researchItem : gameState.ResearchItemsUninvented)
{
researchItem.flags &= ~(RESEARCH_ENTRY_FLAG_RIDE_ALWAYS_RESEARCHED | RESEARCH_ENTRY_FLAG_SCENERY_SET_ALWAYS_RESEARCHED);
}
for (auto& researchItem : gameState.ResearchItemsInvented)
{
researchItem.flags &= ~(RESEARCH_ENTRY_FLAG_RIDE_ALWAYS_RESEARCHED | RESEARCH_ENTRY_FLAG_SCENERY_SET_ALWAYS_RESEARCHED);
}
}
static void ResearchRemoveNullItems(std::vector<ResearchItem>& items)
{
const auto it = std::remove_if(std::begin(items), std::end(items), [](const ResearchItem& researchItem) {
if (researchItem.type == Research::EntryType::Ride)
{
return GetRideEntryByIndex(researchItem.entryIndex) == nullptr;
}
else
{
return OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(researchItem.entryIndex) == nullptr;
}
});
items.erase(it, std::end(items));
}
static void ResearchMarkItemAsResearched(const ResearchItem& item)
{
if (item.type == Research::EntryType::Ride)
{
const auto* rideEntry = GetRideEntryByIndex(item.entryIndex);
if (rideEntry != nullptr)
{
RideEntrySetInvented(item.entryIndex);
for (auto rideType : rideEntry->ride_type)
{
if (rideType != RIDE_TYPE_NULL)
{
RideTypeSetInvented(rideType);
}
}
}
}
else if (item.type == Research::EntryType::Scenery)
{
const auto sgEntry = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(item.entryIndex);
if (sgEntry != nullptr)
{
for (const auto& sceneryEntry : sgEntry->SceneryEntries)
{
ScenerySetInvented(sceneryEntry);
}
}
}
}
static void ResearchRebuildInventedTables()
{
auto& gameState = GetGameState();
SetEveryRideTypeNotInvented();
SetEveryRideEntryInvented();
SetEveryRideEntryNotInvented();
SetAllSceneryItemsNotInvented();
for (const auto& item : gameState.ResearchItemsInvented)
{
// Ignore item, if the research of it is in progress
if (gameState.ResearchProgressStage == RESEARCH_STAGE_DESIGNING
|| gameState.ResearchProgressStage == RESEARCH_STAGE_COMPLETING_DESIGN)
{
if (item == gameState.ResearchNextItem)
{
continue;
}
}
ResearchMarkItemAsResearched(item);
}
MarkAllUnrestrictedSceneryAsInvented();
}
static void ResearchAddAllMissingItems(bool isResearched)
{
auto& gameState = GetGameState();
// Mark base ridetypes as seen if they exist in the invented research list.
bool seenBaseEntry[MAX_RIDE_OBJECTS]{};
for (auto const& researchItem : gameState.ResearchItemsInvented)
{
ObjectEntryIndex index = researchItem.baseRideType;
seenBaseEntry[index] = true;
}
// Unlock and add research entries to the invented list for ride types whose base ridetype has been seen.
for (ObjectEntryIndex i = 0; i < MAX_RIDE_OBJECTS; i++)
{
const auto* rideEntry = GetRideEntryByIndex(i);
if (rideEntry != nullptr)
{
for (uint8_t j = 0; j < RCT2::ObjectLimits::MaxRideTypesPerRideEntry; j++)
{
if (seenBaseEntry[rideEntry->ride_type[j]])
{
RideEntrySetInvented(i);
ResearchInsertRideEntry(i, true);
break;
}
}
}
}
// Mark base ridetypes as seen if they exist in the uninvented research list.
for (auto const& researchItem : gameState.ResearchItemsUninvented)
{
ObjectEntryIndex index = researchItem.baseRideType;
seenBaseEntry[index] = true;
}
// Only add Rides to uninvented research that haven't had their base ridetype seen.
// This prevents rct2 grouped rides from only unlocking the first train.
for (ObjectEntryIndex i = 0; i < MAX_RIDE_OBJECTS; i++)
{
const auto* rideEntry = GetRideEntryByIndex(i);
if (rideEntry != nullptr)
{
bool baseSeen = false;
for (uint8_t j = 0; j < RCT2::ObjectLimits::MaxRideTypesPerRideEntry; j++)
{
if (seenBaseEntry[rideEntry->ride_type[j]])
{
baseSeen = true;
break;
}
}
if (!baseSeen)
{
ResearchInsertRideEntry(i, isResearched);
}
}
}
for (ObjectEntryIndex i = 0; i < MAX_SCENERY_GROUP_OBJECTS; i++)
{
const auto* groupEntry = OpenRCT2::ObjectManager::GetObjectEntry<SceneryGroupEntry>(i);
if (groupEntry != nullptr)
{
ResearchInsertSceneryGroupEntry(i, isResearched);
}
}
}
void ResearchFix()
{
auto& gameState = GetGameState();
// Remove null entries from the research list
ResearchRemoveNullItems(gameState.ResearchItemsInvented);
ResearchRemoveNullItems(gameState.ResearchItemsUninvented);
// Add missing entries to the research list
// If research is complete, mark all the missing items as available
ResearchAddAllMissingItems(gameState.ResearchProgressStage == RESEARCH_STAGE_FINISHED_ALL);
// Now rebuild all the tables that say whether a ride or scenery item is invented
ResearchRebuildInventedTables();
ResearchUpdateUncompletedTypes();
}
void ResearchItemsMakeAllUnresearched()
{
auto& gameState = GetGameState();
gameState.ResearchItemsUninvented.insert(
gameState.ResearchItemsUninvented.end(), std::make_move_iterator(gameState.ResearchItemsInvented.begin()),
std::make_move_iterator(gameState.ResearchItemsInvented.end()));
gameState.ResearchItemsInvented.clear();
}
void ResearchItemsMakeAllResearched()
{
auto& gameState = GetGameState();
gameState.ResearchItemsInvented.insert(
gameState.ResearchItemsInvented.end(), std::make_move_iterator(gameState.ResearchItemsUninvented.begin()),
std::make_move_iterator(gameState.ResearchItemsUninvented.end()));
gameState.ResearchItemsUninvented.clear();
}
/**
*
* rct2: 0x00685A93
*/
void ResearchItemsShuffle()
{
auto& gameState = GetGameState();
std::shuffle(
std::begin(gameState.ResearchItemsUninvented), std::end(gameState.ResearchItemsUninvented),
std::default_random_engine{});
}
bool ResearchItem::IsAlwaysResearched() const
{
return (flags & (RESEARCH_ENTRY_FLAG_RIDE_ALWAYS_RESEARCHED | RESEARCH_ENTRY_FLAG_SCENERY_SET_ALWAYS_RESEARCHED)) != 0;
}
bool ResearchItem::IsNull() const
{
return entryIndex == OBJECT_ENTRY_INDEX_NULL;
}
void ResearchItem::SetNull()
{
entryIndex = OBJECT_ENTRY_INDEX_NULL;
}
bool ResearchItem::Exists() const
{
auto& gameState = GetGameState();
for (auto const& researchItem : gameState.ResearchItemsUninvented)
{
if (researchItem == *this)
{
return true;
}
}
for (auto const& researchItem : gameState.ResearchItemsInvented)
{
if (researchItem == *this)
{
return true;
}
}
return false;
}
// clang-format off
static constexpr StringId _editorInventionsResearchCategories[] = {
STR_RESEARCH_NEW_TRANSPORT_RIDES,
STR_RESEARCH_NEW_GENTLE_RIDES,
STR_RESEARCH_NEW_ROLLER_COASTERS,
STR_RESEARCH_NEW_THRILL_RIDES,
STR_RESEARCH_NEW_WATER_RIDES,
STR_RESEARCH_NEW_SHOPS_AND_STALLS,
STR_RESEARCH_NEW_SCENERY_AND_THEMING,
};
// clang-format on
StringId ResearchItem::GetCategoryInventionString() const
{
const auto categoryValue = EnumValue(category);
Guard::Assert(categoryValue <= 6, "Unsupported category invention string");
return _editorInventionsResearchCategories[categoryValue];
}
// clang-format off
static constexpr StringId _researchCategoryNames[] = {
STR_RESEARCH_CATEGORY_TRANSPORT,
STR_RESEARCH_CATEGORY_GENTLE,
STR_RESEARCH_CATEGORY_ROLLERCOASTER,
STR_RESEARCH_CATEGORY_THRILL,
STR_RESEARCH_CATEGORY_WATER,
STR_RESEARCH_CATEGORY_SHOP,
STR_RESEARCH_CATEGORY_SCENERY_GROUP,
};
// clang-format on
StringId ResearchItem::GetCategoryName() const
{
const auto categoryValue = EnumValue(category);
Guard::Assert(categoryValue <= 6, "Unsupported category name");
return _researchCategoryNames[categoryValue];
}
bool ResearchItem::operator==(const ResearchItem& rhs) const
{
return (entryIndex == rhs.entryIndex && baseRideType == rhs.baseRideType && type == rhs.type);
}
static BitSet<RIDE_TYPE_COUNT> _seenRideType = {};
static void ResearchUpdateFirstOfType(ResearchItem* researchItem)
{
if (researchItem->IsNull())
return;
if (researchItem->type != Research::EntryType::Ride)
return;
auto rideType = researchItem->baseRideType;
if (rideType >= RIDE_TYPE_COUNT)
{
LOG_ERROR("Research item has non-existent ride type index %d", rideType);
return;
}
researchItem->flags &= ~RESEARCH_ENTRY_FLAG_FIRST_OF_TYPE;
const auto& rtd = GetRideTypeDescriptor(rideType);
if (rtd.HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
{
researchItem->flags |= RESEARCH_ENTRY_FLAG_FIRST_OF_TYPE;
return;
}
if (!_seenRideType[rideType])
researchItem->flags |= RESEARCH_ENTRY_FLAG_FIRST_OF_TYPE;
}
static void ResearchMarkRideTypeAsSeen(const ResearchItem& researchItem)
{
auto rideType = researchItem.baseRideType;
if (rideType >= RIDE_TYPE_COUNT)
return;
_seenRideType[rideType] = true;
}
void ResearchDetermineFirstOfType()
{
auto& gameState = GetGameState();
_seenRideType.reset();
for (const auto& researchItem : gameState.ResearchItemsInvented)
{
if (researchItem.type != Research::EntryType::Ride)
continue;
auto rideType = researchItem.baseRideType;
if (rideType >= RIDE_TYPE_COUNT)
continue;
const auto& rtd = GetRideTypeDescriptor(rideType);
if (rtd.HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
continue;
// The last research item will also be present in gameState.ResearchItemsInvented.
// Avoid marking its ride type as "invented" prematurely.
if (gameState.ResearchLastItem.has_value() && !gameState.ResearchLastItem->IsNull()
&& researchItem == gameState.ResearchLastItem.value())
continue;
// The next research item is (sometimes?) also present in gameState.ResearchItemsInvented, even though it isn't invented
// yet(!)
if (gameState.ResearchNextItem.has_value() && !gameState.ResearchNextItem->IsNull()
&& researchItem == gameState.ResearchNextItem.value())
continue;
ResearchMarkRideTypeAsSeen(researchItem);
}
if (gameState.ResearchLastItem.has_value())
{
ResearchUpdateFirstOfType(&gameState.ResearchLastItem.value());
ResearchMarkRideTypeAsSeen(gameState.ResearchLastItem.value());
}
if (gameState.ResearchNextItem.has_value())
{
ResearchUpdateFirstOfType(&gameState.ResearchNextItem.value());
ResearchMarkRideTypeAsSeen(gameState.ResearchNextItem.value());
}
for (auto& researchItem : gameState.ResearchItemsUninvented)
{
// The next research item is (sometimes?) also present in gameState.ResearchItemsUninvented
if (gameState.ResearchNextItem.has_value() && !gameState.ResearchNextItem->IsNull()
&& researchItem.baseRideType == gameState.ResearchNextItem.value().baseRideType)
{
// Copy the "first of type" flag.
researchItem.flags = gameState.ResearchNextItem->flags;
continue;
}
ResearchUpdateFirstOfType(&researchItem);
ResearchMarkRideTypeAsSeen(researchItem);
}
}