diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg
index 11a09cc61e..0f3a3a4ab2 100644
--- a/binaries/data/config/default.cfg
+++ b/binaries/data/config/default.cfg
@@ -128,6 +128,9 @@ antialiasing = "disabled"
sharpening = "disabled"
sharpness = 0.3
+; Quality used for actors.
+max_actor_quality=200
+
; Quality level of shader effects (set to 10 to display all effects)
materialmgr.quality = 2.0
diff --git a/binaries/data/mods/public/art/actors/actor.rnc b/binaries/data/mods/public/art/actors/actor.rnc
deleted file mode 100644
index 68e96fd8e2..0000000000
--- a/binaries/data/mods/public/art/actors/actor.rnc
+++ /dev/null
@@ -1,71 +0,0 @@
-namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
-##
-# NOTE: To modify this Relax NG grammar, edit the Relax NG Compact (.rnc) file
-# and use a converter tool like trang to generate the Relax NG XML (.rng) file
-##
-
-element actor {
- attribute version { xsd:positiveInteger }, (
- element group {
- element variant {
- attribute name { text }? &
- attribute file { text }? &
- attribute frequency { xsd:nonNegativeInteger }? &
- element mesh {
- text
- }? &
- element textures {
- element texture {
- attribute file { text }? &
- attribute name { text }
- }*
- }? &
- element decal {
- attribute width { xsd:float } & # X
- attribute depth { xsd:float } & # Z
- attribute angle { xsd:float } &
- attribute offsetx { xsd:float } &
- attribute offsetz { xsd:float }
- }? &
- element particles {
- attribute file { text }
- }? &
- element color { list {
- xsd:nonNegativeInteger, # R
- xsd:nonNegativeInteger, # G
- xsd:nonNegativeInteger # B
- } }? &
- element animations {
- element animation {
- attribute name { text } &
- attribute id { text }? &
- attribute frequency { xsd:nonNegativeInteger }? &
- attribute file { text }? &
- attribute speed { xsd:nonNegativeInteger } &
- attribute event { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }? &
- attribute load { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }? &
- attribute sound { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }?
- }*
- }? &
- element props {
- element prop {
- (attribute actor { text }? &
- attribute attachpoint { text } &
- attribute minheight { xsd:float }? &
- attribute maxheight { xsd:float }? &
- attribute selectable { "true" | "false" }?)
- }*
- }?
- }*
- }* &
- element castshadow { # flag; true if present
- empty
- }? &
- element float { # flag; true if present
- empty
- }? &
- element material {
- text
- }?
- )
-}
diff --git a/binaries/data/mods/public/art/actors/actor.rng b/binaries/data/mods/public/art/actors/actor.rng
index ffdc2cfad4..c4216e83ac 100644
--- a/binaries/data/mods/public/art/actors/actor.rng
+++ b/binaries/data/mods/public/art/actors/actor.rng
@@ -1,194 +1,263 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ 255
+ 0
+
+
+ low
+ medium
+ high
+
+
+
+
+
+
+ Minimum quality - this is inclusive.
+
+
+
+
+
+ Maximum quality - this is exclusive.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- 0
- 1
-
-
-
-
-
-
- 0
- 1
-
-
-
-
-
-
- 0
- 1
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- false
-
-
-
-
-
-
-
+
+ The quality level to use for this actor. This is the maximum value at which this version of the actor will be used. If not specified, the maximum possible value is assumed.
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/binaries/data/mods/public/gui/options/options.json b/binaries/data/mods/public/gui/options/options.json
index ce7c494d46..7c582b2cbb 100644
--- a/binaries/data/mods/public/gui/options/options.json
+++ b/binaries/data/mods/public/gui/options/options.json
@@ -152,6 +152,17 @@
"min": 0,
"max": 1
},
+ {
+ "type": "dropdown",
+ "label": "Model quality",
+ "tooltip": "Model quality setting.",
+ "config": "max_actor_quality",
+ "list": [
+ { "value": 100, "label": "Low", "tooltip": "Simpler models for better performance." },
+ { "value": 150, "label": "Medium", "tooltip": "Average quality and average performance." },
+ { "value": 200, "label": "High", "tooltip": "High quality models." }
+ ]
+ },
{
"type": "slider",
"label": "Shader effects",
diff --git a/source/graphics/Model.cpp b/source/graphics/Model.cpp
index 1a1f483bf1..98c32e6d6c 100644
--- a/source/graphics/Model.cpp
+++ b/source/graphics/Model.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -499,10 +499,6 @@ void CModel::CopyAnimationFrom(CModel* source)
m_Anim = source->m_Anim;
m_AnimTime = source->m_AnimTime;
- m_Flags &= ~MODELFLAG_CASTSHADOWS;
- if (source->m_Flags & MODELFLAG_CASTSHADOWS)
- m_Flags |= MODELFLAG_CASTSHADOWS;
-
m_ObjectBounds.SetEmpty();
InvalidateBounds();
}
diff --git a/source/graphics/ObjectBase.cpp b/source/graphics/ObjectBase.cpp
index 31a1b6dadd..d7c3da50bd 100644
--- a/source/graphics/ObjectBase.cpp
+++ b/source/graphics/ObjectBase.cpp
@@ -31,11 +31,113 @@
#include
-CObjectBase::CObjectBase(CObjectManager& objectManager)
-: m_ObjectManager(objectManager)
+namespace {
+ int GetQuality(CStr& value)
+ {
+ if (value == "low")
+ return 100;
+ else if (value == "medium")
+ return 150;
+ else if (value == "high")
+ return 200;
+ else
+ return value.ToInt();
+ }
+}
+
+CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8 qualityLevel)
+: m_ObjectManager(objectManager), m_ActorDef(actorDef)
{
+ m_QualityLevel = qualityLevel;
m_Properties.m_CastShadows = false;
m_Properties.m_FloatOnWater = false;
+
+ // Remove leading art/actors/ & include quality level.
+ m_Identifier = m_ActorDef.m_Pathname.string8().substr(11) + CStr::FromInt(m_QualityLevel);
+}
+
+std::unique_ptr CObjectBase::CopyWithQuality(u8 newQualityLevel) const
+{
+ std::unique_ptr ret = std::make_unique(m_ObjectManager, m_ActorDef, newQualityLevel);
+ // No need to actually change any quality-related stuff here, we assume that this is a copy for props.
+ ret->m_VariantGroups = m_VariantGroups;
+ ret->m_Material = m_Material;
+ ret->m_Properties = m_Properties;
+ return ret;
+}
+
+void CObjectBase::Load(const CXeromyces& XeroFile, const XMBElement& root)
+{
+ // Define all the elements used in the XML file
+#define EL(x) int el_##x = XeroFile.GetElementID(#x)
+#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
+ EL(castshadow);
+ EL(float);
+ EL(group);
+ EL(material);
+ AT(maxquality);
+ AT(minquality);
+#undef AT
+#undef EL
+
+
+ // Set up the group vector to avoid reallocation and copying later.
+ {
+ int groups = 0;
+ XERO_ITER_EL(root, child)
+ {
+ if (child.GetNodeName() == el_group)
+ ++groups;
+ }
+
+ m_VariantGroups.reserve(groups);
+ }
+
+ // (This XML-reading code is rather worryingly verbose...)
+
+ auto shouldSkip = [&](XMBElement& node) {
+ XERO_ITER_ATTR(node, attr)
+ {
+ if (attr.Name == at_minquality && GetQuality(attr.Value) > m_QualityLevel)
+ return true;
+ else if (attr.Name == at_maxquality && GetQuality(attr.Value) <= m_QualityLevel)
+ return true;
+ }
+ return false;
+ };
+
+ XERO_ITER_EL(root, child)
+ {
+ int child_name = child.GetNodeName();
+
+ if (shouldSkip(child))
+ continue;
+
+ if (child_name == el_group)
+ {
+ std::vector& currentGroup = m_VariantGroups.emplace_back();
+ currentGroup.reserve(child.GetChildNodes().size());
+ XERO_ITER_EL(child, variant)
+ {
+ if (shouldSkip(variant))
+ continue;
+
+ LoadVariant(XeroFile, variant, currentGroup.emplace_back());
+ }
+
+ if (currentGroup.size() == 0)
+ LOGERROR("Actor group has zero variants ('%s')", m_Identifier);
+ }
+ else if (child_name == el_castshadow)
+ m_Properties.m_CastShadows = true;
+ else if (child_name == el_float)
+ m_Properties.m_FloatOnWater = true;
+ else if (child_name == el_material)
+ m_Material = VfsPath("art/materials") / child.GetText().FromUTF8();
+ }
+
+ if (m_Material.empty())
+ m_Material = VfsPath("art/materials/default.xml");
}
void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant)
@@ -87,7 +189,7 @@ void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& vari
{
// Open up an external file to load.
// Don't crash hard when failures happen, but log them and continue
- m_UsedFiles.insert(attr.Value);
+ m_ActorDef.m_UsedFiles.insert(attr.Value);
CXeromyces XeroVariant;
if (XeroVariant.Load(g_VFS, "art/variants/" + attr.Value) == PSRETURN_OK)
{
@@ -152,7 +254,7 @@ void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& vari
// For particle hotloading, it's easiest to reload the entire actor,
// so remember the relevant particle file as a dependency for this actor
- m_UsedFiles.insert(file);
+ m_ActorDef.m_UsedFiles.insert(file);
}
else if (option_name == el_color)
{
@@ -213,105 +315,7 @@ void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& vari
}
}
-bool CObjectBase::Load(const VfsPath& pathname)
-{
- m_UsedFiles.clear();
- m_UsedFiles.insert(pathname);
-
- CXeromyces XeroFile;
- if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK)
- return false;
-
- // Define all the elements used in the XML file
- #define EL(x) int el_##x = XeroFile.GetElementID(#x)
- #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
- EL(actor);
- EL(castshadow);
- EL(float);
- EL(group);
- EL(material);
- #undef AT
- #undef EL
-
- XMBElement root = XeroFile.GetRoot();
-
- if (root.GetNodeName() != el_actor)
- {
- LOGERROR("Invalid actor format (unrecognised root element '%s')", XeroFile.GetElementString(root.GetNodeName()).c_str());
- return false;
- }
-
- m_VariantGroups.clear();
-
- m_Pathname = pathname;
- m_ShortName = pathname.Basename().string();
-
-
- // Set up the vector> m_Variants to contain the right number
- // of elements, to avoid wasteful copying/reallocation later.
- {
- // Count the variants in each group
- std::vector variantGroupSizes;
- XERO_ITER_EL(root, child)
- {
- if (child.GetNodeName() == el_group)
- variantGroupSizes.push_back(child.GetChildNodes().size());
- }
-
- m_VariantGroups.resize(variantGroupSizes.size());
- // Set each vector to match the number of variants
- for (size_t i = 0; i < variantGroupSizes.size(); ++i)
- m_VariantGroups[i].resize(variantGroupSizes[i]);
- }
-
-
- // (This XML-reading code is rather worryingly verbose...)
-
- std::vector >::iterator currentGroup = m_VariantGroups.begin();
-
- XERO_ITER_EL(root, child)
- {
- int child_name = child.GetNodeName();
-
- if (child_name == el_group)
- {
- std::vector::iterator currentVariant = currentGroup->begin();
- XERO_ITER_EL(child, variant)
- {
- LoadVariant(XeroFile, variant, *currentVariant);
- ++currentVariant;
- }
-
- if (currentGroup->size() == 0)
- LOGERROR("Actor group has zero variants ('%s')", pathname.string8());
-
- ++currentGroup;
- }
- else if (child_name == el_castshadow)
- m_Properties.m_CastShadows = true;
- else if (child_name == el_float)
- m_Properties.m_FloatOnWater = true;
- else if (child_name == el_material)
- m_Material = VfsPath("art/materials") / child.GetText().FromUTF8();
- }
-
- if (m_Material.empty())
- m_Material = VfsPath("art/materials/default.xml");
-
- return true;
-}
-
-bool CObjectBase::Reload()
-{
- return Load(m_Pathname);
-}
-
-bool CObjectBase::UsesFile(const VfsPath& pathname)
-{
- return m_UsedFiles.find(pathname) != m_UsedFiles.end();
-}
-
-std::vector CObjectBase::CalculateVariationKey(const std::vector >& selections)
+std::vector CObjectBase::CalculateVariationKey(const std::vector*>& selections) const
{
// (TODO: see CObjectManager::FindObjectVariation for an opportunity to
// call this function a bit less frequently)
@@ -328,7 +332,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector chosenProps;
- for (std::vector >::iterator grp = m_VariantGroups.begin();
+ for (std::vector >::const_iterator grp = m_VariantGroups.begin();
grp != m_VariantGroups.end();
++grp)
{
@@ -349,7 +353,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector >::const_iterator selset = selections.begin(); selset < selections.end(); ++selset)
+ for (const std::set* selset : selections)
{
ENSURE(grp->size() < 256); // else they won't fit in 'choices'
@@ -376,7 +380,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector CObjectBase::CalculateVariationKey(const std::vector::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
{
- CObjectBase* prop = m_ObjectManager.FindObjectBase(it->second);
+ CActorDef* prop = m_ObjectManager.FindActorDef(it->second);
if (prop)
{
- std::vector propChoices = prop->CalculateVariationKey(selections);
+ std::vector propChoices = prop->GetBase(m_QualityLevel)->CalculateVariationKey(selections);
choices.insert(choices.end(), propChoices.begin(), propChoices.end());
}
}
@@ -400,7 +404,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector& variationKey)
+const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& variationKey) const
{
Variation variation;
@@ -408,7 +412,7 @@ const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector&
// chosen variant from each group. (Except variationKey has some bits stuck
// on the end for props, but we don't care about those in here.)
- std::vector >::iterator grp = m_VariantGroups.begin();
+ std::vector >::const_iterator grp = m_VariantGroups.begin();
std::vector::const_iterator match = variationKey.begin();
for ( ;
grp != m_VariantGroups.end() && match != variationKey.end();
@@ -428,7 +432,7 @@ const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector&
}
// Get the matched variant
- CObjectBase::Variant& var ((*grp)[id]);
+ const CObjectBase::Variant& var ((*grp)[id]);
// Apply its data:
@@ -449,50 +453,44 @@ const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector&
// original should be erased, and replaced by the two new ones.
//
// So, erase all existing props which are overridden by this variant:
- for (std::vector::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
+ for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
variation.props.erase(it->m_PropPointName);
// and then insert the new ones:
- for (std::vector::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
+ for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
if (! it->m_ModelName.empty()) // if the name is empty then the overridden prop is just deleted
variation.props.insert(make_pair(it->m_PropPointName, *it));
// Same idea applies for animations.
// So, erase all existing animations which are overridden by this variant:
- for (std::vector::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
+ for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
variation.anims.erase(it->m_AnimName);
// and then insert the new ones:
- for (std::vector::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
+ for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
variation.anims.insert(make_pair(it->m_AnimName, *it));
// Same for samplers, though perhaps not strictly necessary:
- for (std::vector::iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
+ for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
variation.samplers.erase(it->m_SamplerName.string());
- for (std::vector::iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
+ for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
variation.samplers.insert(make_pair(it->m_SamplerName.string(), *it));
}
return variation;
}
-std::set CObjectBase::CalculateRandomVariation(uint32_t seed, const std::set& initialSelections)
+std::set CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector>& initialSelections) const
{
rng_t rng;
rng.seed(seed);
- std::set remainingSelections = CalculateRandomRemainingSelections(rng, std::vector >(1, initialSelections));
- remainingSelections.insert(initialSelections.begin(), initialSelections.end());
+ std::set remainingSelections = CalculateRandomRemainingSelections(rng, initialSelections);
+ for (const std::set& sel : initialSelections)
+ remainingSelections.insert(sel.begin(), sel.end());
return remainingSelections; // now actually a complete set of selections
}
-std::set CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector >& initialSelections)
-{
- rng_t rng;
- rng.seed(seed);
- return CalculateRandomRemainingSelections(rng, initialSelections);
-}
-
-std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector >& initialSelections)
+std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector>& initialSelections) const
{
std::set remainingSelections;
std::multimap chosenProps;
@@ -507,7 +505,7 @@ std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const
// When choosing randomly, make use of each variant's frequency. If all
// variants have frequency 0, treat them as if they were 1.
- for (std::vector >::iterator grp = m_VariantGroups.begin();
+ for (std::vector >::const_iterator grp = m_VariantGroups.begin();
grp != m_VariantGroups.end();
++grp)
{
@@ -587,7 +585,7 @@ std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const
// Remember which props were chosen, so we can call CalculateRandomVariation on them
// at the end.
- Variant& var ((*grp)[match]);
+ const Variant& var ((*grp)[match]);
// Erase all existing props which are overridden by this variant:
for (const Prop& prop : var.m_Props)
chosenProps.erase(prop.m_PropPointName);
@@ -600,19 +598,19 @@ std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const
// Load each prop, and add their required selections to ours:
for (std::multimap::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
{
- CObjectBase* prop = m_ObjectManager.FindObjectBase(it->second);
+ CActorDef* prop = m_ObjectManager.FindActorDef(it->second);
if (prop)
{
std::vector > propInitialSelections = initialSelections;
if (!remainingSelections.empty())
propInitialSelections.push_back(remainingSelections);
- std::set propRemainingSelections = prop->CalculateRandomRemainingSelections(rng, propInitialSelections);
+ std::set propRemainingSelections = prop->GetBase(m_QualityLevel)->CalculateRandomRemainingSelections(rng, propInitialSelections);
remainingSelections.insert(propRemainingSelections.begin(), propRemainingSelections.end());
// Add the prop's used files to our own (recursively) so we can hotload
// when any prop is changed
- m_UsedFiles.insert(prop->m_UsedFiles.begin(), prop->m_UsedFiles.end());
+ m_ActorDef.m_UsedFiles.insert(prop->m_UsedFiles.begin(), prop->m_UsedFiles.end());
}
}
@@ -677,9 +675,9 @@ std::vector > CObjectBase::GetVariantGroups() const
{
if (! props[k].m_ModelName.empty())
{
- CObjectBase* prop = m_ObjectManager.FindObjectBase(props[k].m_ModelName.c_str());
+ CActorDef* prop = m_ObjectManager.FindActorDef(props[k].m_ModelName.c_str());
if (prop)
- objectsQueue.push(prop);
+ objectsQueue.push(prop->GetBase(m_QualityLevel).get());
}
}
}
@@ -688,3 +686,266 @@ std::vector > CObjectBase::GetVariantGroups() const
return groups;
}
+
+void CObjectBase::GetQualitySplits(std::vector& splits) const
+{
+ std::vector::iterator it = std::find_if(splits.begin(), splits.end(), [this](u8 qualityLevel) { return qualityLevel >= m_QualityLevel; });
+ if (it == splits.end() || *it != m_QualityLevel)
+ splits.emplace(it, m_QualityLevel);
+
+ for (const std::vector& group : m_VariantGroups)
+ for (const Variant& variant : group)
+ for (const Prop& prop : variant.m_Props)
+ {
+ // TODO: we probably should clean those up after XML load.
+ if (prop.m_ModelName.empty())
+ continue;
+
+ CActorDef* propActor = m_ObjectManager.FindActorDef(prop.m_ModelName.c_str());
+ if (!propActor)
+ continue;
+
+ std::vector newSplits = propActor->QualityLevels();
+ if (newSplits.size() <= 1)
+ continue;
+
+ // This is not entirely optimal since we might loop though redundant quality levels, but that shouldn't matter.
+ // Custom implementation because this is inplace, std::set_union needs a 3rd vector.
+ std::vector::iterator v1 = splits.begin();
+ std::vector::iterator v2 = newSplits.begin();
+ while (v2 != newSplits.end())
+ {
+ if (v1 == splits.end() || *v1 > *v2)
+ {
+ v1 = ++splits.insert(v1, *v2);
+ ++v2;
+ }
+ else if (*v1 == *v2)
+ {
+ ++v1;
+ ++v2;
+ }
+ else
+ ++v1;
+ }
+ }
+}
+
+const CStr& CObjectBase::GetIdentifier() const
+{
+ return m_Identifier;
+}
+
+bool CObjectBase::UsesFile(const VfsPath& pathname) const
+{
+ return m_ActorDef.UsesFile(pathname);
+}
+
+
+CActorDef::CActorDef(CObjectManager& objectManager) : m_ObjectManager(objectManager)
+{
+}
+
+std::vector CActorDef::QualityLevels() const
+{
+ std::vector splits;
+ splits.reserve(m_ObjectBases.size());
+ for (const std::shared_ptr& base : m_ObjectBases)
+ splits.emplace_back(base->m_QualityLevel);
+ return splits;
+}
+
+const std::shared_ptr& CActorDef::GetBase(u8 QualityLevel) const
+{
+ for (const std::shared_ptr& base : m_ObjectBases)
+ if (base->m_QualityLevel >= QualityLevel)
+ return base;
+ // This code path ought to be impossible to take,
+ // because by construction we must have at least one valid CObjectBase of quality 255
+ // (which necessarily fits the u8 comparison above).
+ // However compilers will warn that we return a reference to a local temporary if I return nullptr,
+ // so just return something sane instead.
+ ENSURE(false);
+ return m_ObjectBases.back();
+}
+
+bool CActorDef::Load(const VfsPath& pathname)
+{
+ m_UsedFiles.clear();
+ m_UsedFiles.insert(pathname);
+
+ m_ObjectBases.clear();
+
+ CXeromyces XeroFile;
+ if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK)
+ return false;
+
+ // Define all the elements used in the XML file
+#define EL(x) int el_##x = XeroFile.GetElementID(#x)
+#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
+ EL(actor);
+ EL(inline);
+ EL(qualitylevels);
+ AT(file);
+ AT(inline);
+ AT(quality);
+ AT(version);
+#undef AT
+#undef EL
+
+ XMBElement root = XeroFile.GetRoot();
+
+ if (root.GetNodeName() != el_actor && root.GetNodeName() != el_qualitylevels)
+ {
+ LOGERROR("Invalid actor format (actor '%s', unrecognised root element '%s')",
+ pathname.string8().c_str(), XeroFile.GetElementString(root.GetNodeName()).c_str());
+ return false;
+ }
+
+ m_Pathname = pathname;
+
+ if (root.GetNodeName() == el_actor)
+ {
+ std::unique_ptr base = std::make_unique(m_ObjectManager, *this, 255);
+ base->Load(XeroFile, root);
+ m_ObjectBases.emplace_back(std::move(base));
+ }
+ else
+ {
+ XERO_ITER_ATTR(root, attr)
+ {
+ if (attr.Name == at_version && attr.Value.ToInt() != 1)
+ {
+ LOGERROR("Invalid actor format (actor '%s', version %i is not supported)",
+ pathname.string8().c_str(), attr.Value.ToInt());
+ return false;
+ }
+ }
+ u8 quality = 0;
+ XMBElement inlineActor;
+ XERO_ITER_EL(root, child)
+ {
+ if (child.GetNodeName() == el_inline)
+ inlineActor = child;
+ }
+ XERO_ITER_EL(root, actor)
+ {
+ if (actor.GetNodeName() != el_actor)
+ continue;
+ bool found_quality = false;
+ bool use_inline = false;
+ CStr file;
+ XERO_ITER_ATTR(actor, attr)
+ {
+ if (attr.Name == at_quality)
+ {
+ int v = GetQuality(attr.Value);
+ if (v > 255)
+ {
+ LOGERROR("Qualitylevel to attribute must not be above 255 (file %s)", pathname.string8());
+ return false;
+ }
+ if (v <= quality)
+ {
+ LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8());
+ return false;
+ }
+ quality = v;
+ found_quality = true;
+ }
+ else if (attr.Name == at_file)
+ {
+ if (attr.Value.empty())
+ LOGWARNING("Empty actor file specified (file %s)", pathname.string8());
+ file = attr.Value;
+ }
+ else if (attr.Name == at_inline)
+ use_inline = true;
+ }
+ if (!found_quality)
+ quality = 255;
+ std::unique_ptr base = std::make_unique(m_ObjectManager, *this, quality);
+ if (use_inline)
+ {
+ if (inlineActor.GetNodeName() == -1)
+ {
+ LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname.string8());
+ return false;
+ }
+ base->Load(XeroFile, inlineActor);
+ }
+ else if (file.empty())
+ base->Load(XeroFile, actor);
+ else
+ {
+ if (actor.GetChildNodes().size() > 0)
+ LOGWARNING("Actor definition refers to file but has children elements, they will be ignored (file %s)", pathname.string8());
+
+ // Open up an external file to load.
+ // Don't crash hard when failures happen, but log them and continue
+ CXeromyces XeroActor;
+ if (XeroActor.Load(g_VFS, "art/actors/" + file, "actor") == PSRETURN_OK)
+ {
+ const XMBElement& root = XeroActor.GetRoot();
+ if (root.GetNodeName() != el_actor)
+ {
+ LOGERROR("Included actors cannot define quality levels (opening %s from file %s)", file, pathname.string8());
+ return false;
+ }
+ base->Load(XeroActor, root);
+ }
+ else
+ {
+ LOGERROR("Could not open actor file at path %s (file %s)", file, pathname.string8());
+ return false;
+ }
+ m_UsedFiles.insert(file);
+ }
+ m_ObjectBases.emplace_back(std::move(base));
+ }
+ if (quality != 255)
+ {
+ LOGERROR("Quality levels must go up to 255 (file %s)", pathname.string8().c_str());
+ return false;
+ }
+ }
+
+ // For each quality level, check if we need to further split (because of props).
+ std::vector splits = QualityLevels();
+ for (const std::shared_ptr& base : m_ObjectBases)
+ base->GetQualitySplits(splits);
+ ENSURE(splits.size() >= 1);
+ if (splits.size() > 5)
+ {
+ LOGERROR("Too many quality levels (%i) for actor %s", splits.size(), pathname.string8().c_str());
+ return false;
+ }
+
+ std::vector>::iterator it = m_ObjectBases.begin();
+ std::vector::const_iterator qualityLevels = splits.begin();
+ while (it != m_ObjectBases.end())
+ if ((*it)->m_QualityLevel > *qualityLevels)
+ {
+ it = ++m_ObjectBases.emplace(it, (*it)->CopyWithQuality(*qualityLevels));
+ ++qualityLevels;
+ }
+ else if ((*it)->m_QualityLevel == *qualityLevels)
+ {
+ ++it;
+ ++qualityLevels;
+ }
+ else
+ ++it;
+
+ return true;
+}
+
+bool CActorDef::Reload()
+{
+ return Load(m_Pathname);
+}
+
+bool CActorDef::UsesFile(const VfsPath& pathname) const
+{
+ return m_UsedFiles.find(pathname) != m_UsedFiles.end();
+}
diff --git a/source/graphics/ObjectBase.h b/source/graphics/ObjectBase.h
index b8f64f7534..65190d953a 100644
--- a/source/graphics/ObjectBase.h
+++ b/source/graphics/ObjectBase.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -22,7 +22,9 @@
#include "ps/CStr.h"
#include "ps/CStrIntern.h"
+class CActorDef;
class CModel;
+class CObjectEntry;
class CObjectManager;
class CSkeletonAnim;
class CXeromyces;
@@ -30,12 +32,24 @@ class XMBElement;
#include
#include