LCOV - code coverage report
Current view: top level - source/graphics - ObjectBase.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 1 506 0.2 %
Date: 2023-01-19 00:18:29 Functions: 2 24 8.3 %

          Line data    Source code
       1             : /* Copyright (C) 2022 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include <algorithm>
      21             : #include <queue>
      22             : 
      23             : #include "ObjectBase.h"
      24             : 
      25             : #include "ObjectManager.h"
      26             : #include "ps/XML/Xeromyces.h"
      27             : #include "ps/Filesystem.h"
      28             : #include "ps/CLogger.h"
      29             : #include "lib/timer.h"
      30             : #include "maths/MathUtil.h"
      31             : 
      32             : #include <boost/random/uniform_int_distribution.hpp>
      33             : 
      34             : namespace
      35             : {
      36             : /**
      37             :  * The maximal quality for an actor.
      38             :  */
      39             : static constexpr int MAX_QUALITY = 255;
      40             : 
      41             : /**
      42             :  * How many quality levels a given actor can have.
      43             :  */
      44             : static constexpr int MAX_LEVELS_PER_ACTOR_DEF = 5;
      45             : 
      46           0 : int GetQuality(const CStr& value)
      47             : {
      48           0 :     if (value == "low")
      49           0 :         return 100;
      50           0 :     else if (value == "medium")
      51           0 :         return 150;
      52           0 :     else if (value == "high")
      53           0 :         return 200;
      54             :     else
      55           0 :         return value.ToInt();
      56             : }
      57             : } // anonymous namespace
      58             : 
      59           0 : CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8 qualityLevel)
      60           0 : : m_ObjectManager(objectManager), m_ActorDef(actorDef)
      61             : {
      62           0 :     m_QualityLevel = qualityLevel;
      63           0 :     m_Properties.m_CastShadows = false;
      64           0 :     m_Properties.m_FloatOnWater = false;
      65             : 
      66             :     // Remove leading art/actors/ & include quality level.
      67           0 :     m_Identifier = m_ActorDef.m_Pathname.string8().substr(11) + CStr::FromInt(m_QualityLevel);
      68           0 : }
      69             : 
      70           0 : std::unique_ptr<CObjectBase> CObjectBase::CopyWithQuality(u8 newQualityLevel) const
      71             : {
      72           0 :     std::unique_ptr<CObjectBase> ret = std::make_unique<CObjectBase>(m_ObjectManager, m_ActorDef, newQualityLevel);
      73             :     // No need to actually change any quality-related stuff here, we assume that this is a copy for props.
      74           0 :     ret->m_VariantGroups = m_VariantGroups;
      75           0 :     ret->m_Material = m_Material;
      76           0 :     ret->m_Properties = m_Properties;
      77           0 :     return ret;
      78             : }
      79             : 
      80           0 : bool CObjectBase::Load(const CXeromyces& XeroFile, const XMBElement& root)
      81             : {
      82             :     // Define all the elements used in the XML file
      83             : #define EL(x) int el_##x = XeroFile.GetElementID(#x)
      84             : #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
      85           0 :     EL(castshadow);
      86           0 :     EL(float);
      87           0 :     EL(group);
      88           0 :     EL(material);
      89           0 :     AT(maxquality);
      90           0 :     AT(minquality);
      91             : #undef AT
      92             : #undef EL
      93             : 
      94             : 
      95             :     // Set up the group vector to avoid reallocation and copying later.
      96             :     {
      97           0 :         int groups = 0;
      98           0 :         XERO_ITER_EL(root, child)
      99             :         {
     100           0 :             if (child.GetNodeName() == el_group)
     101           0 :                 ++groups;
     102             :         }
     103             : 
     104           0 :         m_VariantGroups.reserve(groups);
     105             :     }
     106             : 
     107             :     // (This XML-reading code is rather worryingly verbose...)
     108             : 
     109           0 :     auto shouldSkip = [&](XMBElement& node) {
     110           0 :         XERO_ITER_ATTR(node, attr)
     111             :         {
     112           0 :             if (attr.Name == at_minquality && GetQuality(attr.Value) > m_QualityLevel)
     113           0 :                 return true;
     114           0 :             else if (attr.Name == at_maxquality && GetQuality(attr.Value) <= m_QualityLevel)
     115           0 :                 return true;
     116             :         }
     117           0 :         return false;
     118           0 :     };
     119             : 
     120           0 :     XERO_ITER_EL(root, child)
     121             :     {
     122           0 :         int child_name = child.GetNodeName();
     123             : 
     124           0 :         if (shouldSkip(child))
     125           0 :             continue;
     126             : 
     127           0 :         if (child_name == el_group)
     128             :         {
     129           0 :             std::vector<Variant>& currentGroup = m_VariantGroups.emplace_back();
     130           0 :             currentGroup.reserve(child.GetChildNodes().size());
     131           0 :             XERO_ITER_EL(child, variant)
     132             :             {
     133           0 :                 if (shouldSkip(variant))
     134           0 :                     continue;
     135             : 
     136           0 :                 if (!LoadVariant(XeroFile, variant, currentGroup.emplace_back()))
     137           0 :                     return false;
     138             :             }
     139             : 
     140           0 :             if (currentGroup.size() == 0)
     141             :             {
     142           0 :                 LOGERROR("Actor group has zero variants ('%s')", m_Identifier);
     143           0 :                 return false;
     144             :             }
     145             :         }
     146           0 :         else if (child_name == el_castshadow)
     147           0 :             m_Properties.m_CastShadows = true;
     148           0 :         else if (child_name == el_float)
     149           0 :             m_Properties.m_FloatOnWater = true;
     150           0 :         else if (child_name == el_material)
     151           0 :             m_Material = VfsPath("art/materials") / child.GetText().FromUTF8();
     152             :     }
     153             : 
     154           0 :     if (m_Material.empty())
     155           0 :         m_Material = VfsPath("art/materials/default.xml");
     156             : 
     157           0 :     return true;
     158             : }
     159             : 
     160           0 : bool CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant)
     161             : {
     162             :     #define EL(x) int el_##x = XeroFile.GetElementID(#x)
     163             :     #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
     164           0 :     EL(animation);
     165           0 :     EL(animations);
     166           0 :     EL(color);
     167           0 :     EL(decal);
     168           0 :     EL(mesh);
     169           0 :     EL(particles);
     170           0 :     EL(prop);
     171           0 :     EL(props);
     172           0 :     EL(texture);
     173           0 :     EL(textures);
     174           0 :     EL(variant);
     175           0 :     AT(actor);
     176           0 :     AT(angle);
     177           0 :     AT(attachpoint);
     178           0 :     AT(depth);
     179           0 :     AT(event);
     180           0 :     AT(file);
     181           0 :     AT(frequency);
     182           0 :     AT(id);
     183           0 :     AT(load);
     184           0 :     AT(maxheight);
     185           0 :     AT(minheight);
     186           0 :     AT(name);
     187           0 :     AT(offsetx);
     188           0 :     AT(offsetz);
     189           0 :     AT(selectable);
     190           0 :     AT(sound);
     191           0 :     AT(speed);
     192           0 :     AT(width);
     193             :     #undef AT
     194             :     #undef EL
     195             : 
     196           0 :     if (variant.GetNodeName() != el_variant)
     197             :     {
     198           0 :         LOGERROR("Invalid variant format (unrecognised root element '%s')", XeroFile.GetElementString(variant.GetNodeName()));
     199           0 :         return false;
     200             :     }
     201             : 
     202             :     // Load variants first, so that they can be overriden if necessary.
     203           0 :     XERO_ITER_ATTR(variant, attr)
     204             :     {
     205           0 :         if (attr.Name == at_file)
     206             :         {
     207             :             // Open up an external file to load.
     208             :             // Don't crash hard when failures happen, but log them and continue
     209           0 :             m_ActorDef.m_UsedFiles.insert(attr.Value);
     210           0 :             CXeromyces XeroVariant;
     211           0 :             if (XeroVariant.Load(g_VFS, "art/variants/" + attr.Value) == PSRETURN_OK)
     212             :             {
     213           0 :                 XMBElement variantRoot = XeroVariant.GetRoot();
     214           0 :                 if (!LoadVariant(XeroVariant, variantRoot, currentVariant))
     215           0 :                     return false;
     216             :             }
     217             :             else
     218             :             {
     219           0 :                 LOGERROR("Could not open path %s", attr.Value);
     220           0 :                 return false;
     221             :             }
     222             :             // Continue loading extra definitions in this variant to allow nested files
     223             :         }
     224             :     }
     225             : 
     226           0 :     XERO_ITER_ATTR(variant, attr)
     227             :     {
     228           0 :         if (attr.Name == at_name)
     229           0 :             currentVariant.m_VariantName = attr.Value.LowerCase();
     230           0 :         else if (attr.Name == at_frequency)
     231           0 :             currentVariant.m_Frequency = attr.Value.ToInt();
     232             :     }
     233             : 
     234           0 :     XERO_ITER_EL(variant, option)
     235             :     {
     236           0 :         int option_name = option.GetNodeName();
     237             : 
     238           0 :         if (option_name == el_mesh)
     239             :         {
     240           0 :             currentVariant.m_ModelFilename = VfsPath("art/meshes") / option.GetText().FromUTF8();
     241             :         }
     242           0 :         else if (option_name == el_textures)
     243             :         {
     244           0 :             XERO_ITER_EL(option, textures_element)
     245             :             {
     246           0 :                 if (textures_element.GetNodeName() != el_texture)
     247             :                 {
     248           0 :                     LOGERROR("<textures> can only contain <texture> elements.");
     249           0 :                     return false;
     250             :                 }
     251             : 
     252           0 :                 Samp samp;
     253           0 :                 XERO_ITER_ATTR(textures_element, se)
     254             :                 {
     255           0 :                     if (se.Name == at_file)
     256           0 :                         samp.m_SamplerFile = VfsPath("art/textures/skins") / se.Value.FromUTF8();
     257           0 :                     else if (se.Name == at_name)
     258           0 :                         samp.m_SamplerName = CStrIntern(se.Value);
     259             :                 }
     260           0 :                 currentVariant.m_Samplers.push_back(samp);
     261             :             }
     262             :         }
     263           0 :         else if (option_name == el_decal)
     264             :         {
     265           0 :             XMBAttributeList attrs = option.GetAttributes();
     266           0 :             Decal decal;
     267           0 :             decal.m_SizeX = attrs.GetNamedItem(at_width).ToFloat();
     268           0 :             decal.m_SizeZ = attrs.GetNamedItem(at_depth).ToFloat();
     269           0 :             decal.m_Angle = DEGTORAD(attrs.GetNamedItem(at_angle).ToFloat());
     270           0 :             decal.m_OffsetX = attrs.GetNamedItem(at_offsetx).ToFloat();
     271           0 :             decal.m_OffsetZ = attrs.GetNamedItem(at_offsetz).ToFloat();
     272           0 :             currentVariant.m_Decal = decal;
     273             :         }
     274           0 :         else if (option_name == el_particles)
     275             :         {
     276           0 :             XMBAttributeList attrs = option.GetAttributes();
     277           0 :             VfsPath file = VfsPath("art/particles") / attrs.GetNamedItem(at_file).FromUTF8();
     278           0 :             currentVariant.m_Particles = file;
     279             : 
     280             :             // For particle hotloading, it's easiest to reload the entire actor,
     281             :             // so remember the relevant particle file as a dependency for this actor
     282           0 :             m_ActorDef.m_UsedFiles.insert(file);
     283             :         }
     284           0 :         else if (option_name == el_color)
     285             :         {
     286           0 :             currentVariant.m_Color = option.GetText();
     287             :         }
     288           0 :         else if (option_name == el_animations)
     289             :         {
     290           0 :             XERO_ITER_EL(option, anim_element)
     291             :             {
     292           0 :                 if (anim_element.GetNodeName() != el_animation)
     293             :                 {
     294           0 :                     LOGERROR("<animations> can only contain <animations> elements.");
     295           0 :                     return false;
     296             :                 }
     297             : 
     298           0 :                 Anim anim;
     299           0 :                 XERO_ITER_ATTR(anim_element, ae)
     300             :                 {
     301           0 :                     if (ae.Name == at_name)
     302           0 :                         anim.m_AnimName = ae.Value;
     303           0 :                     else if (ae.Name == at_id)
     304           0 :                         anim.m_ID = ae.Value;
     305           0 :                     else if (ae.Name == at_frequency)
     306           0 :                         anim.m_Frequency = ae.Value.ToInt();
     307           0 :                     else if (ae.Name == at_file)
     308           0 :                         anim.m_FileName = VfsPath("art/animation") / ae.Value.FromUTF8();
     309           0 :                     else if (ae.Name == at_speed)
     310           0 :                         anim.m_Speed = ae.Value.ToInt() > 0 ? ae.Value.ToInt() / 100.f : 1.f;
     311           0 :                     else if (ae.Name == at_event)
     312           0 :                         anim.m_ActionPos = Clamp(ae.Value.ToFloat(), 0.f, 1.f);
     313           0 :                     else if (ae.Name == at_load)
     314           0 :                         anim.m_ActionPos2 = Clamp(ae.Value.ToFloat(), 0.f, 1.f);
     315           0 :                     else if (ae.Name == at_sound)
     316           0 :                         anim.m_SoundPos = Clamp(ae.Value.ToFloat(), 0.f, 1.f);
     317             :                 }
     318           0 :                 currentVariant.m_Anims.push_back(anim);
     319             :             }
     320             :         }
     321           0 :         else if (option_name == el_props)
     322             :         {
     323           0 :             XERO_ITER_EL(option, prop_element)
     324             :             {
     325           0 :                 ENSURE(prop_element.GetNodeName() == el_prop);
     326             : 
     327           0 :                 Prop prop;
     328           0 :                 XERO_ITER_ATTR(prop_element, pe)
     329             :                 {
     330           0 :                     if (pe.Name == at_attachpoint)
     331           0 :                         prop.m_PropPointName = pe.Value;
     332           0 :                     else if (pe.Name == at_actor)
     333           0 :                         prop.m_ModelName = pe.Value.FromUTF8();
     334           0 :                     else if (pe.Name == at_minheight)
     335           0 :                         prop.m_minHeight = pe.Value.ToFloat();
     336           0 :                     else if (pe.Name == at_maxheight)
     337           0 :                         prop.m_maxHeight = pe.Value.ToFloat();
     338           0 :                     else if (pe.Name == at_selectable)
     339           0 :                         prop.m_selectable = pe.Value != "false";
     340             :                 }
     341           0 :                 currentVariant.m_Props.push_back(prop);
     342             :             }
     343             :         }
     344             :     }
     345           0 :     return true;
     346             : }
     347             : 
     348           0 : std::vector<u8> CObjectBase::CalculateVariationKey(const std::vector<const std::set<CStr>*>& selections) const
     349             : {
     350             :     // (TODO: see CObjectManager::FindObjectVariation for an opportunity to
     351             :     // call this function a bit less frequently)
     352             : 
     353             :     // Calculate a complete list of choices, one per group, based on the
     354             :     // supposedly-complete selections (i.e. not making random choices at this
     355             :     // stage).
     356             :     // In each group, if one of the variants has a name matching a string in the
     357             :     // first 'selections', set use that one.
     358             :     // Otherwise, try with the next (lower priority) selections set, and repeat.
     359             :     // Otherwise, choose the first variant (arbitrarily).
     360             : 
     361           0 :     std::vector<u8> choices;
     362             : 
     363           0 :     std::multimap<CStr, CStrW> chosenProps;
     364             : 
     365           0 :     for (std::vector<std::vector<CObjectBase::Variant> >::const_iterator grp = m_VariantGroups.begin();
     366           0 :         grp != m_VariantGroups.end();
     367             :         ++grp)
     368             :     {
     369             :         // Ignore groups with nothing inside. (A warning will have been
     370             :         // emitted by the loading code.)
     371           0 :         if (grp->size() == 0)
     372           0 :             continue;
     373             : 
     374           0 :         int match = -1; // -1 => none found yet
     375             : 
     376             :         // If there's only a single variant, choose that one
     377           0 :         if (grp->size() == 1)
     378             :         {
     379           0 :             match = 0;
     380             :         }
     381             :         else
     382             :         {
     383             :             // Determine the first variant that matches the provided strings,
     384             :             // starting with the highest priority selections set:
     385             : 
     386           0 :             for (const std::set<CStr>* selset : selections)
     387             :             {
     388           0 :                 ENSURE(grp->size() < 256); // else they won't fit in 'choices'
     389             : 
     390           0 :                 for (size_t i = 0; i < grp->size(); ++i)
     391             :                 {
     392           0 :                     if (selset->count((*grp)[i].m_VariantName))
     393             :                     {
     394           0 :                         match = (u8)i;
     395           0 :                         break;
     396             :                     }
     397             :                 }
     398             : 
     399             :                 // Stop after finding the first match
     400           0 :                 if (match != -1)
     401           0 :                     break;
     402             :             }
     403             : 
     404             :             // If no match, just choose the first
     405           0 :             if (match == -1)
     406           0 :                 match = 0;
     407             :         }
     408             : 
     409           0 :         choices.push_back(match);
     410             :         // Remember which props were chosen, so we can call CalculateVariationKey on them
     411             :         // at the end.
     412             :         // Erase all existing props which are overridden by this variant:
     413           0 :         const Variant& var((*grp)[match]);
     414             : 
     415           0 :         for (const Prop& prop : var.m_Props)
     416           0 :             chosenProps.erase(prop.m_PropPointName);
     417             :         // and then insert the new ones:
     418           0 :         for (const Prop& prop : var.m_Props)
     419           0 :             if (!prop.m_ModelName.empty())
     420           0 :                 chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName));
     421             :     }
     422             : 
     423             :     // Load each prop, and add their CalculateVariationKey to our key:
     424           0 :     for (std::multimap<CStr, CStrW>::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
     425             :     {
     426           0 :         if (auto [success, prop] = m_ObjectManager.FindActorDef(it->second); success)
     427             :         {
     428           0 :             std::vector<u8> propChoices = prop.GetBase(m_QualityLevel)->CalculateVariationKey(selections);
     429           0 :             choices.insert(choices.end(), propChoices.begin(), propChoices.end());
     430             :         }
     431             :     }
     432             : 
     433           0 :     return choices;
     434             : }
     435             : 
     436           0 : const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector<u8>& variationKey) const
     437             : {
     438           0 :     Variation variation;
     439             : 
     440             :     // variationKey should correspond with m_Variants, giving the id of the
     441             :     // chosen variant from each group. (Except variationKey has some bits stuck
     442             :     // on the end for props, but we don't care about those in here.)
     443             : 
     444           0 :     std::vector<std::vector<CObjectBase::Variant> >::const_iterator grp = m_VariantGroups.begin();
     445           0 :     std::vector<u8>::const_iterator match = variationKey.begin();
     446           0 :     for ( ;
     447           0 :         grp != m_VariantGroups.end() && match != variationKey.end();
     448             :         ++grp, ++match)
     449             :     {
     450             :         // Ignore groups with nothing inside. (A warning will have been
     451             :         // emitted by the loading code.)
     452           0 :         if (grp->size() == 0)
     453           0 :             continue;
     454             : 
     455           0 :         size_t id = *match;
     456           0 :         if (id >= grp->size())
     457             :         {
     458             :             // This should be impossible
     459           0 :             debug_warn(L"BuildVariation: invalid variant id");
     460           0 :             continue;
     461             :         }
     462             : 
     463             :         // Get the matched variant
     464           0 :         const CObjectBase::Variant& var ((*grp)[id]);
     465             : 
     466             :         // Apply its data:
     467             : 
     468           0 :         if (! var.m_ModelFilename.empty())
     469           0 :             variation.model = var.m_ModelFilename;
     470             : 
     471           0 :         if (var.m_Decal.m_SizeX && var.m_Decal.m_SizeZ)
     472           0 :             variation.decal = var.m_Decal;
     473             : 
     474           0 :         if (! var.m_Particles.empty())
     475           0 :             variation.particles = var.m_Particles;
     476             : 
     477           0 :         if (! var.m_Color.empty())
     478           0 :             variation.color = var.m_Color;
     479             : 
     480             :         // If one variant defines one prop attached to e.g. "root", and this
     481             :         // variant defines two different props with the same attachpoint, the one
     482             :         // original should be erased, and replaced by the two new ones.
     483             :         //
     484             :         // So, erase all existing props which are overridden by this variant:
     485           0 :         for (std::vector<CObjectBase::Prop>::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
     486           0 :             variation.props.erase(it->m_PropPointName);
     487             :         // and then insert the new ones:
     488           0 :         for (std::vector<CObjectBase::Prop>::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
     489           0 :             if (! it->m_ModelName.empty()) // if the name is empty then the overridden prop is just deleted
     490           0 :                 variation.props.insert(make_pair(it->m_PropPointName, *it));
     491             : 
     492             :         // Same idea applies for animations.
     493             :         // So, erase all existing animations which are overridden by this variant:
     494           0 :         for (std::vector<CObjectBase::Anim>::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
     495           0 :             variation.anims.erase(it->m_AnimName);
     496             :         // and then insert the new ones:
     497           0 :         for (std::vector<CObjectBase::Anim>::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
     498           0 :             variation.anims.insert(make_pair(it->m_AnimName, *it));
     499             : 
     500             :         // Same for samplers, though perhaps not strictly necessary:
     501           0 :         for (std::vector<CObjectBase::Samp>::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
     502           0 :             variation.samplers.erase(it->m_SamplerName.string());
     503           0 :         for (std::vector<CObjectBase::Samp>::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
     504           0 :             variation.samplers.insert(make_pair(it->m_SamplerName.string(), *it));
     505             :     }
     506             : 
     507           0 :     return variation;
     508             : }
     509             : 
     510           0 : std::set<CStr> CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector<std::set<CStr>>& initialSelections) const
     511             : {
     512           0 :     rng_t rng;
     513           0 :     rng.seed(seed);
     514             : 
     515           0 :     std::set<CStr> remainingSelections = CalculateRandomRemainingSelections(rng, initialSelections);
     516           0 :     for (const std::set<CStr>& sel : initialSelections)
     517           0 :         remainingSelections.insert(sel.begin(), sel.end());
     518             : 
     519           0 :     return remainingSelections; // now actually a complete set of selections
     520             : }
     521             : 
     522           0 : std::set<CStr> CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector<std::set<CStr>>& initialSelections) const
     523             : {
     524           0 :     std::set<CStr> remainingSelections;
     525           0 :     std::multimap<CStr, CStrW> chosenProps;
     526             : 
     527             :     // Calculate a complete list of selections, so there is at least one
     528             :     // (and in most cases only one) per group.
     529             :     // In each group, if one of the variants has a name matching a string in
     530             :     // 'selections', use that one.
     531             :     // If more than one matches, choose randomly from those matching ones.
     532             :     // If none match, choose randomly from all variants.
     533             :     //
     534             :     // When choosing randomly, make use of each variant's frequency. If all
     535             :     // variants have frequency 0, treat them as if they were 1.
     536             : 
     537           0 :     CObjectManager::VariantDiversity diversity = m_ObjectManager.GetVariantDiversity();
     538             : 
     539           0 :     for (std::vector<std::vector<Variant> >::const_iterator grp = m_VariantGroups.begin();
     540           0 :         grp != m_VariantGroups.end();
     541             :         ++grp)
     542             :     {
     543             :         // Ignore groups with nothing inside. (A warning will have been
     544             :         // emitted by the loading code.)
     545           0 :         if (grp->size() == 0)
     546           0 :             continue;
     547             : 
     548           0 :         int match = -1; // -1 => none found yet
     549             : 
     550             :         // If there's only a single variant, choose that one
     551           0 :         if (grp->size() == 1)
     552             :         {
     553           0 :             match = 0;
     554             :         }
     555             :         else
     556             :         {
     557             :             // See if a variant (or several, but we only care about the first)
     558             :             // is already matched by the selections we've made, keeping their
     559             :             // priority order into account
     560             : 
     561           0 :             for (size_t s = 0; s < initialSelections.size(); ++s)
     562             :             {
     563           0 :                 for (size_t i = 0; i < grp->size(); ++i)
     564             :                 {
     565           0 :                     if (initialSelections[s].count((*grp)[i].m_VariantName))
     566             :                     {
     567           0 :                         match = (int)i;
     568           0 :                         break;
     569             :                     }
     570             :                 }
     571             : 
     572           0 :                 if (match >= 0)
     573           0 :                     break;
     574             :             }
     575             : 
     576             :             // If there was one, we don't need to do anything now because there's
     577             :             // already something to choose. Otherwise, choose randomly from the others.
     578           0 :             if (match == -1)
     579             :             {
     580             :                 // Sum the frequencies
     581           0 :                 int totalFreq = 0;
     582           0 :                 for (size_t i = 0; i < grp->size(); ++i)
     583           0 :                     totalFreq += (*grp)[i].m_Frequency;
     584             : 
     585             :                 // Someone might be silly and set all variants to have freq==0, in
     586             :                 // which case we just pretend they're all 1
     587           0 :                 bool allZero = (totalFreq == 0);
     588           0 :                 if (allZero)
     589           0 :                     totalFreq = (int)grp->size();
     590             : 
     591             :                 // Choose a random number in the interval [0..totalFreq) to choose one of the variants.
     592             :                 // If the diversity is "none", force 0 to return the first valid variant.
     593           0 :                 int randNum = diversity == CObjectManager::VariantDiversity::NONE ? 0 : boost::random::uniform_int_distribution<int>(0, totalFreq-1)(rng);
     594           0 :                 for (size_t i = 0; i < grp->size(); ++i)
     595             :                 {
     596           0 :                     randNum -= (allZero ? 1 : (*grp)[i].m_Frequency);
     597           0 :                     if (randNum < 0)
     598             :                     {
     599             :                         // (If this change to 'remainingSelections' interferes with earlier choices, then
     600             :                         // we'll get some non-fatal inconsistencies that just break the randomness. But that
     601             :                         // shouldn't happen, much.)
     602             :                         // (As an example, suppose you have a group with variants "a" and "b", and another
     603             :                         // with variants "a" and "c"; now if random selection choses "b" for the first
     604             :                         // and "a" for the second, then the selection of "a" from the second group will
     605             :                         // cause "a" to be used in the first instead of the "b").
     606           0 :                         match = (int)i;
     607             : 
     608             :                         // In limited diversity, somewhat-randomly continue. This cuts variants to about a third,
     609             :                         // though not quite because we must pick a variant so the actual probability is more complex.
     610             :                         // (It's also dependent on actor files not containing too many 0-frequency variants)
     611           0 :                         if (diversity == CObjectManager::VariantDiversity::LIMITED && (i % 3 != 0))
     612             :                         {
     613             :                             // Reset to 0 or we'll just pick every subsequent variant.
     614           0 :                             randNum = 0;
     615           0 :                             continue;
     616             :                         }
     617           0 :                         break;
     618             :                     }
     619             :                 }
     620           0 :                 ENSURE(match != -1);
     621             :                 // This should always succeed; otherwise it
     622             :                 // wouldn't have chosen any of the variants.
     623           0 :                 remainingSelections.insert((*grp)[match].m_VariantName);
     624             :             }
     625             :         }
     626             : 
     627             :         // Remember which props were chosen, so we can call CalculateRandomVariation on them
     628             :         // at the end.
     629           0 :         const Variant& var ((*grp)[match]);
     630             :         // Erase all existing props which are overridden by this variant:
     631           0 :         for (const Prop& prop : var.m_Props)
     632           0 :             chosenProps.erase(prop.m_PropPointName);
     633             :         // and then insert the new ones:
     634           0 :         for (const Prop& prop : var.m_Props)
     635           0 :             if (!prop.m_ModelName.empty())
     636           0 :                 chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName));
     637             :     }
     638             : 
     639             :     // Load each prop, and add their required selections to ours:
     640           0 :     for (std::multimap<CStr, CStrW>::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
     641             :     {
     642           0 :         if (auto [success, prop] = m_ObjectManager.FindActorDef(it->second); success)
     643             :         {
     644           0 :             std::vector<std::set<CStr> > propInitialSelections = initialSelections;
     645           0 :             if (!remainingSelections.empty())
     646           0 :                 propInitialSelections.push_back(remainingSelections);
     647             : 
     648           0 :             std::set<CStr> propRemainingSelections = prop.GetBase(m_QualityLevel)->CalculateRandomRemainingSelections(rng, propInitialSelections);
     649           0 :             remainingSelections.insert(propRemainingSelections.begin(), propRemainingSelections.end());
     650             : 
     651             :             // Add the prop's used files to our own (recursively) so we can hotload
     652             :             // when any prop is changed
     653           0 :             m_ActorDef.m_UsedFiles.insert(prop.m_UsedFiles.begin(), prop.m_UsedFiles.end());
     654             :         }
     655             :     }
     656             : 
     657           0 :     return remainingSelections;
     658             : }
     659             : 
     660           0 : std::vector<std::vector<CStr> > CObjectBase::GetVariantGroups() const
     661             : {
     662           0 :     std::vector<std::vector<CStr> > groups;
     663             : 
     664             :     // Queue of objects (main actor plus props (recursively)) to be processed
     665           0 :     std::queue<const CObjectBase*> objectsQueue;
     666           0 :     objectsQueue.push(this);
     667             : 
     668             :     // Set of objects already processed, so we don't do them more than once
     669           0 :     std::set<const CObjectBase*> objectsProcessed;
     670             : 
     671           0 :     while (!objectsQueue.empty())
     672             :     {
     673           0 :         const CObjectBase* obj = objectsQueue.front();
     674           0 :         objectsQueue.pop();
     675             :         // Ignore repeated objects (likely to be props)
     676           0 :         if (objectsProcessed.find(obj) != objectsProcessed.end())
     677           0 :             continue;
     678             : 
     679           0 :         objectsProcessed.insert(obj);
     680             : 
     681             :         // Iterate through the list of groups
     682           0 :         for (size_t i = 0; i < obj->m_VariantGroups.size(); ++i)
     683             :         {
     684             :             // Copy the group's variant names into a new vector
     685           0 :             std::vector<CStr> group;
     686           0 :             group.reserve(obj->m_VariantGroups[i].size());
     687           0 :             for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j)
     688           0 :                 group.push_back(obj->m_VariantGroups[i][j].m_VariantName);
     689             : 
     690             :             // If this group is identical to one elsewhere, don't bother listing
     691             :             // it twice.
     692             :             // Linear search is theoretically not very efficient, but hopefully
     693             :             // we don't have enough props for that to matter...
     694           0 :             bool dupe = false;
     695           0 :             for (size_t j = 0; j < groups.size(); ++j)
     696             :             {
     697           0 :                 if (groups[j] == group)
     698             :                 {
     699           0 :                     dupe = true;
     700           0 :                     break;
     701             :                 }
     702             :             }
     703           0 :             if (dupe)
     704           0 :                 continue;
     705             : 
     706             :             // Add non-trivial groups (i.e. not just one entry) to the returned list
     707           0 :             if (obj->m_VariantGroups[i].size() > 1)
     708           0 :                 groups.push_back(group);
     709             : 
     710             :             // Add all props onto the queue to be considered
     711           0 :             for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j)
     712             :             {
     713           0 :                 const std::vector<Prop>& props = obj->m_VariantGroups[i][j].m_Props;
     714           0 :                 for (size_t k = 0; k < props.size(); ++k)
     715           0 :                     if (!props[k].m_ModelName.empty())
     716           0 :                         if (auto [success, prop] = m_ObjectManager.FindActorDef(props[k].m_ModelName.c_str()); success)
     717           0 :                             objectsQueue.push(prop.GetBase(m_QualityLevel).get());
     718             :             }
     719             :         }
     720             :     }
     721             : 
     722           0 :     return groups;
     723             : }
     724             : 
     725           0 : void CObjectBase::GetQualitySplits(std::vector<u8>& splits) const
     726             : {
     727           0 :     std::vector<u8>::iterator it = std::find_if(splits.begin(), splits.end(), [this](u8 qualityLevel) { return qualityLevel >= m_QualityLevel; });
     728           0 :     if (it == splits.end() ||  *it != m_QualityLevel)
     729           0 :         splits.emplace(it, m_QualityLevel);
     730             : 
     731           0 :     for (const std::vector<Variant>& group : m_VariantGroups)
     732           0 :         for (const Variant& variant : group)
     733           0 :             for (const Prop& prop : variant.m_Props)
     734             :             {
     735             :                 // TODO: we probably should clean those up after XML load.
     736           0 :                 if (prop.m_ModelName.empty())
     737           0 :                     continue;
     738             : 
     739           0 :                 auto [success, propActor] = m_ObjectManager.FindActorDef(prop.m_ModelName.c_str());
     740           0 :                 if (!success)
     741           0 :                     continue;
     742             : 
     743           0 :                 std::vector<u8> newSplits = propActor.QualityLevels();
     744           0 :                 if (newSplits.size() <= 1)
     745           0 :                     continue;
     746             : 
     747             :                 // This is not entirely optimal since we might loop though redundant quality levels, but that shouldn't matter.
     748             :                 // Custom implementation because this is inplace, std::set_union needs a 3rd vector.
     749           0 :                 std::vector<u8>::iterator v1 = splits.begin();
     750           0 :                 std::vector<u8>::iterator v2 = newSplits.begin();
     751           0 :                 while (v2 != newSplits.end())
     752             :                 {
     753           0 :                     if (v1 == splits.end() || *v1 > *v2)
     754             :                     {
     755           0 :                         v1 = ++splits.insert(v1, *v2);
     756           0 :                         ++v2;
     757             :                     }
     758           0 :                     else if (*v1 == *v2)
     759             :                     {
     760           0 :                         ++v1;
     761           0 :                         ++v2;
     762             :                     }
     763             :                     else
     764           0 :                         ++v1;
     765             :                 }
     766             :             }
     767           0 : }
     768             : 
     769           0 : const CStr& CObjectBase::GetIdentifier() const
     770             : {
     771           0 :     return m_Identifier;
     772             : }
     773             : 
     774           0 : bool CObjectBase::UsesFile(const VfsPath& pathname) const
     775             : {
     776           0 :     return m_ActorDef.UsesFile(pathname);
     777             : }
     778             : 
     779             : 
     780           0 : CActorDef::CActorDef(CObjectManager& objectManager) : m_ObjectManager(objectManager)
     781             : {
     782           0 : }
     783             : 
     784           0 : std::set<CStr> CActorDef::PickSelectionsAtRandom(uint32_t seed) const
     785             : {
     786             :     // Use the selections from the highest quality actor - this lets artists maintain compatibility (or not)
     787             :     // when going to lower quality levels.
     788           0 :     std::vector<std::set<CStr>> noSelections;
     789           0 :     return GetBase(255)->CalculateRandomRemainingSelections(seed, noSelections);
     790             : }
     791             : 
     792           0 : std::vector<u8> CActorDef::QualityLevels() const
     793             : {
     794           0 :     std::vector<u8> splits;
     795           0 :     splits.reserve(m_ObjectBases.size());
     796           0 :     for (const std::shared_ptr<CObjectBase>& base : m_ObjectBases)
     797           0 :         splits.emplace_back(base->m_QualityLevel);
     798           0 :     return splits;
     799             : }
     800             : 
     801           0 : const std::shared_ptr<CObjectBase>& CActorDef::GetBase(u8 QualityLevel) const
     802             : {
     803           0 :     for (const std::shared_ptr<CObjectBase>& base : m_ObjectBases)
     804           0 :         if (base->m_QualityLevel >= QualityLevel)
     805           0 :             return base;
     806             :     // This code path ought to be impossible to take,
     807             :     // because by construction we must have at least one valid CObjectBase of quality MAX_QUALITY
     808             :     // (which necessarily fits the u8 comparison above).
     809             :     // However compilers will warn that we return a reference to a local temporary if I return nullptr,
     810             :     // so just return something sane instead.
     811           0 :     ENSURE(false);
     812           0 :     return m_ObjectBases.back();
     813             : }
     814             : 
     815           0 : bool CActorDef::Load(const VfsPath& pathname)
     816             : {
     817           0 :     m_UsedFiles.clear();
     818           0 :     m_UsedFiles.insert(pathname);
     819             : 
     820           0 :     m_ObjectBases.clear();
     821             : 
     822           0 :     CXeromyces XeroFile;
     823           0 :     if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK)
     824           0 :         return false;
     825             : 
     826             :     // Define all the elements used in the XML file
     827             : #define EL(x) int el_##x = XeroFile.GetElementID(#x)
     828             : #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
     829           0 :     EL(actor);
     830           0 :     EL(inline);
     831           0 :     EL(qualitylevels);
     832           0 :     AT(file);
     833           0 :     AT(inline);
     834           0 :     AT(quality);
     835           0 :     AT(version);
     836             : #undef AT
     837             : #undef EL
     838             : 
     839           0 :     XMBElement root = XeroFile.GetRoot();
     840             : 
     841           0 :     if (root.GetNodeName() != el_actor && root.GetNodeName() != el_qualitylevels)
     842             :     {
     843           0 :         LOGERROR("Invalid actor format (actor '%s', unrecognised root element '%s')",
     844             :                  pathname.string8().c_str(), XeroFile.GetElementString(root.GetNodeName()));
     845           0 :         return false;
     846             :     }
     847             : 
     848           0 :     m_Pathname = pathname;
     849             : 
     850           0 :     if (root.GetNodeName() == el_actor)
     851             :     {
     852           0 :         std::unique_ptr<CObjectBase> base = std::make_unique<CObjectBase>(m_ObjectManager, *this, MAX_QUALITY);
     853           0 :         if (!base->Load(XeroFile, root))
     854             :         {
     855           0 :             LOGERROR("Invalid actor (actor '%s')", pathname.string8());
     856           0 :             return false;
     857             :         }
     858           0 :         m_ObjectBases.emplace_back(std::move(base));
     859             :     }
     860             :     else
     861             :     {
     862           0 :         XERO_ITER_ATTR(root, attr)
     863             :         {
     864           0 :             if (attr.Name == at_version && attr.Value.ToInt() != 1)
     865             :             {
     866           0 :                 LOGERROR("Invalid actor format (actor '%s', version %i is not supported)",
     867             :                          pathname.string8().c_str(), attr.Value.ToInt());
     868           0 :                 return false;
     869             :             }
     870             :         }
     871           0 :         u8 quality = 0;
     872           0 :         XMBElement inlineActor;
     873           0 :         XERO_ITER_EL(root, child)
     874             :         {
     875           0 :             if (child.GetNodeName() == el_inline)
     876           0 :                 inlineActor = child;
     877             :         }
     878           0 :         XERO_ITER_EL(root, actor)
     879             :         {
     880           0 :             if (actor.GetNodeName() != el_actor)
     881           0 :                 continue;
     882           0 :             bool found_quality = false;
     883           0 :             bool use_inline = false;
     884           0 :             CStr file;
     885           0 :             XERO_ITER_ATTR(actor, attr)
     886             :             {
     887           0 :                 if (attr.Name == at_quality)
     888             :                 {
     889           0 :                     int v = GetQuality(attr.Value);
     890           0 :                     if (v > MAX_QUALITY)
     891             :                     {
     892           0 :                         LOGERROR("Quality levels can only go up to %i (file %s)", MAX_QUALITY, pathname.string8());
     893           0 :                         return false;
     894             :                     }
     895           0 :                     if (v <= quality)
     896             :                     {
     897           0 :                         LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8());
     898           0 :                         return false;
     899             :                     }
     900           0 :                     quality = v;
     901           0 :                     found_quality = true;
     902             :                 }
     903           0 :                 else if (attr.Name == at_file)
     904             :                 {
     905           0 :                     if (attr.Value.empty())
     906           0 :                         LOGWARNING("Empty actor file specified (file %s)", pathname.string8());
     907           0 :                     file = attr.Value;
     908             :                 }
     909           0 :                 else if (attr.Name == at_inline)
     910           0 :                     use_inline = true;
     911             :             }
     912           0 :             if (!found_quality)
     913           0 :                 quality = MAX_QUALITY;
     914           0 :             std::unique_ptr<CObjectBase> base = std::make_unique<CObjectBase>(m_ObjectManager, *this, quality);
     915           0 :             if (use_inline)
     916             :             {
     917           0 :                 if (inlineActor.GetNodeName() == -1)
     918             :                 {
     919           0 :                     LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname.string8());
     920           0 :                     return false;
     921             :                 }
     922           0 :                 if (!base->Load(XeroFile, inlineActor))
     923             :                 {
     924           0 :                     LOGERROR("Invalid inline actor (actor '%s')", pathname.string8());
     925           0 :                     return false;
     926             :                 }
     927             : 
     928             :             }
     929           0 :             else if (file.empty())
     930             :             {
     931           0 :                 if (!base->Load(XeroFile, actor))
     932             :                 {
     933           0 :                     LOGERROR("Invalid actor (actor '%s')", pathname.string8());
     934           0 :                     return false;
     935             :                 }
     936             :             }
     937             :             else
     938             :             {
     939           0 :                 if (actor.GetChildNodes().size() > 0)
     940           0 :                     LOGWARNING("Actor definition refers to file but has children elements, they will be ignored (file %s)", pathname.string8());
     941             : 
     942             :                 // Open up an external file to load.
     943             :                 // Don't crash hard when failures happen, but log them and continue
     944           0 :                 CXeromyces XeroActor;
     945           0 :                 if (XeroActor.Load(g_VFS, VfsPath("art/actors/") / file, "actor") == PSRETURN_OK)
     946             :                 {
     947           0 :                     const XMBElement& root = XeroActor.GetRoot();
     948           0 :                     if (root.GetNodeName() == XeroActor.GetElementID("qualitylevels"))
     949             :                     {
     950           0 :                         LOGERROR("Included actors cannot define quality levels (opening %s from file %s)", file, pathname.string8());
     951           0 :                         return false;
     952             :                     }
     953           0 :                     if (!base->Load(XeroActor, root))
     954             :                     {
     955           0 :                         LOGERROR("Invalid actor (actor '%s' loaded from '%s')", file, pathname.string8());
     956           0 :                         return false;
     957             :                     }
     958             :                 }
     959             :                 else
     960             :                 {
     961           0 :                     LOGERROR("Could not open actor file at path %s (file %s)", file, pathname.string8());
     962           0 :                     return false;
     963             :                 }
     964           0 :                 m_UsedFiles.insert(file);
     965             :             }
     966           0 :             m_ObjectBases.emplace_back(std::move(base));
     967             :         }
     968           0 :         if (quality != MAX_QUALITY)
     969             :         {
     970           0 :             LOGERROR("The highest quality level must be %i, but the highest level found was %i (file %s)", MAX_QUALITY, quality, pathname.string8().c_str());
     971           0 :             return false;
     972             :         }
     973             :     }
     974             : 
     975             :     // For each quality level, check if we need to further split (because of props).
     976           0 :     std::vector<u8> splits = QualityLevels();
     977           0 :     for (const std::shared_ptr<CObjectBase>& base : m_ObjectBases)
     978           0 :         base->GetQualitySplits(splits);
     979           0 :     ENSURE(splits.size() >= 1);
     980           0 :     if (splits.size() > MAX_LEVELS_PER_ACTOR_DEF)
     981             :     {
     982           0 :         LOGERROR("Too many quality levels (%i) for actor %s (max %i)", splits.size(), pathname.string8().c_str(), MAX_LEVELS_PER_ACTOR_DEF);
     983           0 :         return false;
     984             :     }
     985             : 
     986           0 :     std::vector<std::shared_ptr<CObjectBase>>::iterator it = m_ObjectBases.begin();
     987           0 :     std::vector<u8>::const_iterator qualityLevels = splits.begin();
     988           0 :     while (it != m_ObjectBases.end())
     989           0 :         if ((*it)->m_QualityLevel > *qualityLevels)
     990             :         {
     991           0 :             it = ++m_ObjectBases.emplace(it, (*it)->CopyWithQuality(*qualityLevels));
     992           0 :             ++qualityLevels;
     993             :         }
     994           0 :         else if ((*it)->m_QualityLevel == *qualityLevels)
     995             :         {
     996           0 :             ++it;
     997           0 :             ++qualityLevels;
     998             :         }
     999             :         else
    1000           0 :             ++it;
    1001             : 
    1002           0 :     return true;
    1003             : }
    1004             : 
    1005           0 : bool CActorDef::UsesFile(const VfsPath& pathname) const
    1006             : {
    1007           0 :     return m_UsedFiles.find(pathname) != m_UsedFiles.end();
    1008             : }
    1009             : 
    1010           0 : void CActorDef::LoadErrorPlaceholder(const VfsPath& pathname)
    1011             : {
    1012           0 :     m_UsedFiles.clear();
    1013           0 :     m_ObjectBases.clear();
    1014           0 :     m_UsedFiles.emplace(pathname);
    1015           0 :     m_Pathname = pathname;
    1016           0 :     m_ObjectBases.emplace_back(std::make_shared<CObjectBase>(m_ObjectManager, *this, MAX_QUALITY));
    1017           3 : }

Generated by: LCOV version 1.13