1 /* Copyright (C) 2018 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.hpp>
33
CObjectBase(CObjectManager & objectManager)34 CObjectBase::CObjectBase(CObjectManager& objectManager)
35 : m_ObjectManager(objectManager)
36 {
37 m_Properties.m_CastShadows = false;
38 m_Properties.m_FloatOnWater = false;
39 }
40
LoadVariant(const CXeromyces & XeroFile,const XMBElement & variant,Variant & currentVariant)41 void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant)
42 {
43 #define EL(x) int el_##x = XeroFile.GetElementID(#x)
44 #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
45 EL(animation);
46 EL(animations);
47 EL(color);
48 EL(decal);
49 EL(mesh);
50 EL(particles);
51 EL(prop);
52 EL(props);
53 EL(texture);
54 EL(textures);
55 EL(variant);
56 AT(actor);
57 AT(angle);
58 AT(attachpoint);
59 AT(depth);
60 AT(event);
61 AT(file);
62 AT(frequency);
63 AT(id);
64 AT(load);
65 AT(maxheight);
66 AT(minheight);
67 AT(name);
68 AT(offsetx);
69 AT(offsetz);
70 AT(selectable);
71 AT(sound);
72 AT(speed);
73 AT(width);
74 #undef AT
75 #undef EL
76
77 if (variant.GetNodeName() != el_variant)
78 {
79 LOGERROR("Invalid variant format (unrecognised root element '%s')", XeroFile.GetElementString(variant.GetNodeName()).c_str());
80 return;
81 }
82
83 XERO_ITER_ATTR(variant, attr)
84 {
85 if (attr.Name == at_file)
86 {
87 // Open up an external file to load.
88 // Don't crash hard when failures happen, but log them and continue
89 m_UsedFiles.insert(attr.Value);
90 CXeromyces XeroVariant;
91 if (XeroVariant.Load(g_VFS, "art/variants/" + attr.Value) == PSRETURN_OK)
92 {
93 XMBElement variantRoot = XeroVariant.GetRoot();
94 LoadVariant(XeroVariant, variantRoot, currentVariant);
95 }
96 else
97 LOGERROR("Could not open path %s", attr.Value);
98 // Continue loading extra definitions in this variant to allow nested files
99 }
100 else if (attr.Name == at_name)
101 currentVariant.m_VariantName = attr.Value.LowerCase();
102 else if (attr.Name == at_frequency)
103 currentVariant.m_Frequency = attr.Value.ToInt();
104 }
105
106 XERO_ITER_EL(variant, option)
107 {
108 int option_name = option.GetNodeName();
109
110 if (option_name == el_mesh)
111 {
112 currentVariant.m_ModelFilename = VfsPath("art/meshes") / option.GetText().FromUTF8();
113 }
114 else if (option_name == el_textures)
115 {
116 XERO_ITER_EL(option, textures_element)
117 {
118 ENSURE(textures_element.GetNodeName() == el_texture);
119
120 Samp samp;
121 XERO_ITER_ATTR(textures_element, se)
122 {
123 if (se.Name == at_file)
124 samp.m_SamplerFile = VfsPath("art/textures/skins") / se.Value.FromUTF8();
125 else if (se.Name == at_name)
126 samp.m_SamplerName = CStrIntern(se.Value);
127 }
128 currentVariant.m_Samplers.push_back(samp);
129 }
130 }
131 else if (option_name == el_decal)
132 {
133 XMBAttributeList attrs = option.GetAttributes();
134 Decal decal;
135 decal.m_SizeX = attrs.GetNamedItem(at_width).ToFloat();
136 decal.m_SizeZ = attrs.GetNamedItem(at_depth).ToFloat();
137 decal.m_Angle = DEGTORAD(attrs.GetNamedItem(at_angle).ToFloat());
138 decal.m_OffsetX = attrs.GetNamedItem(at_offsetx).ToFloat();
139 decal.m_OffsetZ = attrs.GetNamedItem(at_offsetz).ToFloat();
140 currentVariant.m_Decal = decal;
141 }
142 else if (option_name == el_particles)
143 {
144 XMBAttributeList attrs = option.GetAttributes();
145 VfsPath file = VfsPath("art/particles") / attrs.GetNamedItem(at_file).FromUTF8();
146 currentVariant.m_Particles = file;
147
148 // For particle hotloading, it's easiest to reload the entire actor,
149 // so remember the relevant particle file as a dependency for this actor
150 m_UsedFiles.insert(file);
151 }
152 else if (option_name == el_color)
153 {
154 currentVariant.m_Color = option.GetText();
155 }
156 else if (option_name == el_animations)
157 {
158 XERO_ITER_EL(option, anim_element)
159 {
160 ENSURE(anim_element.GetNodeName() == el_animation);
161
162 Anim anim;
163 XERO_ITER_ATTR(anim_element, ae)
164 {
165 if (ae.Name == at_name)
166 anim.m_AnimName = ae.Value;
167 else if (ae.Name == at_id)
168 anim.m_ID = ae.Value;
169 else if (ae.Name == at_frequency)
170 anim.m_Frequency = ae.Value.ToInt();
171 else if (ae.Name == at_file)
172 anim.m_FileName = VfsPath("art/animation") / ae.Value.FromUTF8();
173 else if (ae.Name == at_speed)
174 anim.m_Speed = ae.Value.ToInt() > 0 ? ae.Value.ToInt() / 100.f : 1.f;
175 else if (ae.Name == at_event)
176 anim.m_ActionPos = clamp(ae.Value.ToFloat(), 0.f, 1.f);
177 else if (ae.Name == at_load)
178 anim.m_ActionPos2 = clamp(ae.Value.ToFloat(), 0.f, 1.f);
179 else if (ae.Name == at_sound)
180 anim.m_SoundPos = clamp(ae.Value.ToFloat(), 0.f, 1.f);
181 }
182 currentVariant.m_Anims.push_back(anim);
183 }
184 }
185 else if (option_name == el_props)
186 {
187 XERO_ITER_EL(option, prop_element)
188 {
189 ENSURE(prop_element.GetNodeName() == el_prop);
190
191 Prop prop;
192 XERO_ITER_ATTR(prop_element, pe)
193 {
194 if (pe.Name == at_attachpoint)
195 prop.m_PropPointName = pe.Value;
196 else if (pe.Name == at_actor)
197 prop.m_ModelName = pe.Value.FromUTF8();
198 else if (pe.Name == at_minheight)
199 prop.m_minHeight = pe.Value.ToFloat();
200 else if (pe.Name == at_maxheight)
201 prop.m_maxHeight = pe.Value.ToFloat();
202 else if (pe.Name == at_selectable)
203 prop.m_selectable = pe.Value != "false";
204 }
205 currentVariant.m_Props.push_back(prop);
206 }
207 }
208 }
209 }
210
Load(const VfsPath & pathname)211 bool CObjectBase::Load(const VfsPath& pathname)
212 {
213 m_UsedFiles.clear();
214 m_UsedFiles.insert(pathname);
215
216 CXeromyces XeroFile;
217 if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK)
218 return false;
219
220 // Define all the elements used in the XML file
221 #define EL(x) int el_##x = XeroFile.GetElementID(#x)
222 #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
223 EL(actor);
224 EL(castshadow);
225 EL(float);
226 EL(group);
227 EL(material);
228 #undef AT
229 #undef EL
230
231 XMBElement root = XeroFile.GetRoot();
232
233 if (root.GetNodeName() != el_actor)
234 {
235 LOGERROR("Invalid actor format (unrecognised root element '%s')", XeroFile.GetElementString(root.GetNodeName()).c_str());
236 return false;
237 }
238
239 m_VariantGroups.clear();
240
241 m_Pathname = pathname;
242 m_ShortName = pathname.Basename().string();
243
244
245 // Set up the vector<vector<T>> m_Variants to contain the right number
246 // of elements, to avoid wasteful copying/reallocation later.
247 {
248 // Count the variants in each group
249 std::vector<int> variantGroupSizes;
250 XERO_ITER_EL(root, child)
251 {
252 if (child.GetNodeName() == el_group)
253 variantGroupSizes.push_back(child.GetChildNodes().size());
254 }
255
256 m_VariantGroups.resize(variantGroupSizes.size());
257 // Set each vector to match the number of variants
258 for (size_t i = 0; i < variantGroupSizes.size(); ++i)
259 m_VariantGroups[i].resize(variantGroupSizes[i]);
260 }
261
262
263 // (This XML-reading code is rather worryingly verbose...)
264
265 std::vector<std::vector<Variant> >::iterator currentGroup = m_VariantGroups.begin();
266
267 XERO_ITER_EL(root, child)
268 {
269 int child_name = child.GetNodeName();
270
271 if (child_name == el_group)
272 {
273 std::vector<Variant>::iterator currentVariant = currentGroup->begin();
274 XERO_ITER_EL(child, variant)
275 {
276 LoadVariant(XeroFile, variant, *currentVariant);
277 ++currentVariant;
278 }
279
280 if (currentGroup->size() == 0)
281 LOGERROR("Actor group has zero variants ('%s')", pathname.string8());
282
283 ++currentGroup;
284 }
285 else if (child_name == el_castshadow)
286 m_Properties.m_CastShadows = true;
287 else if (child_name == el_float)
288 m_Properties.m_FloatOnWater = true;
289 else if (child_name == el_material)
290 m_Material = VfsPath("art/materials") / child.GetText().FromUTF8();
291 }
292
293 if (m_Material.empty())
294 m_Material = VfsPath("art/materials/default.xml");
295
296 return true;
297 }
298
Reload()299 bool CObjectBase::Reload()
300 {
301 return Load(m_Pathname);
302 }
303
UsesFile(const VfsPath & pathname)304 bool CObjectBase::UsesFile(const VfsPath& pathname)
305 {
306 return m_UsedFiles.find(pathname) != m_UsedFiles.end();
307 }
308
CalculateVariationKey(const std::vector<std::set<CStr>> & selections)309 std::vector<u8> CObjectBase::CalculateVariationKey(const std::vector<std::set<CStr> >& selections)
310 {
311 // (TODO: see CObjectManager::FindObjectVariation for an opportunity to
312 // call this function a bit less frequently)
313
314 // Calculate a complete list of choices, one per group, based on the
315 // supposedly-complete selections (i.e. not making random choices at this
316 // stage).
317 // In each group, if one of the variants has a name matching a string in the
318 // first 'selections', set use that one.
319 // Otherwise, try with the next (lower priority) selections set, and repeat.
320 // Otherwise, choose the first variant (arbitrarily).
321
322 std::vector<u8> choices;
323
324 std::multimap<CStr, CStrW> chosenProps;
325
326 for (std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_VariantGroups.begin();
327 grp != m_VariantGroups.end();
328 ++grp)
329 {
330 // Ignore groups with nothing inside. (A warning will have been
331 // emitted by the loading code.)
332 if (grp->size() == 0)
333 continue;
334
335 int match = -1; // -1 => none found yet
336
337 // If there's only a single variant, choose that one
338 if (grp->size() == 1)
339 {
340 match = 0;
341 }
342 else
343 {
344 // Determine the first variant that matches the provided strings,
345 // starting with the highest priority selections set:
346
347 for (std::vector<std::set<CStr> >::const_iterator selset = selections.begin(); selset < selections.end(); ++selset)
348 {
349 ENSURE(grp->size() < 256); // else they won't fit in 'choices'
350
351 for (size_t i = 0; i < grp->size(); ++i)
352 {
353 if (selset->count((*grp)[i].m_VariantName))
354 {
355 match = (u8)i;
356 break;
357 }
358 }
359
360 // Stop after finding the first match
361 if (match != -1)
362 break;
363 }
364
365 // If no match, just choose the first
366 if (match == -1)
367 match = 0;
368 }
369
370 choices.push_back(match);
371 // Remember which props were chosen, so we can call CalculateVariationKey on them
372 // at the end.
373 // Erase all existing props which are overridden by this variant:
374 Variant& var((*grp)[match]);
375
376 for (const Prop& prop : var.m_Props)
377 chosenProps.erase(prop.m_PropPointName);
378 // and then insert the new ones:
379 for (const Prop& prop : var.m_Props)
380 if (!prop.m_ModelName.empty())
381 chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName));
382 }
383
384 // Load each prop, and add their CalculateVariationKey to our key:
385 for (std::multimap<CStr, CStrW>::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
386 {
387 CObjectBase* prop = m_ObjectManager.FindObjectBase(it->second);
388 if (prop)
389 {
390 std::vector<u8> propChoices = prop->CalculateVariationKey(selections);
391 choices.insert(choices.end(), propChoices.begin(), propChoices.end());
392 }
393 }
394
395 return choices;
396 }
397
BuildVariation(const std::vector<u8> & variationKey)398 const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector<u8>& variationKey)
399 {
400 Variation variation;
401
402 // variationKey should correspond with m_Variants, giving the id of the
403 // chosen variant from each group. (Except variationKey has some bits stuck
404 // on the end for props, but we don't care about those in here.)
405
406 std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_VariantGroups.begin();
407 std::vector<u8>::const_iterator match = variationKey.begin();
408 for ( ;
409 grp != m_VariantGroups.end() && match != variationKey.end();
410 ++grp, ++match)
411 {
412 // Ignore groups with nothing inside. (A warning will have been
413 // emitted by the loading code.)
414 if (grp->size() == 0)
415 continue;
416
417 size_t id = *match;
418 if (id >= grp->size())
419 {
420 // This should be impossible
421 debug_warn(L"BuildVariation: invalid variant id");
422 continue;
423 }
424
425 // Get the matched variant
426 CObjectBase::Variant& var ((*grp)[id]);
427
428 // Apply its data:
429
430 if (! var.m_ModelFilename.empty())
431 variation.model = var.m_ModelFilename;
432
433 if (var.m_Decal.m_SizeX && var.m_Decal.m_SizeZ)
434 variation.decal = var.m_Decal;
435
436 if (! var.m_Particles.empty())
437 variation.particles = var.m_Particles;
438
439 if (! var.m_Color.empty())
440 variation.color = var.m_Color;
441
442 // If one variant defines one prop attached to e.g. "root", and this
443 // variant defines two different props with the same attachpoint, the one
444 // original should be erased, and replaced by the two new ones.
445 //
446 // So, erase all existing props which are overridden by this variant:
447 for (std::vector<CObjectBase::Prop>::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
448 variation.props.erase(it->m_PropPointName);
449 // and then insert the new ones:
450 for (std::vector<CObjectBase::Prop>::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
451 if (! it->m_ModelName.empty()) // if the name is empty then the overridden prop is just deleted
452 variation.props.insert(make_pair(it->m_PropPointName, *it));
453
454 // Same idea applies for animations.
455 // So, erase all existing animations which are overridden by this variant:
456 for (std::vector<CObjectBase::Anim>::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
457 variation.anims.erase(it->m_AnimName);
458 // and then insert the new ones:
459 for (std::vector<CObjectBase::Anim>::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
460 variation.anims.insert(make_pair(it->m_AnimName, *it));
461
462 // Same for samplers, though perhaps not strictly necessary:
463 for (std::vector<CObjectBase::Samp>::iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
464 variation.samplers.erase(it->m_SamplerName.string());
465 for (std::vector<CObjectBase::Samp>::iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
466 variation.samplers.insert(make_pair(it->m_SamplerName.string(), *it));
467 }
468
469 return variation;
470 }
471
CalculateRandomVariation(uint32_t seed,const std::set<CStr> & initialSelections)472 std::set<CStr> CObjectBase::CalculateRandomVariation(uint32_t seed, const std::set<CStr>& initialSelections)
473 {
474 rng_t rng;
475 rng.seed(seed);
476
477 std::set<CStr> remainingSelections = CalculateRandomRemainingSelections(rng, std::vector<std::set<CStr> >(1, initialSelections));
478 remainingSelections.insert(initialSelections.begin(), initialSelections.end());
479
480 return remainingSelections; // now actually a complete set of selections
481 }
482
CalculateRandomRemainingSelections(uint32_t seed,const std::vector<std::set<CStr>> & initialSelections)483 std::set<CStr> CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector<std::set<CStr> >& initialSelections)
484 {
485 rng_t rng;
486 rng.seed(seed);
487 return CalculateRandomRemainingSelections(rng, initialSelections);
488 }
489
CalculateRandomRemainingSelections(rng_t & rng,const std::vector<std::set<CStr>> & initialSelections)490 std::set<CStr> CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector<std::set<CStr> >& initialSelections)
491 {
492 std::set<CStr> remainingSelections;
493 std::multimap<CStr, CStrW> chosenProps;
494
495 // Calculate a complete list of selections, so there is at least one
496 // (and in most cases only one) per group.
497 // In each group, if one of the variants has a name matching a string in
498 // 'selections', use that one.
499 // If more than one matches, choose randomly from those matching ones.
500 // If none match, choose randomly from all variants.
501 //
502 // When choosing randomly, make use of each variant's frequency. If all
503 // variants have frequency 0, treat them as if they were 1.
504
505 for (std::vector<std::vector<Variant> >::iterator grp = m_VariantGroups.begin();
506 grp != m_VariantGroups.end();
507 ++grp)
508 {
509 // Ignore groups with nothing inside. (A warning will have been
510 // emitted by the loading code.)
511 if (grp->size() == 0)
512 continue;
513
514 int match = -1; // -1 => none found yet
515
516 // If there's only a single variant, choose that one
517 if (grp->size() == 1)
518 {
519 match = 0;
520 }
521 else
522 {
523 // See if a variant (or several, but we only care about the first)
524 // is already matched by the selections we've made, keeping their
525 // priority order into account
526
527 for (size_t s = 0; s < initialSelections.size(); ++s)
528 {
529 for (size_t i = 0; i < grp->size(); ++i)
530 {
531 if (initialSelections[s].count((*grp)[i].m_VariantName))
532 {
533 match = (int)i;
534 break;
535 }
536 }
537
538 if (match >= 0)
539 break;
540 }
541
542 // If there was one, we don't need to do anything now because there's
543 // already something to choose. Otherwise, choose randomly from the others.
544 if (match == -1)
545 {
546 // Sum the frequencies
547 int totalFreq = 0;
548 for (size_t i = 0; i < grp->size(); ++i)
549 totalFreq += (*grp)[i].m_Frequency;
550
551 // Someone might be silly and set all variants to have freq==0, in
552 // which case we just pretend they're all 1
553 bool allZero = (totalFreq == 0);
554 if (allZero) totalFreq = (int)grp->size();
555
556 // Choose a random number in the interval [0..totalFreq)
557 int randNum = boost::uniform_int<>(0, totalFreq-1)(rng);
558
559 // and use that to choose one of the variants
560 for (size_t i = 0; i < grp->size(); ++i)
561 {
562 randNum -= (allZero ? 1 : (*grp)[i].m_Frequency);
563 if (randNum < 0)
564 {
565 remainingSelections.insert((*grp)[i].m_VariantName);
566 // (If this change to 'remainingSelections' interferes with earlier choices, then
567 // we'll get some non-fatal inconsistencies that just break the randomness. But that
568 // shouldn't happen, much.)
569 // (As an example, suppose you have a group with variants "a" and "b", and another
570 // with variants "a" and "c"; now if random selection choses "b" for the first
571 // and "a" for the second, then the selection of "a" from the second group will
572 // cause "a" to be used in the first instead of the "b").
573 match = (int)i;
574 break;
575 }
576 }
577 ENSURE(randNum < 0);
578 // This should always succeed; otherwise it
579 // wouldn't have chosen any of the variants.
580 }
581 }
582
583 // Remember which props were chosen, so we can call CalculateRandomVariation on them
584 // at the end.
585 Variant& var ((*grp)[match]);
586 // Erase all existing props which are overridden by this variant:
587 for (const Prop& prop : var.m_Props)
588 chosenProps.erase(prop.m_PropPointName);
589 // and then insert the new ones:
590 for (const Prop& prop : var.m_Props)
591 if (!prop.m_ModelName.empty())
592 chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName));
593 }
594
595 // Load each prop, and add their required selections to ours:
596 for (std::multimap<CStr, CStrW>::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
597 {
598 CObjectBase* prop = m_ObjectManager.FindObjectBase(it->second);
599 if (prop)
600 {
601 std::vector<std::set<CStr> > propInitialSelections = initialSelections;
602 if (!remainingSelections.empty())
603 propInitialSelections.push_back(remainingSelections);
604
605 std::set<CStr> propRemainingSelections = prop->CalculateRandomRemainingSelections(rng, propInitialSelections);
606 remainingSelections.insert(propRemainingSelections.begin(), propRemainingSelections.end());
607
608 // Add the prop's used files to our own (recursively) so we can hotload
609 // when any prop is changed
610 m_UsedFiles.insert(prop->m_UsedFiles.begin(), prop->m_UsedFiles.end());
611 }
612 }
613
614 return remainingSelections;
615 }
616
GetVariantGroups() const617 std::vector<std::vector<CStr> > CObjectBase::GetVariantGroups() const
618 {
619 std::vector<std::vector<CStr> > groups;
620
621 // Queue of objects (main actor plus props (recursively)) to be processed
622 std::queue<const CObjectBase*> objectsQueue;
623 objectsQueue.push(this);
624
625 // Set of objects already processed, so we don't do them more than once
626 std::set<const CObjectBase*> objectsProcessed;
627
628 while (!objectsQueue.empty())
629 {
630 const CObjectBase* obj = objectsQueue.front();
631 objectsQueue.pop();
632 // Ignore repeated objects (likely to be props)
633 if (objectsProcessed.find(obj) != objectsProcessed.end())
634 continue;
635
636 objectsProcessed.insert(obj);
637
638 // Iterate through the list of groups
639 for (size_t i = 0; i < obj->m_VariantGroups.size(); ++i)
640 {
641 // Copy the group's variant names into a new vector
642 std::vector<CStr> group;
643 group.reserve(obj->m_VariantGroups[i].size());
644 for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j)
645 group.push_back(obj->m_VariantGroups[i][j].m_VariantName);
646
647 // If this group is identical to one elsewhere, don't bother listing
648 // it twice.
649 // Linear search is theoretically not very efficient, but hopefully
650 // we don't have enough props for that to matter...
651 bool dupe = false;
652 for (size_t j = 0; j < groups.size(); ++j)
653 {
654 if (groups[j] == group)
655 {
656 dupe = true;
657 break;
658 }
659 }
660 if (dupe)
661 continue;
662
663 // Add non-trivial groups (i.e. not just one entry) to the returned list
664 if (obj->m_VariantGroups[i].size() > 1)
665 groups.push_back(group);
666
667 // Add all props onto the queue to be considered
668 for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j)
669 {
670 const std::vector<Prop>& props = obj->m_VariantGroups[i][j].m_Props;
671 for (size_t k = 0; k < props.size(); ++k)
672 {
673 if (! props[k].m_ModelName.empty())
674 {
675 CObjectBase* prop = m_ObjectManager.FindObjectBase(props[k].m_ModelName.c_str());
676 if (prop)
677 objectsQueue.push(prop);
678 }
679 }
680 }
681 }
682 }
683
684 return groups;
685 }
686