1
0
Fork 0

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:
James Turner 2023-08-12 12:46:52 +01:00
parent fcc112c780
commit 49e8967174
6 changed files with 248 additions and 34 deletions

View file

@ -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);

View file

@ -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

View file

@ -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
)

View file

@ -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");

View 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()));
}
}

View 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:
};