Skip to content

Commit

Permalink
#6095: don't create menu items for unsupported game features
Browse files Browse the repository at this point in the history
The menu.xml allows a new "gamefeature" attribute to be applied to a
<menuItem> node. If present, this feature will be looked up in the
current game using the hasFeature() method, and the menu item will not
be created if the feature is not present.

This is used to hide the "Make Structural" and "Make Detail" menu items
for games which are not Quake 3.
  • Loading branch information
Matthew Mott committed Sep 20, 2022
1 parent bb8c3bd commit 7862067
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 63 deletions.
5 changes: 5 additions & 0 deletions install/games/darkmod.game
Expand Up @@ -37,6 +37,11 @@
</texture>
</filetypes>

<!-- Optional game-specific features to expose -->
<features>
<feature>hot_reload</feature>
</features>

<!-- Filters to use for the filter system -->
<filtersystem>
<filter name="World geometry">
Expand Down
4 changes: 2 additions & 2 deletions install/menu.xml
Expand Up @@ -244,8 +244,8 @@
<menuItem name="makevisportal" caption="Make Visportal" command="MakeVisportal" icon="make_visportal.png" />

<menuSeparator />
<menuItem name="brushmakedetail" caption="Make Detail" command="BrushMakeDetail" />
<menuItem name="brushmakestructural" caption="Make Structural" command="BrushMakeStructural" />
<menuItem name="brushmakedetail" caption="Make Detail" command="BrushMakeDetail" gamefeature="detail_brushes" />
<menuItem name="brushmakestructural" caption="Make Structural" command="BrushMakeStructural" gamefeature="detail_brushes" />
</subMenu>

<subMenu name="patch" caption="&amp;Patch">
Expand Down
118 changes: 62 additions & 56 deletions radiant/ui/menu/MenuElement.cpp
Expand Up @@ -2,6 +2,7 @@

#include "i18n.h"
#include "itextstream.h"
#include "igame.h"

#include "string/split.h"
#include "string/join.h"
Expand Down Expand Up @@ -200,62 +201,67 @@ void MenuElement::setAccelerator(const std::string& accelStr)

MenuElementPtr MenuElement::CreateFromNode(const xml::Node& node)
{
MenuElementPtr item;

std::string nodeName = node.getName();

if (nodeName == "menuItem")
{
item = std::make_shared<MenuItem>();

// Get the EventPtr according to the event
item->setEvent(node.getAttributeValue("command"));
item->setIcon(node.getAttributeValue("icon"));
}
else if (nodeName == "menuSeparator")
{
item = std::make_shared<MenuSeparator>();
}
else if (nodeName == "subMenu")
{
item = std::make_shared<MenuFolder>();
}
else if (nodeName == "menu")
{
item = std::make_shared<MenuBar>();
}
else
{
rError() << "MenuElement: Unknown node found: " << node.getName() << std::endl;
return item;
}

item->setName(node.getAttributeValue("name"));

// Put the caption through gettext before passing it to setCaption
item->setCaption(_(node.getAttributeValue("caption").c_str()));

// Parse subnodes
xml::NodeList childNodes = node.getChildren();

for (const xml::Node& childNode : childNodes)
{
if (childNode.getName() == "text" || childNode.getName() == "comment")
{
continue;
}

// Allocate a new child item
MenuElementPtr childItem = CreateFromNode(childNode);

// Add the child to the list
if (childItem)
{
item->addChild(childItem);
}
}

return item;
MenuElementPtr item;

// Don't create anything if the menu item requires a non-existent game feature
if (auto feature = node.getAttributeValue("gamefeature");
!feature.empty() && !GlobalGameManager().currentGame()->hasFeature(feature))
{
return {};
}

std::string nodeName = node.getName();

if (nodeName == "menuItem")
{
item = std::make_shared<MenuItem>();

// Get the EventPtr according to the event
item->setEvent(node.getAttributeValue("command"));
item->setIcon(node.getAttributeValue("icon"));
}
else if (nodeName == "menuSeparator")
{
item = std::make_shared<MenuSeparator>();
}
else if (nodeName == "subMenu")
{
item = std::make_shared<MenuFolder>();
}
else if (nodeName == "menu")
{
item = std::make_shared<MenuBar>();
}
else
{
rError() << "MenuElement: Unknown node found: " << node.getName() << std::endl;
return item;
}

item->setName(node.getAttributeValue("name"));

// Put the caption through gettext before passing it to setCaption
item->setCaption(_(node.getAttributeValue("caption").c_str()));

// Parse subnodes
xml::NodeList childNodes = node.getChildren();

for (const xml::Node& childNode : childNodes)
{
if (childNode.getName() == "text" || childNode.getName() == "comment")
{
continue;
}

// Allocate a new child item
MenuElementPtr childItem = CreateFromNode(childNode);

// Add the child to the list
if (childItem)
item->addChild(childItem);
}

return item;
}

MenuElementPtr MenuElement::CreateForType(ItemType type)
Expand Down
16 changes: 11 additions & 5 deletions radiant/ui/menu/MenuElement.h
Expand Up @@ -126,11 +126,17 @@ class MenuElement :
// Tries to (recursively) locate the MenuElement by looking up the path
MenuElementPtr find(const std::string& menuPath);

/**
* Parses the given XML node recursively and creates all items from the
* information it finds. Returns the constructed MenuElement.
*/
static MenuElementPtr CreateFromNode(const xml::Node& node);
/**
* @brief Parses the given XML node recursively and creates all items from the
* information it finds.
*
* @param node
* XML node containing the menu information.
*
* @return Constructed MenuElement pointer or null if the menu item is not required in
* the current game configuration.
*/
static MenuElementPtr CreateFromNode(const xml::Node& node);

/**
* Constructs a menu element for the given type
Expand Down
4 changes: 4 additions & 0 deletions test/Game.cpp
Expand Up @@ -64,6 +64,10 @@ TEST_F(GameTest, GetOptionalFeatures)
// Only Quake 3 should have the "detail_brushes" feature
EXPECT_FALSE((*tdm)->hasFeature("detail_brushes"));
EXPECT_TRUE((*q3)->hasFeature("detail_brushes"));

// Only Dark Mod should have the "hot_reload" feature
EXPECT_TRUE((*tdm)->hasFeature("hot_reload"));
EXPECT_FALSE((*q3)->hasFeature("hot_reload"));
}

}

0 comments on commit 7862067

Please sign in to comment.