Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg +++ ps/trunk/binaries/data/config/default.cfg @@ -128,6 +128,9 @@ 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 Index: ps/trunk/binaries/data/mods/public/art/actors/actor.rnc =================================================================== --- ps/trunk/binaries/data/mods/public/art/actors/actor.rnc +++ ps/trunk/binaries/data/mods/public/art/actors/actor.rnc @@ -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 - }? - ) -} Index: ps/trunk/binaries/data/mods/public/art/actors/actor.rng =================================================================== --- ps/trunk/binaries/data/mods/public/art/actors/actor.rng +++ ps/trunk/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. + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - + + + Index: ps/trunk/binaries/data/mods/public/gui/options/options.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/options/options.json +++ ps/trunk/binaries/data/mods/public/gui/options/options.json @@ -153,6 +153,17 @@ "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", "tooltip": "Number of shader effects. REQUIRES GAME RESTART", Index: ps/trunk/source/graphics/Model.cpp =================================================================== --- ps/trunk/source/graphics/Model.cpp +++ ps/trunk/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: ps/trunk/source/graphics/ObjectBase.h =================================================================== --- ps/trunk/source/graphics/ObjectBase.h +++ ps/trunk/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 @@ #include #include +#include #include #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 +132,33 @@ 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(); + // Return a string identifying this actor uniquely (includes quality level information); + const CStr& GetIdentifier() const; /** * Returns whether this object (including any possible props) * uses the given file. (This is used for hotloading.) */ - bool UsesFile(const VfsPath& pathname); - - // filename that this was loaded from - VfsPath m_Pathname; + bool UsesFile(const VfsPath& pathname) const; - // short human-readable name - CStrW m_ShortName; struct { // cast shadows from this object @@ -179,20 +170,100 @@ // the material file VfsPath m_Material; + // Quality level - part of the data resource path. + u8 m_QualityLevel; + 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; + + // Used to identify this actor uniquely in the ObjectManager (and for debug). + CStr m_Identifier; std::vector< std::vector > m_VariantGroups; CObjectManager& m_ObjectManager; +}; - std::unordered_set m_UsedFiles; +/** + * 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; - void LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant); + 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; }; #endif Index: ps/trunk/source/graphics/ObjectBase.cpp =================================================================== --- ps/trunk/source/graphics/ObjectBase.cpp +++ ps/trunk/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 @@ { // 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,266 @@ 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(); +} Index: ps/trunk/source/graphics/ObjectEntry.h =================================================================== --- ps/trunk/source/graphics/ObjectEntry.h +++ ps/trunk/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: ps/trunk/source/graphics/ObjectEntry.cpp =================================================================== --- ps/trunk/source/graphics/ObjectEntry.cpp +++ ps/trunk/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) { @@ -72,7 +72,7 @@ str << variation.color; int r, g, b; if (! (str >> r >> g >> b)) // Any trailing data is ignored - LOGERROR("Actor '%s' has invalid RGB color '%s'", utf8_from_wstring(m_Base->m_ShortName), variation.color); + LOGERROR("Actor '%s' has invalid RGB color '%s'", m_Base->GetIdentifier(), variation.color); else m_Color = CColor(r/255.0f, g/255.0f, b/255.0f, 1.0f); } @@ -130,7 +130,7 @@ model->InitModel(modeldef); if (m_Samplers.empty()) - LOGERROR("Actor '%s' has no textures.", utf8_from_wstring(m_Base->m_ShortName)); + LOGERROR("Actor '%s' has no textures.", m_Base->GetIdentifier()); for (const CObjectBase::Samp& samp : m_Samplers) { @@ -148,7 +148,7 @@ { if (std::find_if(m_Samplers.begin(), m_Samplers.end(), [&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requSampName; }) == m_Samplers.end()) - LOGERROR("Actor %s: required texture sampler %s not found (material %s)", utf8_from_wstring(m_Base->m_ShortName), requSampName.string().c_str(), m_Base->m_Material.string8().c_str()); + LOGERROR("Actor %s: required texture sampler %s not found (material %s)", m_Base->GetIdentifier(), requSampName.string().c_str(), m_Base->m_Material.string8().c_str()); } // calculate initial object space bounds, based on vertex positions @@ -209,10 +209,13 @@ 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)); + LOGERROR("Failed to build prop model \"%s\" on actor \"%s\"", utf8_from_wstring(prop.m_ModelName), m_Base->GetIdentifier()); continue; } @@ -243,7 +246,7 @@ propmodel->ToCModel()->SetAnimation(oe->GetRandomAnimation("idle")); } else - LOGERROR("Failed to find matching prop point called \"%s\" in model \"%s\" for actor \"%s\"", ppn, m_ModelName.string8(), utf8_from_wstring(m_Base->m_ShortName)); + LOGERROR("Failed to find matching prop point called \"%s\" in model \"%s\" for actor \"%s\"", ppn, m_ModelName.string8(), m_Base->GetIdentifier()); } // Setup flags. Index: ps/trunk/source/graphics/ObjectManager.h =================================================================== --- ps/trunk/source/graphics/ObjectManager.h +++ ps/trunk/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,16 @@ #ifndef INCLUDED_OBJECTMANAGER #define INCLUDED_OBJECTMANAGER -#include -#include -#include - #include "ps/CStr.h" #include "lib/file/vfs/vfs_path.h" +#include +#include +#include +#include + +class CActorDef; +class CConfigDBHook; class CMeshManager; class CObjectBase; class CObjectEntry; @@ -41,13 +44,13 @@ // Unique identifier of an actor variation struct ObjectKey { - ObjectKey(const CStrW& name, const std::vector& var) - : ActorName(name), ActorVariation(var) {} + ObjectKey(const CStr& identifier, const std::vector& var) + : ObjectBaseIdentifier(identifier), ActorVariation(var) {} bool operator< (const CObjectManager::ObjectKey& a) const; private: - CStrW ActorName; + CStr ObjectBaseIdentifier; std::vector ActorVariation; }; @@ -65,13 +68,22 @@ void UnloadObjects(); - CObjectEntry* FindObject(const CStrW& objname); - void DeleteObject(CObjectEntry* entry); + CActorDef* FindActorDef(const CStrW& actorName); - CObjectBase* FindObjectBase(const CStrW& objname); + /** + * Get the object entry for a given actor & the given selections list. + * @param selections - a possibly incomplete list of selections. + * @param seed - the randomness seed to use to complete the random selections. + */ + CObjectEntry* FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed); - CObjectEntry* FindObjectVariation(const CStrW& objname, const std::vector >& selections); - CObjectEntry* FindObjectVariation(CObjectBase* base, const std::vector >& selections); + /** + * @see FindObjectVariation. + * These take a complete selection. These are pointers to sets that are + * guaranteed to exist (pointers are used to avoid copying the sets). + */ + CObjectEntry* FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections); + CObjectEntry* FindObjectVariation(const CStrW& objname, const std::vector*>& completeSelections); /** * Get the terrain object that actors managed by this manager should be linked @@ -85,13 +97,22 @@ */ Status ReloadChangedFile(const VfsPath& path); -private: + + /** + * Reload actors that have a quality setting. Used when changing the actor quality. + */ + void ActorQualityChanged(); + CMeshManager& m_MeshManager; CSkeletonAnimManager& m_SkeletonAnimManager; CSimulation2& m_Simulation; - std::map m_Objects; - std::map m_ObjectBases; + u8 m_QualityLevel = 100; + std::unique_ptr m_QualityHook; + + // TODO: define a hash and switch to unordered_map + std::map> m_Objects; + std::unordered_map> m_ActorDefs; }; #endif Index: ps/trunk/source/graphics/ObjectManager.cpp =================================================================== --- ps/trunk/source/graphics/ObjectManager.cpp +++ ps/trunk/source/graphics/ObjectManager.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 @@ -22,6 +22,7 @@ #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "ps/CLogger.h" +#include "ps/ConfigDB.h" #include "ps/Game.h" #include "ps/Profile.h" #include "ps/Filesystem.h" @@ -30,12 +31,11 @@ #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpVisual.h" - bool CObjectManager::ObjectKey::operator< (const CObjectManager::ObjectKey& a) const { - if (ActorName < a.ActorName) + if (ObjectBaseIdentifier < a.ObjectBaseIdentifier) return true; - else if (ActorName > a.ActorName) + else if (ObjectBaseIdentifier > a.ObjectBaseIdentifier) return false; else return ActorVariation < a.ActorVariation; @@ -51,6 +51,8 @@ { RegisterFileReloadFunc(ReloadChangedFileCB, this); + m_QualityHook = std::make_unique(g_ConfigDB.RegisterHookAndCall("max_actor_quality", [this]() { this->ActorQualityChanged(); })); + if (!CXeromyces::AddValidator(g_VFS, "actor", "art/actors/actor.rng")) LOGERROR("CObjectManager: failed to load actor grammar file 'art/actors/actor.rng'"); } @@ -59,92 +61,74 @@ { UnloadObjects(); + g_ConfigDB.UnregisterHook(std::move(m_QualityHook)); + UnregisterFileReloadFunc(ReloadChangedFileCB, this); } - -CObjectBase* CObjectManager::FindObjectBase(const CStrW& objectname) +CActorDef* CObjectManager::FindActorDef(const CStrW& actorName) { - ENSURE(!objectname.empty()); - - // See if the base type has been loaded yet: + ENSURE(!actorName.empty()); - std::map::iterator it = m_ObjectBases.find(objectname); - if (it != m_ObjectBases.end()) - return it->second; + decltype(m_ActorDefs)::iterator it = m_ActorDefs.find(actorName); + if (it != m_ActorDefs.end()) + return it->second.get(); - // Not already loaded, so try to load it: + std::unique_ptr actor = std::make_unique(*this); - CObjectBase* obj = new CObjectBase(*this); + VfsPath pathname = VfsPath("art/actors/") / actorName; - VfsPath pathname = VfsPath("art/actors/") / objectname; - - if (obj->Load(pathname)) - { - m_ObjectBases[objectname] = obj; - return obj; - } - else - delete obj; + if (actor->Load(pathname)) + return m_ActorDefs.emplace(actorName, std::move(actor)).first->second.get(); - LOGERROR("CObjectManager::FindObjectBase(): Cannot find object '%s'", utf8_from_wstring(objectname)); + LOGERROR("CObjectManager::FindActorDef(): Cannot find actor '%s'", utf8_from_wstring(actorName)); - return 0; + return nullptr; } -CObjectEntry* CObjectManager::FindObject(const CStrW& objname) +CObjectEntry* CObjectManager::FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed) { - std::vector > selections; // TODO - should this really be empty? - return FindObjectVariation(objname, selections); -} - -CObjectEntry* CObjectManager::FindObjectVariation(const CStrW& objname, const std::vector >& selections) -{ - CObjectBase* base = FindObjectBase(objname); + if (!actor) + return nullptr; - if (! base) - return NULL; + const std::shared_ptr& base = actor->GetBase(m_QualityLevel); - return FindObjectVariation(base, selections); + std::vector*> completeSelections; + for (const std::set& selectionSet : selections) + completeSelections.emplace_back(&selectionSet); + // To maintain a consistent look between quality levels, first complete with the highest-quality variants. + // then complete again at the required quality level (since not all variants may be available). + std::set highQualitySelections = actor->GetBase(255)->CalculateRandomRemainingSelections(seed, selections); + completeSelections.emplace_back(&highQualitySelections); + // We don't have to pass the high-quality selections here because they have higher priority anyways. + std::set remainingSelections = base->CalculateRandomRemainingSelections(seed, selections); + completeSelections.emplace_back(&remainingSelections); + return FindObjectVariation(base, completeSelections); } -CObjectEntry* CObjectManager::FindObjectVariation(CObjectBase* base, const std::vector >& selections) +CObjectEntry* CObjectManager::FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections) { - PROFILE("object variation loading"); + PROFILE2("FindObjectVariation"); // Look to see whether this particular variation has already been loaded - - std::vector choices = base->CalculateVariationKey(selections); - ObjectKey key (base->m_Pathname.string(), choices); - - std::map::iterator it = m_Objects.find(key); + std::vector choices = base->CalculateVariationKey(completeSelections); + ObjectKey key (base->GetIdentifier(), choices); + decltype(m_Objects)::iterator it = m_Objects.find(key); if (it != m_Objects.end() && !it->second->m_Outdated) - return it->second; + return it->second.get(); - // If it hasn't been loaded, load it now + // If it hasn't been loaded, load it now. - // TODO: If there was an existing ObjectEntry, but it's outdated (due to hotloading), - // we'll get a memory leak when replacing its entry in m_Objects. The problem is - // some CUnits might still have a pointer to the old ObjectEntry so we probably can't - // just delete it now. Probably we need to redesign the caching/hotloading system so it - // makes more sense (e.g. use shared_ptr); for now I'll just leak, to avoid making the logic - // more complex than it is already is, since this only matters for the rare case of hotloading. - - CObjectEntry* obj = new CObjectEntry(base, m_Simulation); // TODO: type ? + std::unique_ptr obj = std::make_unique(base, m_Simulation); // TODO (for some efficiency): use the pre-calculated choices for this object, // which has already worked out what to do for props, instead of passing the // selections into BuildVariation and having it recalculate the props' choices. - if (! obj->BuildVariation(selections, choices, *this)) - { - DeleteObject(obj); - return NULL; - } + if (!obj->BuildVariation(completeSelections, choices, *this)) + return nullptr; - m_Objects[key] = obj; - - return obj; + return m_Objects.emplace(key, std::move(obj)).first->second.get(); } CTerrain* CObjectManager::GetTerrain() @@ -155,53 +139,52 @@ return cmpTerrain->GetCTerrain(); } -void CObjectManager::DeleteObject(CObjectEntry* entry) -{ - std::function&)> second_equals = - [&entry](const std::pair& a) { return a.second == entry; }; - - std::map::iterator it = m_Objects.begin(); - while (m_Objects.end() != (it = find_if(it, m_Objects.end(), second_equals))) - it = m_Objects.erase(it); - - delete entry; -} - - void CObjectManager::UnloadObjects() { - for (const std::pair& p : m_Objects) - delete p.second; m_Objects.clear(); - - for (const std::pair& p : m_ObjectBases) - delete p.second; - m_ObjectBases.clear(); + m_ActorDefs.clear(); } Status CObjectManager::ReloadChangedFile(const VfsPath& path) { // Mark old entries as outdated so we don't reload them from the cache - for (std::map::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it) + for (std::map>::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it) if (it->second->m_Base->UsesFile(path)) it->second->m_Outdated = true; + const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); + // Reload actors that use a changed object - for (std::map::iterator it = m_ObjectBases.begin(); it != m_ObjectBases.end(); ++it) + for (std::unordered_map>::iterator it = m_ActorDefs.begin(); it != m_ActorDefs.end(); ++it) { - if (it->second->UsesFile(path)) - { - it->second->Reload(); - - // Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the - // object with all correct variations, and we don't want to waste space storing it just for the - // rare occurrence of hotloading, so we'll tell the component (which does preserve the information) - // to do the reloading itself - const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); - for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) - static_cast(eit->second)->Hotload(it->first); - } + if (!it->second->UsesFile(path)) + continue; + it->second->Reload(); + + // Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the + // object with all correct variations, and we don't want to waste space storing it just for the + // rare occurrence of hotloading, so we'll tell the component (which does preserve the information) + // to do the reloading itself + for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) + static_cast(eit->second)->Hotload(it->first); } - return INFO::OK; } + +void CObjectManager::ActorQualityChanged() +{ + int quality; + CFG_GET_VAL("max_actor_quality", quality); + if (quality == m_QualityLevel) + return; + + m_QualityLevel = quality > 255 ? 255 : quality < 0 ? 0 : quality; + + // No need to reload entries or actors, but we do need to reload all units. + const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual); + for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) + static_cast(eit->second)->Hotload(); + + // Trigger an interpolate call - needed because the game is generally paused & models disappear otherwise. + m_Simulation.Interpolate(0.f, 0.f, 0.f); +} Index: ps/trunk/source/graphics/Unit.h =================================================================== --- ps/trunk/source/graphics/Unit.h +++ ps/trunk/source/graphics/Unit.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2016 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 @@ -24,6 +24,7 @@ #include "ps/CStr.h" #include "simulation2/system/Entity.h" // entity_id_t +class CActorDef; class CModelAbstract; class CObjectEntry; class CObjectManager; @@ -38,8 +39,7 @@ NONCOPYABLE(CUnit); private: // Private constructor. Needs complete list of selections for the variation. - CUnit(CObjectEntry* object, CObjectManager& objectManager, - const std::set& actorSelections, uint32_t seed); + CUnit(CObjectManager& objectManager, const CActorDef& actor, uint32_t seed); public: // Attempt to create a unit with the given actor, with a set of @@ -80,12 +80,14 @@ void SetActorSelections(const std::set& selections); private: - // object from which unit was created; never NULL - CObjectEntry* m_Object; - // object model representation; never NULL - CModelAbstract* m_Model; + // Actor for the unit + const CActorDef& m_Actor; + // object from which unit was created; never NULL once fully created. + CObjectEntry* m_Object = nullptr; + // object model representation; never NULL once fully created. + CModelAbstract* m_Model = nullptr; - CUnitAnimation* m_Animation; + CUnitAnimation* m_Animation = nullptr; // unique (per map) ID number for units created in the editor, as a // permanent way of referencing them. Index: ps/trunk/source/graphics/Unit.cpp =================================================================== --- ps/trunk/source/graphics/Unit.cpp +++ ps/trunk/source/graphics/Unit.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 @@ -26,16 +26,11 @@ #include "SkeletonAnimDef.h" #include "UnitAnimation.h" -CUnit::CUnit(CObjectEntry* object, CObjectManager& objectManager, - const std::set& actorSelections, uint32_t seed) -: m_Object(object), m_Model(object->m_Model->Clone()), - m_ID(INVALID_ENTITY), m_ActorSelections(actorSelections), - m_ObjectManager(objectManager), m_Seed(seed) -{ - if (m_Model->ToCModel()) - m_Animation = new CUnitAnimation(m_ID, m_Model->ToCModel(), m_Object); - else - m_Animation = NULL; +#include "ps/CLogger.h" + +CUnit::CUnit(CObjectManager& objectManager, const CActorDef& actor, uint32_t seed) +: m_ID(INVALID_ENTITY), m_ObjectManager(objectManager), m_Actor(actor), m_Seed(seed), m_Animation(nullptr) +{ } CUnit::~CUnit() @@ -46,22 +41,19 @@ CUnit* CUnit::Create(const CStrW& actorName, uint32_t seed, const std::set& selections, CObjectManager& objectManager) { - CObjectBase* base = objectManager.FindObjectBase(actorName); - - if (! base) - return NULL; - - std::set actorSelections = base->CalculateRandomVariation(seed, selections); - - std::vector > selectionsVec; - selectionsVec.push_back(actorSelections); + CActorDef* actor = objectManager.FindActorDef(actorName); - CObjectEntry* obj = objectManager.FindObjectVariation(base, selectionsVec); + if (!actor) + return nullptr; - if (! obj) - return NULL; - - return new CUnit(obj, objectManager, actorSelections, seed); + CUnit* unit = new CUnit(objectManager, *actor, seed); + unit->SetActorSelections(selections); // Calls ReloadObject(). + if (!unit->m_Model) + { + delete unit; + return nullptr; + } + return unit; } void CUnit::UpdateModel(float frameTime) @@ -107,7 +99,7 @@ std::set entitySelections; for (const std::pair& selection : m_EntitySelections) entitySelections.insert(selection.second); - std::vector > selections; + std::vector> selections; selections.push_back(entitySelections); selections.push_back(m_ActorSelections); @@ -116,14 +108,24 @@ // expects the selectors passed to it to be complete. // see http://trac.wildfiregames.com/ticket/979 + // If these selections give a different object, change this unit to use it // Use the entity ID as randomization seed (same as when the unit was first created) - std::set remainingSelections = m_Object->m_Base->CalculateRandomRemainingSelections(m_Seed, selections); - if (!remainingSelections.empty()) - selections.push_back(remainingSelections); + CObjectEntry* newObject = m_ObjectManager.FindObjectVariation(&m_Actor, selections, m_Seed); + if (!newObject) + { + LOGERROR("Error loading object variation (actor: %s)", m_Actor.GetPathname().string8()); + // Don't delete the unit, don't override our current (valid) state. + return; + } - // If these selections give a different object, change this unit to use it - CObjectEntry* newObject = m_ObjectManager.FindObjectVariation(m_Object->m_Base, selections); - if (newObject && newObject != m_Object) + if (!m_Object) + { + m_Object = newObject; + m_Model = newObject->m_Model->Clone(); + if (m_Model->ToCModel()) + m_Animation = new CUnitAnimation(m_ID, m_Model->ToCModel(), m_Object); + } + else if (m_Object && newObject != m_Object) { // Clone the new object's base (non-instance) model CModelAbstract* newModel = newObject->m_Model->Clone(); Index: ps/trunk/source/ps/ConfigDB.h =================================================================== --- ps/trunk/source/ps/ConfigDB.h +++ ps/trunk/source/ps/ConfigDB.h @@ -54,10 +54,16 @@ using CConfigValueSet = std::vector; +// Opaque data type so that callers that hook into ConfigDB can delete their hooks. +// Would be defined in CConfigDB but then it couldn't be forward-declared, which is rather annoying. +// Actually defined below - requires access to CConfigDB. +class CConfigDBHook; + #define g_ConfigDB CConfigDB::GetSingleton() class CConfigDB : public Singleton { + friend CConfigDBHook; public: /** * Attempt to retrieve the value of a config variable with the given name; @@ -168,30 +174,15 @@ bool WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value); - - // Opaque data type so that callers that hook into ConfigDB can delete their hooks. - class hook_t - { - friend class CConfigDB; - public: - // Point the moved-from hook to end, which is checked for in UnregisterHook, - // to avoid a double-erase error. - hook_t(hook_t&& h) { ptr = std::move(h.ptr); h.ptr = m_Hooks.end(); } - hook_t(const hook_t&) = delete; - private: - hook_t(std::multimap>::iterator p) : ptr(p) {}; - - std::multimap>::iterator ptr; - }; - /** * Register a simple lambda that will be called anytime the value changes in any namespace * This is simple on purpose, the hook is responsible for checking if it should do something. * When RegisterHookAndCall is called, the hook is immediately triggered. */ - hook_t RegisterHookAndCall(const CStr& name, std::function hook); + CConfigDBHook RegisterHookAndCall(const CStr& name, std::function hook); - void UnregisterHook(hook_t&& hook); + void UnregisterHook(CConfigDBHook&& hook); + void UnregisterHook(std::unique_ptr&& hook); private: static std::map m_Map[]; @@ -200,6 +191,22 @@ static bool m_HasChanges[]; }; +class CConfigDBHook +{ + friend class CConfigDB; +public: + CConfigDBHook() = delete; + // Point the moved-from hook to end, which is checked for in UnregisterHook, + // to avoid a double-erase error. + CConfigDBHook(CConfigDBHook&& h) : configDB(h.configDB) { ptr = std::move(h.ptr); h.ptr = configDB.m_Hooks.end(); } + CConfigDBHook(const CConfigDBHook&) = delete; +private: + CConfigDBHook(CConfigDB& cdb, std::multimap>::iterator p) : configDB(cdb), ptr(p) {}; + + std::multimap>::iterator ptr; + CConfigDB& configDB; +}; + // stores the value of the given key into . this quasi-template // convenience wrapper on top of GetValue simplifies user code Index: ps/trunk/source/ps/ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/ConfigDB.cpp +++ ps/trunk/source/ps/ConfigDB.cpp @@ -488,16 +488,21 @@ return ret; } -CConfigDB::hook_t CConfigDB::RegisterHookAndCall(const CStr& name, std::function hook) +CConfigDBHook CConfigDB::RegisterHookAndCall(const CStr& name, std::function hook) { hook(); - return m_Hooks.emplace(name, hook); + return CConfigDBHook(*this, m_Hooks.emplace(name, hook)); } -void CConfigDB::UnregisterHook(CConfigDB::hook_t&& hook) +void CConfigDB::UnregisterHook(CConfigDBHook&& hook) { if (hook.ptr != m_Hooks.end()) m_Hooks.erase(hook.ptr); } +void CConfigDB::UnregisterHook(std::unique_ptr&& hook) +{ + UnregisterHook(std::move(*hook.get())); +} + #undef CHECK_NS Index: ps/trunk/source/renderer/RenderingOptions.cpp =================================================================== --- ps/trunk/source/renderer/RenderingOptions.cpp +++ ps/trunk/source/renderer/RenderingOptions.cpp @@ -31,8 +31,8 @@ class CRenderingOptions::ConfigHooks { public: - std::vector::iterator begin() { return hooks.begin(); } - std::vector::iterator end() { return hooks.end(); } + std::vector::iterator begin() { return hooks.begin(); } + std::vector::iterator end() { return hooks.end(); } template void Setup(CStr8 name, T& variable) { @@ -44,7 +44,7 @@ } void clear() { hooks.clear(); } private: - std::vector hooks; + std::vector hooks; }; RenderPath RenderPathEnum::FromString(const CStr8& name) @@ -186,7 +186,7 @@ void CRenderingOptions::ClearHooks() { if (CConfigDB::IsInitialised()) - for (CConfigDB::hook_t& hook : *m_ConfigHooks) + for (CConfigDBHook& hook : *m_ConfigHooks) g_ConfigDB.UnregisterHook(std::move(hook)); m_ConfigHooks->clear(); } Index: ps/trunk/source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpVisualActor.cpp +++ ps/trunk/source/simulation2/components/CCmpVisualActor.cpp @@ -385,13 +385,6 @@ return m_Unit->GetModel().GetTransform().GetTranslation(); } - virtual std::wstring GetActorShortName() const - { - if (!m_Unit) - return L""; - return m_Unit->GetObject().m_Base->m_ShortName; - } - virtual std::wstring GetProjectileActor() const { if (!m_Unit) @@ -556,7 +549,7 @@ if (!m_Unit) return; - if (name != m_ActorName) + if (!name.empty() && name != m_ActorName) return; ReloadActor(); Index: ps/trunk/source/simulation2/components/ICmpVisual.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpVisual.h +++ ps/trunk/source/simulation2/components/ICmpVisual.h @@ -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 @@ -55,12 +55,6 @@ virtual CVector3D GetPosition() const = 0; /** - * Return the short name of the actor that's being displayed, or the empty string on error. - * (Not safe for use in simulation code.) - */ - virtual std::wstring GetActorShortName() const = 0; - - /** * Return the filename of the actor to be used for projectiles from this unit, or the empty string if none. * (Not safe for use in simulation code.) */ @@ -166,8 +160,9 @@ * Called when an actor file has been modified and reloaded dynamically. * If this component uses the named actor file, it should regenerate its actor * to pick up the new definitions. + * If name is empty, this reloads all the time. This is used when global quality settings change. */ - virtual void Hotload(const VfsPath& name) = 0; + virtual void Hotload(const VfsPath& name = L"") = 0; DECLARE_INTERFACE_TYPE(Visual) }; Index: ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.h =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.h +++ ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.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 @@ -51,5 +51,9 @@ wxCheckBox* m_Float; wxComboBox* m_Material; + // Some data is not modifiable in the editor + // but should be persisted so for convenience keep a copy of the last loaded file. + AtObj m_Actor; + DECLARE_EVENT_TABLE(); }; Index: ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.cpp +++ ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.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 @@ -156,6 +156,12 @@ // old-style actor format version = -1; } + else if (in["qualitylevels"].defined()) + { + // New-style, multiple-quality-levels actor. + wxLogError(_("Cannot edit actors with multiple quality levels. If you want to use the actor editor, use the `` format and edit the referenced files.")); + return AtObj(); + } else if (in["actor"].defined()) version = (in["actor"]["@version"].defined()) ? (*in["actor"]["@version"]).getLong() : 0; else @@ -314,6 +320,8 @@ AtObj actor (*data["actor"]); m_ActorEditorListCtrl->ImportData(actor); + m_Actor = actor; + m_CastShadows->SetValue(actor["castshadow"].defined()); m_Float->SetValue(actor["float"].defined()); m_Material->SetValue((wxString)actor["material"]); @@ -326,12 +334,21 @@ actor.set("@version", "1"); - if (m_CastShadows->IsChecked()) + + AtObj castShadow = *m_Actor["castshadow"]; + if (m_CastShadows->IsChecked() && castShadow.defined()) + actor.set("castshadow", castShadow); + else if (m_CastShadows->IsChecked()) actor.set("castshadow", ""); - if (m_Float->IsChecked()) + AtObj floatObj = *m_Actor["float"]; + if (m_Float->IsChecked() && floatObj) + actor.set("float", floatObj); + else if (m_Float->IsChecked()) actor.set("float", ""); + AtObj material = *m_Actor["material"]; + actor.set("material", material); if (m_Material->GetValue().length()) actor.set("material", m_Material->GetValue()); Index: ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp +++ ps/trunk/source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp @@ -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 @@ -46,9 +46,12 @@ #undef COLOR + AddColumnType(_("Group"), 50, "@group", new FieldEditCtrl_Boolean()); AddColumnType(_("Variant"), 90, "@name", new FieldEditCtrl_Text()); AddColumnType(_("Base File"), 90, "@file", new FieldEditCtrl_File(_T("art/variants/"), _("Variants (*.xml)|*.xml|All files (*.*)|*.*"))); AddColumnType(_("Ratio"), 50, "@frequency", new FieldEditCtrl_Text()); + AddColumnType(_("Min Quality"),50, "@minquality",new FieldEditCtrl_Text()); + AddColumnType(_("Max Quality"),50, "@maxquality",new FieldEditCtrl_Text()); AddColumnType(_("Model"), 140, "mesh", new FieldEditCtrl_File(_T("art/meshes/"), _("Mesh files (*.pmd, *.dae)|*.pmd;*.dae|All files (*.*)|*.*"))); AddColumnType(_("Particles"), 90, "particles", new FieldEditCtrl_File(_T("art/particles/"), _("Particle file (*.xml)|*.xml|All files (*.*)|*.*"))); AddColumnType(_("Textures"), 250, "textures", new FieldEditCtrl_Dialog(&TexListEditor::Create)); @@ -66,8 +69,9 @@ for (AtIter variant = group["variant"]; variant.defined(); ++variant) AddRow(variant); - AtObj blank; - AddRow(blank); + AtObj gr = *group; + gr.add("@group", "true"); + AddRow(gr); } UpdateDisplay(); @@ -81,10 +85,17 @@ for (size_t i = 0; i < m_ListData.size(); ++i) { - if (IsRowBlank((int)i)) + if (std::string(m_ListData[i]["@group"]) == "true") { if (group.defined()) + { + group.unset("@group"); + if (m_ListData[i]["@minquality"].hasContent()) + group.set("@minquality", m_ListData[i]["@minquality"]); + if (m_ListData[i]["@maxquality"].hasContent()) + group.set("@maxquality", m_ListData[i]["@maxquality"]); out.add("group", group); + } group = AtObj(); } else Index: ps/trunk/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.h =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.h +++ ps/trunk/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.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 @@ -50,6 +50,14 @@ ////////////////////////////////////////////////////////////////////////// +class FieldEditCtrl_Boolean : public FieldEditCtrl +{ +protected: + void StartEdit(wxWindow* parent, wxRect rect, long row, int col); +}; + +////////////////////////////////////////////////////////////////////////// + class FieldEditCtrl_List : public FieldEditCtrl { public: Index: ps/trunk/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp +++ ps/trunk/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp @@ -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 @@ -81,6 +81,21 @@ ////////////////////////////////////////////////////////////////////////// +void FieldEditCtrl_Boolean::StartEdit(wxWindow* parent, wxRect rect, long row, int col) +{ + wxArrayString choices; + + // The famous three-valued boolean. + choices.Add("true"); + choices.Add("false"); + choices.Add(""); + + ListCtrlValidator validator((EditableListCtrl*)parent, row, col); + new QuickComboBox(parent, rect, choices, validator); +} + +////////////////////////////////////////////////////////////////////////// + FieldEditCtrl_List::FieldEditCtrl_List(const char* listType) : m_ListType(listType) {