/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ObjectManager.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Game.h" #include "ps/Profile.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpVisual.h" bool CObjectManager::ObjectKey::operator< (const CObjectManager::ObjectKey& a) const { if (ObjectBaseIdentifier < a.ObjectBaseIdentifier) return true; else if (ObjectBaseIdentifier > a.ObjectBaseIdentifier) return false; else return ActorVariation < a.ActorVariation; } static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } CObjectManager::CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation) : m_MeshManager(meshManager), m_SkeletonAnimManager(skeletonAnimManager), m_Simulation(simulation) { RegisterFileReloadFunc(ReloadChangedFileCB, this); m_QualityHook = std::make_unique(g_ConfigDB.RegisterHookAndCall("max_actor_quality", [this]() { ActorQualityChanged(); })); m_VariantDiversityHook = std::make_unique(g_ConfigDB.RegisterHookAndCall("variant_diversity", [this]() { VariantDiversityChanged(); })); if (!CXeromyces::AddValidator(g_VFS, "actor", "art/actors/actor.rng")) LOGERROR("CObjectManager: failed to load actor grammar file 'art/actors/actor.rng'"); } CObjectManager::~CObjectManager() { UnloadObjects(); UnregisterFileReloadFunc(ReloadChangedFileCB, this); } std::pair CObjectManager::FindActorDef(const CStrW& actorName) { ENSURE(!actorName.empty()); decltype(m_ActorDefs)::iterator it = m_ActorDefs.find(actorName); if (it != m_ActorDefs.end() && !it->second.outdated) return { true, *it->second.obj }; std::unique_ptr actor = std::make_unique(*this); VfsPath pathname = VfsPath("art/actors/") / actorName; bool success = true; if (!actor->Load(pathname)) { // In case of failure, load a placeholder - we want to have an actor around for hotloading. // (this will leave garbage actors in the object manager if loading files with typos in the name, // but that's unlikely to be a large memory problem). LOGERROR("CObjectManager::FindActorDef(): Cannot find actor '%s'", utf8_from_wstring(actorName)); actor->LoadErrorPlaceholder(pathname); success = false; } return { success, *m_ActorDefs.insert_or_assign(actorName, std::move(actor)).first->second.obj }; } CObjectEntry* CObjectManager::FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed) { if (!actor) return nullptr; const std::shared_ptr& base = actor->GetBase(m_QualityLevel); std::vector*> completeSelections; for (const std::set& selectionSet : selections) completeSelections.emplace_back(&selectionSet); // To maintain a consistent look between quality levels, first complete with the highest-quality variants. // then complete again at the required quality level (since not all variants may be available). std::set highQualitySelections = actor->GetBase(255)->CalculateRandomRemainingSelections(seed, selections); completeSelections.emplace_back(&highQualitySelections); // We don't have to pass the high-quality selections here because they have higher priority anyways. std::set remainingSelections = base->CalculateRandomRemainingSelections(seed, selections); completeSelections.emplace_back(&remainingSelections); return FindObjectVariation(base, completeSelections); } CObjectEntry* CObjectManager::FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections) { PROFILE2("FindObjectVariation"); // Look to see whether this particular variation has already been loaded std::vector choices = base->CalculateVariationKey(completeSelections); ObjectKey key (base->GetIdentifier(), choices); decltype(m_Objects)::iterator it = m_Objects.find(key); if (it != m_Objects.end() && !it->second.outdated) return it->second.obj.get(); // If it hasn't been loaded, load it now. std::unique_ptr obj = std::make_unique(base, m_Simulation); // TODO (for some efficiency): use the pre-calculated choices for this object, // which has already worked out what to do for props, instead of passing the // selections into BuildVariation and having it recalculate the props' choices. if (!obj->BuildVariation(completeSelections, choices, *this)) return nullptr; return m_Objects.insert_or_assign(key, std::move(obj)).first->second.obj.get(); } CTerrain* CObjectManager::GetTerrain() { CmpPtr cmpTerrain(m_Simulation, SYSTEM_ENTITY); if (!cmpTerrain) return NULL; return cmpTerrain->GetCTerrain(); } CObjectManager::VariantDiversity CObjectManager::GetVariantDiversity() const { return m_VariantDiversity; } void CObjectManager::UnloadObjects() { m_Objects.clear(); m_ActorDefs.clear(); } Status CObjectManager::ReloadChangedFile(const VfsPath& path) { bool changed = false; // Mark old entries as outdated so we don't reload them from the cache for (std::pair>& object : m_Objects) if (!object.second.outdated && object.second.obj->m_Base->UsesFile(path)) { object.second.outdated = true; changed = true; } const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); // Reload actors that use a changed object for (std::pair>& actor : m_ActorDefs) { if (!actor.second.outdated && actor.second.obj->UsesFile(path)) { actor.second.outdated = true; changed = true; // Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the // object with all correct variations, and we don't want to waste space storing it just for the // rare occurrence of hotloading, so we'll tell the component (which does preserve the information) // to do the reloading itself for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) static_cast(eit->second)->Hotload(actor.first); } } if (changed) // Trigger an interpolate call - needed because the game may be paused & if so, models disappear. m_Simulation.Interpolate(0.f, 0.f, 0.f); return INFO::OK; } void CObjectManager::ActorQualityChanged() { int quality; CFG_GET_VAL("max_actor_quality", quality); if (quality == m_QualityLevel) return; m_QualityLevel = quality > 255 ? 255 : quality < 0 ? 0 : quality; // No need to reload entries or actors, but we do need to reload all units. const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) static_cast(eit->second)->Hotload(); // Trigger an interpolate call - needed because the game is generally paused & models disappear otherwise. m_Simulation.Interpolate(0.f, 0.f, 0.f); } void CObjectManager::VariantDiversityChanged() { CStr value; CFG_GET_VAL("variant_diversity", value); VariantDiversity variantDiversity = VariantDiversity::FULL; if (value == "none") variantDiversity = VariantDiversity::NONE; else if (value == "limited") variantDiversity = VariantDiversity::LIMITED; // Otherwise assume full. if (variantDiversity == m_VariantDiversity) return; m_VariantDiversity = variantDiversity; // Mark old entries as outdated so we don't reload them from the cache. for (std::pair>& object : m_Objects) object.second.outdated = true; // Reload actors. for (std::pair>& actor : m_ActorDefs) actor.second.outdated = true; // Reload visual actor components. const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) static_cast(eit->second)->Hotload(); // Trigger an interpolate call - needed because the game is generally paused & models disappear otherwise. m_Simulation.Interpolate(0.f, 0.f, 0.f); }