Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg
+++ binaries/data/config/default.cfg
@@ -47,6 +47,9 @@
xres = 0
yres = 0
+; Quality used for actors.
+max_actor_quality=200
+
; Force a non-standard bit depth (if 0 then use the current desktop bit depth)
bpp = 0
Index: binaries/data/mods/public/art/actors/actor.rng
===================================================================
--- binaries/data/mods/public/art/actors/actor.rng
+++ binaries/data/mods/public/art/actors/actor.rng
@@ -1,194 +1,263 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ 255
+ 0
+
+
+ low
+ medium
+ high
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- 0
- 1
-
-
-
-
-
-
- 0
- 1
-
-
-
-
-
-
- 0
- 1
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- false
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
Index: binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_b.xml
===================================================================
--- binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_b.xml
+++ binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_b.xml
@@ -1,47 +1,79 @@
-
-
-
-
- props/new/dude_head.dae
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 184 151 98
-
-
- 255 205 125
-
-
- 153 145 129
-
-
- 129 152 153
-
-
- 8 8 8
-
-
- 250 130 78
-
-
- objectcolor.xml
-
+
+
+
+
+ props/new/dude_head.dae
+
+
+
+
+
+
+
+
+
+
+
+
+ 255 205 125
+
+
+ 153 145 129
+
+
+ 8 8 8
+
+
+ 250 130 78
+
+
+ objectcolor.xml
+
+
+
+
+
+ props/new/dude_head.dae
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 184 151 98
+
+
+ 255 205 125
+
+
+ 153 145 129
+
+
+ 129 152 153
+
+
+ 8 8 8
+
+
+ 250 130 78
+
+
+ objectcolor.xml
+
+
Index: binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_e.xml
===================================================================
--- binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_e.xml
+++ binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_e.xml
@@ -1,41 +1,67 @@
-
-
-
-
- props/new/head_beard_small.dae
-
-
- props/new/head_beard_large.dae
-
-
- props/new/head_beard_wide.dae
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 184 151 98
-
-
- 255 205 125
-
-
- 153 145 129
-
-
- 129 152 153
-
-
- objectcolor.xml
-
+
+
+
+
+ props/new/head_beard_large.dae
+
+
+
+
+
+
+
+
+
+ 184 151 98
+
+
+ 255 205 125
+
+
+ 153 145 129
+
+
+ objectcolor.xml
+
+
+
+
+
+ props/new/head_beard_small.dae
+
+
+ props/new/head_beard_large.dae
+
+
+ props/new/head_beard_wide.dae
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 184 151 98
+
+
+ 255 205 125
+
+
+ 153 145 129
+
+
+ 129 152 153
+
+
+ objectcolor.xml
+
+
Index: binaries/data/mods/public/art/actors/props/units/shields/aspis_athen_e.xml
===================================================================
--- binaries/data/mods/public/art/actors/props/units/shields/aspis_athen_e.xml
+++ binaries/data/mods/public/art/actors/props/units/shields/aspis_athen_e.xml
@@ -1,28 +1,80 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- player_trans_parallax_spec_helmet.xml
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ player_trans.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ player_trans_parallax_spec_helmet.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ player_trans_parallax_spec_helmet.xml
+
+
Index: binaries/data/mods/public/art/actors/props/units/shields/props/aspis_back_bronze_01.xml
===================================================================
--- binaries/data/mods/public/art/actors/props/units/shields/props/aspis_back_bronze_01.xml
+++ binaries/data/mods/public/art/actors/props/units/shields/props/aspis_back_bronze_01.xml
@@ -1,15 +1,28 @@
-
-
-
-
- props/shield/aspis_01_back.dae
-
-
-
-
-
-
-
- no_trans_parallax_spec.xml
-
+
+
+
+
+ props/shield/aspis_01_back.dae
+
+
+
+
+
+ default.xml
+
+
+
+
+
+ props/shield/aspis_01_back.dae
+
+
+
+
+
+
+
+ no_trans_parallax_spec.xml
+
+
Index: binaries/data/mods/public/art/actors/props/units/weapons/arrow_back.xml
===================================================================
--- binaries/data/mods/public/art/actors/props/units/weapons/arrow_back.xml
+++ binaries/data/mods/public/art/actors/props/units/weapons/arrow_back.xml
@@ -1,13 +1,17 @@
-
-
-
-
- props/weap_arrow_back.dae
-
-
-
-
-
- default.xml
-
+
+
+
+
+
+
+
+ props/weap_arrow_back.dae
+
+
+
+
+
+ default.xml
+
+
Index: binaries/data/mods/public/art/actors/props/units/weapons/bow_short.xml
===================================================================
--- binaries/data/mods/public/art/actors/props/units/weapons/bow_short.xml
+++ binaries/data/mods/public/art/actors/props/units/weapons/bow_short.xml
@@ -1,24 +1,28 @@
-
-
-
-
- props/weapons/bow_greek.dae
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- player_trans.xml
-
+
+
+
+
+
+
+
+ props/weapons/bow_greek.dae
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ player_trans.xml
+
+
Index: binaries/data/mods/public/art/actors/props/units/weapons/spear_hoplite.xml
===================================================================
--- binaries/data/mods/public/art/actors/props/units/weapons/spear_hoplite.xml
+++ binaries/data/mods/public/art/actors/props/units/weapons/spear_hoplite.xml
@@ -1,13 +1,14 @@
-
-
-
-
-
-
- props/spear_hoplite.dae
-
-
-
-
-
+
+
+
+
+
+
+
+ props/spear_hoplite.dae
+
+
+
+
+
Index: binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml
+++ binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml
@@ -1,53 +1,155 @@
-
-
-
-
- skeletal/new/m_tunic_short.dae
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- player_trans.xml
-
+
+
+
+
+
+ skeletal/new/m_tunic_short.dae
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ player_trans.xml
+
+
+
+
+
+ skeletal/new/m_tunic_short.dae
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ player_trans.xml
+
+
+
+
+
+ skeletal/new/m_tunic_short.dae
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ player_trans.xml
+
+
Index: binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml
+++ binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml
@@ -1,215 +1,6 @@
-
-
-
-
- skeletal/new/m_armor_tunic_short.dae
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- player_trans_spec.xml
-
+
+
+
+
+
Index: binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e_base.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e_base.xml
+++ binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e_base.xml
@@ -2,7 +2,15 @@
-
+
+ skeletal/new/m_armor_tunic_short.dae
+
+
+
+
+
+
+
skeletal/new/m_armor_tunic_short.dae
@@ -12,7 +20,7 @@
-
+
@@ -35,17 +43,17 @@
-
+
-
+
-
+
@@ -60,22 +68,22 @@
-
+
-
+
-
+
-
+
@@ -93,13 +101,13 @@
-
+
-
+
@@ -111,19 +119,19 @@
-
+
-
+
-
+
@@ -147,7 +155,7 @@
-
+
@@ -159,13 +167,13 @@
-
+
-
+
Index: binaries/data/mods/public/gui/options/options.json
===================================================================
--- binaries/data/mods/public/gui/options/options.json
+++ 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",
Index: source/graphics/Model.cpp
===================================================================
--- source/graphics/Model.cpp
+++ 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 @@
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();
}
Index: source/graphics/ObjectBase.h
===================================================================
--- source/graphics/ObjectBase.h
+++ 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;
@@ -34,8 +36,19 @@
#include
#include
+/**
+ * Maintains the tree of possible objects from a specific actor definition at a given quality level.
+ * An Object Base is made of:
+ * - a material
+ * - a few properties (float on water / casts shadow / ...)
+ * - a number of variant groups.
+ * Any actual object in game will pick a variant from each group (see ObjectEntry).
+ */
class CObjectBase
{
+ friend CActorDef;
+
+ // See CopyWithQuality() below.
NONCOPYABLE(CObjectBase);
public:
struct Anim
@@ -118,56 +131,32 @@
std::multimap samplers;
};
- CObjectBase(CObjectManager& objectManager);
+ CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8 QualityLevel);
+
+ // Returns a set of selection such that, added to initialSelections, CalculateVariationKey can proceed.
+ std::set CalculateRandomRemainingSelections(uint32_t seed, const std::vector>& initialSelections) const;
// Get the variation key (indices of chosen variants from each group)
- // based on the selection strings
- std::vector CalculateVariationKey(const std::vector >& selections);
+ // based on the selection strings.
+ // Should not have to make a random choice: the selections should be complete.
+ std::vector CalculateVariationKey(const std::vector*>& selections) const;
// Get the final actor data, combining all selected variants
- const Variation BuildVariation(const std::vector& variationKey);
-
- // Get a set of selection strings that are complete enough to specify an
- // exact variation of the actor, using the initial selections wherever possible
- // and choosing randomly where a choice is necessary.
- std::set CalculateRandomVariation(uint32_t seed, const std::set& initialSelections);
-
- // Given a prioritized vector of selection string sets that partially specify
- // a variation, calculates a remaining set of selection strings such that the resulting
- // set merged with the initial selections fully specifies an exact variation of
- // the actor. The resulting selections are selected randomly, but only where a choice
- // is necessary (i.e. where there are multiple variants but the initial selections,
- // applied in priority order, fail to select one).
- std::set CalculateRandomRemainingSelections(uint32_t seed, const std::vector >& initialSelections);
+ const Variation BuildVariation(const std::vector& variationKey) const;
// Get a list of variant groups for this object, plus for all possible
// props. Duplicated groups are removed, if several props share the same
// variant names.
std::vector > GetVariantGroups() const;
- /**
- * Initialise this object by loading from the given file.
- * Returns false on error.
- */
- bool Load(const VfsPath& pathname);
-
- /**
- * Reload this object from the file that it was previously loaded from.
- * Returns false on error.
- */
- bool Reload();
+ CStrW GetPathname() const;
/**
* Returns whether this object (including any possible props)
* uses the given file. (This is used for hotloading.)
*/
- bool UsesFile(const VfsPath& pathname);
+ bool UsesFile(const VfsPath& pathname) const;
- // filename that this was loaded from
- VfsPath m_Pathname;
-
- // short human-readable name
- CStrW m_ShortName;
struct {
// cast shadows from this object
@@ -179,20 +168,100 @@
// the material file
VfsPath m_Material;
+ // Quality level - part of the data resource path.
+ u8 m_QualityLevel;
+
+ // short human-readable name
+ CStrW m_ShortName;
+
private:
+ // Private interface for CActorDef/ObjectEntry
+
+ /**
+ * Acts as an explicit copy constructor, for a new quality level.
+ * Note that this does not reload the actor, so this setting will only change props.
+ */
+ std::unique_ptr CopyWithQuality(u8 newQualityLevel) const;
+
// A low-quality RNG like rand48 causes visible non-random patterns (particularly
// in large grids of the same actor with consecutive seeds, e.g. forests),
// so use a better one that appears to avoid those patterns
using rng_t = boost::mt19937;
+ std::set CalculateRandomRemainingSelections(rng_t& rng, const std::vector>& initialSelections) const;
- std::set CalculateRandomRemainingSelections(rng_t& rng, const std::vector >& initialSelections);
+ /**
+ * Get all quality levels at which this object changes (includes props).
+ * Intended to be called by CActorFef.
+ * @param splits - a sorted vector of unique quality splits.
+ */
+ void GetQualitySplits(std::vector& splits) const;
+
+ void Load(const CXeromyces& XeroFile, const XMBElement& base);
+ void LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant);
+
+private:
+ // Backref to the owning actor.
+ CActorDef& m_ActorDef;
std::vector< std::vector > m_VariantGroups;
CObjectManager& m_ObjectManager;
+};
+
+/**
+ * Represents an actor file. Actors can contain various quality levels.
+ * An ActorDef maintains a CObjectBase for each specified quality level, and provides access to it.
+ */
+class CActorDef
+{
+ // Friend these three so they can use GetBase.
+ friend class CObjectManager;
+ friend class CObjectBase;
+ friend class CObjectEntry;
+
+ NONCOPYABLE(CActorDef);
+public:
+
+ CActorDef(CObjectManager& objectManager);
+
+ std::vector QualityLevels() const;
+
+ VfsPath GetPathname() const { return m_Pathname; }
+
+// Interface accessible from CObjectManager / CObjectBase
+protected:
+ /**
+ * Return the Object base matching the given quality level.
+ */
+ const std::shared_ptr& GetBase(u8 QualityLevel) const;
+
+ /**
+ * Initialise this object by loading from the given file.
+ * Returns false on error.
+ */
+ bool Load(const VfsPath& pathname);
+
+ /**
+ * Reload this object from the file that it was previously loaded from.
+ * Returns false on error.
+ */
+ bool Reload();
+
+ /**
+ * Returns whether this actor (including any possible props)
+ * uses the given file. (This is used for hotloading.)
+ */
+ bool UsesFile(const VfsPath& pathname) const;
+
+ // filename that this was loaded from
+ VfsPath m_Pathname;
+
+private:
+ CObjectManager& m_ObjectManager;
+
+ // std::shared_ptr to avoid issues during hotloading.
+ std::vector> m_ObjectBases;
std::unordered_set m_UsedFiles;
-
- void LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant);
};
#endif
Index: source/graphics/ObjectBase.cpp
===================================================================
--- source/graphics/ObjectBase.cpp
+++ 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;
+
+ m_ShortName = m_ActorDef.m_Pathname.string() + CStrW::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_ShortName.ToUTF8());
+ }
+ 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 @@
{
// 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 @@
// 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 @@
}
}
-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::multimap 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 @@
// Determine the first variant that matches the provided strings,
// starting with the highest priority selections set:
- for (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 @@
// Remember which props were chosen, so we can call CalculateVariationKey on them
// at the end.
// Erase all existing props which are overridden by this variant:
- Variant& var((*grp)[match]);
+ const Variant& var((*grp)[match]);
for (const Prop& prop : var.m_Props)
chosenProps.erase(prop.m_PropPointName);
@@ -389,10 +393,10 @@
// Load each prop, and add their CalculateVariationKey to our key:
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 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 @@
return choices;
}
-const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& variationKey)
+const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& variationKey) const
{
Variation variation;
@@ -408,7 +412,7 @@
// 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 @@
}
// Get the matched variant
- CObjectBase::Variant& var ((*grp)[id]);
+ const CObjectBase::Variant& var ((*grp)[id]);
// Apply its data:
@@ -449,50 +453,44 @@
// 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 @@
// 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 @@
// 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 @@
// 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 @@
{
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,255 @@
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;
+ }
+ }
+}
+
+CStrW CObjectBase::GetPathname() const
+{
+ return m_ActorDef.m_Pathname.string();
+}
+
+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;
+ 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(qualitylevels);
+ EL(actor);
+ EL(inline);
+ AT(maxquality);
+ AT(file);
+ AT(inline);
+#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
+ {
+ u8 max_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_maxquality = false;
+ bool use_inline = false;
+ CStr file;
+ XERO_ITER_ATTR(actor, attr)
+ {
+ if (attr.Name == at_maxquality)
+ {
+ 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 <= max_quality)
+ {
+ LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8());
+ return false;
+ }
+ max_quality = v;
+ found_maxquality = 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_maxquality)
+ max_quality = 255;
+ std::unique_ptr base = std::make_unique(m_ObjectManager, *this, max_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 (max_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();
+}
Index: source/graphics/ObjectEntry.h
===================================================================
--- source/graphics/ObjectEntry.h
+++ source/graphics/ObjectEntry.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
@@ -40,16 +40,16 @@
NONCOPYABLE(CObjectEntry);
public:
- CObjectEntry(CObjectBase* base, CSimulation2& simulation);
+ CObjectEntry(const std::shared_ptr& base, CSimulation2& simulation);
~CObjectEntry();
// Construct this actor, using the specified variation selections
- bool BuildVariation(const std::vector >& selections,
+ bool BuildVariation(const std::vector*>& completeSelections,
const std::vector& variationKey, CObjectManager& objectManager);
// Base actor. Contains all the things that don't change between
// different variations of the actor.
- CObjectBase* m_Base;
+ std::shared_ptr m_Base;
// samplers list
std::vector m_Samplers;
Index: source/graphics/ObjectEntry.cpp
===================================================================
--- source/graphics/ObjectEntry.cpp
+++ source/graphics/ObjectEntry.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
@@ -39,7 +39,7 @@
#include
-CObjectEntry::CObjectEntry(CObjectBase* base, CSimulation2& simulation) :
+CObjectEntry::CObjectEntry(const std::shared_ptr& base, CSimulation2& simulation) :
m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Model(NULL), m_Outdated(false), m_Simulation(simulation)
{
}
@@ -53,7 +53,7 @@
}
-bool CObjectEntry::BuildVariation(const std::vector >& selections,
+bool CObjectEntry::BuildVariation(const std::vector*>& completeSelections,
const std::vector& variationKey,
CObjectManager& objectManager)
{
@@ -209,7 +209,10 @@
continue;
}
- CObjectEntry* oe = objectManager.FindObjectVariation(prop.m_ModelName.c_str(), selections);
+ CObjectEntry* oe = nullptr;
+ if (CActorDef* actorDef = objectManager.FindActorDef(prop.m_ModelName.c_str()); actorDef)
+ oe = objectManager.FindObjectVariation(actorDef->GetBase(m_Base->m_QualityLevel), completeSelections);
+
if (!oe)
{
LOGERROR("Failed to build prop model \"%s\" on actor \"%s\"", utf8_from_wstring(prop.m_ModelName), utf8_from_wstring(m_Base->m_ShortName));
Index: source/graphics/ObjectManager.h
===================================================================
--- source/graphics/ObjectManager.h
+++ source/graphics/ObjectManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2009 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
@@ -18,13 +18,15 @@
#ifndef INCLUDED_OBJECTMANAGER
#define INCLUDED_OBJECTMANAGER
-#include
-#include