/* Copyright (C) 2022 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 #include #include "ObjectBase.h" #include "ObjectManager.h" #include "ps/XML/Xeromyces.h" #include "ps/Filesystem.h" #include "ps/CLogger.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include namespace { /** * The maximal quality for an actor. */ static constexpr int MAX_QUALITY = 255; /** * How many quality levels a given actor can have. */ static constexpr int MAX_LEVELS_PER_ACTOR_DEF = 5; int GetQuality(const CStr& value) { if (value == "low") return 100; else if (value == "medium") return 150; else if (value == "high") return 200; else return value.ToInt(); } } // anonymous namespace CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8 qualityLevel) : m_ObjectManager(objectManager), m_ActorDef(actorDef) { m_QualityLevel = qualityLevel; m_Properties.m_CastShadows = false; m_Properties.m_FloatOnWater = false; // Remove leading art/actors/ & include quality level. m_Identifier = m_ActorDef.m_Pathname.string8().substr(11) + CStr::FromInt(m_QualityLevel); } std::unique_ptr CObjectBase::CopyWithQuality(u8 newQualityLevel) const { std::unique_ptr ret = std::make_unique(m_ObjectManager, m_ActorDef, newQualityLevel); // No need to actually change any quality-related stuff here, we assume that this is a copy for props. ret->m_VariantGroups = m_VariantGroups; ret->m_Material = m_Material; ret->m_Properties = m_Properties; return ret; } bool CObjectBase::Load(const CXeromyces& XeroFile, const XMBElement& root) { // Define all the elements used in the XML file #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) EL(castshadow); EL(float); EL(group); EL(material); AT(maxquality); AT(minquality); #undef AT #undef EL // Set up the group vector to avoid reallocation and copying later. { int groups = 0; XERO_ITER_EL(root, child) { if (child.GetNodeName() == el_group) ++groups; } m_VariantGroups.reserve(groups); } // (This XML-reading code is rather worryingly verbose...) auto shouldSkip = [&](XMBElement& node) { XERO_ITER_ATTR(node, attr) { if (attr.Name == at_minquality && GetQuality(attr.Value) > m_QualityLevel) return true; else if (attr.Name == at_maxquality && GetQuality(attr.Value) <= m_QualityLevel) return true; } return false; }; XERO_ITER_EL(root, child) { int child_name = child.GetNodeName(); if (shouldSkip(child)) continue; if (child_name == el_group) { std::vector& currentGroup = m_VariantGroups.emplace_back(); currentGroup.reserve(child.GetChildNodes().size()); XERO_ITER_EL(child, variant) { if (shouldSkip(variant)) continue; if (!LoadVariant(XeroFile, variant, currentGroup.emplace_back())) return false; } if (currentGroup.size() == 0) { LOGERROR("Actor group has zero variants ('%s')", m_Identifier); return false; } } else if (child_name == el_castshadow) m_Properties.m_CastShadows = true; else if (child_name == el_float) m_Properties.m_FloatOnWater = true; else if (child_name == el_material) m_Material = VfsPath("art/materials") / child.GetText().FromUTF8(); } if (m_Material.empty()) m_Material = VfsPath("art/materials/default.xml"); return true; } bool CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant) { #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) EL(animation); EL(animations); EL(color); EL(decal); EL(mesh); EL(particles); EL(prop); EL(props); EL(texture); EL(textures); EL(variant); AT(actor); AT(angle); AT(attachpoint); AT(depth); AT(event); AT(file); AT(frequency); AT(id); AT(load); AT(maxheight); AT(minheight); AT(name); AT(offsetx); AT(offsetz); AT(selectable); AT(sound); AT(speed); AT(width); #undef AT #undef EL if (variant.GetNodeName() != el_variant) { LOGERROR("Invalid variant format (unrecognised root element '%s')", XeroFile.GetElementString(variant.GetNodeName())); return false; } // Load variants first, so that they can be overriden if necessary. XERO_ITER_ATTR(variant, attr) { if (attr.Name == at_file) { // Open up an external file to load. // Don't crash hard when failures happen, but log them and continue m_ActorDef.m_UsedFiles.insert(attr.Value); CXeromyces XeroVariant; if (XeroVariant.Load(g_VFS, "art/variants/" + attr.Value) == PSRETURN_OK) { XMBElement variantRoot = XeroVariant.GetRoot(); if (!LoadVariant(XeroVariant, variantRoot, currentVariant)) return false; } else { LOGERROR("Could not open path %s", attr.Value); return false; } // Continue loading extra definitions in this variant to allow nested files } } XERO_ITER_ATTR(variant, attr) { if (attr.Name == at_name) currentVariant.m_VariantName = attr.Value.LowerCase(); else if (attr.Name == at_frequency) currentVariant.m_Frequency = attr.Value.ToInt(); } XERO_ITER_EL(variant, option) { int option_name = option.GetNodeName(); if (option_name == el_mesh) { currentVariant.m_ModelFilename = VfsPath("art/meshes") / option.GetText().FromUTF8(); } else if (option_name == el_textures) { XERO_ITER_EL(option, textures_element) { if (textures_element.GetNodeName() != el_texture) { LOGERROR(" can only contain elements."); return false; } Samp samp; XERO_ITER_ATTR(textures_element, se) { if (se.Name == at_file) samp.m_SamplerFile = VfsPath("art/textures/skins") / se.Value.FromUTF8(); else if (se.Name == at_name) samp.m_SamplerName = CStrIntern(se.Value); } currentVariant.m_Samplers.push_back(samp); } } else if (option_name == el_decal) { XMBAttributeList attrs = option.GetAttributes(); Decal decal; decal.m_SizeX = attrs.GetNamedItem(at_width).ToFloat(); decal.m_SizeZ = attrs.GetNamedItem(at_depth).ToFloat(); decal.m_Angle = DEGTORAD(attrs.GetNamedItem(at_angle).ToFloat()); decal.m_OffsetX = attrs.GetNamedItem(at_offsetx).ToFloat(); decal.m_OffsetZ = attrs.GetNamedItem(at_offsetz).ToFloat(); currentVariant.m_Decal = decal; } else if (option_name == el_particles) { XMBAttributeList attrs = option.GetAttributes(); VfsPath file = VfsPath("art/particles") / attrs.GetNamedItem(at_file).FromUTF8(); currentVariant.m_Particles = file; // For particle hotloading, it's easiest to reload the entire actor, // so remember the relevant particle file as a dependency for this actor m_ActorDef.m_UsedFiles.insert(file); } else if (option_name == el_color) { currentVariant.m_Color = option.GetText(); } else if (option_name == el_animations) { XERO_ITER_EL(option, anim_element) { if (anim_element.GetNodeName() != el_animation) { LOGERROR(" can only contain elements."); return false; } Anim anim; XERO_ITER_ATTR(anim_element, ae) { if (ae.Name == at_name) anim.m_AnimName = ae.Value; else if (ae.Name == at_id) anim.m_ID = ae.Value; else if (ae.Name == at_frequency) anim.m_Frequency = ae.Value.ToInt(); else if (ae.Name == at_file) anim.m_FileName = VfsPath("art/animation") / ae.Value.FromUTF8(); else if (ae.Name == at_speed) anim.m_Speed = ae.Value.ToInt() > 0 ? ae.Value.ToInt() / 100.f : 1.f; else if (ae.Name == at_event) anim.m_ActionPos = Clamp(ae.Value.ToFloat(), 0.f, 1.f); else if (ae.Name == at_load) anim.m_ActionPos2 = Clamp(ae.Value.ToFloat(), 0.f, 1.f); else if (ae.Name == at_sound) anim.m_SoundPos = Clamp(ae.Value.ToFloat(), 0.f, 1.f); } currentVariant.m_Anims.push_back(anim); } } else if (option_name == el_props) { XERO_ITER_EL(option, prop_element) { ENSURE(prop_element.GetNodeName() == el_prop); Prop prop; XERO_ITER_ATTR(prop_element, pe) { if (pe.Name == at_attachpoint) prop.m_PropPointName = pe.Value; else if (pe.Name == at_actor) prop.m_ModelName = pe.Value.FromUTF8(); else if (pe.Name == at_minheight) prop.m_minHeight = pe.Value.ToFloat(); else if (pe.Name == at_maxheight) prop.m_maxHeight = pe.Value.ToFloat(); else if (pe.Name == at_selectable) prop.m_selectable = pe.Value != "false"; } currentVariant.m_Props.push_back(prop); } } } return true; } std::vector CObjectBase::CalculateVariationKey(const std::vector*>& selections) const { // (TODO: see CObjectManager::FindObjectVariation for an opportunity to // call this function a bit less frequently) // Calculate a complete list of choices, one per group, based on the // supposedly-complete selections (i.e. not making random choices at this // stage). // In each group, if one of the variants has a name matching a string in the // first 'selections', set use that one. // Otherwise, try with the next (lower priority) selections set, and repeat. // Otherwise, choose the first variant (arbitrarily). std::vector choices; std::multimap chosenProps; for (std::vector >::const_iterator grp = m_VariantGroups.begin(); grp != m_VariantGroups.end(); ++grp) { // Ignore groups with nothing inside. (A warning will have been // emitted by the loading code.) if (grp->size() == 0) continue; int match = -1; // -1 => none found yet // If there's only a single variant, choose that one if (grp->size() == 1) { match = 0; } else { // Determine the first variant that matches the provided strings, // starting with the highest priority selections set: for (const std::set* selset : selections) { ENSURE(grp->size() < 256); // else they won't fit in 'choices' for (size_t i = 0; i < grp->size(); ++i) { if (selset->count((*grp)[i].m_VariantName)) { match = (u8)i; break; } } // Stop after finding the first match if (match != -1) break; } // If no match, just choose the first if (match == -1) match = 0; } choices.push_back(match); // Remember which props were chosen, so we can call CalculateVariationKey on them // at the end. // Erase all existing props which are overridden by this variant: const Variant& var((*grp)[match]); for (const Prop& prop : var.m_Props) chosenProps.erase(prop.m_PropPointName); // and then insert the new ones: for (const Prop& prop : var.m_Props) if (!prop.m_ModelName.empty()) chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName)); } // Load each prop, and add their CalculateVariationKey to our key: for (std::multimap::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it) { if (auto [success, prop] = m_ObjectManager.FindActorDef(it->second); success) { std::vector propChoices = prop.GetBase(m_QualityLevel)->CalculateVariationKey(selections); choices.insert(choices.end(), propChoices.begin(), propChoices.end()); } } return choices; } const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& variationKey) const { Variation variation; // variationKey should correspond with m_Variants, giving the id of the // chosen variant from each group. (Except variationKey has some bits stuck // on the end for props, but we don't care about those in here.) std::vector >::const_iterator grp = m_VariantGroups.begin(); std::vector::const_iterator match = variationKey.begin(); for ( ; grp != m_VariantGroups.end() && match != variationKey.end(); ++grp, ++match) { // Ignore groups with nothing inside. (A warning will have been // emitted by the loading code.) if (grp->size() == 0) continue; size_t id = *match; if (id >= grp->size()) { // This should be impossible debug_warn(L"BuildVariation: invalid variant id"); continue; } // Get the matched variant const CObjectBase::Variant& var ((*grp)[id]); // Apply its data: if (! var.m_ModelFilename.empty()) variation.model = var.m_ModelFilename; if (var.m_Decal.m_SizeX && var.m_Decal.m_SizeZ) variation.decal = var.m_Decal; if (! var.m_Particles.empty()) variation.particles = var.m_Particles; if (! var.m_Color.empty()) variation.color = var.m_Color; // If one variant defines one prop attached to e.g. "root", and this // variant defines two different props with the same attachpoint, the one // original should be erased, and replaced by the two new ones. // // So, erase all existing props which are overridden by this variant: for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it) variation.props.erase(it->m_PropPointName); // and then insert the new ones: for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it) if (! it->m_ModelName.empty()) // if the name is empty then the overridden prop is just deleted variation.props.insert(make_pair(it->m_PropPointName, *it)); // Same idea applies for animations. // So, erase all existing animations which are overridden by this variant: for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it) variation.anims.erase(it->m_AnimName); // and then insert the new ones: for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it) variation.anims.insert(make_pair(it->m_AnimName, *it)); // Same for samplers, though perhaps not strictly necessary: for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it) variation.samplers.erase(it->m_SamplerName.string()); for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it) variation.samplers.insert(make_pair(it->m_SamplerName.string(), *it)); } return variation; } std::set CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector>& initialSelections) const { rng_t rng; rng.seed(seed); std::set remainingSelections = CalculateRandomRemainingSelections(rng, initialSelections); for (const std::set& sel : initialSelections) remainingSelections.insert(sel.begin(), sel.end()); return remainingSelections; // now actually a complete set of selections } std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector>& initialSelections) const { std::set remainingSelections; std::multimap chosenProps; // Calculate a complete list of selections, so there is at least one // (and in most cases only one) per group. // In each group, if one of the variants has a name matching a string in // 'selections', use that one. // If more than one matches, choose randomly from those matching ones. // If none match, choose randomly from all variants. // // When choosing randomly, make use of each variant's frequency. If all // variants have frequency 0, treat them as if they were 1. CObjectManager::VariantDiversity diversity = m_ObjectManager.GetVariantDiversity(); for (std::vector >::const_iterator grp = m_VariantGroups.begin(); grp != m_VariantGroups.end(); ++grp) { // Ignore groups with nothing inside. (A warning will have been // emitted by the loading code.) if (grp->size() == 0) continue; int match = -1; // -1 => none found yet // If there's only a single variant, choose that one if (grp->size() == 1) { match = 0; } else { // See if a variant (or several, but we only care about the first) // is already matched by the selections we've made, keeping their // priority order into account for (size_t s = 0; s < initialSelections.size(); ++s) { for (size_t i = 0; i < grp->size(); ++i) { if (initialSelections[s].count((*grp)[i].m_VariantName)) { match = (int)i; break; } } if (match >= 0) break; } // If there was one, we don't need to do anything now because there's // already something to choose. Otherwise, choose randomly from the others. if (match == -1) { // Sum the frequencies int totalFreq = 0; for (size_t i = 0; i < grp->size(); ++i) totalFreq += (*grp)[i].m_Frequency; // Someone might be silly and set all variants to have freq==0, in // which case we just pretend they're all 1 bool allZero = (totalFreq == 0); if (allZero) totalFreq = (int)grp->size(); // Choose a random number in the interval [0..totalFreq) to choose one of the variants. // If the diversity is "none", force 0 to return the first valid variant. int randNum = diversity == CObjectManager::VariantDiversity::NONE ? 0 : boost::random::uniform_int_distribution(0, totalFreq-1)(rng); for (size_t i = 0; i < grp->size(); ++i) { randNum -= (allZero ? 1 : (*grp)[i].m_Frequency); if (randNum < 0) { // (If this change to 'remainingSelections' interferes with earlier choices, then // we'll get some non-fatal inconsistencies that just break the randomness. But that // shouldn't happen, much.) // (As an example, suppose you have a group with variants "a" and "b", and another // with variants "a" and "c"; now if random selection choses "b" for the first // and "a" for the second, then the selection of "a" from the second group will // cause "a" to be used in the first instead of the "b"). match = (int)i; // In limited diversity, somewhat-randomly continue. This cuts variants to about a third, // though not quite because we must pick a variant so the actual probability is more complex. // (It's also dependent on actor files not containing too many 0-frequency variants) if (diversity == CObjectManager::VariantDiversity::LIMITED && (i % 3 != 0)) { // Reset to 0 or we'll just pick every subsequent variant. randNum = 0; continue; } break; } } ENSURE(match != -1); // This should always succeed; otherwise it // wouldn't have chosen any of the variants. remainingSelections.insert((*grp)[match].m_VariantName); } } // Remember which props were chosen, so we can call CalculateRandomVariation on them // at the end. const Variant& var ((*grp)[match]); // Erase all existing props which are overridden by this variant: for (const Prop& prop : var.m_Props) chosenProps.erase(prop.m_PropPointName); // and then insert the new ones: for (const Prop& prop : var.m_Props) if (!prop.m_ModelName.empty()) chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName)); } // Load each prop, and add their required selections to ours: for (std::multimap::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it) { if (auto [success, prop] = m_ObjectManager.FindActorDef(it->second); success) { std::vector > propInitialSelections = initialSelections; if (!remainingSelections.empty()) propInitialSelections.push_back(remainingSelections); std::set propRemainingSelections = prop.GetBase(m_QualityLevel)->CalculateRandomRemainingSelections(rng, propInitialSelections); remainingSelections.insert(propRemainingSelections.begin(), propRemainingSelections.end()); // Add the prop's used files to our own (recursively) so we can hotload // when any prop is changed m_ActorDef.m_UsedFiles.insert(prop.m_UsedFiles.begin(), prop.m_UsedFiles.end()); } } return remainingSelections; } std::vector > CObjectBase::GetVariantGroups() const { std::vector > groups; // Queue of objects (main actor plus props (recursively)) to be processed std::queue objectsQueue; objectsQueue.push(this); // Set of objects already processed, so we don't do them more than once std::set objectsProcessed; while (!objectsQueue.empty()) { const CObjectBase* obj = objectsQueue.front(); objectsQueue.pop(); // Ignore repeated objects (likely to be props) if (objectsProcessed.find(obj) != objectsProcessed.end()) continue; objectsProcessed.insert(obj); // Iterate through the list of groups for (size_t i = 0; i < obj->m_VariantGroups.size(); ++i) { // Copy the group's variant names into a new vector std::vector group; group.reserve(obj->m_VariantGroups[i].size()); for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j) group.push_back(obj->m_VariantGroups[i][j].m_VariantName); // If this group is identical to one elsewhere, don't bother listing // it twice. // Linear search is theoretically not very efficient, but hopefully // we don't have enough props for that to matter... bool dupe = false; for (size_t j = 0; j < groups.size(); ++j) { if (groups[j] == group) { dupe = true; break; } } if (dupe) continue; // Add non-trivial groups (i.e. not just one entry) to the returned list if (obj->m_VariantGroups[i].size() > 1) groups.push_back(group); // Add all props onto the queue to be considered for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j) { const std::vector& props = obj->m_VariantGroups[i][j].m_Props; for (size_t k = 0; k < props.size(); ++k) if (!props[k].m_ModelName.empty()) if (auto [success, prop] = m_ObjectManager.FindActorDef(props[k].m_ModelName.c_str()); success) objectsQueue.push(prop.GetBase(m_QualityLevel).get()); } } } return groups; } void CObjectBase::GetQualitySplits(std::vector& splits) const { std::vector::iterator it = std::find_if(splits.begin(), splits.end(), [this](u8 qualityLevel) { return qualityLevel >= m_QualityLevel; }); if (it == splits.end() || *it != m_QualityLevel) splits.emplace(it, m_QualityLevel); for (const std::vector& group : m_VariantGroups) for (const Variant& variant : group) for (const Prop& prop : variant.m_Props) { // TODO: we probably should clean those up after XML load. if (prop.m_ModelName.empty()) continue; auto [success, propActor] = m_ObjectManager.FindActorDef(prop.m_ModelName.c_str()); if (!success) continue; std::vector newSplits = propActor.QualityLevels(); if (newSplits.size() <= 1) continue; // This is not entirely optimal since we might loop though redundant quality levels, but that shouldn't matter. // Custom implementation because this is inplace, std::set_union needs a 3rd vector. std::vector::iterator v1 = splits.begin(); std::vector::iterator v2 = newSplits.begin(); while (v2 != newSplits.end()) { if (v1 == splits.end() || *v1 > *v2) { v1 = ++splits.insert(v1, *v2); ++v2; } else if (*v1 == *v2) { ++v1; ++v2; } else ++v1; } } } const CStr& CObjectBase::GetIdentifier() const { return m_Identifier; } bool CObjectBase::UsesFile(const VfsPath& pathname) const { return m_ActorDef.UsesFile(pathname); } CActorDef::CActorDef(CObjectManager& objectManager) : m_ObjectManager(objectManager) { } std::set CActorDef::PickSelectionsAtRandom(uint32_t seed) const { // Use the selections from the highest quality actor - this lets artists maintain compatibility (or not) // when going to lower quality levels. std::vector> noSelections; return GetBase(255)->CalculateRandomRemainingSelections(seed, noSelections); } std::vector CActorDef::QualityLevels() const { std::vector splits; splits.reserve(m_ObjectBases.size()); for (const std::shared_ptr& base : m_ObjectBases) splits.emplace_back(base->m_QualityLevel); return splits; } const std::shared_ptr& CActorDef::GetBase(u8 QualityLevel) const { for (const std::shared_ptr& base : m_ObjectBases) if (base->m_QualityLevel >= QualityLevel) return base; // This code path ought to be impossible to take, // because by construction we must have at least one valid CObjectBase of quality MAX_QUALITY // (which necessarily fits the u8 comparison above). // However compilers will warn that we return a reference to a local temporary if I return nullptr, // so just return something sane instead. ENSURE(false); return m_ObjectBases.back(); } bool CActorDef::Load(const VfsPath& pathname) { m_UsedFiles.clear(); m_UsedFiles.insert(pathname); m_ObjectBases.clear(); CXeromyces XeroFile; if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK) return false; // Define all the elements used in the XML file #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) EL(actor); EL(inline); EL(qualitylevels); AT(file); AT(inline); AT(quality); AT(version); #undef AT #undef EL XMBElement root = XeroFile.GetRoot(); if (root.GetNodeName() != el_actor && root.GetNodeName() != el_qualitylevels) { LOGERROR("Invalid actor format (actor '%s', unrecognised root element '%s')", pathname.string8().c_str(), XeroFile.GetElementString(root.GetNodeName())); return false; } m_Pathname = pathname; if (root.GetNodeName() == el_actor) { std::unique_ptr base = std::make_unique(m_ObjectManager, *this, MAX_QUALITY); if (!base->Load(XeroFile, root)) { LOGERROR("Invalid actor (actor '%s')", pathname.string8()); return false; } m_ObjectBases.emplace_back(std::move(base)); } else { XERO_ITER_ATTR(root, attr) { if (attr.Name == at_version && attr.Value.ToInt() != 1) { LOGERROR("Invalid actor format (actor '%s', version %i is not supported)", pathname.string8().c_str(), attr.Value.ToInt()); return false; } } u8 quality = 0; XMBElement inlineActor; XERO_ITER_EL(root, child) { if (child.GetNodeName() == el_inline) inlineActor = child; } XERO_ITER_EL(root, actor) { if (actor.GetNodeName() != el_actor) continue; bool found_quality = false; bool use_inline = false; CStr file; XERO_ITER_ATTR(actor, attr) { if (attr.Name == at_quality) { int v = GetQuality(attr.Value); if (v > MAX_QUALITY) { LOGERROR("Quality levels can only go up to %i (file %s)", MAX_QUALITY, pathname.string8()); return false; } if (v <= quality) { LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8()); return false; } quality = v; found_quality = true; } else if (attr.Name == at_file) { if (attr.Value.empty()) LOGWARNING("Empty actor file specified (file %s)", pathname.string8()); file = attr.Value; } else if (attr.Name == at_inline) use_inline = true; } if (!found_quality) quality = MAX_QUALITY; std::unique_ptr base = std::make_unique(m_ObjectManager, *this, quality); if (use_inline) { if (inlineActor.GetNodeName() == -1) { LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname.string8()); return false; } if (!base->Load(XeroFile, inlineActor)) { LOGERROR("Invalid inline actor (actor '%s')", pathname.string8()); return false; } } else if (file.empty()) { if (!base->Load(XeroFile, actor)) { LOGERROR("Invalid actor (actor '%s')", pathname.string8()); return false; } } else { if (actor.GetChildNodes().size() > 0) LOGWARNING("Actor definition refers to file but has children elements, they will be ignored (file %s)", pathname.string8()); // Open up an external file to load. // Don't crash hard when failures happen, but log them and continue CXeromyces XeroActor; if (XeroActor.Load(g_VFS, VfsPath("art/actors/") / file, "actor") == PSRETURN_OK) { const XMBElement& root = XeroActor.GetRoot(); if (root.GetNodeName() == XeroActor.GetElementID("qualitylevels")) { LOGERROR("Included actors cannot define quality levels (opening %s from file %s)", file, pathname.string8()); return false; } if (!base->Load(XeroActor, root)) { LOGERROR("Invalid actor (actor '%s' loaded from '%s')", file, pathname.string8()); return false; } } else { LOGERROR("Could not open actor file at path %s (file %s)", file, pathname.string8()); return false; } m_UsedFiles.insert(file); } m_ObjectBases.emplace_back(std::move(base)); } if (quality != MAX_QUALITY) { LOGERROR("The highest quality level must be %i, but the highest level found was %i (file %s)", MAX_QUALITY, quality, pathname.string8().c_str()); return false; } } // For each quality level, check if we need to further split (because of props). std::vector splits = QualityLevels(); for (const std::shared_ptr& base : m_ObjectBases) base->GetQualitySplits(splits); ENSURE(splits.size() >= 1); if (splits.size() > MAX_LEVELS_PER_ACTOR_DEF) { LOGERROR("Too many quality levels (%i) for actor %s (max %i)", splits.size(), pathname.string8().c_str(), MAX_LEVELS_PER_ACTOR_DEF); return false; } std::vector>::iterator it = m_ObjectBases.begin(); std::vector::const_iterator qualityLevels = splits.begin(); while (it != m_ObjectBases.end()) if ((*it)->m_QualityLevel > *qualityLevels) { it = ++m_ObjectBases.emplace(it, (*it)->CopyWithQuality(*qualityLevels)); ++qualityLevels; } else if ((*it)->m_QualityLevel == *qualityLevels) { ++it; ++qualityLevels; } else ++it; return true; } bool CActorDef::UsesFile(const VfsPath& pathname) const { return m_UsedFiles.find(pathname) != m_UsedFiles.end(); } void CActorDef::LoadErrorPlaceholder(const VfsPath& pathname) { m_UsedFiles.clear(); m_ObjectBases.clear(); m_UsedFiles.emplace(pathname); m_Pathname = pathname; m_ObjectBases.emplace_back(std::make_shared(m_ObjectManager, *this, MAX_QUALITY)); }