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