Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -47,6 +47,9 @@ xres = 0 yres = 0 +; Quality used for actors. +max_actor_quality=200 + ; Force a non-standard bit depth (if 0 then use the current desktop bit depth) bpp = 0 Index: binaries/data/mods/public/art/actors/actor.rng =================================================================== --- binaries/data/mods/public/art/actors/actor.rng +++ binaries/data/mods/public/art/actors/actor.rng @@ -1,194 +1,266 @@ - + - - - - - - - - - - - - - + + + + 255 + 0 + + + low + medium + high + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - 0 - 1 - - - - - - - 0 - 1 - - - - - - - 0 - 1 - - - - - - - - + + + + + + + + + + + 0 + 1 + + + + + + + 0 + 1 + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - true - false - - - - - - - + + + - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + Index: binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_b.xml =================================================================== --- binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_b.xml +++ binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_b.xml @@ -1,47 +1,79 @@ - - - - - props/new/dude_head.dae - - - - - - - - - - - - - - - - - - - - - - 184 151 98 - - - 255 205 125 - - - 153 145 129 - - - 129 152 153 - - - 8 8 8 - - - 250 130 78 - - - objectcolor.xml - + + + + + props/new/dude_head.dae + + + + + + + + + + + + + 255 205 125 + + + 153 145 129 + + + 8 8 8 + + + 250 130 78 + + + objectcolor.xml + + + + + + props/new/dude_head.dae + + + + + + + + + + + + + + + + + + + + + + 184 151 98 + + + 255 205 125 + + + 153 145 129 + + + 129 152 153 + + + 8 8 8 + + + 250 130 78 + + + objectcolor.xml + + Index: binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_e.xml =================================================================== --- binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_e.xml +++ binaries/data/mods/public/art/actors/props/units/heads/new/head_hele_e.xml @@ -1,41 +1,67 @@ - - - - - props/new/head_beard_small.dae - - - props/new/head_beard_large.dae - - - props/new/head_beard_wide.dae - - - - - - - - - - - - - - - - 184 151 98 - - - 255 205 125 - - - 153 145 129 - - - 129 152 153 - - - objectcolor.xml - + + + + + props/new/head_beard_large.dae + + + + + + + + + + 184 151 98 + + + 255 205 125 + + + 153 145 129 + + + objectcolor.xml + + + + + + props/new/head_beard_small.dae + + + props/new/head_beard_large.dae + + + props/new/head_beard_wide.dae + + + + + + + + + + + + + + + + 184 151 98 + + + 255 205 125 + + + 153 145 129 + + + 129 152 153 + + + objectcolor.xml + + Index: binaries/data/mods/public/art/actors/props/units/shields/aspis_athen_e.xml =================================================================== --- binaries/data/mods/public/art/actors/props/units/shields/aspis_athen_e.xml +++ binaries/data/mods/public/art/actors/props/units/shields/aspis_athen_e.xml @@ -1,28 +1,80 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - player_trans_parallax_spec_helmet.xml - + + + + + + + + + + + + + + + + + + + + + + + + player_trans.xml + + + + + + + + + + + + + + + + + + + + + + + + + + player_trans_parallax_spec_helmet.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + player_trans_parallax_spec_helmet.xml + + Index: binaries/data/mods/public/art/actors/props/units/shields/props/aspis_back_bronze_01.xml =================================================================== --- binaries/data/mods/public/art/actors/props/units/shields/props/aspis_back_bronze_01.xml +++ binaries/data/mods/public/art/actors/props/units/shields/props/aspis_back_bronze_01.xml @@ -1,15 +1,28 @@ - - - - - props/shield/aspis_01_back.dae - - - - - - - - no_trans_parallax_spec.xml - + + + + + props/shield/aspis_01_back.dae + + + + + + default.xml + + + + + + props/shield/aspis_01_back.dae + + + + + + + + no_trans_parallax_spec.xml + + Index: binaries/data/mods/public/art/actors/props/units/weapons/arrow_back.xml =================================================================== --- binaries/data/mods/public/art/actors/props/units/weapons/arrow_back.xml +++ binaries/data/mods/public/art/actors/props/units/weapons/arrow_back.xml @@ -1,13 +1,17 @@ - - - - - props/weap_arrow_back.dae - - - - - - default.xml - + + + + + + + + props/weap_arrow_back.dae + + + + + + default.xml + + Index: binaries/data/mods/public/art/actors/props/units/weapons/bow_short.xml =================================================================== --- binaries/data/mods/public/art/actors/props/units/weapons/bow_short.xml +++ binaries/data/mods/public/art/actors/props/units/weapons/bow_short.xml @@ -1,24 +1,28 @@ - - - - - props/weapons/bow_greek.dae - - - - - - - - - - - - - - - - - player_trans.xml - + + + + + + + + props/weapons/bow_greek.dae + + + + + + + + + + + + + + + + + player_trans.xml + + Index: binaries/data/mods/public/art/actors/props/units/weapons/spear_hoplite.xml =================================================================== --- binaries/data/mods/public/art/actors/props/units/weapons/spear_hoplite.xml +++ binaries/data/mods/public/art/actors/props/units/weapons/spear_hoplite.xml @@ -1,13 +1,14 @@ - - - - - - - props/spear_hoplite.dae - - - - - + + + + + + + + props/spear_hoplite.dae + + + + + Index: binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml =================================================================== --- binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml +++ binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml @@ -1,53 +1,155 @@ - - - - - skeletal/new/m_tunic_short.dae - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - player_trans.xml - + + + + + + skeletal/new/m_tunic_short.dae + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + player_trans.xml + + + + + + skeletal/new/m_tunic_short.dae + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + player_trans.xml + + + + + + skeletal/new/m_tunic_short.dae + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + player_trans.xml + + Index: binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml =================================================================== --- binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml +++ binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml @@ -1,215 +1,6 @@ - - - - - skeletal/new/m_armor_tunic_short.dae - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - player_trans_spec.xml - + + + + + Index: binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e_base.xml =================================================================== --- binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e_base.xml +++ binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e_base.xml @@ -2,7 +2,15 @@ - + + skeletal/new/m_armor_tunic_short.dae + + + + + + + skeletal/new/m_armor_tunic_short.dae @@ -12,7 +20,7 @@ - + @@ -35,17 +43,17 @@ - + - + - + @@ -60,22 +68,22 @@ - + - + - + - + @@ -93,13 +101,13 @@ - + - + @@ -111,19 +119,19 @@ - + - + - + @@ -147,7 +155,7 @@ - + @@ -159,13 +167,13 @@ - + - + Index: binaries/data/mods/public/gui/options/options.json =================================================================== --- binaries/data/mods/public/gui/options/options.json +++ binaries/data/mods/public/gui/options/options.json @@ -152,6 +152,17 @@ "min": 0, "max": 1 }, + { + "type": "dropdown", + "label": "Model quality", + "tooltip": "Model quality setting.", + "config": "max_actor_quality", + "list": [ + { "value": 100, "label": "Low", "tooltip": "Simpler models for better performance." }, + { "value": 150, "label": "Medium", "tooltip": "Average quality and average performance." }, + { "value": 200, "label": "High", "tooltip": "High quality models." } + ] + }, { "type": "slider", "label": "Shader effects", Index: source/graphics/Model.cpp =================================================================== --- source/graphics/Model.cpp +++ source/graphics/Model.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -499,10 +499,6 @@ m_Anim = source->m_Anim; m_AnimTime = source->m_AnimTime; - m_Flags &= ~MODELFLAG_CASTSHADOWS; - if (source->m_Flags & MODELFLAG_CASTSHADOWS) - m_Flags |= MODELFLAG_CASTSHADOWS; - m_ObjectBounds.SetEmpty(); InvalidateBounds(); } Index: source/graphics/ObjectBase.h =================================================================== --- source/graphics/ObjectBase.h +++ source/graphics/ObjectBase.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -22,7 +22,9 @@ #include "ps/CStr.h" #include "ps/CStrIntern.h" +class CActorDef; class CModel; +class CObjectEntry; class CObjectManager; class CSkeletonAnim; class CXeromyces; @@ -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); + bool UsesFile(const VfsPath& pathname) const; - // filename that this was loaded from - VfsPath m_Pathname; - - // short human-readable name - CStrW m_ShortName; struct { // cast shadows from this object @@ -179,20 +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; +}; + +/** + * Represents an actor file. Actors can contain various quality levels. + * An ActorDef maintains a CObjectBase for each specified quality level, and provides access to it. + */ +class CActorDef +{ + // Friend these three so they can use GetBase. + friend class CObjectManager; + friend class CObjectBase; + friend class CObjectEntry; + + NONCOPYABLE(CActorDef); +public: + + CActorDef(CObjectManager& objectManager); + + std::vector QualityLevels() const; + + VfsPath GetPathname() const { return m_Pathname; } + +// Interface accessible from CObjectManager / CObjectBase +protected: + /** + * Return the Object base matching the given quality level. + */ + const std::shared_ptr& GetBase(u8 QualityLevel) const; + + /** + * Initialise this object by loading from the given file. + * Returns false on error. + */ + bool Load(const VfsPath& pathname); + + /** + * Reload this object from the file that it was previously loaded from. + * Returns false on error. + */ + bool Reload(); + + /** + * Returns whether this actor (including any possible props) + * uses the given file. (This is used for hotloading.) + */ + bool UsesFile(const VfsPath& pathname) const; + + // filename that this was loaded from + VfsPath m_Pathname; + +private: + CObjectManager& m_ObjectManager; + + // std::shared_ptr to avoid issues during hotloading. + std::vector> m_ObjectBases; std::unordered_set m_UsedFiles; - - void LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant); }; #endif Index: source/graphics/ObjectBase.cpp =================================================================== --- source/graphics/ObjectBase.cpp +++ source/graphics/ObjectBase.cpp @@ -31,11 +31,114 @@ #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(12) + 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 +190,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 +255,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 +316,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 +333,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 +354,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 +381,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 +394,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 +405,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 +413,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 +433,7 @@ } // Get the matched variant - CObjectBase::Variant& var ((*grp)[id]); + const CObjectBase::Variant& var ((*grp)[id]); // Apply its data: @@ -449,50 +454,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 +506,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 +586,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 +599,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 +676,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 +687,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(maxquality); + 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 max_quality = 0; + XMBElement inlineActor; + XERO_ITER_EL(root, child) + { + if (child.GetNodeName() == el_inline) + inlineActor = child; + } + XERO_ITER_EL(root, actor) + { + if (actor.GetNodeName() != el_actor) + continue; + bool found_maxquality = false; + bool use_inline = false; + CStr file; + XERO_ITER_ATTR(actor, attr) + { + if (attr.Name == at_maxquality) + { + int v = GetQuality(attr.Value); + if (v > 255) + { + LOGERROR("Qualitylevel to attribute must not be above 255 (file %s)", pathname.string8()); + return false; + } + if (v <= max_quality) + { + LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8()); + return false; + } + max_quality = v; + found_maxquality = true; + } + else if (attr.Name == at_file) + { + if (attr.Value.empty()) + LOGWARNING("Empty actor file specified (file %s)", pathname.string8()); + file = attr.Value; + } + else if (attr.Name == at_inline) + use_inline = true; + } + if (!found_maxquality) + max_quality = 255; + std::unique_ptr base = std::make_unique(m_ObjectManager, *this, max_quality); + if (use_inline) + { + if (inlineActor.GetNodeName() == -1) + { + LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname.string8()); + return false; + } + base->Load(XeroFile, inlineActor); + } + else if (file.empty()) + base->Load(XeroFile, actor); + else + { + if (actor.GetChildNodes().size() > 0) + LOGWARNING("Actor definition refers to file but has children elements, they will be ignored (file %s)", pathname.string8()); + + // Open up an external file to load. + // Don't crash hard when failures happen, but log them and continue + CXeromyces XeroActor; + if (XeroActor.Load(g_VFS, "art/actors/" + file, "actor") == PSRETURN_OK) + { + const XMBElement& root = XeroActor.GetRoot(); + if (root.GetNodeName() != el_actor) + { + LOGERROR("Included actors cannot define quality levels (opening %s from file %s)", file, pathname.string8()); + return false; + } + base->Load(XeroActor, root); + } + else + { + LOGERROR("Could not open actor file at path %s (file %s)", file, pathname.string8()); + return false; + } + m_UsedFiles.insert(file); + } + m_ObjectBases.emplace_back(std::move(base)); + } + if (max_quality != 255) + { + LOGERROR("Quality levels must go up to 255 (file %s)", pathname.string8().c_str()); + return false; + } + } + + // For each quality level, check if we need to further split (because of props). + std::vector splits = QualityLevels(); + for (const std::shared_ptr& base : m_ObjectBases) + base->GetQualitySplits(splits); + ENSURE(splits.size() >= 1); + if (splits.size() > 5) + { + LOGERROR("Too many quality levels (%i) for actor %s", splits.size(), pathname.string8().c_str()); + return false; + } + + std::vector>::iterator it = m_ObjectBases.begin(); + std::vector::const_iterator qualityLevels = splits.begin(); + while (it != m_ObjectBases.end()) + if ((*it)->m_QualityLevel > *qualityLevels) + { + it = ++m_ObjectBases.emplace(it, (*it)->CopyWithQuality(*qualityLevels)); + ++qualityLevels; + } + else if ((*it)->m_QualityLevel == *qualityLevels) + { + ++it; + ++qualityLevels; + } + else + ++it; + + return true; +} + +bool CActorDef::Reload() +{ + return Load(m_Pathname); +} + +bool CActorDef::UsesFile(const VfsPath& pathname) const +{ + return m_UsedFiles.find(pathname) != m_UsedFiles.end(); +} Index: source/graphics/ObjectEntry.h =================================================================== --- source/graphics/ObjectEntry.h +++ source/graphics/ObjectEntry.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -40,16 +40,16 @@ NONCOPYABLE(CObjectEntry); public: - CObjectEntry(CObjectBase* base, CSimulation2& simulation); + CObjectEntry(const std::shared_ptr& base, CSimulation2& simulation); ~CObjectEntry(); // Construct this actor, using the specified variation selections - bool BuildVariation(const std::vector >& selections, + bool BuildVariation(const std::vector*>& completeSelections, const std::vector& variationKey, CObjectManager& objectManager); // Base actor. Contains all the things that don't change between // different variations of the actor. - CObjectBase* m_Base; + std::shared_ptr m_Base; // samplers list std::vector m_Samplers; Index: source/graphics/ObjectEntry.cpp =================================================================== --- source/graphics/ObjectEntry.cpp +++ source/graphics/ObjectEntry.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -39,7 +39,7 @@ #include -CObjectEntry::CObjectEntry(CObjectBase* base, CSimulation2& simulation) : +CObjectEntry::CObjectEntry(const std::shared_ptr& base, CSimulation2& simulation) : m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Model(NULL), m_Outdated(false), m_Simulation(simulation) { } @@ -53,7 +53,7 @@ } -bool CObjectEntry::BuildVariation(const std::vector >& selections, +bool CObjectEntry::BuildVariation(const std::vector*>& completeSelections, const std::vector& variationKey, CObjectManager& objectManager) { @@ -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: source/graphics/ObjectManager.h =================================================================== --- source/graphics/ObjectManager.h +++ source/graphics/ObjectManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -18,13 +18,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: source/graphics/ObjectManager.cpp =================================================================== --- source/graphics/ObjectManager.cpp +++ 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: - - std::map::iterator it = m_ObjectBases.find(objectname); - if (it != m_ObjectBases.end()) - return it->second; - - // Not already loaded, so try to load it: + ENSURE(!actorName.empty()); - CObjectBase* obj = new CObjectBase(*this); + decltype(m_ActorDefs)::iterator it = m_ActorDefs.find(actorName); + if (it != m_ActorDefs.end()) + return it->second.get(); - VfsPath pathname = VfsPath("art/actors/") / objectname; + std::unique_ptr actor = std::make_unique(*this); - if (obj->Load(pathname)) - { - m_ObjectBases[objectname] = obj; - return obj; - } - else - delete obj; + VfsPath pathname = VfsPath("art/actors/") / actorName; - LOGERROR("CObjectManager::FindObjectBase(): Cannot find object '%s'", utf8_from_wstring(objectname)); + if (actor->Load(pathname)) + return m_ActorDefs.emplace(actorName, std::move(actor)).first->second.get(); - return 0; -} + LOGERROR("CObjectManager::FindActorDef(): Cannot find actor '%s'", utf8_from_wstring(actorName)); -CObjectEntry* CObjectManager::FindObject(const CStrW& objname) -{ - std::vector > selections; // TODO - should this really be empty? - return FindObjectVariation(objname, selections); + return nullptr; } -CObjectEntry* CObjectManager::FindObjectVariation(const CStrW& objname, const std::vector >& selections) +CObjectEntry* CObjectManager::FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed) { - CObjectBase* base = FindObjectBase(objname); - - if (! base) - return NULL; - - return FindObjectVariation(base, 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(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(); + 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); +} Index: source/graphics/Unit.h =================================================================== --- source/graphics/Unit.h +++ 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: source/graphics/Unit.cpp =================================================================== --- source/graphics/Unit.cpp +++ 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,9 @@ #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) +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 +39,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); - - CObjectEntry* obj = objectManager.FindObjectVariation(base, selectionsVec); - - if (! obj) - return NULL; - - return new CUnit(obj, objectManager, actorSelections, seed); + CActorDef* actor = objectManager.FindActorDef(actorName); + + if (!actor) + return nullptr; + + 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 +97,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 +106,24 @@ // 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(); Index: source/ps/ConfigDB.h =================================================================== --- source/ps/ConfigDB.h +++ 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: source/ps/ConfigDB.cpp =================================================================== --- source/ps/ConfigDB.cpp +++ 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: source/renderer/RenderingOptions.cpp =================================================================== --- source/renderer/RenderingOptions.cpp +++ 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: source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- source/simulation2/components/CCmpVisualActor.cpp +++ 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: source/simulation2/components/ICmpVisual.h =================================================================== --- source/simulation2/components/ICmpVisual.h +++ 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 @@ */ 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: source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.h =================================================================== --- source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.h +++ 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: source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.cpp =================================================================== --- source/tools/atlas/AtlasUI/ActorEditor/ActorEditor.cpp +++ 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: source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp =================================================================== --- source/tools/atlas/AtlasUI/ActorEditor/ActorEditorListCtrl.cpp +++ 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: source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.h =================================================================== --- source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.h +++ 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: source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp =================================================================== --- source/tools/atlas/AtlasUI/CustomControls/EditableListCtrl/FieldEditCtrl.cpp +++ 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) {