diff --git a/data/mods/TEST_DATA/EOC.json b/data/mods/TEST_DATA/EOC.json index 46c3771931661..b029e8ff197a9 100644 --- a/data/mods/TEST_DATA/EOC.json +++ b/data/mods/TEST_DATA/EOC.json @@ -186,6 +186,14 @@ "global": true, "effect": { "u_message": "test recurrence" } }, + { + "type": "effect_on_condition", + "id": "EOC_math_diag_w_vars", + "effect": [ + { "set_string_var": "survival", "target_var": { "global_val": "myskill" } }, + { "math": [ "myskill_math", "=", "u_skill(myskill)" ] } + ] + }, { "type": "effect_on_condition", "id": "EOC_jmath_test", diff --git a/doc/NPCs.md b/doc/NPCs.md index 9c0ded13eacd2..1ae1f2a97d66f 100644 --- a/doc/NPCs.md +++ b/doc/NPCs.md @@ -1429,13 +1429,15 @@ Dialogue functions take single-quoted strings as arguments and can return or man This section is a work in progress as functions are ported from `arithmetic` to `math`. +_function arguments are either `s`trings or `v`[ariables](#variables)_ + | Function | Eval | Assign |Scopes | Description | |----------|------|--------|-------|-------------| -| armor | ✅ | ❌ | u, n | Return the numerical value for a characters armor on a body part, for a damage type.
Example:
`"condition": { "math": [ "u_armor('bash', 'torso')", ">=", "5"] }`| -| game_option | ✅ | ❌ | N/A
(global) | Return the numerical value of a game option
Example:
`"condition": { "math": [ "game_option('NPC_SPAWNTIME')", ">=", "5"] }`| -| pain | ✅ | ✅ | u, n | Return or set pain
Example:
`{ "math": [ "n_pain()", "=", "u_pain() + 9000" ] }`| -| skill | ✅ | ✅ | u, n | Return or set skill level
Example:
`"condition": { "math": [ "u_skill('driving')", ">=", "5"] }`| -| weather | ✅ | ✅ | N/A
(global) | Return or set a weather aspect

Aspect must be one of:
`temperature` (in Kelvin),
`humidity` (as percentage),
`pressure` (in millibar),
`windpower` (in mph).

Temperature conversion functions are available: `celsius()`, `fahrenheit()`, `from_celsius()`, and `from_fahrenheit()`.

Examples:
`{ "math": [ "weather('temperature')", "<", "from_fahrenheit( 33 )" ] }`
`{ "math": [ "fahrenheit( weather('temperature') )", "==", "21" ] }`| +| armor(`s`/`v`,`s`/`v`) | ✅ | ❌ | u, n | Return the numerical value for a characters armor on a body part, for a damage type.
Example:
`"condition": { "math": [ "u_armor('bash', 'torso')", ">=", "5"] }`
`"condition": { "math": [ "u_armor(u_dmgtype, u_bp)", ">=", "5"] }`| +| game_option(`s`) | ✅ | ❌ | N/A
(global) | Return the numerical value of a game option
Example:
`"condition": { "math": [ "game_option('NPC_SPAWNTIME')", ">=", "5"] }`| +| pain() | ✅ | ✅ | u, n | Return or set pain
Example:
`{ "math": [ "n_pain()", "=", "u_pain() + 9000" ] }`| +| skill(`s`/`v`) | ✅ | ✅ | u, n | Return or set skill level
Example:
`"condition": { "math": [ "u_skill('driving')", ">=", "5"] }`
`"condition": { "math": [ "u_skill(someskill)", ">=", "5"] }`| +| weather(`s`) | ✅ | ✅ | N/A
(global) | Return or set a weather aspect

Aspect must be one of:
`temperature` (in Kelvin),
`humidity` (as percentage),
`pressure` (in millibar),
`windpower` (in mph).

Temperature conversion functions are available: `celsius()`, `fahrenheit()`, `from_celsius()`, and `from_fahrenheit()`.

Examples:
`{ "math": [ "weather('temperature')", "<", "from_fahrenheit( 33 )" ] }`
`{ "math": [ "fahrenheit( weather('temperature') )", "==", "21" ] }`| ##### u_val shim There is a `val()` shim available that can cover the missing arithmetic functions from `u_val` and `npc_val`: diff --git a/src/cata_utility.h b/src/cata_utility.h index 7615651f7fd11..177b4d0ce791d 100644 --- a/src/cata_utility.h +++ b/src/cata_utility.h @@ -667,4 +667,12 @@ T aggregate( const std::vector &values, aggregate_type agg_func ) } } +// overload pattern for std::variant from https://en.cppreference.com/w/cpp/utility/variant/visit +template +struct overloaded : Ts... { + using Ts::operator()...; +}; +template +explicit overloaded( Ts... ) -> overloaded; + #endif // CATA_SRC_CATA_UTILITY_H diff --git a/src/math_parser.cpp b/src/math_parser.cpp index ae769cdb9858c..ff9e8c35e0d8b 100644 --- a/src/math_parser.cpp +++ b/src/math_parser.cpp @@ -289,8 +289,8 @@ class math_exp::math_exp_impl void maybe_first_argument(); void error( std::string_view str, std::string_view what ); void validate_string( std::string_view str, std::string_view label, std::string_view badlist ); - std::vector _get_strings( std::vector const ¶ms, - size_t nparams ) const; + std::vector _get_diag_vals( std::vector ¶ms, + size_t nparams ) const; }; void math_exp::math_exp_impl::maybe_first_argument() @@ -474,12 +474,12 @@ void math_exp::math_exp_impl::new_func() std::visit( overloaded{ [¶ms, nparams, this]( scoped_diag_eval const & v ) { - std::vector const strings = _get_strings( params, nparams ); + std::vector const strings = _get_diag_vals( params, nparams ); output.emplace( std::in_place_type_t(), v.df->f( v.scope, strings ) ); }, [¶ms, nparams, this]( scoped_diag_ass const & v ) { - std::vector const strings = _get_strings( params, nparams ); + std::vector const strings = _get_diag_vals( params, nparams ); output.emplace( std::in_place_type_t(), v.df->f( v.scope, strings ) ); }, [¶ms, this]( pmath_func v ) @@ -500,20 +500,30 @@ void math_exp::math_exp_impl::new_func() } } -std::vector math_exp::math_exp_impl::_get_strings( std::vector const - ¶ms, size_t nparams ) const +std::vector math_exp::math_exp_impl::_get_diag_vals( std::vector ¶ms, + size_t nparams ) const { - std::vector strings( nparams ); - std::transform( params.begin(), params.end(), strings.begin(), [this]( thingie const & e ) { - if( std::holds_alternative( e.data ) ) { - return std::get( e.data ); - } - throw std::invalid_argument( string_format( - "Parameters for %s() must be strings contained in single quotes", - arity.top().sym.data() ) ); - return std::string{}; - } ); - return strings; + std::vector vals( nparams ); + for( decltype( vals )::size_type i = 0; i < params.size(); i++ ) { + std::visit( overloaded{ + [&vals, i]( std::string & v ) + { + vals[i].data.emplace( std::move( v ) ); + }, + [&vals, i]( var & v ) + { + vals[i].data.emplace( std::move( v.varinfo ) ); + }, + [this]( auto const &/* v */ ) + { + throw std::invalid_argument( + string_format( "Parameters for %s() must be variables or strings contained in single quotes", + arity.top().sym ) ); + }, + }, + params[i].data ); + } + return vals; } void math_exp::math_exp_impl::new_oper() diff --git a/src/math_parser_diag.cpp b/src/math_parser_diag.cpp index bb3bbee81759c..f1138177852f0 100644 --- a/src/math_parser_diag.cpp +++ b/src/math_parser_diag.cpp @@ -4,10 +4,10 @@ #include #include +#include "cata_utility.h" #include "condition.h" #include "dialogue.h" #include "math_parser_shim.h" -#include "mission.h" #include "options.h" #include "units.h" #include "weather.h" @@ -26,8 +26,43 @@ bool is_beta( char scope ) } } // namespace +std::string diag_value::str() const +{ + return std::string{ sv() }; +} + +std::string_view diag_value::sv() const +{ + return std::visit( overloaded{ + []( std::string const & v ) -> std::string const & + { + return v; + }, + []( var_info const & /* v */ ) -> std::string const & + { + throw std::invalid_argument( "Variables are not supported in this context" ); + }, + }, + data ); +} + +std::string diag_value::eval( dialogue const &d ) const +{ + return std::visit( overloaded{ + []( std::string const & v ) + { + return v; + }, + [&d]( var_info const & v ) + { + return read_var_value( v, d ); + }, + }, + data ); +} + std::function u_val( char scope, - std::vector const ¶ms ) + std::vector const ¶ms ) { kwargs_shim const shim( params, scope ); try { @@ -41,7 +76,7 @@ std::function u_val( char scope, } std::function u_val_ass( char scope, - std::vector const ¶ms ) + std::vector const ¶ms ) { kwargs_shim const shim( params, scope ); try { @@ -53,25 +88,25 @@ std::function u_val_ass( char scope, } std::function option_eval( char /* scope */, - std::vector const ¶ms ) + std::vector const ¶ms ) { - return[option = params[0]]( dialogue const & ) { + return[option = params[0].str()]( dialogue const & ) { return get_option( option ); }; } std::function armor_eval( char scope, - std::vector const ¶ms ) + std::vector const ¶ms ) { return[type = params[0], bpid = params[1], beta = is_beta( scope )]( dialogue const & d ) { - damage_type_id dt( type ); - bodypart_id bp( bpid ); + damage_type_id dt( type.eval( d ) ); + bodypart_id bp( bpid.eval( d ) ); return d.actor( beta )->armor_at( dt, bp ); }; } std::function pain_eval( char scope, - std::vector const &/* params */ ) + std::vector const &/* params */ ) { return [beta = is_beta( scope )]( dialogue const & d ) { return d.actor( beta )->pain_cur(); @@ -79,7 +114,7 @@ std::function pain_eval( char scope, } std::function pain_ass( char scope, - std::vector const &/* params */ ) + std::vector const &/* params */ ) { return [beta = is_beta( scope )]( dialogue const & d, double val ) { d.actor( beta )->set_pain( val ); @@ -87,23 +122,23 @@ std::function pain_ass( char scope, } std::function skill_eval( char scope, - std::vector const ¶ms ) + std::vector const ¶ms ) { - return [beta = is_beta( scope ), sid = skill_id( params[0] )]( dialogue const & d ) { - return d.actor( beta )->get_skill_level( sid ); + return [beta = is_beta( scope ), sid = params[0] ]( dialogue const & d ) { + return d.actor( beta )->get_skill_level( skill_id( sid.eval( d ) ) ); }; } std::function skill_ass( char scope, - std::vector const ¶ms ) + std::vector const ¶ms ) { - return [beta = is_beta( scope ), sid = skill_id( params[0] )]( dialogue const & d, double val ) { - return d.actor( beta )->set_skill_level( sid, val ); + return [beta = is_beta( scope ), sid = params[0] ]( dialogue const & d, double val ) { + return d.actor( beta )->set_skill_level( skill_id( sid.eval( d ) ), val ); }; } std::function weather_eval( char /* scope */, - std::vector const ¶ms ) + std::vector const ¶ms ) { if( params[0] == "temperature" ) { return []( dialogue const & ) { @@ -125,11 +160,11 @@ std::function weather_eval( char /* scope */, return get_weather().weather_precise->pressure; }; } - throw std::invalid_argument( "Unknown weather aspect " + params[0] ); + throw std::invalid_argument( "Unknown weather aspect " + params[0].str() ); } std::function weather_ass( char /* scope */, - std::vector const ¶ms ) + std::vector const ¶ms ) { if( params[0] == "temperature" ) { return []( dialogue const &, double val ) { @@ -156,5 +191,5 @@ std::function weather_ass( char /* scope */, get_weather().clear_temp_cache(); }; } - throw std::invalid_argument( "Unknown weather aspect " + params[0] ); + throw std::invalid_argument( "Unknown weather aspect " + params[0].str() ); } diff --git a/src/math_parser_diag.h b/src/math_parser_diag.h index 6c0d0719dba04..6632e68928b23 100644 --- a/src/math_parser_diag.h +++ b/src/math_parser_diag.h @@ -5,8 +5,11 @@ #include #include #include +#include #include +#include "dialogue_helpers.h" + struct dialogue; struct dialogue_func { dialogue_func( std::string_view s_, std::string_view sc_, int n_ ) : symbol( s_ ), @@ -16,9 +19,23 @@ struct dialogue_func { int num_params{}; }; +struct diag_value { + std::string_view sv() const; + std::string str() const; + std::string eval( dialogue const &d ) const; + + using impl_t = std::variant; + impl_t data; +}; + +constexpr bool operator==( diag_value const &lhs, std::string_view rhs ) +{ + return std::holds_alternative( lhs.data ) && std::get( lhs.data ) == rhs; +} + struct dialogue_func_eval : dialogue_func { using f_t = std::function ( * )( char scope, - std::vector const & ); + std::vector const & ); dialogue_func_eval( std::string_view s_, std::string_view sc_, int n_, f_t f_ ) : dialogue_func( s_, sc_, n_ ), f( f_ ) {} @@ -28,7 +45,7 @@ struct dialogue_func_eval : dialogue_func { struct dialogue_func_ass : dialogue_func { using f_t = std::function ( * )( char scope, - std::vector const & ); + std::vector const & ); dialogue_func_ass( std::string_view s_, std::string_view sc_, int n_, f_t f_ ) : dialogue_func( s_, sc_, n_ ), f( f_ ) {} @@ -40,31 +57,31 @@ using pdiag_func_eval = dialogue_func_eval const *; using pdiag_func_ass = dialogue_func_ass const *; std::function u_val( char scope, - std::vector const ¶ms ); + std::vector const ¶ms ); std::function u_val_ass( char scope, - std::vector const ¶ms ); + std::vector const ¶ms ); std::function option_eval( char scope, - std::vector const ¶ms ); + std::vector const ¶ms ); std::function armor_eval( char scope, - std::vector const ¶ms ); + std::vector const ¶ms ); std::function pain_eval( char scope, - std::vector const &/* params */ ); + std::vector const &/* params */ ); std::function pain_ass( char scope, - std::vector const &/* params */ ); + std::vector const &/* params */ ); std::function skill_eval( char scope, - std::vector const ¶ms ); + std::vector const ¶ms ); std::function skill_ass( char scope, - std::vector const ¶ms ); + std::vector const ¶ms ); std::function weather_eval( char /* scope */, - std::vector const ¶ms ); + std::vector const ¶ms ); std::function weather_ass( char /* scope */, - std::vector const ¶ms ); + std::vector const ¶ms ); inline std::array const dialogue_eval_f{ dialogue_func_eval{ "val", "un", -1, u_val }, diff --git a/src/math_parser_impl.h b/src/math_parser_impl.h index 1e6b95c60c089..37c1fe09f30ff 100644 --- a/src/math_parser_impl.h +++ b/src/math_parser_impl.h @@ -8,6 +8,7 @@ #include #include +#include "cata_utility.h" #include "debug.h" #include "dialogue_helpers.h" #include "math_parser_diag.h" @@ -131,13 +132,6 @@ struct thingie { impl_t data; }; -// overload pattern from https://en.cppreference.com/w/cpp/utility/variant/visit -template -struct overloaded : Ts... { - using Ts::operator()...; -}; -template -explicit overloaded( Ts... ) -> overloaded; constexpr double thingie::eval( dialogue &d ) const { return std::visit( overloaded{ diff --git a/src/math_parser_shim.cpp b/src/math_parser_shim.cpp index 02b4c35ed8c08..4375dfa71c8fa 100644 --- a/src/math_parser_shim.cpp +++ b/src/math_parser_shim.cpp @@ -1,18 +1,18 @@ #include "math_parser_shim.h" +#include "math_parser_diag.h" #include "math_parser_func.h" #include #include -#include "condition.h" #include "json_loader.h" #include "string_formatter.h" -kwargs_shim::kwargs_shim( std::vector const &tokens, char scope ) +kwargs_shim::kwargs_shim( std::vector const &tokens, char scope ) { bool positional = false; - for( std::string_view const token : tokens ) { - std::vector parts = tokenize( token, ":", false ); + for( diag_value const &token : tokens ) { + std::vector parts = tokenize( token.sv(), ":", false ); if( parts.size() == 1 && !positional ) { kwargs.emplace( // NOLINTNEXTLINE(cata-translate-string-literal): not a user-visible string @@ -22,7 +22,7 @@ kwargs_shim::kwargs_shim( std::vector const &tokens, char scope ) } else if( parts.size() == 2 ) { kwargs.emplace( parts[0], parts[1] ); } else { - debugmsg( "Too many parts in token %.*s", token.size(), token.data() ); + debugmsg( "Too many parts in token %s", token.sv() ); } } } diff --git a/src/math_parser_shim.h b/src/math_parser_shim.h index b839730780fd8..c58c56d869489 100644 --- a/src/math_parser_shim.h +++ b/src/math_parser_shim.h @@ -6,14 +6,16 @@ #include #include -#include "json.h" +#include "flexbuffer_json.h" + +struct diag_value; // temporary shim that pretends to be a JsonObject for the purpose of reusing code between the new // "math" and the old "arithmetic"/"compare_num"/"u_val" class kwargs_shim { public: - explicit kwargs_shim( std::vector const &tokens, char scope ); + explicit kwargs_shim( std::vector const &tokens, char scope ); std::string get_string( std::string_view key ) const; double get_float( std::string_view key, double def = 0 ) const; diff --git a/tests/eoc_test.cpp b/tests/eoc_test.cpp index d7ae70545f62f..6727518f6e7e3 100644 --- a/tests/eoc_test.cpp +++ b/tests/eoc_test.cpp @@ -22,6 +22,8 @@ static const effect_on_condition_id effect_on_condition_EOC_math_armor( "EOC_math_armor" ); static const effect_on_condition_id effect_on_condition_EOC_math_diag_assign( "EOC_math_diag_assign" ); +static const effect_on_condition_id +effect_on_condition_EOC_math_diag_w_vars( "EOC_math_diag_w_vars" ); static const effect_on_condition_id effect_on_condition_EOC_math_duration( "EOC_math_duration" ); static const effect_on_condition_id effect_on_condition_EOC_math_switch_math( "EOC_math_switch_math" ); @@ -51,6 +53,8 @@ effect_on_condition_EOC_stored_condition_test( "EOC_stored_condition_test" ); static const effect_on_condition_id effect_on_condition_EOC_teleport_test( "EOC_teleport_test" ); static const mtype_id mon_zombie( "mon_zombie" ); + +static const skill_id skill_survival( "survival" ); namespace { void complete_activity( Character &u ) @@ -160,6 +164,19 @@ TEST_CASE( "EOC_jmath", "[eoc][math_parser]" ) CHECK( std::stod( globvars.get_global_value( "npctalk_var_blorgy" ) ) == Approx( 7 ) ); } +TEST_CASE( "EOC_diag_with_vars", "[eoc][math_parser]" ) +{ + global_variables &globvars = get_globals(); + globvars.clear_global_values(); + REQUIRE( globvars.get_global_value( "npctalk_var_myskill_math" ).empty() ); + dialogue d( get_talker_for( get_avatar() ), std::make_unique() ); + effect_on_condition_EOC_math_diag_w_vars->activate( d ); + CHECK( std::stod( globvars.get_global_value( "npctalk_var_myskill_math" ) ) == Approx( 0 ) ); + get_avatar().set_skill_level( skill_survival, 3 ); + effect_on_condition_EOC_math_diag_w_vars->activate( d ); + CHECK( std::stod( globvars.get_global_value( "npctalk_var_myskill_math" ) ) == Approx( 3 ) ); +} + TEST_CASE( "EOC_transform_radius", "[eoc][timed_event]" ) { // no introspection :( diff --git a/tests/math_parser_test.cpp b/tests/math_parser_test.cpp index a07d8ba2c2f03..9f9b06a7cd1c5 100644 --- a/tests/math_parser_test.cpp +++ b/tests/math_parser_test.cpp @@ -9,6 +9,7 @@ #include "math_parser.h" #include "math_parser_func.h" +static const skill_id skill_survival( "survival" ); static const spell_id spell_test_spell_pew( "test_spell_pew" ); // NOLINTNEXTLINE(readability-function-cognitive-complexity): false positive @@ -176,8 +177,8 @@ TEST_CASE( "math_parser_dialogue_integration", "[math_parser]" ) // reading scoped values with u_val shim std::string dmsg = capture_debugmsg_during( [&testexp]() { - CHECK_FALSE( testexp.parse( "u_val( 3 )" ) ); // only quoted strings accepted as parameters - CHECK_FALSE( testexp.parse( "u_val( stamina )" ) ); + CHECK_FALSE( testexp.parse( "u_val( 3 )" ) ); // only quoted strings or variables accepted + CHECK_FALSE( testexp.parse( "u_val(myval)" ) ); // this function doesn't support variables CHECK_FALSE( testexp.parse( "val( 'stamina' )" ) ); // invalid scope for this function } ); CHECK( testexp.parse( "u_val('stamina')" ) ); @@ -190,6 +191,12 @@ TEST_CASE( "math_parser_dialogue_integration", "[math_parser]" ) CHECK( testexp.parse( "u_val('time: 1 m')" ) ); // test get_member() in shim CHECK( testexp.eval( d ) == 60 ); + // evaluating string variables in dialogue functions + globvars.set_global_value( "npctalk_var_someskill", "survival" ); + CHECK( testexp.parse( "u_skill(someskill)" ) ); + get_avatar().set_skill_level( skill_survival, 3 ); + CHECK( testexp.eval( d ) == 3 ); + // assignment to scoped variables CHECK( testexp.parse( "u_testvar", true ) ); testexp.assign( d, 159 );