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