/*****************************************************************************
 * Copyright (c) 2014-2026 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 "ObjectManager.h"

#include "../Context.h"
#include "../Diagnostic.h"
#include "../ParkImporter.h"
#include "../audio/Audio.h"
#include "../core/Console.hpp"
#include "../core/EnumUtils.hpp"
#include "../core/JobPool.h"
#include "../core/Memory.hpp"
#include "../localisation/StringIds.h"
#include "../ride/Ride.h"
#include "../ride/RideAudio.h"
#include "../ui/WindowManager.h"
#include "BannerSceneryEntry.h"
#include "LargeSceneryObject.h"
#include "Object.h"
#include "ObjectLimits.h"
#include "ObjectList.h"
#include "ObjectRepository.h"
#include "PathAdditionObject.h"
#include "RideObject.h"
#include "SceneryGroupObject.h"
#include "SmallSceneryObject.h"
#include "WallObject.h"

#include <algorithm>
#include <array>
#include <atomic>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_set>

namespace OpenRCT2
{

    /**
     * Represents an object that is to be loaded or is loaded and ready
     * to be placed in an object list.
     */
    struct ObjectToLoad
    {
        const ObjectRepositoryItem* RepositoryItem{};
        Object* LoadedObject{};
        ObjectEntryIndex Index{};
    };

    class ObjectManager final : public IObjectManager
    {
    private:
        IObjectRepository& _objectRepository;

        std::array<std::vector<Object*>, EnumValue(ObjectType::count)> _loadedObjects;
        std::array<std::vector<ObjectEntryIndex>, RIDE_TYPE_COUNT> _rideTypeToObjectMap;

        // Used to return a safe empty vector back from GetAllRideEntries, can be removed when std::span is available
        std::vector<ObjectEntryIndex> _nullRideTypeEntries;

    public:
        explicit ObjectManager(IObjectRepository& objectRepository)
            : _objectRepository(objectRepository)
        {
            UpdateSceneryGroupIndexes();
            ResetTypeToRideEntryIndexMap();
        }

        ~ObjectManager() override
        {
            UnloadAll();
        }

        Object* GetLoadedObject(ObjectType objectType, size_t index) override
        {
            // This is sometimes done deliberately (to avoid boilerplate), so no need to log_warn for this.
            if (index == kObjectEntryIndexNull)
            {
                return nullptr;
            }

            if (index >= static_cast<size_t>(getObjectEntryGroupCount(objectType)))
            {
#ifdef DEBUG
                if (index != kObjectEntryIndexNull)
                {
                    LOG_WARNING("Object index %u exceeds maximum for type %d.", index, objectType);
                }
#endif
                return nullptr;
            }

            const auto& list = GetObjectList(objectType);
            if (index >= list.size())
            {
                return nullptr;
            }

            return list[index];
        }

        Object* GetLoadedObject(const ObjectEntryDescriptor& entry) override
        {
            const ObjectRepositoryItem* ori = _objectRepository.FindObject(entry);
            if (ori == nullptr)
                return nullptr;

            return ori->LoadedObject.get();
        }

        ObjectEntryIndex GetLoadedObjectEntryIndex(std::string_view identifier) override
        {
            const auto* obj = GetLoadedObject(ObjectEntryDescriptor(identifier));
            if (obj != nullptr)
            {
                return GetLoadedObjectEntryIndex(obj);
            }
            return kObjectEntryIndexNull;
        }

        ObjectEntryIndex GetLoadedObjectEntryIndex(const ObjectEntryDescriptor& descriptor) override
        {
            auto obj = GetLoadedObject(descriptor);
            if (obj != nullptr)
            {
                return GetLoadedObjectEntryIndex(obj);
            }
            return kObjectEntryIndexNull;
        }

        ObjectEntryIndex GetLoadedObjectEntryIndex(const Object* object) override
        {
            ObjectEntryIndex result = kObjectEntryIndexNull;
            size_t index = GetLoadedObjectIndex(object);
            if (index != SIZE_MAX)
            {
                ObjectGetTypeEntryIndex(index, nullptr, &result);
            }
            return result;
        }

        ObjectList GetLoadedObjects() override
        {
            ObjectList objectList;
            for (auto objectType : getAllObjectTypes())
            {
                auto maxObjectsOfType = static_cast<ObjectEntryIndex>(getObjectEntryGroupCount(objectType));
                for (ObjectEntryIndex i = 0; i < maxObjectsOfType; i++)
                {
                    auto obj = GetLoadedObject(objectType, i);
                    if (obj != nullptr)
                    {
                        objectList.SetObject(i, obj->GetDescriptor());
                    }
                }
            }
            return objectList;
        }

        std::unique_ptr<Object> LoadTempObject(std::string_view id, bool loadImages) override
        {
            const ObjectRepositoryItem* ori = _objectRepository.FindObject(id);
            if (ori == nullptr)
            {
                LOG_ERROR("Object '%s' not found in repository.", std::string{ id }.c_str());
                return nullptr;
            }

            auto object = _objectRepository.LoadObject(ori, loadImages);
            return object;
        }

        Object* LoadObject(std::string_view identifier) override
        {
            const ObjectRepositoryItem* ori = _objectRepository.FindObject(identifier);
            return RepositoryItemToObject(ori);
        }

        Object* LoadObject(const RCTObjectEntry* entry) override
        {
            const ObjectRepositoryItem* ori = _objectRepository.FindObject(entry);
            return RepositoryItemToObject(ori);
        }

        Object* LoadObject(const ObjectEntryDescriptor& descriptor) override
        {
            const ObjectRepositoryItem* ori = _objectRepository.FindObject(descriptor);
            return RepositoryItemToObject(ori);
        }

        Object* LoadObject(const ObjectEntryDescriptor& descriptor, ObjectEntryIndex slot) override
        {
            const ObjectRepositoryItem* ori = _objectRepository.FindObject(descriptor);
            return RepositoryItemToObject(ori, slot);
        }

        Object* LoadRepositoryItem(const ObjectRepositoryItem& ori) override
        {
            return RepositoryItemToObject(&ori);
        }

        void LoadObjects(const ObjectList& objectList, const bool reportProgress) override
        {
            // Find all the required objects
            auto requiredObjects = GetRequiredObjects(objectList);

            // Load the required objects
            LoadObjects(requiredObjects, reportProgress);

            // Update indices.
            UpdateSceneryGroupIndexes();
            ResetTypeToRideEntryIndexMap();
        }

        void UnloadObjects(const std::vector<ObjectEntryDescriptor>& entries) override
        {
            // TODO there are two performance issues here:
            //        - FindObject for every entry which is a dictionary lookup
            //        - GetLoadedObjectIndex for every entry which enumerates _loadedList

            size_t numObjectsUnloaded = 0;
            for (const auto& descriptor : entries)
            {
                const auto* ori = _objectRepository.FindObject(descriptor);
                if (ori != nullptr)
                {
                    auto* loadedObject = ori->LoadedObject.get();
                    if (loadedObject != nullptr)
                    {
                        UnloadObject(loadedObject);
                        numObjectsUnloaded++;
                    }
                }
            }

            if (numObjectsUnloaded > 0)
            {
                UpdateSceneryGroupIndexes();
                ResetTypeToRideEntryIndexMap();
            }
        }

        void UnloadAllTransient() override
        {
            UnloadAll(true);
        }

        void UnloadAll() override
        {
            UnloadAll(false);
        }

        void ResetObjects() override
        {
            for (auto& list : _loadedObjects)
            {
                for (auto* loadedObject : list)
                {
                    if (loadedObject != nullptr)
                    {
                        loadedObject->Unload();
                        loadedObject->Load();
                    }
                }
            }
            UpdateSceneryGroupIndexes();
            ResetTypeToRideEntryIndexMap();

            // We will need to replay the title music if the title music object got reloaded
            Audio::StopTitleMusic();
            Audio::PlayTitleMusic();
            RideAudio::StopAllChannels();
        }

        std::vector<const ObjectRepositoryItem*> GetPackableObjects() override
        {
            std::vector<const ObjectRepositoryItem*> objects;
            size_t numObjects = _objectRepository.GetNumObjects();
            for (size_t i = 0; i < numObjects; i++)
            {
                const ObjectRepositoryItem* item = &_objectRepository.GetObjects()[i];
                if (item->LoadedObject != nullptr && IsObjectCustom(item))
                {
                    objects.push_back(item);
                }
            }
            return objects;
        }

        static StringId GetObjectSourceGameString(const ObjectSourceGame sourceGame)
        {
            switch (sourceGame)
            {
                case ObjectSourceGame::rct1:
                    return STR_SCENARIO_CATEGORY_RCT1;
                case ObjectSourceGame::addedAttractions:
                    return STR_SCENARIO_CATEGORY_RCT1_AA;
                case ObjectSourceGame::loopyLandscapes:
                    return STR_SCENARIO_CATEGORY_RCT1_LL;
                case ObjectSourceGame::rct2:
                    return STR_ROLLERCOASTER_TYCOON_2_DROPDOWN;
                case ObjectSourceGame::wackyWorlds:
                    return STR_OBJECT_FILTER_WW;
                case ObjectSourceGame::timeTwister:
                    return STR_OBJECT_FILTER_TT;
                case ObjectSourceGame::openRCT2Official:
                    return STR_OBJECT_FILTER_OPENRCT2_OFFICIAL;
                default:
                    return STR_OBJECT_FILTER_CUSTOM;
            }
        }

        const std::vector<ObjectEntryIndex>& GetAllRideEntries(ride_type_t rideType) override
        {
            if (rideType >= RIDE_TYPE_COUNT)
            {
                // Return an empty vector
                return _nullRideTypeEntries;
            }
            return _rideTypeToObjectMap[rideType];
        }

    private:
        std::vector<Object*>& GetObjectList(ObjectType type)
        {
            auto typeIndex = EnumValue(type);
            return _loadedObjects[typeIndex];
        }

        void UnloadAll(bool onlyTransient)
        {
            for (auto type : getAllObjectTypes())
            {
                if (!onlyTransient || !IsIntransientObjectType(type))
                {
                    auto& list = GetObjectList(type);
                    for (auto* loadedObject : list)
                    {
                        UnloadObject(loadedObject);
                    }
                    list.clear();
                }
            }
            UpdateSceneryGroupIndexes();
            ResetTypeToRideEntryIndexMap();
        }

        Object* LoadObject(ObjectEntryIndex slot, std::string_view identifier)
        {
            const ObjectRepositoryItem* ori = _objectRepository.FindObject(identifier);
            return RepositoryItemToObject(ori, slot);
        }

        Object* RepositoryItemToObject(const ObjectRepositoryItem* ori, std::optional<ObjectEntryIndex> slot = {})
        {
            if (ori == nullptr)
                return nullptr;

            Object* loadedObject = ori->LoadedObject.get();
            if (loadedObject != nullptr)
                return loadedObject;

            ObjectType objectType = ori->Type;
            if (slot)
            {
                auto& list = GetObjectList(objectType);
                if (list.size() > *slot && list[*slot] != nullptr)
                {
                    // Slot already taken
                    return nullptr;
                }
            }
            else
            {
                slot = FindSpareSlot(objectType);
            }
            if (slot)
            {
                auto* object = GetOrLoadObject(ori);
                if (object != nullptr)
                {
                    auto& list = GetObjectList(objectType);
                    if (list.size() <= *slot)
                    {
                        list.resize(*slot + 1);
                    }
                    loadedObject = object;
                    list[*slot] = object;
                    UpdateSceneryGroupIndexes();
                    ResetTypeToRideEntryIndexMap();
                }
            }
            return loadedObject;
        }

        std::optional<ObjectEntryIndex> FindSpareSlot(ObjectType objectType)
        {
            auto& list = GetObjectList(objectType);
            auto it = std::find(list.begin(), list.end(), nullptr);
            if (it != list.end())
            {
                return static_cast<ObjectEntryIndex>(std::distance(list.begin(), it));
            }

            auto maxSize = getObjectEntryGroupCount(objectType);
            if (list.size() < static_cast<size_t>(maxSize))
            {
                list.emplace_back();
                return static_cast<ObjectEntryIndex>(list.size() - 1);
            }
            return std::nullopt;
        }

        size_t GetLoadedObjectIndex(const Object* object)
        {
            Guard::ArgumentNotNull(object, GUARD_LINE);

            auto result = std::numeric_limits<size_t>().max();
            auto& list = GetObjectList(object->GetObjectType());
            auto it = std::find(list.begin(), list.end(), object);
            if (it != list.end())
            {
                result = std::distance(list.begin(), it);
            }
            return result;
        }

        void UnloadObject(Object* object)
        {
            if (object == nullptr)
                return;

            // Because it's possible to have the same loaded object for multiple
            // slots, we have to make sure find and set all of them to nullptr
            auto& list = GetObjectList(object->GetObjectType());
            std::replace(list.begin(), list.end(), object, static_cast<Object*>(nullptr));

            object->Unload();

            // TODO try to prevent doing a repository search
            const auto* ori = _objectRepository.FindObject(object->GetDescriptor());
            if (ori != nullptr)
            {
                _objectRepository.UnregisterLoadedObject(ori, object);
            }
        }

        void UnloadObjectsExcept(const std::vector<Object*>& newLoadedObjects)
        {
            // Build a hash set for quick checking
            auto exceptSet = std::unordered_set<Object*>();
            for (auto& object : newLoadedObjects)
            {
                if (object != nullptr)
                {
                    exceptSet.insert(object);
                }
            }

            // Unload objects that are not in the hash set
            size_t totalObjectsLoaded = 0;
            size_t numObjectsUnloaded = 0;
            for (auto type : getAllObjectTypes())
            {
                if (!IsIntransientObjectType(type))
                {
                    auto& list = GetObjectList(type);
                    for (auto& object : list)
                    {
                        if (object == nullptr)
                            continue;

                        totalObjectsLoaded++;
                        if (exceptSet.find(object) == exceptSet.end())
                        {
                            UnloadObject(object);
                            object = nullptr;
                            numObjectsUnloaded++;
                        }
                    }
                }
            }

            LOG_VERBOSE("%u / %u objects unloaded", numObjectsUnloaded, totalObjectsLoaded);
        }

        template<typename T>
        void UpdateSceneryGroupIndexes(ObjectType type)
        {
            auto& list = GetObjectList(type);
            for (auto* loadedObject : list)
            {
                if (loadedObject != nullptr)
                {
                    auto* sceneryEntry = static_cast<T*>(loadedObject->GetLegacyData());
                    sceneryEntry->scenery_tab_id = GetPrimarySceneryGroupEntryIndex(loadedObject);
                }
            }
        }

        void UpdateSceneryGroupIndexes()
        {
            UpdateSceneryGroupIndexes<SmallSceneryEntry>(ObjectType::smallScenery);
            UpdateSceneryGroupIndexes<LargeSceneryEntry>(ObjectType::largeScenery);
            UpdateSceneryGroupIndexes<WallSceneryEntry>(ObjectType::walls);
            UpdateSceneryGroupIndexes<BannerSceneryEntry>(ObjectType::banners);
            UpdateSceneryGroupIndexes<PathAdditionEntry>(ObjectType::pathAdditions);

            auto& list = GetObjectList(ObjectType::sceneryGroup);
            for (auto* loadedObject : list)
            {
                auto sgObject = static_cast<SceneryGroupObject*>(loadedObject);
                if (sgObject != nullptr)
                {
                    sgObject->UpdateEntryIndexes();
                }
            }
        }

        ObjectEntryIndex GetPrimarySceneryGroupEntryIndex(Object* loadedObject)
        {
            auto* sceneryObject = dynamic_cast<SceneryObject*>(loadedObject);
            const auto& primarySGEntry = sceneryObject->GetPrimarySceneryGroup();
            Object* sgObject = GetLoadedObject(primarySGEntry);

            auto entryIndex = kObjectEntryIndexNull;
            if (sgObject != nullptr)
            {
                entryIndex = GetLoadedObjectEntryIndex(sgObject);
            }
            return entryIndex;
        }

        std::vector<ObjectToLoad> GetRequiredObjects(const ObjectList& objectList)
        {
            std::vector<ObjectToLoad> requiredObjects;
            std::vector<ObjectEntryDescriptor> missingObjects;

            for (auto objectType : getAllObjectTypes())
            {
                auto& descriptors = objectList.GetList(objectType);
                auto maxSize = static_cast<size_t>(getObjectEntryGroupCount(objectType));
                auto listSize = static_cast<ObjectEntryIndex>(std::min(descriptors.size(), maxSize));
                for (ObjectEntryIndex i = 0; i < listSize; i++)
                {
                    const auto& entry = objectList.GetObject(objectType, i);
                    if (entry.HasValue())
                    {
                        const auto* ori = _objectRepository.FindObject(entry);
                        if (ori == nullptr && entry.GetType() == ObjectType::scenarioMeta)
                        {
                            continue;
                        }

                        if (ori == nullptr)
                        {
                            missingObjects.push_back(entry);
                            ReportMissingObject(entry);
                        }

                        ObjectToLoad otl;
                        otl.RepositoryItem = ori;
                        otl.Index = i;
                        requiredObjects.push_back(otl);
                    }
                }
            }

            if (!missingObjects.empty())
            {
                throw ObjectLoadException(std::move(missingObjects));
            }

            return requiredObjects;
        }

        void ReportProgress(size_t numLoaded, size_t numRequired)
        {
            constexpr auto kObjectLoadMinProgress = 10;
            constexpr auto kObjectLoadMaxProgress = 90;
            constexpr auto kObjectLoadProgressRange = kObjectLoadMaxProgress - kObjectLoadMinProgress;

            const auto currentProgress = kObjectLoadMinProgress + (numLoaded * kObjectLoadProgressRange / numRequired);
            GetContext()->SetProgress(static_cast<uint32_t>(currentProgress), 100, STR_STRING_M_PERCENT);
        }

        void LoadObjects(std::vector<ObjectToLoad>& requiredObjects, bool reportProgress)
        {
            std::vector<Object*> objects;
            std::vector<Object*> newLoadedObjects;
            std::vector<ObjectEntryDescriptor> badObjects;

            // Create a list of objects that are currently not loaded but required.
            std::vector<const ObjectRepositoryItem*> objectsToLoad;
            for (auto& requiredObject : requiredObjects)
            {
                auto* repositoryItem = requiredObject.RepositoryItem;
                if (repositoryItem == nullptr)
                {
                    continue;
                }

                auto* loadedObject = repositoryItem->LoadedObject.get();
                if (loadedObject == nullptr)
                {
                    objectsToLoad.push_back(repositoryItem);
                }
            }

            // De-duplicate the list, since loading happens in parallel we can't have it race the repository item.
            std::sort(objectsToLoad.begin(), objectsToLoad.end());
            objectsToLoad.erase(std::unique(objectsToLoad.begin(), objectsToLoad.end()), objectsToLoad.end());

            // Prepare for loading objects multi-threaded
            std::atomic<int> numProcessed = 0;
            auto numRequired = objectsToLoad.size();
            std::mutex commonMutex;
            auto loadSingleObject = [&](const ObjectRepositoryItem* requiredObject) {
                // Object requires to be loaded, if the object successfully loads it will register it
                // as a loaded object otherwise placed into the badObjects list.
                auto newObject = _objectRepository.LoadObject(requiredObject);

                std::lock_guard<std::mutex> guard(commonMutex);
                if (newObject == nullptr)
                {
                    badObjects.push_back(ObjectEntryDescriptor(requiredObject->ObjectEntry));
                    ReportObjectLoadProblem(&requiredObject->ObjectEntry);
                }
                else
                {
                    newLoadedObjects.push_back(newObject.get());
                    // Connect the ori to the registered object
                    _objectRepository.RegisterLoadedObject(requiredObject, std::move(newObject));
                }

                numProcessed.fetch_add(1);
            };

            auto completionFn = [&]() {
                auto processed = numProcessed.load();
                if (reportProgress && (processed % 100) == 0)
                    ReportProgress(processed, numRequired);
            };

            // Dispatch loading the objects
            JobPool jobs{};
            for (auto* object : objectsToLoad)
            {
                jobs.AddTask([object, &loadSingleObject]() { loadSingleObject(object); }, completionFn);
            }

            // Wait until all jobs are fully completed
            jobs.Join();

            // Assign the loaded objects to the required objects
            for (auto& requiredObject : requiredObjects)
            {
                auto* repositoryItem = requiredObject.RepositoryItem;
                if (repositoryItem == nullptr)
                {
                    continue;
                }
                auto* loadedObject = repositoryItem->LoadedObject.get();
                if (loadedObject == nullptr)
                {
                    continue;
                }
                requiredObject.LoadedObject = loadedObject;
                objects.push_back(loadedObject);
            }

            // Load objects
            for (auto* obj : newLoadedObjects)
            {
                obj->Load();
            }

            if (!badObjects.empty())
            {
                // Unload all the new objects we loaded
                for (auto* object : newLoadedObjects)
                {
                    UnloadObject(object);
                }
                throw ObjectLoadException(std::move(badObjects));
            }

            // Unload objects which are not in the required list.
            if (objects.empty())
            {
                UnloadAllTransient();
            }
            else
            {
                UnloadObjectsExcept(objects);
            }

            // Set the new object lists
            for (auto type : getAllObjectTypes())
            {
                if (!IsIntransientObjectType(type))
                {
                    auto& list = GetObjectList(type);
                    list.clear();
                }
            }
            for (auto& otl : requiredObjects)
            {
                auto objectType = otl.LoadedObject->GetObjectType();
                auto& list = GetObjectList(objectType);
                if (list.size() <= otl.Index)
                {
                    list.resize(otl.Index + 1);
                }
                list[otl.Index] = otl.LoadedObject;
            }

            LOG_VERBOSE("%u / %u new objects loaded", newLoadedObjects.size(), requiredObjects.size());
        }

        Object* GetOrLoadObject(const ObjectRepositoryItem* ori)
        {
            auto* loadedObject = ori->LoadedObject.get();
            if (loadedObject != nullptr)
                return loadedObject;

            // Try to load object
            auto object = _objectRepository.LoadObject(ori);
            if (object != nullptr)
            {
                loadedObject = object.get();

                object->Load();

                // Connect the ori to the registered object
                _objectRepository.RegisterLoadedObject(ori, std::move(object));
            }

            return loadedObject;
        }

        void ResetTypeToRideEntryIndexMap()
        {
            // Clear all ride objects
            for (auto& v : _rideTypeToObjectMap)
            {
                v.clear();
            }

            // Build object lists
            const auto maxRideObjects = static_cast<size_t>(getObjectEntryGroupCount(ObjectType::ride));
            for (size_t i = 0; i < maxRideObjects; i++)
            {
                auto* rideObject = static_cast<RideObject*>(GetLoadedObject(ObjectType::ride, i));
                if (rideObject == nullptr)
                    continue;

                const auto& entry = rideObject->GetEntry();

                for (auto rideType : entry.ride_type)
                {
                    if (rideType < _rideTypeToObjectMap.size())
                    {
                        auto& v = _rideTypeToObjectMap[rideType];
                        v.push_back(static_cast<ObjectEntryIndex>(i));
                    }
                }
            }
        }

        static void ReportMissingObject(const ObjectEntryDescriptor& entry)
        {
            std::string name(entry.GetName());
            Console::Error::WriteLine("[%s] Object not found.", name.c_str());
        }

        void ReportObjectLoadProblem(const RCTObjectEntry* entry)
        {
            utf8 objName[kDatNameLength + 1] = { 0 };
            std::copy_n(entry->name, kDatNameLength, objName);
            Console::Error::WriteLine("[%s] Object could not be loaded.", objName);
        }
    };

    std::unique_ptr<IObjectManager> CreateObjectManager(IObjectRepository& objectRepository)
    {
        return std::make_unique<ObjectManager>(objectRepository);
    }

    Object* ObjectManagerGetLoadedObject(const ObjectEntryDescriptor& entry)
    {
        auto& objectManager = GetContext()->GetObjectManager();
        Object* loadedObject = objectManager.GetLoadedObject(entry);
        return loadedObject;
    }

    ObjectEntryIndex ObjectManagerGetLoadedObjectEntryIndex(const Object* loadedObject)
    {
        auto& objectManager = GetContext()->GetObjectManager();
        auto entryIndex = objectManager.GetLoadedObjectEntryIndex(loadedObject);
        return entryIndex;
    }

    ObjectEntryIndex ObjectManagerGetLoadedObjectEntryIndex(const ObjectEntryDescriptor& entry)
    {
        return ObjectManagerGetLoadedObjectEntryIndex(ObjectManagerGetLoadedObject(entry));
    }

    Object* ObjectManagerLoadObject(const RCTObjectEntry* entry)
    {
        auto& objectManager = GetContext()->GetObjectManager();
        Object* loadedObject = objectManager.LoadObject(entry);
        return loadedObject;
    }

    void ObjectManagerUnloadObjects(const std::vector<ObjectEntryDescriptor>& entries)
    {
        auto& objectManager = GetContext()->GetObjectManager();
        objectManager.UnloadObjects(entries);
    }

    void ObjectManagerUnloadAllObjects()
    {
        auto& objectManager = GetContext()->GetObjectManager();
        objectManager.UnloadAllTransient();
    }

    StringId ObjectManagerGetSourceGameString(const ObjectSourceGame sourceGame)
    {
        return ObjectManager::GetObjectSourceGameString(sourceGame);
    }
} // namespace OpenRCT2
