prop indirection on property-adjust, multiply
Make the min, max and factor arguments to propert-adjust and property-multiply optonally use a -prop version to read a value from the global property tree.
This commit is contained in:
parent
fcc112c780
commit
49e8967174
6 changed files with 248 additions and 34 deletions
|
@ -1,8 +1,11 @@
|
|||
// fg_commands.cxx - internal FGFS commands.
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include "config.h"
|
||||
#endif
|
||||
/*
|
||||
* SPDX-FileName: fg_commands.hxx
|
||||
* SPDX-FileComment: built-in commands for FlightGear.
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include <config.h>
|
||||
|
||||
#include <string.h> // strcmp()
|
||||
|
||||
|
@ -15,6 +18,7 @@
|
|||
#include <simgear/debug/logstream.hxx>
|
||||
#include <simgear/io/iostreams/sgstream.hxx>
|
||||
#include <simgear/math/sg_random.hxx>
|
||||
#include <simgear/misc/simgear_optional.hxx>
|
||||
#include <simgear/props/props.hxx>
|
||||
#include <simgear/props/props_io.hxx>
|
||||
#include <simgear/sg_inlines.h>
|
||||
|
@ -118,41 +122,81 @@ split_value (double full_value, const char * mask,
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrive a typed value from a node, either from <foo> directly or indirectly
|
||||
*
|
||||
* @param node - base node from the command to look inside
|
||||
* @param name - direct name of the argument, eg 'min' or 'max'
|
||||
* @param indirectName - indirect name to use, eg, 'min-path'. If empty, the name is formed using the base
|
||||
* name and appending '-prop', eg 'min-prop' and 'max-prop'.
|
||||
* @return std::optional<T>
|
||||
*/
|
||||
template <class T>
|
||||
static simgear::optional<T>
|
||||
getValueIndirect(const SGPropertyNode* node, const std::string& name, const std::string& indirectName = {})
|
||||
{
|
||||
auto indirectNode = node->getChild(indirectName.empty() ? (name + "-prop") : indirectName);
|
||||
if (indirectNode) {
|
||||
const auto resolvedNode = fgGetNode(indirectNode->getStringValue());
|
||||
if (resolvedNode) {
|
||||
return resolvedNode->getValue<T>();
|
||||
}
|
||||
|
||||
// if the path is not valid, warn
|
||||
SG_LOG(SG_GENERAL, SG_DEV_WARN, "getValueIndirect: property:" << indirectNode->getNameString() << " has value '" << indirectNode->getStringValue() << "' which was not found in the global property tree. Falling back to "
|
||||
"value defined by argument '"
|
||||
<< name << "'");
|
||||
|
||||
// deliberate fall through here, so we use the value from the direct prop
|
||||
}
|
||||
|
||||
auto directNode = node->getChild(name);
|
||||
if (directNode)
|
||||
return directNode->getValue<T>();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp or wrap a value as specified.
|
||||
*/
|
||||
static void
|
||||
limit_value (double * value, const SGPropertyNode * arg)
|
||||
static double
|
||||
limit_value(double value, const SGPropertyNode* arg)
|
||||
{
|
||||
const SGPropertyNode * min_node = arg->getChild("min");
|
||||
const SGPropertyNode * max_node = arg->getChild("max");
|
||||
const auto minv = getValueIndirect<double>(arg, "min");
|
||||
const auto maxv = getValueIndirect<double>(arg, "max");
|
||||
|
||||
bool wrap = arg->getBoolValue("wrap");
|
||||
|
||||
if (min_node == 0 || max_node == 0)
|
||||
wrap = false;
|
||||
// we can wrap only if requested, and
|
||||
const bool canWrap = (minv && maxv);
|
||||
const bool wantsWrap = arg->getBoolValue("wrap");
|
||||
const bool wrap = canWrap && wantsWrap;
|
||||
if (wantsWrap && !canWrap) {
|
||||
SG_LOG(SG_GENERAL, SG_DEV_WARN, "limit_value: wrap requested, but no min/max values defined");
|
||||
}
|
||||
|
||||
if (wrap) { // wrap such that min <= x < max
|
||||
double min_val = min_node->getDoubleValue();
|
||||
double max_val = max_node->getDoubleValue();
|
||||
double resolution = arg->getDoubleValue("resolution");
|
||||
const double resolution = arg->getDoubleValue("resolution");
|
||||
if (resolution > 0.0) {
|
||||
// snap to (min + N*resolution), taking special care to handle imprecision
|
||||
int n = (int)floor((*value - min_val) / resolution + 0.5);
|
||||
int steps = (int)floor((max_val - min_val) / resolution + 0.5);
|
||||
int n = (int)floor((value - minv.value()) / resolution + 0.5);
|
||||
int steps = (int)floor((maxv.value() - minv.value()) / resolution + 0.5);
|
||||
SG_NORMALIZE_RANGE(n, 0, steps);
|
||||
*value = min_val + resolution * n;
|
||||
return minv.value() + resolution * n;
|
||||
} else {
|
||||
// plain circular wrapping
|
||||
SG_NORMALIZE_RANGE(*value, min_val, max_val);
|
||||
return SGMiscd::normalizePeriodic(minv.value(), maxv.value(), value);
|
||||
}
|
||||
} else { // clamp such that min <= x <= max
|
||||
if ((min_node != 0) && (*value < min_node->getDoubleValue()))
|
||||
*value = min_node->getDoubleValue();
|
||||
else if ((max_node != 0) && (*value > max_node->getDoubleValue()))
|
||||
*value = max_node->getDoubleValue();
|
||||
if (minv && (value < minv.value())) {
|
||||
return minv.value();
|
||||
}
|
||||
|
||||
if (maxv && (value > maxv.value())) {
|
||||
return maxv.value();
|
||||
}
|
||||
}
|
||||
|
||||
return value; // return unmodified value
|
||||
}
|
||||
|
||||
static bool
|
||||
|
@ -545,7 +589,7 @@ do_property_adjust (const SGPropertyNode * arg, SGPropertyNode * root)
|
|||
split_value(prop->getDoubleValue(), arg->getStringValue("mask", "all").c_str(),
|
||||
&unmodifiable, &modifiable);
|
||||
modifiable += amount;
|
||||
limit_value(&modifiable, arg);
|
||||
modifiable = limit_value(modifiable, arg);
|
||||
|
||||
prop->setDoubleValue(unmodifiable + modifiable);
|
||||
|
||||
|
@ -571,13 +615,17 @@ static bool
|
|||
do_property_multiply (const SGPropertyNode * arg, SGPropertyNode * root)
|
||||
{
|
||||
SGPropertyNode * prop = get_prop(arg,root);
|
||||
double factor = arg->getDoubleValue("factor", 1.0);
|
||||
auto factorValue = getValueIndirect<double>(arg, "factor");
|
||||
if (!factorValue) {
|
||||
SG_LOG(SG_GENERAL, SG_DEV_WARN, "property-multiply: missing factor/factor-prop argument");
|
||||
return false;
|
||||
}
|
||||
|
||||
double unmodifiable, modifiable;
|
||||
split_value(prop->getDoubleValue(), arg->getStringValue("mask", "all").c_str(),
|
||||
&unmodifiable, &modifiable);
|
||||
modifiable *= factor;
|
||||
limit_value(&modifiable, arg);
|
||||
modifiable *= factorValue.value();
|
||||
modifiable = limit_value(modifiable, arg);
|
||||
|
||||
prop->setDoubleValue(unmodifiable + modifiable);
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
// fg_commands.hxx - built-in commands for FlightGear.
|
||||
|
||||
#ifndef __FG_COMMANDS_HXX
|
||||
#define __FG_COMMANDS_HXX
|
||||
/*
|
||||
* SPDX-FileName: fg_commands.hxx
|
||||
* SPDX-FileComment: built-in commands for FlightGear.
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Initialize the built-in commands.
|
||||
|
@ -10,6 +14,3 @@ void fgInitCommands ();
|
|||
|
||||
void fgInitSceneCommands();
|
||||
|
||||
// end of fg_commands.hxx
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,6 +4,7 @@ set(TESTSUITE_SOURCES
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/test_autosaveMigration.cxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_posinit.cxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_timeManager.cxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_commands.cxx
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
@ -12,5 +13,6 @@ set(TESTSUITE_HEADERS
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/test_autosaveMigration.hxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_posinit.hxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_timeManager.hxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test_commands.hxx
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
|
|
@ -18,11 +18,12 @@
|
|||
*/
|
||||
|
||||
#include "test_autosaveMigration.hxx"
|
||||
#include "test_commands.hxx"
|
||||
#include "test_posinit.hxx"
|
||||
#include "test_timeManager.hxx"
|
||||
|
||||
|
||||
// Set up the unit tests.
|
||||
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AutosaveMigrationTests, "Unit tests");
|
||||
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(PosInitTests, "Unit tests");
|
||||
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(TimeManagerTests, "Unit tests");
|
||||
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CommandsTests, "Unit tests");
|
||||
|
|
127
test_suite/unit_tests/Main/test_commands.cxx
Normal file
127
test_suite/unit_tests/Main/test_commands.cxx
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* SPDX-FileName: test_Commands.cxx
|
||||
* SPDX-FileComment: Unit tests for built-in commands
|
||||
* SPDX-FileCopyrightText: Copyright (C) 2023 James Turner
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "test_commands.hxx"
|
||||
#include "config.h"
|
||||
|
||||
#include <simgear/structure/commands.hxx>
|
||||
|
||||
#include "Main/fg_commands.hxx"
|
||||
#include "Main/fg_props.hxx"
|
||||
#include "Main/globals.hxx"
|
||||
|
||||
#include "test_suite/FGTestApi/testGlobals.hxx"
|
||||
|
||||
using namespace std::string_literals;
|
||||
using namespace flightgear;
|
||||
|
||||
void CommandsTests::setUp()
|
||||
{
|
||||
FGTestApi::setUp::initTestGlobals("commands");
|
||||
fgLoadProps("defaults.xml", globals->get_props());
|
||||
|
||||
fgInitCommands();
|
||||
}
|
||||
|
||||
void CommandsTests::tearDown()
|
||||
{
|
||||
delete SGCommandMgr::instance();
|
||||
}
|
||||
|
||||
void CommandsTests::testPropertyAdjustCommand()
|
||||
{
|
||||
auto propAdjust = SGCommandMgr::instance()->getCommand("property-adjust");
|
||||
|
||||
{
|
||||
fgSetDouble("/foo", 10.0);
|
||||
|
||||
SGPropertyNode_ptr arg(new SGPropertyNode);
|
||||
arg->setStringValue("property", "/foo");
|
||||
arg->setDoubleValue("step", 1.0);
|
||||
CPPUNIT_ASSERT((*propAdjust)(arg, globals->get_props()));
|
||||
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("property-adjust step failed", 11.0, fgGetDouble("/foo"), 0.0001);
|
||||
}
|
||||
|
||||
{
|
||||
fgSetDouble("/foo", 10.0);
|
||||
SGPropertyNode_ptr arg(new SGPropertyNode);
|
||||
arg->setStringValue("property", "/foo");
|
||||
arg->setDoubleValue("step", 5.0);
|
||||
arg->setDoubleValue("max", 12.0);
|
||||
CPPUNIT_ASSERT((*propAdjust)(arg, globals->get_props()));
|
||||
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("property-adjust step with max", 12.0, fgGetDouble("/foo"), 0.0001);
|
||||
}
|
||||
|
||||
{
|
||||
fgSetDouble("/foo", 30.0);
|
||||
fgSetDouble("/wib/bar", 33.0);
|
||||
|
||||
SGPropertyNode_ptr arg(new SGPropertyNode);
|
||||
arg->setStringValue("property", "/foo");
|
||||
arg->setDoubleValue("step", 5.0);
|
||||
arg->setStringValue("max-prop", "/wib/bar");
|
||||
CPPUNIT_ASSERT((*propAdjust)(arg, globals->get_props()));
|
||||
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("property-adjust step with max from prop", 33.0, fgGetDouble("/foo"), 0.0001);
|
||||
}
|
||||
|
||||
// check fallback code path if max-prop is missing
|
||||
{
|
||||
fgSetDouble("/foo", 30.0);
|
||||
fgSetDouble("/wib/bar", 33.0);
|
||||
|
||||
SGPropertyNode_ptr arg(new SGPropertyNode);
|
||||
arg->setStringValue("property", "/foo");
|
||||
arg->setDoubleValue("step", 5.0);
|
||||
arg->setStringValue("max-prop", "/wib/xxxbar");
|
||||
arg->setDoubleValue("max", 34.0);
|
||||
CPPUNIT_ASSERT((*propAdjust)(arg, globals->get_props()));
|
||||
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("property-adjust step with max from prop", 34.0, fgGetDouble("/foo"), 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
void CommandsTests::testPropertyMultiplyCommand()
|
||||
{
|
||||
auto cmd = SGCommandMgr::instance()->getCommand("property-multiply");
|
||||
|
||||
{
|
||||
fgSetDouble("/foo", 10.0);
|
||||
|
||||
SGPropertyNode_ptr arg(new SGPropertyNode);
|
||||
arg->setStringValue("property", "/foo");
|
||||
arg->setDoubleValue("factor", 4.0);
|
||||
CPPUNIT_ASSERT((*cmd)(arg, globals->get_props()));
|
||||
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("property-multiply failed", 40.0, fgGetDouble("/foo"), 0.0001);
|
||||
}
|
||||
|
||||
{
|
||||
fgSetDouble("/foo", 10.0);
|
||||
fgSetDouble("/bar", 5.0);
|
||||
|
||||
SGPropertyNode_ptr arg(new SGPropertyNode);
|
||||
arg->setStringValue("property", "/foo");
|
||||
arg->setStringValue("factor-prop", "/bar");
|
||||
CPPUNIT_ASSERT((*cmd)(arg, globals->get_props()));
|
||||
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("property-multiply failed", 50.0, fgGetDouble("/foo"), 0.0001);
|
||||
}
|
||||
|
||||
// missing factor
|
||||
{
|
||||
fgSetDouble("/foo", 10.0);
|
||||
|
||||
SGPropertyNode_ptr arg(new SGPropertyNode);
|
||||
arg->setStringValue("property", "/foo");
|
||||
|
||||
// check the command fails
|
||||
CPPUNIT_ASSERT(!(*cmd)(arg, globals->get_props()));
|
||||
}
|
||||
}
|
35
test_suite/unit_tests/Main/test_commands.hxx
Normal file
35
test_suite/unit_tests/Main/test_commands.hxx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* SPDX-FileName: test_Commands.hxx
|
||||
* SPDX-FileComment: Unit tests for built-in commands
|
||||
* SPDX-FileCopyrightText: Copyright (C) 2023 James Turner
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
|
||||
class CommandsTests : public CppUnit::TestFixture
|
||||
{
|
||||
// Set up the test suite.
|
||||
CPPUNIT_TEST_SUITE(CommandsTests);
|
||||
CPPUNIT_TEST(testPropertyAdjustCommand);
|
||||
CPPUNIT_TEST(testPropertyMultiplyCommand);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
// Set up function for each test.
|
||||
void setUp();
|
||||
|
||||
// Clean up after each test.
|
||||
void tearDown();
|
||||
|
||||
// The tests.
|
||||
void testPropertyAdjustCommand();
|
||||
void testPropertyMultiplyCommand();
|
||||
|
||||
private:
|
||||
};
|
Loading…
Add table
Reference in a new issue