1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
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.
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.
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/>.
18 #include "precompiled.h"
23 #include "ObjectBase.h"
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"
32 #include <boost/random/uniform_int_distribution.hpp>
37 * The maximal quality for an actor.
39 static constexpr int MAX_QUALITY
= 255;
42 * How many quality levels a given actor can have.
44 static constexpr int MAX_LEVELS_PER_ACTOR_DEF
= 5;
46 int GetQuality(const CStr
& value
)
50 else if (value
== "medium")
52 else if (value
== "high")
57 } // anonymous namespace
59 CObjectBase::CObjectBase(CObjectManager
& objectManager
, CActorDef
& actorDef
, u8 qualityLevel
)
60 : m_ObjectManager(objectManager
), m_ActorDef(actorDef
)
62 m_QualityLevel
= qualityLevel
;
63 m_Properties
.m_CastShadows
= false;
64 m_Properties
.m_FloatOnWater
= false;
66 // Remove leading art/actors/ & include quality level.
67 m_Identifier
= m_ActorDef
.m_Pathname
.string8().substr(11) + CStr::FromInt(m_QualityLevel
);
70 std::unique_ptr
<CObjectBase
> CObjectBase::CopyWithQuality(u8 newQualityLevel
) const
72 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 ret
->m_VariantGroups
= m_VariantGroups
;
75 ret
->m_Material
= m_Material
;
76 ret
->m_Properties
= m_Properties
;
80 bool CObjectBase::Load(const CXeromyces
& XeroFile
, const XMBElement
& root
)
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)
95 // Set up the group vector to avoid reallocation and copying later.
98 XERO_ITER_EL(root
, child
)
100 if (child
.GetNodeName() == el_group
)
104 m_VariantGroups
.reserve(groups
);
107 // (This XML-reading code is rather worryingly verbose...)
109 auto shouldSkip
= [&](XMBElement
& node
) {
110 XERO_ITER_ATTR(node
, attr
)
112 if (attr
.Name
== at_minquality
&& GetQuality(attr
.Value
) > m_QualityLevel
)
114 else if (attr
.Name
== at_maxquality
&& GetQuality(attr
.Value
) <= m_QualityLevel
)
120 XERO_ITER_EL(root
, child
)
122 int child_name
= child
.GetNodeName();
124 if (shouldSkip(child
))
127 if (child_name
== el_group
)
129 std::vector
<Variant
>& currentGroup
= m_VariantGroups
.emplace_back();
130 currentGroup
.reserve(child
.GetChildNodes().size());
131 XERO_ITER_EL(child
, variant
)
133 if (shouldSkip(variant
))
136 if (!LoadVariant(XeroFile
, variant
, currentGroup
.emplace_back()))
140 if (currentGroup
.size() == 0)
142 LOGERROR("Actor group has zero variants ('%s')", m_Identifier
);
146 else if (child_name
== el_castshadow
)
147 m_Properties
.m_CastShadows
= true;
148 else if (child_name
== el_float
)
149 m_Properties
.m_FloatOnWater
= true;
150 else if (child_name
== el_material
)
151 m_Material
= VfsPath("art/materials") / child
.GetText().FromUTF8();
154 if (m_Material
.empty())
155 m_Material
= VfsPath("art/materials/default.xml");
160 bool CObjectBase::LoadVariant(const CXeromyces
& XeroFile
, const XMBElement
& variant
, Variant
& currentVariant
)
162 #define EL(x) int el_##x = XeroFile.GetElementID(#x)
163 #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
196 if (variant
.GetNodeName() != el_variant
)
198 LOGERROR("Invalid variant format (unrecognised root element '%s')", XeroFile
.GetElementString(variant
.GetNodeName()));
202 // Load variants first, so that they can be overriden if necessary.
203 XERO_ITER_ATTR(variant
, attr
)
205 if (attr
.Name
== at_file
)
207 // Open up an external file to load.
208 // Don't crash hard when failures happen, but log them and continue
209 m_ActorDef
.m_UsedFiles
.insert(attr
.Value
);
210 CXeromyces XeroVariant
;
211 if (XeroVariant
.Load(g_VFS
, "art/variants/" + attr
.Value
) == PSRETURN_OK
)
213 XMBElement variantRoot
= XeroVariant
.GetRoot();
214 if (!LoadVariant(XeroVariant
, variantRoot
, currentVariant
))
219 LOGERROR("Could not open path %s", attr
.Value
);
222 // Continue loading extra definitions in this variant to allow nested files
226 XERO_ITER_ATTR(variant
, attr
)
228 if (attr
.Name
== at_name
)
229 currentVariant
.m_VariantName
= attr
.Value
.LowerCase();
230 else if (attr
.Name
== at_frequency
)
231 currentVariant
.m_Frequency
= attr
.Value
.ToInt();
234 XERO_ITER_EL(variant
, option
)
236 int option_name
= option
.GetNodeName();
238 if (option_name
== el_mesh
)
240 currentVariant
.m_ModelFilename
= VfsPath("art/meshes") / option
.GetText().FromUTF8();
242 else if (option_name
== el_textures
)
244 XERO_ITER_EL(option
, textures_element
)
246 if (textures_element
.GetNodeName() != el_texture
)
248 LOGERROR("<textures> can only contain <texture> elements.");
253 XERO_ITER_ATTR(textures_element
, se
)
255 if (se
.Name
== at_file
)
256 samp
.m_SamplerFile
= VfsPath("art/textures/skins") / se
.Value
.FromUTF8();
257 else if (se
.Name
== at_name
)
258 samp
.m_SamplerName
= CStrIntern(se
.Value
);
260 currentVariant
.m_Samplers
.push_back(samp
);
263 else if (option_name
== el_decal
)
265 XMBAttributeList attrs
= option
.GetAttributes();
267 decal
.m_SizeX
= attrs
.GetNamedItem(at_width
).ToFloat();
268 decal
.m_SizeZ
= attrs
.GetNamedItem(at_depth
).ToFloat();
269 decal
.m_Angle
= DEGTORAD(attrs
.GetNamedItem(at_angle
).ToFloat());
270 decal
.m_OffsetX
= attrs
.GetNamedItem(at_offsetx
).ToFloat();
271 decal
.m_OffsetZ
= attrs
.GetNamedItem(at_offsetz
).ToFloat();
272 currentVariant
.m_Decal
= decal
;
274 else if (option_name
== el_particles
)
276 XMBAttributeList attrs
= option
.GetAttributes();
277 VfsPath file
= VfsPath("art/particles") / attrs
.GetNamedItem(at_file
).FromUTF8();
278 currentVariant
.m_Particles
= file
;
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 m_ActorDef
.m_UsedFiles
.insert(file
);
284 else if (option_name
== el_color
)
286 currentVariant
.m_Color
= option
.GetText();
288 else if (option_name
== el_animations
)
290 XERO_ITER_EL(option
, anim_element
)
292 if (anim_element
.GetNodeName() != el_animation
)
294 LOGERROR("<animations> can only contain <animations> elements.");
299 XERO_ITER_ATTR(anim_element
, ae
)
301 if (ae
.Name
== at_name
)
302 anim
.m_AnimName
= ae
.Value
;
303 else if (ae
.Name
== at_id
)
304 anim
.m_ID
= ae
.Value
;
305 else if (ae
.Name
== at_frequency
)
306 anim
.m_Frequency
= ae
.Value
.ToInt();
307 else if (ae
.Name
== at_file
)
308 anim
.m_FileName
= VfsPath("art/animation") / ae
.Value
.FromUTF8();
309 else if (ae
.Name
== at_speed
)
310 anim
.m_Speed
= ae
.Value
.ToInt() > 0 ? ae
.Value
.ToInt() / 100.f
: 1.f
;
311 else if (ae
.Name
== at_event
)
312 anim
.m_ActionPos
= Clamp(ae
.Value
.ToFloat(), 0.f
, 1.f
);
313 else if (ae
.Name
== at_load
)
314 anim
.m_ActionPos2
= Clamp(ae
.Value
.ToFloat(), 0.f
, 1.f
);
315 else if (ae
.Name
== at_sound
)
316 anim
.m_SoundPos
= Clamp(ae
.Value
.ToFloat(), 0.f
, 1.f
);
318 currentVariant
.m_Anims
.push_back(anim
);
321 else if (option_name
== el_props
)
323 XERO_ITER_EL(option
, prop_element
)
325 ENSURE(prop_element
.GetNodeName() == el_prop
);
328 XERO_ITER_ATTR(prop_element
, pe
)
330 if (pe
.Name
== at_attachpoint
)
331 prop
.m_PropPointName
= pe
.Value
;
332 else if (pe
.Name
== at_actor
)
333 prop
.m_ModelName
= pe
.Value
.FromUTF8();
334 else if (pe
.Name
== at_minheight
)
335 prop
.m_minHeight
= pe
.Value
.ToFloat();
336 else if (pe
.Name
== at_maxheight
)
337 prop
.m_maxHeight
= pe
.Value
.ToFloat();
338 else if (pe
.Name
== at_selectable
)
339 prop
.m_selectable
= pe
.Value
!= "false";
341 currentVariant
.m_Props
.push_back(prop
);
348 std::vector
<u8
> CObjectBase::CalculateVariationKey(const std::vector
<const std::set
<CStr
>*>& selections
) const
350 // (TODO: see CObjectManager::FindObjectVariation for an opportunity to
351 // call this function a bit less frequently)
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
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).
361 std::vector
<u8
> choices
;
363 std::multimap
<CStr
, CStrW
> chosenProps
;
365 for (std::vector
<std::vector
<CObjectBase::Variant
> >::const_iterator grp
= m_VariantGroups
.begin();
366 grp
!= m_VariantGroups
.end();
369 // Ignore groups with nothing inside. (A warning will have been
370 // emitted by the loading code.)
371 if (grp
->size() == 0)
374 int match
= -1; // -1 => none found yet
376 // If there's only a single variant, choose that one
377 if (grp
->size() == 1)
383 // Determine the first variant that matches the provided strings,
384 // starting with the highest priority selections set:
386 for (const std::set
<CStr
>* selset
: selections
)
388 ENSURE(grp
->size() < 256); // else they won't fit in 'choices'
390 for (size_t i
= 0; i
< grp
->size(); ++i
)
392 if (selset
->count((*grp
)[i
].m_VariantName
))
399 // Stop after finding the first match
404 // If no match, just choose the first
409 choices
.push_back(match
);
410 // Remember which props were chosen, so we can call CalculateVariationKey on them
412 // Erase all existing props which are overridden by this variant:
413 const Variant
& var((*grp
)[match
]);
415 for (const Prop
& prop
: var
.m_Props
)
416 chosenProps
.erase(prop
.m_PropPointName
);
417 // and then insert the new ones:
418 for (const Prop
& prop
: var
.m_Props
)
419 if (!prop
.m_ModelName
.empty())
420 chosenProps
.insert(make_pair(prop
.m_PropPointName
, prop
.m_ModelName
));
423 // Load each prop, and add their CalculateVariationKey to our key:
424 for (std::multimap
<CStr
, CStrW
>::iterator it
= chosenProps
.begin(); it
!= chosenProps
.end(); ++it
)
426 if (auto [success
, prop
] = m_ObjectManager
.FindActorDef(it
->second
); success
)
428 std::vector
<u8
> propChoices
= prop
.GetBase(m_QualityLevel
)->CalculateVariationKey(selections
);
429 choices
.insert(choices
.end(), propChoices
.begin(), propChoices
.end());
436 const CObjectBase::Variation
CObjectBase::BuildVariation(const std::vector
<u8
>& variationKey
) const
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.)
444 std::vector
<std::vector
<CObjectBase::Variant
> >::const_iterator grp
= m_VariantGroups
.begin();
445 std::vector
<u8
>::const_iterator match
= variationKey
.begin();
447 grp
!= m_VariantGroups
.end() && match
!= variationKey
.end();
450 // Ignore groups with nothing inside. (A warning will have been
451 // emitted by the loading code.)
452 if (grp
->size() == 0)
456 if (id
>= grp
->size())
458 // This should be impossible
459 debug_warn(L
"BuildVariation: invalid variant id");
463 // Get the matched variant
464 const CObjectBase::Variant
& var ((*grp
)[id
]);
468 if (! var
.m_ModelFilename
.empty())
469 variation
.model
= var
.m_ModelFilename
;
471 if (var
.m_Decal
.m_SizeX
&& var
.m_Decal
.m_SizeZ
)
472 variation
.decal
= var
.m_Decal
;
474 if (! var
.m_Particles
.empty())
475 variation
.particles
= var
.m_Particles
;
477 if (! var
.m_Color
.empty())
478 variation
.color
= var
.m_Color
;
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.
484 // So, erase all existing props which are overridden by this variant:
485 for (std::vector
<CObjectBase::Prop
>::const_iterator it
= var
.m_Props
.begin(); it
!= var
.m_Props
.end(); ++it
)
486 variation
.props
.erase(it
->m_PropPointName
);
487 // and then insert the new ones:
488 for (std::vector
<CObjectBase::Prop
>::const_iterator it
= var
.m_Props
.begin(); it
!= var
.m_Props
.end(); ++it
)
489 if (! it
->m_ModelName
.empty()) // if the name is empty then the overridden prop is just deleted
490 variation
.props
.insert(make_pair(it
->m_PropPointName
, *it
));
492 // Same idea applies for animations.
493 // So, erase all existing animations which are overridden by this variant:
494 for (std::vector
<CObjectBase::Anim
>::const_iterator it
= var
.m_Anims
.begin(); it
!= var
.m_Anims
.end(); ++it
)
495 variation
.anims
.erase(it
->m_AnimName
);
496 // and then insert the new ones:
497 for (std::vector
<CObjectBase::Anim
>::const_iterator it
= var
.m_Anims
.begin(); it
!= var
.m_Anims
.end(); ++it
)
498 variation
.anims
.insert(make_pair(it
->m_AnimName
, *it
));
500 // Same for samplers, though perhaps not strictly necessary:
501 for (std::vector
<CObjectBase::Samp
>::const_iterator it
= var
.m_Samplers
.begin(); it
!= var
.m_Samplers
.end(); ++it
)
502 variation
.samplers
.erase(it
->m_SamplerName
.string());
503 for (std::vector
<CObjectBase::Samp
>::const_iterator it
= var
.m_Samplers
.begin(); it
!= var
.m_Samplers
.end(); ++it
)
504 variation
.samplers
.insert(make_pair(it
->m_SamplerName
.string(), *it
));
510 std::set
<CStr
> CObjectBase::CalculateRandomRemainingSelections(uint32_t seed
, const std::vector
<std::set
<CStr
>>& initialSelections
) const
515 std::set
<CStr
> remainingSelections
= CalculateRandomRemainingSelections(rng
, initialSelections
);
516 for (const std::set
<CStr
>& sel
: initialSelections
)
517 remainingSelections
.insert(sel
.begin(), sel
.end());
519 return remainingSelections
; // now actually a complete set of selections
522 std::set
<CStr
> CObjectBase::CalculateRandomRemainingSelections(rng_t
& rng
, const std::vector
<std::set
<CStr
>>& initialSelections
) const
524 std::set
<CStr
> remainingSelections
;
525 std::multimap
<CStr
, CStrW
> chosenProps
;
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.
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.
537 CObjectManager::VariantDiversity diversity
= m_ObjectManager
.GetVariantDiversity();
539 for (std::vector
<std::vector
<Variant
> >::const_iterator grp
= m_VariantGroups
.begin();
540 grp
!= m_VariantGroups
.end();
543 // Ignore groups with nothing inside. (A warning will have been
544 // emitted by the loading code.)
545 if (grp
->size() == 0)
548 int match
= -1; // -1 => none found yet
550 // If there's only a single variant, choose that one
551 if (grp
->size() == 1)
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
561 for (size_t s
= 0; s
< initialSelections
.size(); ++s
)
563 for (size_t i
= 0; i
< grp
->size(); ++i
)
565 if (initialSelections
[s
].count((*grp
)[i
].m_VariantName
))
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.
580 // Sum the frequencies
582 for (size_t i
= 0; i
< grp
->size(); ++i
)
583 totalFreq
+= (*grp
)[i
].m_Frequency
;
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 bool allZero
= (totalFreq
== 0);
589 totalFreq
= (int)grp
->size();
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 int randNum
= diversity
== CObjectManager::VariantDiversity::NONE
? 0 : boost::random::uniform_int_distribution
<int>(0, totalFreq
-1)(rng
);
594 for (size_t i
= 0; i
< grp
->size(); ++i
)
596 randNum
-= (allZero
? 1 : (*grp
)[i
].m_Frequency
);
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").
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 if (diversity
== CObjectManager::VariantDiversity::LIMITED
&& (i
% 3 != 0))
613 // Reset to 0 or we'll just pick every subsequent variant.
621 // This should always succeed; otherwise it
622 // wouldn't have chosen any of the variants.
623 remainingSelections
.insert((*grp
)[match
].m_VariantName
);
627 // Remember which props were chosen, so we can call CalculateRandomVariation on them
629 const Variant
& var ((*grp
)[match
]);
630 // Erase all existing props which are overridden by this variant:
631 for (const Prop
& prop
: var
.m_Props
)
632 chosenProps
.erase(prop
.m_PropPointName
);
633 // and then insert the new ones:
634 for (const Prop
& prop
: var
.m_Props
)
635 if (!prop
.m_ModelName
.empty())
636 chosenProps
.insert(make_pair(prop
.m_PropPointName
, prop
.m_ModelName
));
639 // Load each prop, and add their required selections to ours:
640 for (std::multimap
<CStr
, CStrW
>::iterator it
= chosenProps
.begin(); it
!= chosenProps
.end(); ++it
)
642 if (auto [success
, prop
] = m_ObjectManager
.FindActorDef(it
->second
); success
)
644 std::vector
<std::set
<CStr
> > propInitialSelections
= initialSelections
;
645 if (!remainingSelections
.empty())
646 propInitialSelections
.push_back(remainingSelections
);
648 std::set
<CStr
> propRemainingSelections
= prop
.GetBase(m_QualityLevel
)->CalculateRandomRemainingSelections(rng
, propInitialSelections
);
649 remainingSelections
.insert(propRemainingSelections
.begin(), propRemainingSelections
.end());
651 // Add the prop's used files to our own (recursively) so we can hotload
652 // when any prop is changed
653 m_ActorDef
.m_UsedFiles
.insert(prop
.m_UsedFiles
.begin(), prop
.m_UsedFiles
.end());
657 return remainingSelections
;
660 std::vector
<std::vector
<CStr
> > CObjectBase::GetVariantGroups() const
662 std::vector
<std::vector
<CStr
> > groups
;
664 // Queue of objects (main actor plus props (recursively)) to be processed
665 std::queue
<const CObjectBase
*> objectsQueue
;
666 objectsQueue
.push(this);
668 // Set of objects already processed, so we don't do them more than once
669 std::set
<const CObjectBase
*> objectsProcessed
;
671 while (!objectsQueue
.empty())
673 const CObjectBase
* obj
= objectsQueue
.front();
675 // Ignore repeated objects (likely to be props)
676 if (objectsProcessed
.find(obj
) != objectsProcessed
.end())
679 objectsProcessed
.insert(obj
);
681 // Iterate through the list of groups
682 for (size_t i
= 0; i
< obj
->m_VariantGroups
.size(); ++i
)
684 // Copy the group's variant names into a new vector
685 std::vector
<CStr
> group
;
686 group
.reserve(obj
->m_VariantGroups
[i
].size());
687 for (size_t j
= 0; j
< obj
->m_VariantGroups
[i
].size(); ++j
)
688 group
.push_back(obj
->m_VariantGroups
[i
][j
].m_VariantName
);
690 // If this group is identical to one elsewhere, don't bother listing
692 // Linear search is theoretically not very efficient, but hopefully
693 // we don't have enough props for that to matter...
695 for (size_t j
= 0; j
< groups
.size(); ++j
)
697 if (groups
[j
] == group
)
706 // Add non-trivial groups (i.e. not just one entry) to the returned list
707 if (obj
->m_VariantGroups
[i
].size() > 1)
708 groups
.push_back(group
);
710 // Add all props onto the queue to be considered
711 for (size_t j
= 0; j
< obj
->m_VariantGroups
[i
].size(); ++j
)
713 const std::vector
<Prop
>& props
= obj
->m_VariantGroups
[i
][j
].m_Props
;
714 for (size_t k
= 0; k
< props
.size(); ++k
)
715 if (!props
[k
].m_ModelName
.empty())
716 if (auto [success
, prop
] = m_ObjectManager
.FindActorDef(props
[k
].m_ModelName
.c_str()); success
)
717 objectsQueue
.push(prop
.GetBase(m_QualityLevel
).get());
725 void CObjectBase::GetQualitySplits(std::vector
<u8
>& splits
) const
727 std::vector
<u8
>::iterator it
= std::find_if(splits
.begin(), splits
.end(), [this](u8 qualityLevel
) { return qualityLevel
>= m_QualityLevel
; });
728 if (it
== splits
.end() || *it
!= m_QualityLevel
)
729 splits
.emplace(it
, m_QualityLevel
);
731 for (const std::vector
<Variant
>& group
: m_VariantGroups
)
732 for (const Variant
& variant
: group
)
733 for (const Prop
& prop
: variant
.m_Props
)
735 // TODO: we probably should clean those up after XML load.
736 if (prop
.m_ModelName
.empty())
739 auto [success
, propActor
] = m_ObjectManager
.FindActorDef(prop
.m_ModelName
.c_str());
743 std::vector
<u8
> newSplits
= propActor
.QualityLevels();
744 if (newSplits
.size() <= 1)
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 std::vector
<u8
>::iterator v1
= splits
.begin();
750 std::vector
<u8
>::iterator v2
= newSplits
.begin();
751 while (v2
!= newSplits
.end())
753 if (v1
== splits
.end() || *v1
> *v2
)
755 v1
= ++splits
.insert(v1
, *v2
);
769 const CStr
& CObjectBase::GetIdentifier() const
774 bool CObjectBase::UsesFile(const VfsPath
& pathname
) const
776 return m_ActorDef
.UsesFile(pathname
);
780 CActorDef::CActorDef(CObjectManager
& objectManager
) : m_ObjectManager(objectManager
)
784 std::set
<CStr
> CActorDef::PickSelectionsAtRandom(uint32_t seed
) const
786 // Use the selections from the highest quality actor - this lets artists maintain compatibility (or not)
787 // when going to lower quality levels.
788 std::vector
<std::set
<CStr
>> noSelections
;
789 return GetBase(255)->CalculateRandomRemainingSelections(seed
, noSelections
);
792 std::vector
<u8
> CActorDef::QualityLevels() const
794 std::vector
<u8
> splits
;
795 splits
.reserve(m_ObjectBases
.size());
796 for (const std::shared_ptr
<CObjectBase
>& base
: m_ObjectBases
)
797 splits
.emplace_back(base
->m_QualityLevel
);
801 const std::shared_ptr
<CObjectBase
>& CActorDef::GetBase(u8 QualityLevel
) const
803 for (const std::shared_ptr
<CObjectBase
>& base
: m_ObjectBases
)
804 if (base
->m_QualityLevel
>= QualityLevel
)
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.
812 return m_ObjectBases
.back();
815 bool CActorDef::Load(const VfsPath
& pathname
)
818 m_UsedFiles
.insert(pathname
);
820 m_ObjectBases
.clear();
823 if (XeroFile
.Load(g_VFS
, pathname
, "actor") != PSRETURN_OK
)
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)
839 XMBElement root
= XeroFile
.GetRoot();
841 if (root
.GetNodeName() != el_actor
&& root
.GetNodeName() != el_qualitylevels
)
843 LOGERROR("Invalid actor format (actor '%s', unrecognised root element '%s')",
844 pathname
.string8().c_str(), XeroFile
.GetElementString(root
.GetNodeName()));
848 m_Pathname
= pathname
;
850 if (root
.GetNodeName() == el_actor
)
852 std::unique_ptr
<CObjectBase
> base
= std::make_unique
<CObjectBase
>(m_ObjectManager
, *this, MAX_QUALITY
);
853 if (!base
->Load(XeroFile
, root
))
855 LOGERROR("Invalid actor (actor '%s')", pathname
.string8());
858 m_ObjectBases
.emplace_back(std::move(base
));
862 XERO_ITER_ATTR(root
, attr
)
864 if (attr
.Name
== at_version
&& attr
.Value
.ToInt() != 1)
866 LOGERROR("Invalid actor format (actor '%s', version %i is not supported)",
867 pathname
.string8().c_str(), attr
.Value
.ToInt());
872 XMBElement inlineActor
;
873 XERO_ITER_EL(root
, child
)
875 if (child
.GetNodeName() == el_inline
)
878 XERO_ITER_EL(root
, actor
)
880 if (actor
.GetNodeName() != el_actor
)
882 bool found_quality
= false;
883 bool use_inline
= false;
885 XERO_ITER_ATTR(actor
, attr
)
887 if (attr
.Name
== at_quality
)
889 int v
= GetQuality(attr
.Value
);
892 LOGERROR("Quality levels can only go up to %i (file %s)", MAX_QUALITY
, pathname
.string8());
897 LOGERROR("Elements must be in increasing quality order (file %s)", pathname
.string8());
901 found_quality
= true;
903 else if (attr
.Name
== at_file
)
905 if (attr
.Value
.empty())
906 LOGWARNING("Empty actor file specified (file %s)", pathname
.string8());
909 else if (attr
.Name
== at_inline
)
913 quality
= MAX_QUALITY
;
914 std::unique_ptr
<CObjectBase
> base
= std::make_unique
<CObjectBase
>(m_ObjectManager
, *this, quality
);
917 if (inlineActor
.GetNodeName() == -1)
919 LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname
.string8());
922 if (!base
->Load(XeroFile
, inlineActor
))
924 LOGERROR("Invalid inline actor (actor '%s')", pathname
.string8());
929 else if (file
.empty())
931 if (!base
->Load(XeroFile
, actor
))
933 LOGERROR("Invalid actor (actor '%s')", pathname
.string8());
939 if (actor
.GetChildNodes().size() > 0)
940 LOGWARNING("Actor definition refers to file but has children elements, they will be ignored (file %s)", pathname
.string8());
942 // Open up an external file to load.
943 // Don't crash hard when failures happen, but log them and continue
944 CXeromyces XeroActor
;
945 if (XeroActor
.Load(g_VFS
, VfsPath("art/actors/") / file
, "actor") == PSRETURN_OK
)
947 const XMBElement
& root
= XeroActor
.GetRoot();
948 if (root
.GetNodeName() == XeroActor
.GetElementID("qualitylevels"))
950 LOGERROR("Included actors cannot define quality levels (opening %s from file %s)", file
, pathname
.string8());
953 if (!base
->Load(XeroActor
, root
))
955 LOGERROR("Invalid actor (actor '%s' loaded from '%s')", file
, pathname
.string8());
961 LOGERROR("Could not open actor file at path %s (file %s)", file
, pathname
.string8());
964 m_UsedFiles
.insert(file
);
966 m_ObjectBases
.emplace_back(std::move(base
));
968 if (quality
!= MAX_QUALITY
)
970 LOGERROR("The highest quality level must be %i, but the highest level found was %i (file %s)", MAX_QUALITY
, quality
, pathname
.string8().c_str());
975 // For each quality level, check if we need to further split (because of props).
976 std::vector
<u8
> splits
= QualityLevels();
977 for (const std::shared_ptr
<CObjectBase
>& base
: m_ObjectBases
)
978 base
->GetQualitySplits(splits
);
979 ENSURE(splits
.size() >= 1);
980 if (splits
.size() > MAX_LEVELS_PER_ACTOR_DEF
)
982 LOGERROR("Too many quality levels (%i) for actor %s (max %i)", splits
.size(), pathname
.string8().c_str(), MAX_LEVELS_PER_ACTOR_DEF
);
986 std::vector
<std::shared_ptr
<CObjectBase
>>::iterator it
= m_ObjectBases
.begin();
987 std::vector
<u8
>::const_iterator qualityLevels
= splits
.begin();
988 while (it
!= m_ObjectBases
.end())
989 if ((*it
)->m_QualityLevel
> *qualityLevels
)
991 it
= ++m_ObjectBases
.emplace(it
, (*it
)->CopyWithQuality(*qualityLevels
));
994 else if ((*it
)->m_QualityLevel
== *qualityLevels
)
1005 bool CActorDef::UsesFile(const VfsPath
& pathname
) const
1007 return m_UsedFiles
.find(pathname
) != m_UsedFiles
.end();
1010 void CActorDef::LoadErrorPlaceholder(const VfsPath
& pathname
)
1012 m_UsedFiles
.clear();
1013 m_ObjectBases
.clear();
1014 m_UsedFiles
.emplace(pathname
);
1015 m_Pathname
= pathname
;
1016 m_ObjectBases
.emplace_back(std::make_shared
<CObjectBase
>(m_ObjectManager
, *this, MAX_QUALITY
));