From 76acc4e146bc192dd9201e208ea61185d9525ee6 Mon Sep 17 00:00:00 2001 From: wraitii Date: Thu, 8 Apr 2021 07:22:24 +0000 Subject: [PATCH] Implement quality levels for actors & corresponding setting. An actor file, as referenced by the VisualActor, can now define different actors for different "quality level" setting. In this initial version, the quality is handled directly by the object manager. Actor format impact: - '' may be used as the root node, containing actor nodes as children. - such actor nodes can refer to a file, or to an inline actor, or simply be inlined. - such actor nodes may have a 'quality' attribute, specifying the maximum quality level of this actor. By default, 255 (the maximum) is implied. - The actor format remains valid, but 'groups', 'variants', 'material', 'castshadow' and 'float' can be given a [minquality, maxquality[ range via XML attributes. Outside of this range, the XML node is ignored (making it possible to define, in a single actor file, several quality levels). Quality is a 0-255 value, with: - Range 0-99 intended for lower level-of-detail actors (billboards, etc.) - Range 100-200 the 'normal' range for models. 100 is "low", 150 "medium", and 200 "high". - Range 201-255 used for higher quality actors that might be used for e.g. cinematics. The range is wide to make it easier to add intermediate levels in the future and it seemed easier given that an integer value of some kind was required anyways. Engine impacts: - A new CActorDef class is introduced, wrapping an art/actors XML file and its different quality levels. ObjectBase remains the definition of a given 'actor', now at a given quality level. - CActorDef imposes a maximal # of quality level for a particular actor definition (5 currently). - CUnit is made to refer to an Actor Definition explicitly, not a particular ObjectBase. - As a minor optimisation, variation keys are calculated on pointer-to-sets-of-selections, instead of raw sets-of-selections, as this reduces copying. - some refactoring, including better const-correctness and hotloading support via std::shared_ptr. Differential Revision: https://code.wildfiregames.com/D3787 This was SVN commit r25210. --- binaries/data/config/default.cfg | 3 + .../data/mods/public/art/actors/actor.rnc | 71 --- .../data/mods/public/art/actors/actor.rng | 443 +++++++++------ .../data/mods/public/gui/options/options.json | 11 + source/graphics/Model.cpp | 6 +- source/graphics/ObjectBase.cpp | 531 +++++++++++++----- source/graphics/ObjectBase.h | 157 ++++-- source/graphics/ObjectEntry.cpp | 21 +- source/graphics/ObjectEntry.h | 8 +- source/graphics/ObjectManager.cpp | 175 +++--- source/graphics/ObjectManager.h | 53 +- source/graphics/Unit.cpp | 66 +-- source/graphics/Unit.h | 18 +- source/ps/ConfigDB.cpp | 11 +- source/ps/ConfigDB.h | 43 +- source/renderer/RenderingOptions.cpp | 8 +- .../components/CCmpVisualActor.cpp | 9 +- source/simulation2/components/ICmpVisual.h | 11 +- .../atlas/AtlasUI/ActorEditor/ActorEditor.cpp | 23 +- .../atlas/AtlasUI/ActorEditor/ActorEditor.h | 6 +- .../ActorEditor/ActorEditorListCtrl.cpp | 19 +- .../EditableListCtrl/FieldEditCtrl.cpp | 17 +- .../EditableListCtrl/FieldEditCtrl.h | 10 +- 23 files changed, 1063 insertions(+), 657 deletions(-) delete mode 100644 binaries/data/mods/public/art/actors/actor.rnc diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index 11a09cc61e..0f3a3a4ab2 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -128,6 +128,9 @@ antialiasing = "disabled" sharpening = "disabled" sharpness = 0.3 +; Quality used for actors. +max_actor_quality=200 + ; Quality level of shader effects (set to 10 to display all effects) materialmgr.quality = 2.0 diff --git a/binaries/data/mods/public/art/actors/actor.rnc b/binaries/data/mods/public/art/actors/actor.rnc deleted file mode 100644 index 68e96fd8e2..0000000000 --- a/binaries/data/mods/public/art/actors/actor.rnc +++ /dev/null @@ -1,71 +0,0 @@ -namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0" -## -# NOTE: To modify this Relax NG grammar, edit the Relax NG Compact (.rnc) file -# and use a converter tool like trang to generate the Relax NG XML (.rng) file -## - -element actor { - attribute version { xsd:positiveInteger }, ( - element group { - element variant { - attribute name { text }? & - attribute file { text }? & - attribute frequency { xsd:nonNegativeInteger }? & - element mesh { - text - }? & - element textures { - element texture { - attribute file { text }? & - attribute name { text } - }* - }? & - element decal { - attribute width { xsd:float } & # X - attribute depth { xsd:float } & # Z - attribute angle { xsd:float } & - attribute offsetx { xsd:float } & - attribute offsetz { xsd:float } - }? & - element particles { - attribute file { text } - }? & - element color { list { - xsd:nonNegativeInteger, # R - xsd:nonNegativeInteger, # G - xsd:nonNegativeInteger # B - } }? & - element animations { - element animation { - attribute name { text } & - attribute id { text }? & - attribute frequency { xsd:nonNegativeInteger }? & - attribute file { text }? & - attribute speed { xsd:nonNegativeInteger } & - attribute event { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }? & - attribute load { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }? & - attribute sound { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }? - }* - }? & - element props { - element prop { - (attribute actor { text }? & - attribute attachpoint { text } & - attribute minheight { xsd:float }? & - attribute maxheight { xsd:float }? & - attribute selectable { "true" | "false" }?) - }* - }? - }* - }* & - element castshadow { # flag; true if present - empty - }? & - element float { # flag; true if present - empty - }? & - element material { - text - }? - ) -} diff --git a/binaries/data/mods/public/art/actors/actor.rng b/binaries/data/mods/public/art/actors/actor.rng index ffdc2cfad4..c4216e83ac 100644 --- a/binaries/data/mods/public/art/actors/actor.rng +++ b/binaries/data/mods/public/art/actors/actor.rng @@ -1,194 +1,263 @@ - - - - - - - - - - - - - - - - - + + + + + 255 + 0 + + + low + medium + high + + + + + + + Minimum quality - this is inclusive. + + + + + + Maximum quality - this is exclusive. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - 0 - 1 - - - - - - - 0 - 1 - - - - - - - 0 - 1 - - - - - - - - + + + + + + + + + + + 0 + 1 + + + + + + + 0 + 1 + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - true - false - - - - - - - + + The quality level to use for this actor. This is the maximum value at which this version of the actor will be used. If not specified, the maximum possible value is assumed. + + - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + diff --git a/binaries/data/mods/public/gui/options/options.json b/binaries/data/mods/public/gui/options/options.json index ce7c494d46..7c582b2cbb 100644 --- a/binaries/data/mods/public/gui/options/options.json +++ b/binaries/data/mods/public/gui/options/options.json @@ -152,6 +152,17 @@ "min": 0, "max": 1 }, + { + "type": "dropdown", + "label": "Model quality", + "tooltip": "Model quality setting.", + "config": "max_actor_quality", + "list": [ + { "value": 100, "label": "Low", "tooltip": "Simpler models for better performance." }, + { "value": 150, "label": "Medium", "tooltip": "Average quality and average performance." }, + { "value": 200, "label": "High", "tooltip": "High quality models." } + ] + }, { "type": "slider", "label": "Shader effects", diff --git a/source/graphics/Model.cpp b/source/graphics/Model.cpp index 1a1f483bf1..98c32e6d6c 100644 --- a/source/graphics/Model.cpp +++ b/source/graphics/Model.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -499,10 +499,6 @@ void CModel::CopyAnimationFrom(CModel* source) m_Anim = source->m_Anim; m_AnimTime = source->m_AnimTime; - m_Flags &= ~MODELFLAG_CASTSHADOWS; - if (source->m_Flags & MODELFLAG_CASTSHADOWS) - m_Flags |= MODELFLAG_CASTSHADOWS; - m_ObjectBounds.SetEmpty(); InvalidateBounds(); } diff --git a/source/graphics/ObjectBase.cpp b/source/graphics/ObjectBase.cpp index 31a1b6dadd..d7c3da50bd 100644 --- a/source/graphics/ObjectBase.cpp +++ b/source/graphics/ObjectBase.cpp @@ -31,11 +31,113 @@ #include -CObjectBase::CObjectBase(CObjectManager& objectManager) -: m_ObjectManager(objectManager) +namespace { + int GetQuality(CStr& value) + { + if (value == "low") + return 100; + else if (value == "medium") + return 150; + else if (value == "high") + return 200; + else + return value.ToInt(); + } +} + +CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8 qualityLevel) +: m_ObjectManager(objectManager), m_ActorDef(actorDef) { + m_QualityLevel = qualityLevel; m_Properties.m_CastShadows = false; m_Properties.m_FloatOnWater = false; + + // Remove leading art/actors/ & include quality level. + m_Identifier = m_ActorDef.m_Pathname.string8().substr(11) + CStr::FromInt(m_QualityLevel); +} + +std::unique_ptr CObjectBase::CopyWithQuality(u8 newQualityLevel) const +{ + std::unique_ptr ret = std::make_unique(m_ObjectManager, m_ActorDef, newQualityLevel); + // No need to actually change any quality-related stuff here, we assume that this is a copy for props. + ret->m_VariantGroups = m_VariantGroups; + ret->m_Material = m_Material; + ret->m_Properties = m_Properties; + return ret; +} + +void CObjectBase::Load(const CXeromyces& XeroFile, const XMBElement& root) +{ + // Define all the elements used in the XML file +#define EL(x) int el_##x = XeroFile.GetElementID(#x) +#define AT(x) int at_##x = XeroFile.GetAttributeID(#x) + EL(castshadow); + EL(float); + EL(group); + EL(material); + AT(maxquality); + AT(minquality); +#undef AT +#undef EL + + + // Set up the group vector to avoid reallocation and copying later. + { + int groups = 0; + XERO_ITER_EL(root, child) + { + if (child.GetNodeName() == el_group) + ++groups; + } + + m_VariantGroups.reserve(groups); + } + + // (This XML-reading code is rather worryingly verbose...) + + auto shouldSkip = [&](XMBElement& node) { + XERO_ITER_ATTR(node, attr) + { + if (attr.Name == at_minquality && GetQuality(attr.Value) > m_QualityLevel) + return true; + else if (attr.Name == at_maxquality && GetQuality(attr.Value) <= m_QualityLevel) + return true; + } + return false; + }; + + XERO_ITER_EL(root, child) + { + int child_name = child.GetNodeName(); + + if (shouldSkip(child)) + continue; + + if (child_name == el_group) + { + std::vector& currentGroup = m_VariantGroups.emplace_back(); + currentGroup.reserve(child.GetChildNodes().size()); + XERO_ITER_EL(child, variant) + { + if (shouldSkip(variant)) + continue; + + LoadVariant(XeroFile, variant, currentGroup.emplace_back()); + } + + if (currentGroup.size() == 0) + LOGERROR("Actor group has zero variants ('%s')", m_Identifier); + } + else if (child_name == el_castshadow) + m_Properties.m_CastShadows = true; + else if (child_name == el_float) + m_Properties.m_FloatOnWater = true; + else if (child_name == el_material) + m_Material = VfsPath("art/materials") / child.GetText().FromUTF8(); + } + + if (m_Material.empty()) + m_Material = VfsPath("art/materials/default.xml"); } void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant) @@ -87,7 +189,7 @@ void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& vari { // Open up an external file to load. // Don't crash hard when failures happen, but log them and continue - m_UsedFiles.insert(attr.Value); + m_ActorDef.m_UsedFiles.insert(attr.Value); CXeromyces XeroVariant; if (XeroVariant.Load(g_VFS, "art/variants/" + attr.Value) == PSRETURN_OK) { @@ -152,7 +254,7 @@ void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& vari // For particle hotloading, it's easiest to reload the entire actor, // so remember the relevant particle file as a dependency for this actor - m_UsedFiles.insert(file); + m_ActorDef.m_UsedFiles.insert(file); } else if (option_name == el_color) { @@ -213,105 +315,7 @@ void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& vari } } -bool CObjectBase::Load(const VfsPath& pathname) -{ - m_UsedFiles.clear(); - m_UsedFiles.insert(pathname); - - CXeromyces XeroFile; - if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK) - return false; - - // Define all the elements used in the XML file - #define EL(x) int el_##x = XeroFile.GetElementID(#x) - #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) - EL(actor); - EL(castshadow); - EL(float); - EL(group); - EL(material); - #undef AT - #undef EL - - XMBElement root = XeroFile.GetRoot(); - - if (root.GetNodeName() != el_actor) - { - LOGERROR("Invalid actor format (unrecognised root element '%s')", XeroFile.GetElementString(root.GetNodeName()).c_str()); - return false; - } - - m_VariantGroups.clear(); - - m_Pathname = pathname; - m_ShortName = pathname.Basename().string(); - - - // Set up the vector> m_Variants to contain the right number - // of elements, to avoid wasteful copying/reallocation later. - { - // Count the variants in each group - std::vector variantGroupSizes; - XERO_ITER_EL(root, child) - { - if (child.GetNodeName() == el_group) - variantGroupSizes.push_back(child.GetChildNodes().size()); - } - - m_VariantGroups.resize(variantGroupSizes.size()); - // Set each vector to match the number of variants - for (size_t i = 0; i < variantGroupSizes.size(); ++i) - m_VariantGroups[i].resize(variantGroupSizes[i]); - } - - - // (This XML-reading code is rather worryingly verbose...) - - std::vector >::iterator currentGroup = m_VariantGroups.begin(); - - XERO_ITER_EL(root, child) - { - int child_name = child.GetNodeName(); - - if (child_name == el_group) - { - std::vector::iterator currentVariant = currentGroup->begin(); - XERO_ITER_EL(child, variant) - { - LoadVariant(XeroFile, variant, *currentVariant); - ++currentVariant; - } - - if (currentGroup->size() == 0) - LOGERROR("Actor group has zero variants ('%s')", pathname.string8()); - - ++currentGroup; - } - else if (child_name == el_castshadow) - m_Properties.m_CastShadows = true; - else if (child_name == el_float) - m_Properties.m_FloatOnWater = true; - else if (child_name == el_material) - m_Material = VfsPath("art/materials") / child.GetText().FromUTF8(); - } - - if (m_Material.empty()) - m_Material = VfsPath("art/materials/default.xml"); - - return true; -} - -bool CObjectBase::Reload() -{ - return Load(m_Pathname); -} - -bool CObjectBase::UsesFile(const VfsPath& pathname) -{ - return m_UsedFiles.find(pathname) != m_UsedFiles.end(); -} - -std::vector CObjectBase::CalculateVariationKey(const std::vector >& selections) +std::vector CObjectBase::CalculateVariationKey(const std::vector*>& selections) const { // (TODO: see CObjectManager::FindObjectVariation for an opportunity to // call this function a bit less frequently) @@ -328,7 +332,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector chosenProps; - for (std::vector >::iterator grp = m_VariantGroups.begin(); + for (std::vector >::const_iterator grp = m_VariantGroups.begin(); grp != m_VariantGroups.end(); ++grp) { @@ -349,7 +353,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector >::const_iterator selset = selections.begin(); selset < selections.end(); ++selset) + for (const std::set* selset : selections) { ENSURE(grp->size() < 256); // else they won't fit in 'choices' @@ -376,7 +380,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector CObjectBase::CalculateVariationKey(const std::vector::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it) { - CObjectBase* prop = m_ObjectManager.FindObjectBase(it->second); + CActorDef* prop = m_ObjectManager.FindActorDef(it->second); if (prop) { - std::vector propChoices = prop->CalculateVariationKey(selections); + std::vector propChoices = prop->GetBase(m_QualityLevel)->CalculateVariationKey(selections); choices.insert(choices.end(), propChoices.begin(), propChoices.end()); } } @@ -400,7 +404,7 @@ std::vector CObjectBase::CalculateVariationKey(const std::vector& variationKey) +const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& variationKey) const { Variation variation; @@ -408,7 +412,7 @@ const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& // chosen variant from each group. (Except variationKey has some bits stuck // on the end for props, but we don't care about those in here.) - std::vector >::iterator grp = m_VariantGroups.begin(); + std::vector >::const_iterator grp = m_VariantGroups.begin(); std::vector::const_iterator match = variationKey.begin(); for ( ; grp != m_VariantGroups.end() && match != variationKey.end(); @@ -428,7 +432,7 @@ const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& } // Get the matched variant - CObjectBase::Variant& var ((*grp)[id]); + const CObjectBase::Variant& var ((*grp)[id]); // Apply its data: @@ -449,50 +453,44 @@ const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& // original should be erased, and replaced by the two new ones. // // So, erase all existing props which are overridden by this variant: - for (std::vector::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it) + for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it) variation.props.erase(it->m_PropPointName); // and then insert the new ones: - for (std::vector::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it) + for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it) if (! it->m_ModelName.empty()) // if the name is empty then the overridden prop is just deleted variation.props.insert(make_pair(it->m_PropPointName, *it)); // Same idea applies for animations. // So, erase all existing animations which are overridden by this variant: - for (std::vector::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it) + for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it) variation.anims.erase(it->m_AnimName); // and then insert the new ones: - for (std::vector::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it) + for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it) variation.anims.insert(make_pair(it->m_AnimName, *it)); // Same for samplers, though perhaps not strictly necessary: - for (std::vector::iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it) + for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it) variation.samplers.erase(it->m_SamplerName.string()); - for (std::vector::iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it) + for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it) variation.samplers.insert(make_pair(it->m_SamplerName.string(), *it)); } return variation; } -std::set CObjectBase::CalculateRandomVariation(uint32_t seed, const std::set& initialSelections) +std::set CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector>& initialSelections) const { rng_t rng; rng.seed(seed); - std::set remainingSelections = CalculateRandomRemainingSelections(rng, std::vector >(1, initialSelections)); - remainingSelections.insert(initialSelections.begin(), initialSelections.end()); + std::set remainingSelections = CalculateRandomRemainingSelections(rng, initialSelections); + for (const std::set& sel : initialSelections) + remainingSelections.insert(sel.begin(), sel.end()); return remainingSelections; // now actually a complete set of selections } -std::set CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector >& initialSelections) -{ - rng_t rng; - rng.seed(seed); - return CalculateRandomRemainingSelections(rng, initialSelections); -} - -std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector >& initialSelections) +std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector>& initialSelections) const { std::set remainingSelections; std::multimap chosenProps; @@ -507,7 +505,7 @@ std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const // When choosing randomly, make use of each variant's frequency. If all // variants have frequency 0, treat them as if they were 1. - for (std::vector >::iterator grp = m_VariantGroups.begin(); + for (std::vector >::const_iterator grp = m_VariantGroups.begin(); grp != m_VariantGroups.end(); ++grp) { @@ -587,7 +585,7 @@ std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const // Remember which props were chosen, so we can call CalculateRandomVariation on them // at the end. - Variant& var ((*grp)[match]); + const Variant& var ((*grp)[match]); // Erase all existing props which are overridden by this variant: for (const Prop& prop : var.m_Props) chosenProps.erase(prop.m_PropPointName); @@ -600,19 +598,19 @@ std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const // Load each prop, and add their required selections to ours: for (std::multimap::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it) { - CObjectBase* prop = m_ObjectManager.FindObjectBase(it->second); + CActorDef* prop = m_ObjectManager.FindActorDef(it->second); if (prop) { std::vector > propInitialSelections = initialSelections; if (!remainingSelections.empty()) propInitialSelections.push_back(remainingSelections); - std::set propRemainingSelections = prop->CalculateRandomRemainingSelections(rng, propInitialSelections); + std::set propRemainingSelections = prop->GetBase(m_QualityLevel)->CalculateRandomRemainingSelections(rng, propInitialSelections); remainingSelections.insert(propRemainingSelections.begin(), propRemainingSelections.end()); // Add the prop's used files to our own (recursively) so we can hotload // when any prop is changed - m_UsedFiles.insert(prop->m_UsedFiles.begin(), prop->m_UsedFiles.end()); + m_ActorDef.m_UsedFiles.insert(prop->m_UsedFiles.begin(), prop->m_UsedFiles.end()); } } @@ -677,9 +675,9 @@ std::vector > CObjectBase::GetVariantGroups() const { if (! props[k].m_ModelName.empty()) { - CObjectBase* prop = m_ObjectManager.FindObjectBase(props[k].m_ModelName.c_str()); + CActorDef* prop = m_ObjectManager.FindActorDef(props[k].m_ModelName.c_str()); if (prop) - objectsQueue.push(prop); + objectsQueue.push(prop->GetBase(m_QualityLevel).get()); } } } @@ -688,3 +686,266 @@ std::vector > CObjectBase::GetVariantGroups() const return groups; } + +void CObjectBase::GetQualitySplits(std::vector& splits) const +{ + std::vector::iterator it = std::find_if(splits.begin(), splits.end(), [this](u8 qualityLevel) { return qualityLevel >= m_QualityLevel; }); + if (it == splits.end() || *it != m_QualityLevel) + splits.emplace(it, m_QualityLevel); + + for (const std::vector& group : m_VariantGroups) + for (const Variant& variant : group) + for (const Prop& prop : variant.m_Props) + { + // TODO: we probably should clean those up after XML load. + if (prop.m_ModelName.empty()) + continue; + + CActorDef* propActor = m_ObjectManager.FindActorDef(prop.m_ModelName.c_str()); + if (!propActor) + continue; + + std::vector newSplits = propActor->QualityLevels(); + if (newSplits.size() <= 1) + continue; + + // This is not entirely optimal since we might loop though redundant quality levels, but that shouldn't matter. + // Custom implementation because this is inplace, std::set_union needs a 3rd vector. + std::vector::iterator v1 = splits.begin(); + std::vector::iterator v2 = newSplits.begin(); + while (v2 != newSplits.end()) + { + if (v1 == splits.end() || *v1 > *v2) + { + v1 = ++splits.insert(v1, *v2); + ++v2; + } + else if (*v1 == *v2) + { + ++v1; + ++v2; + } + else + ++v1; + } + } +} + +const CStr& CObjectBase::GetIdentifier() const +{ + return m_Identifier; +} + +bool CObjectBase::UsesFile(const VfsPath& pathname) const +{ + return m_ActorDef.UsesFile(pathname); +} + + +CActorDef::CActorDef(CObjectManager& objectManager) : m_ObjectManager(objectManager) +{ +} + +std::vector CActorDef::QualityLevels() const +{ + std::vector splits; + splits.reserve(m_ObjectBases.size()); + for (const std::shared_ptr& base : m_ObjectBases) + splits.emplace_back(base->m_QualityLevel); + return splits; +} + +const std::shared_ptr& CActorDef::GetBase(u8 QualityLevel) const +{ + for (const std::shared_ptr& base : m_ObjectBases) + if (base->m_QualityLevel >= QualityLevel) + return base; + // This code path ought to be impossible to take, + // because by construction we must have at least one valid CObjectBase of quality 255 + // (which necessarily fits the u8 comparison above). + // However compilers will warn that we return a reference to a local temporary if I return nullptr, + // so just return something sane instead. + ENSURE(false); + return m_ObjectBases.back(); +} + +bool CActorDef::Load(const VfsPath& pathname) +{ + m_UsedFiles.clear(); + m_UsedFiles.insert(pathname); + + m_ObjectBases.clear(); + + CXeromyces XeroFile; + if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK) + return false; + + // Define all the elements used in the XML file +#define EL(x) int el_##x = XeroFile.GetElementID(#x) +#define AT(x) int at_##x = XeroFile.GetAttributeID(#x) + EL(actor); + EL(inline); + EL(qualitylevels); + AT(file); + AT(inline); + AT(quality); + AT(version); +#undef AT +#undef EL + + XMBElement root = XeroFile.GetRoot(); + + if (root.GetNodeName() != el_actor && root.GetNodeName() != el_qualitylevels) + { + LOGERROR("Invalid actor format (actor '%s', unrecognised root element '%s')", + pathname.string8().c_str(), XeroFile.GetElementString(root.GetNodeName()).c_str()); + return false; + } + + m_Pathname = pathname; + + if (root.GetNodeName() == el_actor) + { + std::unique_ptr base = std::make_unique(m_ObjectManager, *this, 255); + base->Load(XeroFile, root); + m_ObjectBases.emplace_back(std::move(base)); + } + else + { + XERO_ITER_ATTR(root, attr) + { + if (attr.Name == at_version && attr.Value.ToInt() != 1) + { + LOGERROR("Invalid actor format (actor '%s', version %i is not supported)", + pathname.string8().c_str(), attr.Value.ToInt()); + return false; + } + } + u8 quality = 0; + XMBElement inlineActor; + XERO_ITER_EL(root, child) + { + if (child.GetNodeName() == el_inline) + inlineActor = child; + } + XERO_ITER_EL(root, actor) + { + if (actor.GetNodeName() != el_actor) + continue; + bool found_quality = false; + bool use_inline = false; + CStr file; + XERO_ITER_ATTR(actor, attr) + { + if (attr.Name == at_quality) + { + int v = GetQuality(attr.Value); + if (v > 255) + { + LOGERROR("Qualitylevel to attribute must not be above 255 (file %s)", pathname.string8()); + return false; + } + if (v <= quality) + { + LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8()); + return false; + } + quality = v; + found_quality = true; + } + else if (attr.Name == at_file) + { + if (attr.Value.empty()) + LOGWARNING("Empty actor file specified (file %s)", pathname.string8()); + file = attr.Value; + } + else if (attr.Name == at_inline) + use_inline = true; + } + if (!found_quality) + quality = 255; + std::unique_ptr base = std::make_unique(m_ObjectManager, *this, quality); + if (use_inline) + { + if (inlineActor.GetNodeName() == -1) + { + LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname.string8()); + return false; + } + base->Load(XeroFile, inlineActor); + } + else if (file.empty()) + base->Load(XeroFile, actor); + else + { + if (actor.GetChildNodes().size() > 0) + LOGWARNING("Actor definition refers to file but has children elements, they will be ignored (file %s)", pathname.string8()); + + // Open up an external file to load. + // Don't crash hard when failures happen, but log them and continue + CXeromyces XeroActor; + if (XeroActor.Load(g_VFS, "art/actors/" + file, "actor") == PSRETURN_OK) + { + const XMBElement& root = XeroActor.GetRoot(); + if (root.GetNodeName() != el_actor) + { + LOGERROR("Included actors cannot define quality levels (opening %s from file %s)", file, pathname.string8()); + return false; + } + base->Load(XeroActor, root); + } + else + { + LOGERROR("Could not open actor file at path %s (file %s)", file, pathname.string8()); + return false; + } + m_UsedFiles.insert(file); + } + m_ObjectBases.emplace_back(std::move(base)); + } + if (quality != 255) + { + LOGERROR("Quality levels must go up to 255 (file %s)", pathname.string8().c_str()); + return false; + } + } + + // For each quality level, check if we need to further split (because of props). + std::vector splits = QualityLevels(); + for (const std::shared_ptr& base : m_ObjectBases) + base->GetQualitySplits(splits); + ENSURE(splits.size() >= 1); + if (splits.size() > 5) + { + LOGERROR("Too many quality levels (%i) for actor %s", splits.size(), pathname.string8().c_str()); + return false; + } + + std::vector>::iterator it = m_ObjectBases.begin(); + std::vector::const_iterator qualityLevels = splits.begin(); + while (it != m_ObjectBases.end()) + if ((*it)->m_QualityLevel > *qualityLevels) + { + it = ++m_ObjectBases.emplace(it, (*it)->CopyWithQuality(*qualityLevels)); + ++qualityLevels; + } + else if ((*it)->m_QualityLevel == *qualityLevels) + { + ++it; + ++qualityLevels; + } + else + ++it; + + return true; +} + +bool CActorDef::Reload() +{ + return Load(m_Pathname); +} + +bool CActorDef::UsesFile(const VfsPath& pathname) const +{ + return m_UsedFiles.find(pathname) != m_UsedFiles.end(); +} diff --git a/source/graphics/ObjectBase.h b/source/graphics/ObjectBase.h index b8f64f7534..65190d953a 100644 --- a/source/graphics/ObjectBase.h +++ b/source/graphics/ObjectBase.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -22,7 +22,9 @@ #include "ps/CStr.h" #include "ps/CStrIntern.h" +class CActorDef; class CModel; +class CObjectEntry; class CObjectManager; class CSkeletonAnim; class CXeromyces; @@ -30,12 +32,24 @@ class XMBElement; #include #include +#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,33 +132,110 @@ public: 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; + // 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) const; + + + struct { + // cast shadows from this object + bool m_CastShadows; + // float on top of water + bool m_FloatOnWater; + } m_Properties; + + // 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; + + /** + * 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; +}; + +/** + * 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. @@ -158,41 +249,21 @@ public: bool Reload(); /** - * Returns whether this object (including any possible props) + * Returns whether this actor (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 - bool m_CastShadows; - // float on top of water - bool m_FloatOnWater; - } m_Properties; - - // the material file - VfsPath m_Material; - private: - // 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); - - std::vector< std::vector > m_VariantGroups; CObjectManager& m_ObjectManager; - std::unordered_set m_UsedFiles; + // std::shared_ptr to avoid issues during hotloading. + std::vector> m_ObjectBases; - void LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant); + std::unordered_set m_UsedFiles; }; #endif diff --git a/source/graphics/ObjectEntry.cpp b/source/graphics/ObjectEntry.cpp index c48053a21d..c241e08299 100644 --- a/source/graphics/ObjectEntry.cpp +++ b/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 @@ CObjectEntry::~CObjectEntry() } -bool CObjectEntry::BuildVariation(const std::vector >& selections, +bool CObjectEntry::BuildVariation(const std::vector*>& completeSelections, const std::vector& variationKey, CObjectManager& objectManager) { @@ -72,7 +72,7 @@ bool CObjectEntry::BuildVariation(const std::vector >& selections 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 @@ bool CObjectEntry::BuildVariation(const std::vector >& selections 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 @@ bool CObjectEntry::BuildVariation(const std::vector >& selections { 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 @@ bool CObjectEntry::BuildVariation(const std::vector >& selections 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 @@ bool CObjectEntry::BuildVariation(const std::vector >& selections 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. diff --git a/source/graphics/ObjectEntry.h b/source/graphics/ObjectEntry.h index 897ca9e297..5c00b07426 100644 --- a/source/graphics/ObjectEntry.h +++ b/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 @@ class CObjectEntry 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; diff --git a/source/graphics/ObjectManager.cpp b/source/graphics/ObjectManager.cpp index 43e3c9b8f3..3df972505b 100644 --- a/source/graphics/ObjectManager.cpp +++ b/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 @@ CObjectManager::CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager& { 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 @@ CObjectManager::~CObjectManager() { 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()); + ENSURE(!actorName.empty()); - // See if the base type has been loaded yet: + decltype(m_ActorDefs)::iterator it = m_ActorDefs.find(actorName); + if (it != m_ActorDefs.end()) + return it->second.get(); - std::map::iterator it = m_ObjectBases.find(objectname); - if (it != m_ObjectBases.end()) - return it->second; + std::unique_ptr actor = std::make_unique(*this); - // Not already loaded, so try to load it: + VfsPath pathname = VfsPath("art/actors/") / actorName; - CObjectBase* obj = new CObjectBase(*this); + if (actor->Load(pathname)) + return m_ActorDefs.emplace(actorName, std::move(actor)).first->second.get(); - VfsPath pathname = VfsPath("art/actors/") / objectname; + LOGERROR("CObjectManager::FindActorDef(): Cannot find actor '%s'", utf8_from_wstring(actorName)); - if (obj->Load(pathname)) - { - m_ObjectBases[objectname] = obj; - return obj; - } - else - delete obj; - - LOGERROR("CObjectManager::FindObjectBase(): Cannot find object '%s'", utf8_from_wstring(objectname)); - - 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); + if (!actor) + return nullptr; + + const std::shared_ptr& base = actor->GetBase(m_QualityLevel); + + 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(const CStrW& objname, const std::vector >& selections) +CObjectEntry* CObjectManager::FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections) { - CObjectBase* base = FindObjectBase(objname); - - if (! base) - return NULL; - - return FindObjectVariation(base, selections); -} - -CObjectEntry* CObjectManager::FindObjectVariation(CObjectBase* base, const std::vector >& selections) -{ - 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 @@ CTerrain* CObjectManager::GetTerrain() 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(); + 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 - 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); - } + // 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); +} diff --git a/source/graphics/ObjectManager.h b/source/graphics/ObjectManager.h index 80432d59a8..aa1aed514b 100644 --- a/source/graphics/ObjectManager.h +++ b/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 @@ public: // 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 @@ public: 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 @@ public: */ 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 diff --git a/source/graphics/Unit.cpp b/source/graphics/Unit.cpp index 9f66973ad8..c9e35b4f70 100644 --- a/source/graphics/Unit.cpp +++ b/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) +#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) { - if (m_Model->ToCModel()) - m_Animation = new CUnitAnimation(m_ID, m_Model->ToCModel(), m_Object); - else - m_Animation = NULL; } CUnit::~CUnit() @@ -46,22 +41,19 @@ CUnit::~CUnit() CUnit* CUnit::Create(const CStrW& actorName, uint32_t seed, const std::set& selections, CObjectManager& objectManager) { - CObjectBase* base = objectManager.FindObjectBase(actorName); + CActorDef* actor = objectManager.FindActorDef(actorName); - if (! base) - return NULL; + if (!actor) + return nullptr; - std::set actorSelections = base->CalculateRandomVariation(seed, selections); - - std::vector > selectionsVec; - selectionsVec.push_back(actorSelections); - - CObjectEntry* obj = objectManager.FindObjectVariation(base, selectionsVec); - - 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 @@ void CUnit::ReloadObject() 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 @@ void CUnit::ReloadObject() // expects the selectors passed to it to be complete. // see http://trac.wildfiregames.com/ticket/979 - // 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); - // 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) + // Use the entity ID as randomization seed (same as when the unit was first created) + 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 (!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(); diff --git a/source/graphics/Unit.h b/source/graphics/Unit.h index 1170d218d3..2b550c0bcd 100644 --- a/source/graphics/Unit.h +++ b/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 @@ class CUnit 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 @@ public: 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. diff --git a/source/ps/ConfigDB.cpp b/source/ps/ConfigDB.cpp index 8d900b735f..b3151fc152 100644 --- a/source/ps/ConfigDB.cpp +++ b/source/ps/ConfigDB.cpp @@ -488,16 +488,21 @@ bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CS 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 diff --git a/source/ps/ConfigDB.h b/source/ps/ConfigDB.h index 244cf529cd..64faa7e8e1 100644 --- a/source/ps/ConfigDB.h +++ b/source/ps/ConfigDB.h @@ -54,10 +54,16 @@ enum EConfigNamespace 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 @@ public: 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 @@ private: 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 diff --git a/source/renderer/RenderingOptions.cpp b/source/renderer/RenderingOptions.cpp index 0dbebbcccb..b5538bc5f7 100755 --- a/source/renderer/RenderingOptions.cpp +++ b/source/renderer/RenderingOptions.cpp @@ -31,8 +31,8 @@ CRenderingOptions g_RenderingOptions; 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 @@ public: } void clear() { hooks.clear(); } private: - std::vector hooks; + std::vector hooks; }; RenderPath RenderPathEnum::FromString(const CStr8& name) @@ -186,7 +186,7 @@ void CRenderingOptions::ReadConfigAndSetupHooks() 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(); } diff --git a/source/simulation2/components/CCmpVisualActor.cpp b/source/simulation2/components/CCmpVisualActor.cpp index c0ab948eee..fcd15e6ad0 100644 --- a/source/simulation2/components/CCmpVisualActor.cpp +++ b/source/simulation2/components/CCmpVisualActor.cpp @@ -385,13 +385,6 @@ public: 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 @@ public: if (!m_Unit) return; - if (name != m_ActorName) + if (!name.empty() && name != m_ActorName) return; ReloadActor(); diff --git a/source/simulation2/components/ICmpVisual.h b/source/simulation2/components/ICmpVisual.h index 78ec9cf80a..17db3cd582 100644 --- a/source/simulation2/components/ICmpVisual.h +++ b/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 @@ -54,12 +54,6 @@ public: */ 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 @@ public: * 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) }; diff --git a/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.cpp b/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.cpp index 63e6aca8a1..7d2a2821cd 100644 --- a/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.cpp +++ b/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 @@ static AtObj ConvertToLatestFormat(AtObj in) // 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 @@ void ActorEditor::ImportData(AtObj& in) 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 @@ AtObj ActorEditor::ExportData() 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()); diff --git a/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.h b/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.h index 897692d96b..79cc7bb9b9 100644 --- a/source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.h +++ b/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 @@ private: 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(); }; diff --git a/source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp b/source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp index 358f645516..0e376170fa 100644 --- a/source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp +++ b/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 @@ ActorEditorListCtrl::ActorEditorListCtrl(wxWindow* parent) #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 @@ void ActorEditorListCtrl::DoImport(AtObj& in) 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 @@ AtObj ActorEditorListCtrl::DoExport() 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 diff --git a/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp b/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp index e5c2a15454..6e14e23e01 100644 --- a/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp +++ b/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_Color::StartEdit(wxWindow* parent, wxRect WXUNUSED(rect), lon ////////////////////////////////////////////////////////////////////////// +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) { diff --git a/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.h b/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.h index 3220cbabf0..72406f149e 100644 --- a/source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.h +++ b/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 @@ protected: ////////////////////////////////////////////////////////////////////////// +class FieldEditCtrl_Boolean : public FieldEditCtrl +{ +protected: + void StartEdit(wxWindow* parent, wxRect rect, long row, int col); +}; + +////////////////////////////////////////////////////////////////////////// + class FieldEditCtrl_List : public FieldEditCtrl { public: