Changeset View
Changeset View
Standalone View
Standalone View
source/graphics/ObjectManager.cpp
/* Copyright (C) 2020 Wildfire Games. | /* Copyright (C) 2021 Wildfire Games. | ||||
* This file is part of 0 A.D. | * This file is part of 0 A.D. | ||||
* | * | ||||
* 0 A.D. is free software: you can redistribute it and/or modify | * 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 | * it under the terms of the GNU General Public License as published by | ||||
* the Free Software Foundation, either version 2 of the License, or | * the Free Software Foundation, either version 2 of the License, or | ||||
* (at your option) any later version. | * (at your option) any later version. | ||||
* | * | ||||
* 0 A.D. is distributed in the hope that it will be useful, | * 0 A.D. is distributed in the hope that it will be useful, | ||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
* GNU General Public License for more details. | * GNU General Public License for more details. | ||||
* | * | ||||
* You should have received a copy of the GNU General Public License | * You should have received a copy of the GNU General Public License | ||||
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. | * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. | ||||
*/ | */ | ||||
#include "precompiled.h" | #include "precompiled.h" | ||||
#include "ObjectManager.h" | #include "ObjectManager.h" | ||||
#include "graphics/ObjectBase.h" | #include "graphics/ObjectBase.h" | ||||
#include "graphics/ObjectEntry.h" | #include "graphics/ObjectEntry.h" | ||||
#include "ps/CLogger.h" | #include "ps/CLogger.h" | ||||
#include "ps/ConfigDB.h" | |||||
#include "ps/Game.h" | #include "ps/Game.h" | ||||
#include "ps/Profile.h" | #include "ps/Profile.h" | ||||
#include "ps/Filesystem.h" | #include "ps/Filesystem.h" | ||||
#include "ps/XML/Xeromyces.h" | #include "ps/XML/Xeromyces.h" | ||||
#include "simulation2/Simulation2.h" | #include "simulation2/Simulation2.h" | ||||
#include "simulation2/components/ICmpTerrain.h" | #include "simulation2/components/ICmpTerrain.h" | ||||
#include "simulation2/components/ICmpVisual.h" | #include "simulation2/components/ICmpVisual.h" | ||||
struct CObjectManager::QualityHook | |||||
{ | |||||
QualityHook() = delete; | |||||
QualityHook(CConfigDB::hook_t&& h) : hook(std::move(h)) {}; | |||||
CConfigDB::hook_t hook; | |||||
}; | |||||
bool CObjectManager::ObjectKey::operator< (const CObjectManager::ObjectKey& a) const | bool CObjectManager::ObjectKey::operator< (const CObjectManager::ObjectKey& a) const | ||||
{ | { | ||||
if (ActorName < a.ActorName) | if (ActorName < a.ActorName) | ||||
return true; | return true; | ||||
else if (ActorName > a.ActorName) | else if (ActorName > a.ActorName) | ||||
return false; | return false; | ||||
else if (QualityLevel < a.QualityLevel) | |||||
return true; | |||||
else if (QualityLevel > a.QualityLevel) | |||||
return false; | |||||
else | else | ||||
return ActorVariation < a.ActorVariation; | return ActorVariation < a.ActorVariation; | ||||
} | } | ||||
static Status ReloadChangedFileCB(void* param, const VfsPath& path) | static Status ReloadChangedFileCB(void* param, const VfsPath& path) | ||||
{ | { | ||||
return static_cast<CObjectManager*>(param)->ReloadChangedFile(path); | return static_cast<CObjectManager*>(param)->ReloadChangedFile(path); | ||||
} | } | ||||
CObjectManager::CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation) | CObjectManager::CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation) | ||||
: m_MeshManager(meshManager), m_SkeletonAnimManager(skeletonAnimManager), m_Simulation(simulation) | : m_MeshManager(meshManager), m_SkeletonAnimManager(skeletonAnimManager), m_Simulation(simulation) | ||||
{ | { | ||||
RegisterFileReloadFunc(ReloadChangedFileCB, this); | RegisterFileReloadFunc(ReloadChangedFileCB, this); | ||||
m_QualityHook = std::make_unique<QualityHook>(g_ConfigDB.RegisterHookAndCall("max_actor_quality", [this]() { this->ActorQualityChanged(); })); | |||||
if (!CXeromyces::AddValidator(g_VFS, "actor", "art/actors/actor.rng")) | if (!CXeromyces::AddValidator(g_VFS, "actor", "art/actors/actor.rng")) | ||||
LOGERROR("CObjectManager: failed to load actor grammar file 'art/actors/actor.rng'"); | LOGERROR("CObjectManager: failed to load actor grammar file 'art/actors/actor.rng'"); | ||||
} | } | ||||
CObjectManager::~CObjectManager() | CObjectManager::~CObjectManager() | ||||
{ | { | ||||
UnloadObjects(); | UnloadObjects(); | ||||
g_ConfigDB.UnregisterHook(std::move(m_QualityHook->hook)); | |||||
UnregisterFileReloadFunc(ReloadChangedFileCB, this); | UnregisterFileReloadFunc(ReloadChangedFileCB, this); | ||||
} | } | ||||
CActorDef* CObjectManager::FindActorDef(const CStrW& actorName) | |||||
CObjectBase* CObjectManager::FindObjectBase(const CStrW& objectname) | |||||
{ | { | ||||
ENSURE(!objectname.empty()); | ENSURE(!actorName.empty()); | ||||
// See if the base type has been loaded yet: | decltype(m_ActorDefs)::iterator it = m_ActorDefs.find(actorName); | ||||
if (it != m_ActorDefs.end()) | |||||
return it->second.get(); | |||||
std::map<CStrW, CObjectBase*>::iterator it = m_ObjectBases.find(objectname); | std::unique_ptr<CActorDef> actor = std::make_unique<CActorDef>(*this); | ||||
if (it != m_ObjectBases.end()) | |||||
return it->second; | |||||
// Not already loaded, so try to load it: | VfsPath pathname = VfsPath("art/actors/") / actorName; | ||||
CObjectBase* obj = new CObjectBase(*this); | |||||
VfsPath pathname = VfsPath("art/actors/") / objectname; | |||||
if (obj->Load(pathname)) | |||||
{ | |||||
m_ObjectBases[objectname] = obj; | |||||
return obj; | |||||
} | |||||
else | |||||
delete obj; | |||||
LOGERROR("CObjectManager::FindObjectBase(): Cannot find object '%s'", utf8_from_wstring(objectname)); | if (actor->Load(pathname)) | ||||
return m_ActorDefs.emplace(actorName, std::move(actor)).first->second.get(); | |||||
return 0; | LOGERROR("CObjectManager::FindActorDef(): Cannot find actor '%s'", utf8_from_wstring(actorName)); | ||||
} | |||||
CObjectEntry* CObjectManager::FindObject(const CStrW& objname) | return nullptr; | ||||
{ | |||||
std::vector<std::set<CStr> > selections; // TODO - should this really be empty? | |||||
return FindObjectVariation(objname, selections); | |||||
} | } | ||||
CObjectEntry* CObjectManager::FindObjectVariation(const CStrW& objname, const std::vector<std::set<CStr> >& selections) | CObjectEntry* CObjectManager::FindObjectVariation(const CActorDef* actor, const std::vector<std::set<CStr>>& selections, uint32_t seed) | ||||
{ | { | ||||
CObjectBase* base = FindObjectBase(objname); | if (!actor) | ||||
return nullptr; | |||||
if (! base) | const std::shared_ptr<CObjectBase>& base = actor->GetBase(m_QualityLevel); | ||||
return NULL; | |||||
return FindObjectVariation(base, selections); | std::vector<const std::set<CStr>*> completeSelections; | ||||
for (const std::set<CStr>& 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<CStr> 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<CStr> remainingSelections = base->CalculateRandomRemainingSelections(seed, selections); | |||||
completeSelections.emplace_back(&remainingSelections); | |||||
return FindObjectVariation(base, completeSelections); | |||||
} | } | ||||
CObjectEntry* CObjectManager::FindObjectVariation(CObjectBase* base, const std::vector<std::set<CStr> >& selections) | CObjectEntry* CObjectManager::FindObjectVariation(const std::shared_ptr<CObjectBase>& base, const std::vector<const std::set<CStr>*>& completeSelections) | ||||
{ | { | ||||
PROFILE("object variation loading"); | PROFILE2("FindObjectVariation"); | ||||
// Look to see whether this particular variation has already been loaded | // Look to see whether this particular variation has already been loaded | ||||
std::vector<u8> choices = base->CalculateVariationKey(completeSelections); | |||||
std::vector<u8> choices = base->CalculateVariationKey(selections); | ObjectKey key (base->GetPathname(), base->m_QualityLevel, choices); | ||||
ObjectKey key (base->m_Pathname.string(), choices); | decltype(m_Objects)::iterator it = m_Objects.find(key); | ||||
std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.find(key); | |||||
if (it != m_Objects.end() && !it->second->m_Outdated) | if (it != m_Objects.end() && !it->second->m_Outdated) | ||||
return it->second; | return it->second.get(); | ||||
// If it hasn't been loaded, load it now | |||||
// TODO: If there was an existing ObjectEntry, but it's outdated (due to hotloading), | // If it hasn't been loaded, load it now. | ||||
// we'll get a memory leak when replacing its entry in m_Objects. The problem is | |||||
// some CUnits might still have a pointer to the old ObjectEntry so we probably can't | |||||
// just delete it now. Probably we need to redesign the caching/hotloading system so it | |||||
// makes more sense (e.g. use shared_ptr); for now I'll just leak, to avoid making the logic | |||||
// more complex than it is already is, since this only matters for the rare case of hotloading. | |||||
wraitii: This is done as part of the refactoring | |||||
CObjectEntry* obj = new CObjectEntry(base, m_Simulation); // TODO: type ? | std::unique_ptr<CObjectEntry> obj = std::make_unique<CObjectEntry>(base, m_Simulation); | ||||
// TODO (for some efficiency): use the pre-calculated choices for this object, | // 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 | // which has already worked out what to do for props, instead of passing the | ||||
// selections into BuildVariation and having it recalculate the props' choices. | // selections into BuildVariation and having it recalculate the props' choices. | ||||
if (! obj->BuildVariation(selections, choices, *this)) | if (!obj->BuildVariation(completeSelections, choices, *this)) | ||||
{ | return nullptr; | ||||
DeleteObject(obj); | |||||
return NULL; | |||||
} | |||||
m_Objects[key] = obj; | |||||
return obj; | return m_Objects.emplace(key, std::move(obj)).first->second.get(); | ||||
} | } | ||||
CTerrain* CObjectManager::GetTerrain() | CTerrain* CObjectManager::GetTerrain() | ||||
{ | { | ||||
CmpPtr<ICmpTerrain> cmpTerrain(m_Simulation, SYSTEM_ENTITY); | CmpPtr<ICmpTerrain> cmpTerrain(m_Simulation, SYSTEM_ENTITY); | ||||
if (!cmpTerrain) | if (!cmpTerrain) | ||||
return NULL; | return NULL; | ||||
return cmpTerrain->GetCTerrain(); | return cmpTerrain->GetCTerrain(); | ||||
} | } | ||||
void CObjectManager::DeleteObject(CObjectEntry* entry) | |||||
{ | |||||
std::function<bool(const std::pair<ObjectKey, CObjectEntry*>&)> second_equals = | |||||
[&entry](const std::pair<ObjectKey, CObjectEntry*>& a) { return a.second == entry; }; | |||||
std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.begin(); | |||||
while (m_Objects.end() != (it = find_if(it, m_Objects.end(), second_equals))) | |||||
it = m_Objects.erase(it); | |||||
delete entry; | |||||
} | |||||
void CObjectManager::UnloadObjects() | void CObjectManager::UnloadObjects() | ||||
{ | { | ||||
for (const std::pair<const ObjectKey, CObjectEntry*>& p : m_Objects) | |||||
delete p.second; | |||||
m_Objects.clear(); | m_Objects.clear(); | ||||
m_ActorDefs.clear(); | |||||
for (const std::pair<const CStrW, CObjectBase*>& p : m_ObjectBases) | |||||
delete p.second; | |||||
m_ObjectBases.clear(); | |||||
} | } | ||||
Status CObjectManager::ReloadChangedFile(const VfsPath& path) | Status CObjectManager::ReloadChangedFile(const VfsPath& path) | ||||
{ | { | ||||
// Mark old entries as outdated so we don't reload them from the cache | // Mark old entries as outdated so we don't reload them from the cache | ||||
for (std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it) | for (std::map<ObjectKey, std::unique_ptr<CObjectEntry>>::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it) | ||||
if (it->second->m_Base->UsesFile(path)) | if (it->second->m_Base->UsesFile(path)) | ||||
it->second->m_Outdated = true; | it->second->m_Outdated = true; | ||||
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); | |||||
// Reload actors that use a changed object | // Reload actors that use a changed object | ||||
for (std::map<CStrW, CObjectBase*>::iterator it = m_ObjectBases.begin(); it != m_ObjectBases.end(); ++it) | for (std::unordered_map<CStrW, std::unique_ptr<CActorDef>>::iterator it = m_ActorDefs.begin(); it != m_ActorDefs.end(); ++it) | ||||
{ | |||||
if (it->second->UsesFile(path)) | |||||
{ | { | ||||
if (!it->second->UsesFile(path)) | |||||
continue; | |||||
it->second->Reload(); | it->second->Reload(); | ||||
// Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the | // 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 | // 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) | // rare occurrence of hotloading, so we'll tell the component (which does preserve the information) | ||||
// to do the reloading itself | // to do the reloading itself | ||||
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); | |||||
for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) | for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) | ||||
static_cast<ICmpVisual*>(eit->second)->Hotload(it->first); | static_cast<ICmpVisual*>(eit->second)->Hotload(it->first); | ||||
} | } | ||||
return INFO::OK; | |||||
} | } | ||||
return INFO::OK; | void CObjectManager::ActorQualityChanged() | ||||
{ | |||||
int quality; | |||||
CFG_GET_VAL("max_actor_quality", quality); | |||||
m_QualityLevel = quality > 255 ? 255 : quality < 0 ? 0 : quality; | |||||
// Reload everything. | |||||
// This is inefficient, but that's probably OK. | |||||
Not Done Inline ActionsUse our Clamp. :) Stan: Use our Clamp. :) | |||||
for (std::map<ObjectKey, std::unique_ptr<CObjectEntry>>::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it) | |||||
it->second->m_Outdated = true; | |||||
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); | |||||
// Reload actors that use a changed object | |||||
for (std::unordered_map<CStrW, std::unique_ptr<CActorDef>>::iterator it = m_ActorDefs.begin(); it != m_ActorDefs.end(); ++it) | |||||
{ | |||||
it->second->Reload(); | |||||
// 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<ICmpVisual*>(eit->second)->Hotload(it->first); | |||||
} | |||||
// Trigger an interpolate call so that things get actually rendered. | |||||
m_Simulation.Interpolate(0.f, 0.f, 0.f); | |||||
} | } |
Wildfire Games · Phabricator
This is done as part of the refactoring