[Windows] Automated build.
[0ad.git] / source / graphics / ObjectBase.cpp
blob820370a2c04efba38d1aea827a81c3e4f0511e8a
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"
20 #include <algorithm>
21 #include <queue>
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>
34 namespace
36 /**
37 * The maximal quality for an actor.
39 static constexpr int MAX_QUALITY = 255;
41 /**
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)
48 if (value == "low")
49 return 100;
50 else if (value == "medium")
51 return 150;
52 else if (value == "high")
53 return 200;
54 else
55 return value.ToInt();
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;
77 return ret;
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)
85 EL(castshadow);
86 EL(float);
87 EL(group);
88 EL(material);
89 AT(maxquality);
90 AT(minquality);
91 #undef AT
92 #undef EL
95 // Set up the group vector to avoid reallocation and copying later.
97 int groups = 0;
98 XERO_ITER_EL(root, child)
100 if (child.GetNodeName() == el_group)
101 ++groups;
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)
113 return true;
114 else if (attr.Name == at_maxquality && GetQuality(attr.Value) <= m_QualityLevel)
115 return true;
117 return false;
120 XERO_ITER_EL(root, child)
122 int child_name = child.GetNodeName();
124 if (shouldSkip(child))
125 continue;
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))
134 continue;
136 if (!LoadVariant(XeroFile, variant, currentGroup.emplace_back()))
137 return false;
140 if (currentGroup.size() == 0)
142 LOGERROR("Actor group has zero variants ('%s')", m_Identifier);
143 return false;
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");
157 return true;
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)
164 EL(animation);
165 EL(animations);
166 EL(color);
167 EL(decal);
168 EL(mesh);
169 EL(particles);
170 EL(prop);
171 EL(props);
172 EL(texture);
173 EL(textures);
174 EL(variant);
175 AT(actor);
176 AT(angle);
177 AT(attachpoint);
178 AT(depth);
179 AT(event);
180 AT(file);
181 AT(frequency);
182 AT(id);
183 AT(load);
184 AT(maxheight);
185 AT(minheight);
186 AT(name);
187 AT(offsetx);
188 AT(offsetz);
189 AT(selectable);
190 AT(sound);
191 AT(speed);
192 AT(width);
193 #undef AT
194 #undef EL
196 if (variant.GetNodeName() != el_variant)
198 LOGERROR("Invalid variant format (unrecognised root element '%s')", XeroFile.GetElementString(variant.GetNodeName()));
199 return false;
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))
215 return false;
217 else
219 LOGERROR("Could not open path %s", attr.Value);
220 return false;
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.");
249 return false;
252 Samp samp;
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();
266 Decal decal;
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.");
295 return false;
298 Anim anim;
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);
327 Prop 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);
345 return true;
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
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).
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();
367 ++grp)
369 // Ignore groups with nothing inside. (A warning will have been
370 // emitted by the loading code.)
371 if (grp->size() == 0)
372 continue;
374 int match = -1; // -1 => none found yet
376 // If there's only a single variant, choose that one
377 if (grp->size() == 1)
379 match = 0;
381 else
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))
394 match = (u8)i;
395 break;
399 // Stop after finding the first match
400 if (match != -1)
401 break;
404 // If no match, just choose the first
405 if (match == -1)
406 match = 0;
409 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 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());
433 return choices;
436 const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector<u8>& variationKey) const
438 Variation variation;
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();
446 for ( ;
447 grp != m_VariantGroups.end() && match != variationKey.end();
448 ++grp, ++match)
450 // Ignore groups with nothing inside. (A warning will have been
451 // emitted by the loading code.)
452 if (grp->size() == 0)
453 continue;
455 size_t id = *match;
456 if (id >= grp->size())
458 // This should be impossible
459 debug_warn(L"BuildVariation: invalid variant id");
460 continue;
463 // Get the matched variant
464 const CObjectBase::Variant& var ((*grp)[id]);
466 // Apply its data:
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));
507 return variation;
510 std::set<CStr> CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector<std::set<CStr>>& initialSelections) const
512 rng_t rng;
513 rng.seed(seed);
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();
541 ++grp)
543 // Ignore groups with nothing inside. (A warning will have been
544 // emitted by the loading code.)
545 if (grp->size() == 0)
546 continue;
548 int match = -1; // -1 => none found yet
550 // If there's only a single variant, choose that one
551 if (grp->size() == 1)
553 match = 0;
555 else
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))
567 match = (int)i;
568 break;
572 if (match >= 0)
573 break;
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 if (match == -1)
580 // Sum the frequencies
581 int totalFreq = 0;
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);
588 if (allZero)
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);
597 if (randNum < 0)
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 match = (int)i;
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.
614 randNum = 0;
615 continue;
617 break;
620 ENSURE(match != -1);
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
628 // at the end.
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();
674 objectsQueue.pop();
675 // Ignore repeated objects (likely to be props)
676 if (objectsProcessed.find(obj) != objectsProcessed.end())
677 continue;
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
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 bool dupe = false;
695 for (size_t j = 0; j < groups.size(); ++j)
697 if (groups[j] == group)
699 dupe = true;
700 break;
703 if (dupe)
704 continue;
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());
722 return groups;
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())
737 continue;
739 auto [success, propActor] = m_ObjectManager.FindActorDef(prop.m_ModelName.c_str());
740 if (!success)
741 continue;
743 std::vector<u8> newSplits = propActor.QualityLevels();
744 if (newSplits.size() <= 1)
745 continue;
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);
756 ++v2;
758 else if (*v1 == *v2)
760 ++v1;
761 ++v2;
763 else
764 ++v1;
769 const CStr& CObjectBase::GetIdentifier() const
771 return m_Identifier;
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);
798 return splits;
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)
805 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 ENSURE(false);
812 return m_ObjectBases.back();
815 bool CActorDef::Load(const VfsPath& pathname)
817 m_UsedFiles.clear();
818 m_UsedFiles.insert(pathname);
820 m_ObjectBases.clear();
822 CXeromyces XeroFile;
823 if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK)
824 return false;
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 EL(actor);
830 EL(inline);
831 EL(qualitylevels);
832 AT(file);
833 AT(inline);
834 AT(quality);
835 AT(version);
836 #undef AT
837 #undef EL
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()));
845 return false;
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());
856 return false;
858 m_ObjectBases.emplace_back(std::move(base));
860 else
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());
868 return false;
871 u8 quality = 0;
872 XMBElement inlineActor;
873 XERO_ITER_EL(root, child)
875 if (child.GetNodeName() == el_inline)
876 inlineActor = child;
878 XERO_ITER_EL(root, actor)
880 if (actor.GetNodeName() != el_actor)
881 continue;
882 bool found_quality = false;
883 bool use_inline = false;
884 CStr file;
885 XERO_ITER_ATTR(actor, attr)
887 if (attr.Name == at_quality)
889 int v = GetQuality(attr.Value);
890 if (v > MAX_QUALITY)
892 LOGERROR("Quality levels can only go up to %i (file %s)", MAX_QUALITY, pathname.string8());
893 return false;
895 if (v <= quality)
897 LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8());
898 return false;
900 quality = v;
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());
907 file = attr.Value;
909 else if (attr.Name == at_inline)
910 use_inline = true;
912 if (!found_quality)
913 quality = MAX_QUALITY;
914 std::unique_ptr<CObjectBase> base = std::make_unique<CObjectBase>(m_ObjectManager, *this, quality);
915 if (use_inline)
917 if (inlineActor.GetNodeName() == -1)
919 LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname.string8());
920 return false;
922 if (!base->Load(XeroFile, inlineActor))
924 LOGERROR("Invalid inline actor (actor '%s')", pathname.string8());
925 return false;
929 else if (file.empty())
931 if (!base->Load(XeroFile, actor))
933 LOGERROR("Invalid actor (actor '%s')", pathname.string8());
934 return false;
937 else
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());
951 return false;
953 if (!base->Load(XeroActor, root))
955 LOGERROR("Invalid actor (actor '%s' loaded from '%s')", file, pathname.string8());
956 return false;
959 else
961 LOGERROR("Could not open actor file at path %s (file %s)", file, pathname.string8());
962 return false;
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());
971 return false;
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);
983 return false;
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));
992 ++qualityLevels;
994 else if ((*it)->m_QualityLevel == *qualityLevels)
996 ++it;
997 ++qualityLevels;
999 else
1000 ++it;
1002 return true;
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));