From e4af20a6499514cf89a20f9bf639100d47f03756 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Sun, 8 Dec 2019 21:26:49 -0500 Subject: [PATCH 01/11] Detect recipe loops for comestibles Recipe loops are problematic (see last commit). There are none now in the core game, but mods might add them. This code detects recipe loops and marks the items contained within them. Such loops are considered an error reported via debugmsg. This implementation is a little slower than I would prefer. A slightly more advanced cycle-finding algorithm could be used to make it faster. --- src/item_factory.cpp | 13 +++++++ src/itype.h | 3 ++ src/recipe_dictionary.cpp | 78 +++++++++++++++++++++++++++++++++++++++ src/recipe_dictionary.h | 5 +++ 4 files changed, 99 insertions(+) diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 11695c5389478..8560c6cbbea2a 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -440,6 +440,19 @@ void Item_factory::finalize() finalize_pre( *e.second ); finalize_post( *e.second ); } + + // for each item register all (non-obsolete) potential recipes + for( const std::pair &p : recipe_dict ) { + const recipe &rec = p.second; + if( rec.obsolete ) { + continue; + } + const itype_id &result = rec.result(); + auto it = m_templates.find( result ); + if( it != m_templates.end() ) { + it->second.recipes.push_back( p.first ); + } + } } void Item_factory::finalize_item_blacklist() diff --git a/src/itype.h b/src/itype.h index 2dc035a840237..40a2c6186ce42 100644 --- a/src/itype.h +++ b/src/itype.h @@ -965,6 +965,9 @@ struct itype { /** What items can be used to repair this item? @see Item_factory::finalize */ std::set repair; + /** What recipes can make this item */ + std::vector recipes; + /** What faults (if any) can occur */ std::set faults; diff --git a/src/recipe_dictionary.cpp b/src/recipe_dictionary.cpp index 81c6c951f919b..71539f37f07ef 100644 --- a/src/recipe_dictionary.cpp +++ b/src/recipe_dictionary.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "cata_utility.h" @@ -332,6 +333,11 @@ std::map::const_iterator recipe_dictionary::end() const return recipes.end(); } +bool recipe_dictionary::is_item_on_loop( const itype_id &i ) const +{ + return items_on_loops.count( i ); +} + void recipe_dictionary::finalize_internal( std::map &obj ) { for( auto &elem : obj ) { @@ -357,6 +363,75 @@ void recipe_dictionary::finalize_internal( std::map &obj ) } ); } +void recipe_dictionary::find_items_on_loops() +{ + // Check for infinite recipe loops in food (which are problematic for + // nutrient calculations). + // + // Start by building a directed graph of itypes to potential components of + // those itypes. + items_on_loops.clear(); + std::unordered_map> potential_components_of; + for( const itype *i : item_controller->all() ) { + if( !i->comestible || i->item_tags.count( "NUTRIENT_OVERRIDE" ) ) { + continue; + } + std::vector &potential_components = potential_components_of[i->get_id()]; + for( const recipe_id &rec : i->recipes ) { + const requirement_data requirements = rec->requirements(); + const requirement_data::alter_item_comp_vector &component_requirements = + requirements.get_components(); + + for( const std::vector &component_options : component_requirements ) { + for( const item_comp &component_option : component_options ) { + potential_components.push_back( component_option.type ); + } + } + } + } + + // Now check that graph for loops + for( const auto &p : potential_components_of ) { + const itype_id &root = p.first; + std::unordered_map reachable_via; + std::stack> to_check; + to_check.push( root ); + while( !to_check.empty() && !recipe_dict.items_on_loops.count( root ) ) { + itype_id next = to_check.top(); + to_check.pop(); + auto it = potential_components_of.find( next ); + if( it == potential_components_of.end() ) { + continue; + } + for( const itype_id &potential_component : it->second ) { + if( potential_component == root ) { + std::string error_message = + "loop in comestible recipes detected: " + potential_component; + itype_id on_path = next; + while( true ) { + error_message += " -> " + on_path; + items_on_loops.insert( on_path ); + if( on_path == root ) { + break; + } + on_path = reachable_via[on_path]; + } + error_message += ". Such loops can be broken by either removing or altering " + "recipes or marking one of the items involved with the NUTRIENT_OVERRIDE " + "flag"; + debugmsg( error_message ); + break; + } else { + bool inserted = reachable_via.emplace( potential_component, next ).second; + if( inserted ) { + to_check.push( potential_component ); + } + } + } + } + } +} + void recipe_dictionary::finalize() { DynamicDataLoader::get_instance().load_deferred( deferred ); @@ -416,6 +491,8 @@ void recipe_dictionary::finalize() recipe_dict.blueprints.insert( &e.second ); } } + + recipe_dict.find_items_on_loops(); } void recipe_dictionary::reset() @@ -424,6 +501,7 @@ void recipe_dictionary::reset() recipe_dict.autolearn.clear(); recipe_dict.recipes.clear(); recipe_dict.uncraft.clear(); + recipe_dict.items_on_loops.clear(); } void recipe_dictionary::delete_if( const std::function &pred ) diff --git a/src/recipe_dictionary.h b/src/recipe_dictionary.h index dcd0bafaaf1c0..9e9ab4ee6d8f6 100644 --- a/src/recipe_dictionary.h +++ b/src/recipe_dictionary.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "recipe.h" @@ -39,6 +40,8 @@ class recipe_dictionary std::map::const_iterator begin() const; std::map::const_iterator end() const; + bool is_item_on_loop( const itype_id & ) const; + /** Returns disassembly recipe (or null recipe if no match) */ static const recipe &get_uncraft( const itype_id &id ); @@ -63,8 +66,10 @@ class recipe_dictionary std::map uncraft; std::set autolearn; std::set blueprints; + std::unordered_set items_on_loops; static void finalize_internal( std::map &obj ); + void find_items_on_loops(); }; extern recipe_dictionary recipe_dict; From 4afe91bdec1bfb54172d8d157dc8a23d49a5c122 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Sun, 8 Dec 2019 21:29:23 -0500 Subject: [PATCH 02/11] Support the computation of nutrient ranges These functions calculate the range of possible nutrients a particular item or recipe might produce, depending on which ingredients are used within it. --- src/consumption.cpp | 110 +++++++++++++++++++++++++++++++++++++++++++- src/player.h | 6 +++ src/stomach.cpp | 54 ++++++++++++++++++++++ src/stomach.h | 12 +++++ 4 files changed, 180 insertions(+), 2 deletions(-) diff --git a/src/consumption.cpp b/src/consumption.cpp index cd1c12f8b93f2..30645e630cb63 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -23,6 +23,8 @@ #include "morale_types.h" #include "mutation.h" #include "options.h" +#include "recipe.h" +#include "recipe_dictionary.h" #include "stomach.h" #include "string_formatter.h" #include "translations.h" @@ -103,7 +105,9 @@ static int compute_default_effective_kcal( const item &comest, const player &p ) static const trait_id trait_SAPROPHAGE( "SAPROPHAGE" ); static const std::string flag_CARNIVORE_OK( "CARNIVORE_OK" ); - assert( comest.get_comestible() ); + if( !comest.get_comestible() ) { + return 0; + } // As float to avoid rounding too many times float kcal = comest.get_comestible()->default_nutrition.kcal; @@ -145,7 +149,9 @@ static int compute_default_effective_kcal( const item &comest, const player &p ) static std::map compute_default_effective_vitamins( const item &it, const player &p ) { - assert( it.get_comestible() ); + if( !it.get_comestible() ) { + return {}; + } std::map res = it.get_comestible()->default_nutrition.vitamins; @@ -207,6 +213,106 @@ nutrients player::compute_effective_nutrients( const item &comest ) const } } +// Calculate range of nutrients obtainable for a given item when crafted via +// the given recipe +std::pair player::compute_nutrient_range( const item &comest, + const recipe_id &recipe_i ) const +{ + if( !comest.is_comestible() ) { + return {}; + } + + // if item has components, will derive calories from that instead. + if( comest.has_flag( "NUTRIENT_OVERRIDE" ) ) { + nutrients result = compute_default_effective_nutrients( comest, *this ); + return { result, result }; + } else { + nutrients tally_min; + nutrients tally_max; + + const recipe &rec = *recipe_i; + + const requirement_data requirements = rec.requirements(); + const requirement_data::alter_item_comp_vector &component_requirements = + requirements.get_components(); + + for( const std::vector &component_options : component_requirements ) { + nutrients this_min; + nutrients this_max; + bool first = true; + for( const item_comp &component_option : component_options ) { + std::pair component_option_range = + compute_nutrient_range( component_option.type ); + component_option_range.first *= component_option.count; + component_option_range.second *= component_option.count; + + if( first ) { + std::tie( this_min, this_max ) = component_option_range; + first = false; + } else { + this_min.min_in_place( component_option_range.first ); + this_max.max_in_place( component_option_range.second ); + } + } + tally_min += this_min; + tally_max += this_max; + } + + for( const std::pair &byproduct : rec.byproducts ) { + item byproduct_it( byproduct.first, calendar::turn, byproduct.second ); + nutrients byproduct_nutr = compute_default_effective_nutrients( byproduct_it, *this ); + tally_min -= byproduct_nutr; + tally_max -= byproduct_nutr; + } + + int charges = comest.count(); + return { tally_min / charges, tally_max / charges }; + } +} + +// Calculate the range of nturients possible for a given item across all +// possible recipes +std::pair player::compute_nutrient_range( const itype_id &comest_id ) const +{ + const itype *comest = item::find_type( comest_id ); + if( !comest->comestible ) { + return {}; + } + + item comest_it( comest, calendar::turn, 1 ); + // The default nutrients are always a possibility + nutrients min_nutr = compute_default_effective_nutrients( comest_it, *this ); + + if( comest->item_tags.count( "NUTRIENT_OVERRIDE" ) || + recipe_dict.is_item_on_loop( comest->get_id() ) ) { + return { min_nutr, min_nutr }; + } + + nutrients max_nutr = min_nutr; + + for( const recipe_id &rec : comest->recipes ) { + nutrients this_min; + nutrients this_max; + + item result_it = rec->create_result(); + if( result_it.contents.size() == 1 ) { + const item alt_result = result_it.contents.front(); + if( alt_result.typeId() == comest_it.typeId() ) { + result_it = alt_result; + } + } + if( result_it.typeId() != comest_it.typeId() ) { + debugmsg( "When creating recipe result expected %s, got %s\n", + comest_it.typeId(), result_it.typeId() ); + } + std::tie( this_min, this_max ) = compute_nutrient_range( result_it, rec ); + min_nutr.min_in_place( this_min ); + max_nutr.max_in_place( this_max ); + } + + return { min_nutr, max_nutr }; +} + int player::nutrition_for( const item &comest ) const { return compute_effective_nutrients( comest ).kcal / islot_comestible::kcal_per_nutr; diff --git a/src/player.h b/src/player.h index ec2c0e7cdd07d..695ca6106ac58 100644 --- a/src/player.h +++ b/src/player.h @@ -671,6 +671,12 @@ class player : public Character /** Get calorie & vitamin contents for a comestible, taking into * account player traits */ nutrients compute_effective_nutrients( const item & ) const; + /** Get range of possible nutrient content, for a particular recipe, + * depending on choice of ingredients */ + std::pair compute_nutrient_range( const item &, + const recipe_id & ) const; + /** Same, but across arbitrary recipes */ + std::pair compute_nutrient_range( const itype_id & ) const; /** Get vitamin usage rate (minutes per unit) accounting for bionics, mutations and effects */ time_duration vitamin_rate( const vitamin_id &vit ) const; diff --git a/src/stomach.cpp b/src/stomach.cpp index f6cd607dd4393..8828ec66d0a40 100644 --- a/src/stomach.cpp +++ b/src/stomach.cpp @@ -7,6 +7,60 @@ #include "units.h" #include "game.h" #include "itype.h" +#include "vitamin.h" + +void nutrients::min_in_place( const nutrients &r ) +{ + kcal = std::min( kcal, r.kcal ); + for( const std::pair &vit_pair : vitamin::all() ) { + const vitamin_id &vit = vit_pair.first; + int other = r.get_vitamin( vit ); + if( other == 0 ) { + vitamins.erase( vit ); + } else { + auto our_vit = vitamins.find( vit ); + if( our_vit != vitamins.end() ) { + our_vit->second = std::min( our_vit->second, other ); + } + } + } +} + +void nutrients::max_in_place( const nutrients &r ) +{ + kcal = std::max( kcal, r.kcal ); + for( const std::pair &vit_pair : vitamin::all() ) { + const vitamin_id &vit = vit_pair.first; + int other = r.get_vitamin( vit ); + if( other != 0 ) { + int &val = vitamins[vit]; + val = std::max( val, other ); + } + } +} + +int nutrients::get_vitamin( const vitamin_id &vit ) const +{ + auto it = vitamins.find( vit ); + if( it == vitamins.end() ) { + return 0; + } + return it->second; +} + +bool nutrients::operator==( const nutrients &r ) const +{ + if( kcal != r.kcal ) { + return false; + } + for( const std::pair &vit_pair : vitamin::all() ) { + const vitamin_id &vit = vit_pair.first; + if( get_vitamin( vit ) != r.get_vitamin( vit ) ) { + return false; + } + } + return true; +} nutrients &nutrients::operator+=( const nutrients &r ) { diff --git a/src/stomach.h b/src/stomach.h index c2ac702b86096..dc24efee8f0ba 100644 --- a/src/stomach.h +++ b/src/stomach.h @@ -19,6 +19,18 @@ struct nutrients { /** vitamins potentially provided by this comestible (if any) */ std::map vitamins; + /** Replace the values here with the minimum (or maximum) of themselves and the corresponding + * values taken from r. */ + void min_in_place( const nutrients &r ); + void max_in_place( const nutrients &r ); + + int get_vitamin( const vitamin_id & ) const; + + bool operator==( const nutrients &r ) const; + bool operator!=( const nutrients &r ) const { + return !( *this == r ); + } + nutrients &operator+=( const nutrients &r ); nutrients &operator-=( const nutrients &r ); nutrients &operator*=( int r ); From 1bacbe2e453b7c29635c515c3ba3885d94386c68 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Sun, 8 Dec 2019 21:30:30 -0500 Subject: [PATCH 03/11] Display nutrient ranges in item info Currently item info for items with variable nutrient content simply list the default values with a disclaimer that they might be inaccurate. In fact the default values are more or less the only values that *won't* occur when you craft the recipe. Instead, calculate and display the range of possible values for the nutrient content. --- src/crafting_gui.cpp | 9 +----- src/item.cpp | 76 ++++++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/crafting_gui.cpp b/src/crafting_gui.cpp index ca4bf9e5d0850..41c7f84b3ee10 100644 --- a/src/crafting_gui.cpp +++ b/src/crafting_gui.cpp @@ -607,17 +607,10 @@ const recipe *select_crafting_recipe( int &batch_size ) if( last_recipe != current[line] ) { last_recipe = current[line]; tmp = current[line]->create_result(); + tmp.set_var( "recipe_exemplar", last_recipe->ident().str() ); } tmp.info( true, thisItem, count ); - // If it's food that can have variable nutrition, add disclaimer. - // Hidden if the user is attempting to page through components. - if( ( tmp.is_food_container() || tmp.is_food() ) && !tmp.has_flag( "NUTRIENT_OVERRIDE" ) && - display_mode == 0 ) { - ypos += fold_and_print( w_data, point( xpos + 2, ypos ), pane - 2, c_light_gray, - _( "Shown nutrition is estimated, varying with chosen ingredients." ) ); - } - //color needs to be preserved in case part of the previous page was cut off nc_color stored_color = col; if( display_mode > 1 ) { diff --git a/src/item.cpp b/src/item.cpp index 7a1e28e066a03..0569205198a34 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -1322,12 +1322,38 @@ void item::med_info( const item *med_item, std::vector &info, const it void item::food_info( const item *food_item, std::vector &info, const iteminfo_query *parts, int batch, bool debug ) const { - const nutrients nutr = g->u.compute_effective_nutrients( *food_item ); + nutrients min_nutr; + nutrients max_nutr; + + std::string recipe_exemplar = get_var( "recipe_exemplar", "" ); + if( recipe_exemplar.empty() ) { + min_nutr = max_nutr = g->u.compute_effective_nutrients( *food_item ); + } else { + std::tie( min_nutr, max_nutr ) = + g->u.compute_nutrient_range( *food_item, recipe_id( recipe_exemplar ) ); + } + + bool show_nutr = parts->test( iteminfo_parts::FOOD_NUTRITION ) || + parts->test( iteminfo_parts::FOOD_VITAMINS ); + if( min_nutr != max_nutr && show_nutr ) { + info.emplace_back( + "FOOD", _( "Nutrition will vary with chosen ingredients." ) ); + if( recipe_dict.is_item_on_loop( food_item->typeId() ) ) { + info.emplace_back( + "FOOD", _( "Nutrition range cannot be calculated accurately due to " + "recipe loops." ) ); + } + } + const std::string space = " "; - if( nutr.kcal != 0 || food_item->get_comestible()->quench != 0 ) { + if( max_nutr.kcal != 0 || food_item->get_comestible()->quench != 0 ) { if( parts->test( iteminfo_parts::FOOD_NUTRITION ) ) { info.push_back( iteminfo( "FOOD", _( "Calories (kcal): " ), - "", iteminfo::no_newline, nutr.kcal ) ); + "", iteminfo::no_newline, min_nutr.kcal ) ); + if( max_nutr.kcal != min_nutr.kcal ) { + info.push_back( iteminfo( "FOOD", _( "-" ), + "", iteminfo::no_newline, max_nutr.kcal ) ); + } } if( parts->test( iteminfo_parts::FOOD_QUENCH ) ) { info.push_back( iteminfo( "FOOD", space + _( "Quench: " ), @@ -1351,30 +1377,34 @@ void item::food_info( const item *food_item, std::vector &info, info.push_back( iteminfo( "FOOD", _( "Smells like: " ) + food_item->corpse->nname() ) ); } - const std::string required_vits = enumerate_as_string( - nutr.vitamins.begin(), - nutr.vitamins.end(), - []( const std::pair &v ) { + auto format_vitamin = [&]( const std::pair &v, bool display_vitamins ) { + const bool is_vitamin = v.first->type() == vitamin_type::VITAMIN; // only display vitamins that we actually require - return ( g->u.vitamin_rate( v.first ) > 0_turns && v.second != 0 && - v.first->type() == vitamin_type::VITAMIN && !v.first->has_flag( "NO_DISPLAY" ) ) ? - string_format( "%s (%i%%)", v.first.obj().name(), - static_cast( v.second * g->u.vitamin_rate( v.first ) / - 1_days * 100 ) ) : - std::string(); + if( g->u.vitamin_rate( v.first ) == 0_turns || v.second == 0 || + display_vitamins != is_vitamin || v.first->has_flag( "NO_DISPLAY" ) ) { + return std::string(); + } + const double multiplier = g->u.vitamin_rate( v.first ) / 1_days * 100; + const int min_value = min_nutr.get_vitamin( v.first ); + const int max_value = v.second; + const int min_rda = lround( min_value * multiplier ); + const int max_rda = lround( max_value * multiplier ); + const std::string format = min_rda == max_rda ? "%s (%i%%)" : "%s (%i-%i%%)"; + return string_format( format, v.first->name(), min_value, max_value ); + }; + + const std::string required_vits = enumerate_as_string( + max_nutr.vitamins.begin(), + max_nutr.vitamins.end(), + [&]( const std::pair &v ) { + return format_vitamin( v, true ); } ); const std::string effect_vits = enumerate_as_string( - nutr.vitamins.begin(), - nutr.vitamins.end(), - []( const std::pair &v ) { - // only display vitamins that we actually require - return ( g->u.vitamin_rate( v.first ) > 0_turns && v.second != 0 && - v.first->type() != vitamin_type::VITAMIN && !v.first->has_flag( "NO_DISPLAY" ) ) ? - string_format( "%s (%i%%)", v.first.obj().name(), - static_cast( v.second * g->u.vitamin_rate( v.first ) / - 1_days * 100 ) ) : - std::string(); + max_nutr.vitamins.begin(), + max_nutr.vitamins.end(), + [&]( const std::pair &v ) { + return format_vitamin( v, false ); } ); if( !required_vits.empty() && parts->test( iteminfo_parts::FOOD_VITAMINS ) ) { From e7f81905bb7c5a481fe799471c90d511b6350b45 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Tue, 10 Dec 2019 21:33:30 -0500 Subject: [PATCH 04/11] Change namespace of cata_algo.h If we use any namespace, it should be cata (or a subnamespace thereof). It doesn't make sense to be adding other top-level namespaces. --- src/cata_algo.h | 4 ++-- src/npcmove.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cata_algo.h b/src/cata_algo.h index 7337246f30217..a93f27ed18cbf 100644 --- a/src/cata_algo.h +++ b/src/cata_algo.h @@ -5,7 +5,7 @@ #include #include -namespace algo +namespace cata { /** @@ -41,6 +41,6 @@ void sort_by_rating( Iterator begin, Iterator end, RatingFunction rating_func ) } ); } -} // namespace algo +} // namespace cata #endif // CATA_ALGO_H diff --git a/src/npcmove.cpp b/src/npcmove.cpp index bb5afdfad4814..b08ee25201ded 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -2600,7 +2600,7 @@ void npc::move_away_from( const std::vector &spheres, bool no_bashing ) return g->m.passable( elem ); } ); - algo::sort_by_rating( escape_points.begin(), escape_points.end(), [&]( const tripoint & elem ) { + cata::sort_by_rating( escape_points.begin(), escape_points.end(), [&]( const tripoint & elem ) { const int danger = std::accumulate( spheres.begin(), spheres.end(), 0, [&]( const int sum, const sphere & s ) { return sum + std::max( s.radius - rl_dist( elem, s.center ), 0 ); From f98983e5ad5d4e81955b2356a3bab18362724355 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Tue, 10 Dec 2019 21:34:23 -0500 Subject: [PATCH 05/11] Improved cycle finding algorithm Use a O(V+E) cycle-finding algorithm, and factor it out appropriately to clarify the code. --- src/cata_algo.h | 80 +++++++++++++++++++++++++++++++++++++++ src/recipe_dictionary.cpp | 50 ++++++------------------ 2 files changed, 92 insertions(+), 38 deletions(-) diff --git a/src/cata_algo.h b/src/cata_algo.h index a93f27ed18cbf..ef284136f9c87 100644 --- a/src/cata_algo.h +++ b/src/cata_algo.h @@ -3,6 +3,10 @@ #define CATA_ALGO_H #include +#include +#include +#include +#include #include namespace cata @@ -41,6 +45,82 @@ void sort_by_rating( Iterator begin, Iterator end, RatingFunction rating_func ) } ); } +// Implementation detail of below find_cycles +// This explores one branch of the given graph depth-first +template +void find_cycles_impl( + const std::unordered_map> &edges, + const T &v, + std::unordered_set &visited, + std::unordered_map &on_current_branch, + std::vector> &result ) +{ + bool new_vertex = visited.insert( v ).second; + + if( !new_vertex ) { + return; + } + auto it = edges.find( v ); + if( it == edges.end() ) { + return; + } + + for( const T &next_v : it->second ) { + if( next_v == v ) { + // Trivial self-loop + result.push_back( { v } ); + continue; + } + auto previous_match = on_current_branch.find( next_v ); + if( previous_match != on_current_branch.end() ) { + // We have looped back to somewhere along the branch we took to + // reach this vertex, so reconstruct the loop and save it. + std::vector loop; + T on_path = v; + while( true ) { + loop.push_back( on_path ); + if( on_path == next_v ) { + break; + } + on_path = on_current_branch[on_path]; + } + std::reverse( loop.begin(), loop.end() ); + result.push_back( loop ); + } else { + on_current_branch.emplace( next_v, v ); + find_cycles_impl( edges, next_v, visited, on_current_branch, result ); + on_current_branch.erase( next_v ); + } + } +} + +// Find and return a list of all cycles in a directed graph. +// Each T defines a vertex. +// For a vertex a, edges[a] is a list of all the vertices connected by edges +// from a. +// It is acceptable for some vertex keys to be missing from the edges map, if +// those vertices have no out-edges. +// Complexity should be O(V+E) +// Based on https://www.geeksforgeeks.org/detect-cycle-in-a-graph/ +template +std::vector> find_cycles( const std::unordered_map> &edges ) +{ + std::unordered_set visited; + std::unordered_map on_current_branch; + std::vector> result; + + for( const auto &p : edges ) { + const T &root = p.first; + + on_current_branch.emplace( root, root ); + find_cycles_impl( edges, root, visited, on_current_branch, result ); + on_current_branch.erase( root ); + assert( on_current_branch.empty() ); + } + + return result; +} + } // namespace cata #endif // CATA_ALGO_H diff --git a/src/recipe_dictionary.cpp b/src/recipe_dictionary.cpp index 71539f37f07ef..68efd587926c7 100644 --- a/src/recipe_dictionary.cpp +++ b/src/recipe_dictionary.cpp @@ -3,9 +3,9 @@ #include #include #include -#include #include +#include "cata_algo.h" #include "cata_utility.h" #include "init.h" #include "item.h" @@ -391,44 +391,18 @@ void recipe_dictionary::find_items_on_loops() } // Now check that graph for loops - for( const auto &p : potential_components_of ) { - const itype_id &root = p.first; - std::unordered_map reachable_via; - std::stack> to_check; - to_check.push( root ); - while( !to_check.empty() && !recipe_dict.items_on_loops.count( root ) ) { - itype_id next = to_check.top(); - to_check.pop(); - auto it = potential_components_of.find( next ); - if( it == potential_components_of.end() ) { - continue; - } - for( const itype_id &potential_component : it->second ) { - if( potential_component == root ) { - std::string error_message = - "loop in comestible recipes detected: " + potential_component; - itype_id on_path = next; - while( true ) { - error_message += " -> " + on_path; - items_on_loops.insert( on_path ); - if( on_path == root ) { - break; - } - on_path = reachable_via[on_path]; - } - error_message += ". Such loops can be broken by either removing or altering " - "recipes or marking one of the items involved with the NUTRIENT_OVERRIDE " - "flag"; - debugmsg( error_message ); - break; - } else { - bool inserted = reachable_via.emplace( potential_component, next ).second; - if( inserted ) { - to_check.push( potential_component ); - } - } - } + std::vector> loops = cata::find_cycles( potential_components_of ); + for( const std::vector &loop : loops ) { + std::string error_message = + "loop in comestible recipes detected: " + loop.back(); + for( const itype_id &i : loop ) { + error_message += " -> " + i; + items_on_loops.insert( i ); } + error_message += ". Such loops can be broken by either removing or altering " + "recipes or marking one of the items involved with the NUTRIENT_OVERRIDE " + "flag"; + debugmsg( error_message ); } } From 3081118b2878140660f5b7d91539a78901d86107 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Tue, 10 Dec 2019 21:35:33 -0500 Subject: [PATCH 06/11] Tests for cycle-finding algorithm --- tests/algo_test.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/algo_test.cpp diff --git a/tests/algo_test.cpp b/tests/algo_test.cpp new file mode 100644 index 0000000000000..14ba445c960f4 --- /dev/null +++ b/tests/algo_test.cpp @@ -0,0 +1,48 @@ +#define CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER + +#include "cata_algo.h" + +#include "catch/catch.hpp" + +static void check_cycle_finding( std::unordered_map> &g, + std::vector> &expected ) +{ + CAPTURE( g ); + std::vector> loops = cata::find_cycles( g ); + // Canonicalize the list of loops by rotating each to be lexicographically + // least and then sorting. + for( std::vector &loop : loops ) { + auto min = std::min_element( loop.begin(), loop.end() ); + std::rotate( loop.begin(), min, loop.end() ); + } + std::sort( loops.begin(), loops.end() ); + CHECK( loops == expected ); +} + + +TEST_CASE( "find_cycles_small" ) +{ + std::unordered_map> g = { + { 0, { 1 } }, + { 1, { 0 } }, + }; + std::vector> expected = { + { 0, 1 }, + }; + check_cycle_finding( g, expected ); +} +TEST_CASE( "find_cycles" ) +{ + std::unordered_map> g = { + { 0, { 0, 1 } }, + { 1, { 0, 2, 3, 17 } }, + { 2, { 1 } }, + { 3, {} }, + }; + std::vector> expected = { + { 0 }, + { 0, 1 }, + { 1, 2 }, + }; + check_cycle_finding( g, expected ); +} From 14769138c06009e7a4c917fe60192e5db1083586 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Sat, 14 Dec 2019 21:14:12 -0500 Subject: [PATCH 07/11] Add iteminfo tests for nutrient display --- tests/iteminfo_test.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/iteminfo_test.cpp b/tests/iteminfo_test.cpp index ffa2434c47404..a11040fa3062b 100644 --- a/tests/iteminfo_test.cpp +++ b/tests/iteminfo_test.cpp @@ -57,6 +57,7 @@ TEST_CASE( "gun_lists_default_ammo", "[item][iteminfo]" ) "--\n" "Gun is not loaded, so stats below assume the default ammo: wooden broadhead arrow\n" ); } + TEST_CASE( "gun_damage_multiplier_not_integer", "[item][iteminfo]" ) { iteminfo_query q( { iteminfo_parts::GUN_DAMAGE, iteminfo_parts::GUN_DAMAGE_AMMOPROP, @@ -67,3 +68,34 @@ TEST_CASE( "gun_damage_multiplier_not_integer", "[item][iteminfo]" ) "--\n" "Damage: 18*1.25 = 22\n" ); } + +TEST_CASE( "nutrients_in_regular_item", "[item][iteminfo]" ) +{ + iteminfo_query q( { iteminfo_parts::FOOD_NUTRITION, iteminfo_parts::FOOD_VITAMINS, + iteminfo_parts::FOOD_QUENCH + } ); + item i( "icecream" ); + iteminfo_test( + i, q, + "--\n" + "Calories (kcal): 325 " + "Quench: 0\n" + "Vitamins (RDA): Calcium (9%), Vitamin A (9%), and Vitamin B12 (11%)\n" ); +} + +TEST_CASE( "nutrient_ranges_for_recipe_exemplars", "[item][iteminfo]" ) +{ + iteminfo_query q( { iteminfo_parts::FOOD_NUTRITION, iteminfo_parts::FOOD_VITAMINS, + iteminfo_parts::FOOD_QUENCH + } ); + item i( "icecream" ); + i.set_var( "recipe_exemplar", "icecream" ); + iteminfo_test( + i, q, + "--\n" + "Nutrition will vary with chosen ingredients.\n" + "Calories (kcal): 313-" + "469 Quench: 0\n" + "Vitamins (RDA): Calcium (7-28%), Iron (0-83%), " + "Vitamin A (3-11%), Vitamin B12 (2-6%), and Vitamin C (1-85%)\n" ); +} From f5c1962642189e67feae5dae0ff255b44769a031 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 19 Dec 2019 20:50:25 -0500 Subject: [PATCH 08/11] Remove redundant 'else' branch --- src/consumption.cpp | 76 ++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/consumption.cpp b/src/consumption.cpp index 30645e630cb63..aa3299006caa2 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -226,48 +226,48 @@ std::pair player::compute_nutrient_range( const item &come if( comest.has_flag( "NUTRIENT_OVERRIDE" ) ) { nutrients result = compute_default_effective_nutrients( comest, *this ); return { result, result }; - } else { - nutrients tally_min; - nutrients tally_max; - - const recipe &rec = *recipe_i; - - const requirement_data requirements = rec.requirements(); - const requirement_data::alter_item_comp_vector &component_requirements = - requirements.get_components(); - - for( const std::vector &component_options : component_requirements ) { - nutrients this_min; - nutrients this_max; - bool first = true; - for( const item_comp &component_option : component_options ) { - std::pair component_option_range = - compute_nutrient_range( component_option.type ); - component_option_range.first *= component_option.count; - component_option_range.second *= component_option.count; - - if( first ) { - std::tie( this_min, this_max ) = component_option_range; - first = false; - } else { - this_min.min_in_place( component_option_range.first ); - this_max.max_in_place( component_option_range.second ); - } - } - tally_min += this_min; - tally_max += this_max; - } + } + + nutrients tally_min; + nutrients tally_max; + + const recipe &rec = *recipe_i; - for( const std::pair &byproduct : rec.byproducts ) { - item byproduct_it( byproduct.first, calendar::turn, byproduct.second ); - nutrients byproduct_nutr = compute_default_effective_nutrients( byproduct_it, *this ); - tally_min -= byproduct_nutr; - tally_max -= byproduct_nutr; + const requirement_data requirements = rec.requirements(); + const requirement_data::alter_item_comp_vector &component_requirements = + requirements.get_components(); + + for( const std::vector &component_options : component_requirements ) { + nutrients this_min; + nutrients this_max; + bool first = true; + for( const item_comp &component_option : component_options ) { + std::pair component_option_range = + compute_nutrient_range( component_option.type ); + component_option_range.first *= component_option.count; + component_option_range.second *= component_option.count; + + if( first ) { + std::tie( this_min, this_max ) = component_option_range; + first = false; + } else { + this_min.min_in_place( component_option_range.first ); + this_max.max_in_place( component_option_range.second ); + } } + tally_min += this_min; + tally_max += this_max; + } - int charges = comest.count(); - return { tally_min / charges, tally_max / charges }; + for( const std::pair &byproduct : rec.byproducts ) { + item byproduct_it( byproduct.first, calendar::turn, byproduct.second ); + nutrients byproduct_nutr = compute_default_effective_nutrients( byproduct_it, *this ); + tally_min -= byproduct_nutr; + tally_max -= byproduct_nutr; } + + int charges = comest.count(); + return { tally_min / charges, tally_max / charges }; } // Calculate the range of nturients possible for a given item across all From b196175e38097d456719e7189c299bbb5e3f261b Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 19 Dec 2019 21:29:17 -0500 Subject: [PATCH 09/11] Use COOKED and RAW flags in nutrient predictions To predict nutrient content we need to also predict changes to the item flags that occur when cooking. Implement that, and add a test to verify that it works correctly for cooked wild veggies. --- src/consumption.cpp | 30 ++++++++++++++++++++---------- src/player.h | 8 +++++--- tests/comestible_tests.cpp | 21 ++++++++++++++++++++- tests/iteminfo_test.cpp | 2 +- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/consumption.cpp b/src/consumption.cpp index aa3299006caa2..08ea0258df406 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -98,7 +98,8 @@ int player::stomach_capacity() const } // TODO: Move pizza scraping here. -static int compute_default_effective_kcal( const item &comest, const player &p ) +static int compute_default_effective_kcal( const item &comest, const player &p, + const cata::flat_set &extra_flags = {} ) { static const trait_id trait_CARNIVORE( "CARNIVORE" ); static const trait_id trait_GIZZARD( "GIZZARD" ); @@ -113,7 +114,8 @@ static int compute_default_effective_kcal( const item &comest, const player &p ) float kcal = comest.get_comestible()->default_nutrition.kcal; // Many raw foods give less calories, as your body has expends more energy digesting them. - if( comest.has_flag( "RAW" ) && !comest.has_flag( "COOKED" ) ) { + bool cooked = comest.has_flag( "COOKED" ) || extra_flags.count( "COOKED" ); + if( comest.has_flag( "RAW" ) && !cooked ) { kcal *= 0.75f; } @@ -179,9 +181,9 @@ static std::map compute_default_effective_vitamins( // Calculate the effective nutrients for a given item, taking // into account player traits but not item components. static nutrients compute_default_effective_nutrients( const item &comest, - const player &p ) + const player &p, const cata::flat_set &extra_flags = {} ) { - return { compute_default_effective_kcal( comest, p ), + return { compute_default_effective_kcal( comest, p, extra_flags ), compute_default_effective_vitamins( comest, p ) }; } @@ -215,8 +217,9 @@ nutrients player::compute_effective_nutrients( const item &comest ) const // Calculate range of nutrients obtainable for a given item when crafted via // the given recipe -std::pair player::compute_nutrient_range( const item &comest, - const recipe_id &recipe_i ) const +std::pair player::compute_nutrient_range( + const item &comest, const recipe_id &recipe_i, + const cata::flat_set &extra_flags ) const { if( !comest.is_comestible() ) { return {}; @@ -233,6 +236,12 @@ std::pair player::compute_nutrient_range( const item &come const recipe &rec = *recipe_i; + cata::flat_set our_extra_flags = extra_flags; + + if( rec.hot_result() ) { + our_extra_flags.insert( "COOKED" ); + } + const requirement_data requirements = rec.requirements(); const requirement_data::alter_item_comp_vector &component_requirements = requirements.get_components(); @@ -243,7 +252,7 @@ std::pair player::compute_nutrient_range( const item &come bool first = true; for( const item_comp &component_option : component_options ) { std::pair component_option_range = - compute_nutrient_range( component_option.type ); + compute_nutrient_range( component_option.type, our_extra_flags ); component_option_range.first *= component_option.count; component_option_range.second *= component_option.count; @@ -272,7 +281,8 @@ std::pair player::compute_nutrient_range( const item &come // Calculate the range of nturients possible for a given item across all // possible recipes -std::pair player::compute_nutrient_range( const itype_id &comest_id ) const +std::pair player::compute_nutrient_range( + const itype_id &comest_id, const cata::flat_set &extra_flags ) const { const itype *comest = item::find_type( comest_id ); if( !comest->comestible ) { @@ -281,7 +291,7 @@ std::pair player::compute_nutrient_range( const itype_id & item comest_it( comest, calendar::turn, 1 ); // The default nutrients are always a possibility - nutrients min_nutr = compute_default_effective_nutrients( comest_it, *this ); + nutrients min_nutr = compute_default_effective_nutrients( comest_it, *this, extra_flags ); if( comest->item_tags.count( "NUTRIENT_OVERRIDE" ) || recipe_dict.is_item_on_loop( comest->get_id() ) ) { @@ -305,7 +315,7 @@ std::pair player::compute_nutrient_range( const itype_id & debugmsg( "When creating recipe result expected %s, got %s\n", comest_it.typeId(), result_it.typeId() ); } - std::tie( this_min, this_max ) = compute_nutrient_range( result_it, rec ); + std::tie( this_min, this_max ) = compute_nutrient_range( result_it, rec, extra_flags ); min_nutr.min_in_place( this_min ); max_nutr.max_in_place( this_max ); } diff --git a/src/player.h b/src/player.h index 695ca6106ac58..c4097b54f2ec2 100644 --- a/src/player.h +++ b/src/player.h @@ -673,10 +673,12 @@ class player : public Character nutrients compute_effective_nutrients( const item & ) const; /** Get range of possible nutrient content, for a particular recipe, * depending on choice of ingredients */ - std::pair compute_nutrient_range( const item &, - const recipe_id & ) const; + std::pair compute_nutrient_range( + const item &, const recipe_id &, + const cata::flat_set &extra_flags = {} ) const; /** Same, but across arbitrary recipes */ - std::pair compute_nutrient_range( const itype_id & ) const; + std::pair compute_nutrient_range( + const itype_id &, const cata::flat_set &extra_flags = {} ) const; /** Get vitamin usage rate (minutes per unit) accounting for bionics, mutations and effects */ time_duration vitamin_rate( const vitamin_id &vit ) const; diff --git a/tests/comestible_tests.cpp b/tests/comestible_tests.cpp index 1c99a77137e97..9b525f6e5912d 100644 --- a/tests/comestible_tests.cpp +++ b/tests/comestible_tests.cpp @@ -5,7 +5,9 @@ #include #include +#include "avatar.h" #include "catch/catch.hpp" +#include "game.h" #include "itype.h" #include "recipe_dictionary.h" #include "recipe.h" @@ -108,7 +110,7 @@ static item food_or_food_container( const item &it ) return it.is_food_container() ? it.contents.front() : it; } -TEST_CASE( "recipe_permutations" ) +TEST_CASE( "recipe_permutations", "[recipe]" ) { // Are these tests failing? Here's how to fix that: // If the average is over the upper bound, you need to increase the calories for the item @@ -155,3 +157,20 @@ TEST_CASE( "recipe_permutations" ) } } } + +TEST_CASE( "cooked_veggies_get_correct_calorie_prediction", "[recipe]" ) +{ + // This test verifies that predicted calorie ranges properly take into + // account the "RAW"/"COOKED" flags. + const item veggy_wild_cooked( "veggy_wild_cooked" ); + const recipe_id veggy_wild_cooked_recipe( "veggy_wild_cooked" ); + + const avatar &u = g->u; + + nutrients default_nutrition = u.compute_effective_nutrients( veggy_wild_cooked ); + std::pair predicted_nutrition = + u.compute_nutrient_range( veggy_wild_cooked, veggy_wild_cooked_recipe ); + + CHECK( default_nutrition.kcal == predicted_nutrition.first.kcal ); + CHECK( default_nutrition.kcal == predicted_nutrition.second.kcal ); +} diff --git a/tests/iteminfo_test.cpp b/tests/iteminfo_test.cpp index a11040fa3062b..46a1e4ea495e1 100644 --- a/tests/iteminfo_test.cpp +++ b/tests/iteminfo_test.cpp @@ -94,7 +94,7 @@ TEST_CASE( "nutrient_ranges_for_recipe_exemplars", "[item][iteminfo]" ) i, q, "--\n" "Nutrition will vary with chosen ingredients.\n" - "Calories (kcal): 313-" + "Calories (kcal): 317-" "469 Quench: 0\n" "Vitamins (RDA): Calcium (7-28%), Iron (0-83%), " "Vitamin A (3-11%), Vitamin B12 (2-6%), and Vitamin C (1-85%)\n" ); From 853b4b410d56f4c4051a62a6deb705fc44c470d4 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Fri, 20 Dec 2019 07:27:03 -0500 Subject: [PATCH 10/11] Explain curious operator== --- src/stomach.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stomach.cpp b/src/stomach.cpp index 8828ec66d0a40..59717ca597e33 100644 --- a/src/stomach.cpp +++ b/src/stomach.cpp @@ -53,6 +53,8 @@ bool nutrients::operator==( const nutrients &r ) const if( kcal != r.kcal ) { return false; } + // Can't just use vitamins == r.vitamins, because there might be zero + // entries in the map, which need to compare equal to missing entries. for( const std::pair &vit_pair : vitamin::all() ) { const vitamin_id &vit = vit_pair.first; if( get_vitamin( vit ) != r.get_vitamin( vit ) ) { From 065d3556c262a69c9db1904831f6216e4b9196ed Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Fri, 20 Dec 2019 10:08:00 -0500 Subject: [PATCH 11/11] Don't consider blacklisted recipes for nutrients The nutrient predictions were based on a list of recipes cached in each item. It turns out that list could end up with invalid recipe_ids if those recipes became blacklisted. Detect that case and avoid adding such recipes to the list. --- src/item_factory.cpp | 2 +- src/recipe.cpp | 17 +++++++++++++++++ src/recipe.h | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 8560c6cbbea2a..513e76b70619b 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -444,7 +444,7 @@ void Item_factory::finalize() // for each item register all (non-obsolete) potential recipes for( const std::pair &p : recipe_dict ) { const recipe &rec = p.second; - if( rec.obsolete ) { + if( rec.obsolete || rec.will_be_blacklisted() ) { continue; } const itype_id &result = rec.result(); diff --git a/src/recipe.cpp b/src/recipe.cpp index 23979151f4f80..c17b6d77a9260 100644 --- a/src/recipe.cpp +++ b/src/recipe.cpp @@ -483,6 +483,23 @@ std::string recipe::result_name() const return name; } +bool recipe::will_be_blacklisted() const +{ + if( requirements_.is_blacklisted() ) { + return true; + } + + auto any_is_blacklisted = []( const std::vector> &reqs ) { + auto req_is_blacklisted = []( const std::pair &req ) { + return req.first->is_blacklisted(); + }; + + return std::any_of( reqs.begin(), reqs.end(), req_is_blacklisted ); + }; + + return any_is_blacklisted( reqs_internal ) || any_is_blacklisted( reqs_external ); +} + std::function recipe::get_component_filter() const { const item result = create_result(); diff --git a/src/recipe.h b/src/recipe.h index ea75db9f922a5..c5170b370e963 100644 --- a/src/recipe.h +++ b/src/recipe.h @@ -62,6 +62,10 @@ class recipe return requirements_.is_blacklisted(); } + // Slower equivalent of is_blacklisted that needs to be used before + // recipe finalization happens + bool will_be_blacklisted() const; + std::function get_component_filter() const; /** Prevent this recipe from ever being added to the player's learned recipies ( used for special NPC crafting ) */