Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: stack comestibles regardless of rot #4320

Merged
merged 10 commits into from
Mar 10, 2024
5 changes: 5 additions & 0 deletions src/cached_item_options.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "cached_item_options.h"

merge_comestible_t merge_comestible_mode = merge_comestible_t::merge_legacy;

float similarity_threshold;
24 changes: 24 additions & 0 deletions src/cached_item_options.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once
#ifndef CATA_SRC_CACHED_ITEM_OPTIONS_H
#define CATA_SRC_CACHED_ITEM_OPTIONS_H

enum class merge_comestible_t {
merge_legacy,
merge_liquid,
merge_all,
};

/**
* Merge similar comestibles. Legacy: default behavior. Liquid: Merge only liquid comestibles. All: Merge all comestibles.
*/
extern merge_comestible_t merge_comestible_mode;

/**
* Limit maximum allowed staleness difference when merging comestibles.
* The lower the value, the more similar the items must be to merge.
* 0.0: Only merge identical items.
* 1.0: Merge comestibles regardless of its freshness.
*/
extern float similarity_threshold;

#endif // CATA_SRC_CACHED_ITEM_OPTIONS_H
73 changes: 62 additions & 11 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "bodypart.h"
#include "cata_utility.h"
#include "catacharset.h"
#include "cached_item_options.h"
#include "character.h"
#include "character_id.h"
#include "character_encumbrance.h"
Expand Down Expand Up @@ -1003,13 +1004,28 @@ bool item::stacks_with( const item &rhs, bool check_components, bool skip_type_c
if( goes_bad() && rhs.goes_bad() ) {
// Stack items that fall into the same "bucket" of freshness.
// Distant buckets are larger than near ones.
std::pair<int, clipped_unit> my_clipped_time_to_rot =
clipped_time( get_shelf_life() - rot );
std::pair<int, clipped_unit> other_clipped_time_to_rot =
clipped_time( rhs.get_shelf_life() - rhs.rot );
if( my_clipped_time_to_rot != other_clipped_time_to_rot ) {
return false;

switch( merge_comestible_mode ) {
case merge_comestible_t::merge_legacy: {
std::pair<int, clipped_unit> my_clipped_time_to_rot =
clipped_time( get_shelf_life() - rot );
std::pair<int, clipped_unit> other_clipped_time_to_rot =
clipped_time( rhs.get_shelf_life() - rhs.rot );
if( my_clipped_time_to_rot != other_clipped_time_to_rot ) {
return false;
}
}
break;
case merge_comestible_t::merge_liquid: {
if( !made_of( LIQUID ) || !rhs.made_of( LIQUID ) ) {
return false;
}
}
[[fallthrough]];
default:
return std::abs( get_relative_rot() - rhs.get_relative_rot() ) <= similarity_threshold;
}

if( rotten() != rhs.rotten() ) {
// just to be safe that rotten and unrotten food is *never* stacked.
return false;
Expand Down Expand Up @@ -1046,6 +1062,20 @@ bool item::stacks_with( const item &rhs, bool check_components, bool skip_type_c
return contents.stacks_with( rhs.contents );
}

namespace
{

time_duration weighted_averaged_rot( const item *a, const item *b )
{
const int base_charges = a->charges + b->charges;

return base_charges > 0
? ( a->get_rot() * a->charges + b->get_rot() * b->charges ) / base_charges
: 0_seconds;
}

} // namespace

bool item::merge_charges( detached_ptr<item> &&rhs, bool force )
{
if( this == &*rhs ) {
Expand All @@ -1059,6 +1089,8 @@ bool item::merge_charges( detached_ptr<item> &&rhs, bool force )
safe_reference<item>::merge( this, &*rhs );
detached_ptr<item> del = std::move( rhs );

const auto new_rot = weighted_averaged_rot( this, &obj );

// Prevent overflow when either item has "near infinite" charges.
if( charges >= INFINITE_CHARGES / 2 || obj.charges >= INFINITE_CHARGES / 2 ) {
charges = INFINITE_CHARGES;
Expand All @@ -1070,6 +1102,10 @@ bool item::merge_charges( detached_ptr<item> &&rhs, bool force )
( obj.item_counter ) * obj.charges ) / ( charges + obj.charges );
}
charges += obj.charges;

rot = new_rot;
set_age( std::max( age(), obj.age() ) );

return true;
}

Expand Down Expand Up @@ -1720,7 +1756,7 @@ void item::basic_info( std::vector<iteminfo> &info, const iteminfo_query *parts,
info.emplace_back( "BASE", _( "rot (turns): " ),
"", iteminfo::lower_is_better,
to_turns<int>( food->rot ) );
info.emplace_back( "BASE", space + _( "max rot (turns): " ),
info.emplace_back( "BASE", space + _( "shelf life (turns): " ),
"", iteminfo::lower_is_better,
to_turns<int>( food->get_shelf_life() ) );
info.emplace_back( "BASE", _( "last rot: " ),
Expand Down Expand Up @@ -3494,7 +3530,7 @@ void item::combat_info( std::vector<iteminfo> &info, const iteminfo_query *parts
}

void item::contents_info( std::vector<iteminfo> &info, const iteminfo_query *parts, int batch,
bool /*debug*/ ) const
bool debug ) const
{
if( contents.empty() || !parts->test( iteminfo_parts::DESCRIPTION_CONTENTS ) ) {
return;
Expand Down Expand Up @@ -3572,6 +3608,22 @@ void item::contents_info( std::vector<iteminfo> &info, const iteminfo_query *par
}
info.emplace_back( "DESCRIPTION", description.translated() );
}

if( debug && contents_item && contents_item->goes_bad() ) {
info.emplace_back( "CONTAINER", space );
info.emplace_back( "CONTAINER", _( "age (turns): " ),
"", iteminfo::lower_is_better,
to_turns<int>( contents_item->age() ) );
info.emplace_back( "CONTAINER", _( "rot (turns): " ),
"", iteminfo::lower_is_better,
to_turns<int>( contents_item->rot ) );
info.emplace_back( "CONTAINER", space + _( "shelf life (turns): " ),
"", iteminfo::lower_is_better,
to_turns<int>( contents_item->get_shelf_life() ) );
info.emplace_back( "CONTAINER", _( "last rot: " ),
"", iteminfo::lower_is_better,
to_turn<int>( contents_item->last_rot_check ) );
}
}
}
}
Expand Down Expand Up @@ -8646,9 +8698,8 @@ detached_ptr<item> item::fill_with( detached_ptr<item> &&liquid, int amount )
ammo_set( liquid->typeId(), ammo_remaining() + amount );
} else if( is_food_container() ) {
item &cts = contents.front();
// Use maximum rot between the two
cts.set_relative_rot( std::max( cts.get_relative_rot(),
liquid->get_relative_rot() ) );

cts.set_rot( weighted_averaged_rot( &cts, &*liquid ) );
cts.mod_charges( amount );
} else if( !is_container_empty() ) {
// if container already has liquid we need to set the amount
Expand Down
35 changes: 35 additions & 0 deletions src/options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <stdexcept>

#include "calendar.h"
#include "cached_item_options.h"
#include "cata_utility.h"
#include "catacharset.h"
#include "color.h"
Expand Down Expand Up @@ -1234,6 +1235,30 @@ void options_manager::add_options_general()

add_empty_line();

add_option_group( general, Group( "comestible_merging",
to_translation( "Merge similar comestibles" ),
to_translation( "Configure how similar items are stacked." ) ),
[&]( auto & page_id ) {
add( "MERGE_COMESTIBLES", page_id, translate_marker( "Merging Mode" ),
translate_marker( "Merge similar comestibles. Legacy: default behavior. Liquid: Merge only liquid comestibles. All: Merge all comestibles." ), {
{ "legacy", to_translation( "Legacy" ) },
{ "liquid", to_translation( "Liquid" ) },
{ "all", to_translation( "All" ) }
}, "all" );

add( "MERGE_COMESTIBLES_THRESHOLD", general, translate_marker( "Freshness similarity threshold" ),
translate_marker( "Limit maximum allowed staleness difference when merging comestibles."
" The lower the value, the more similar the items must be to merge."
" 0.0: Only merge identical items."
" 1.0: Merge comestibles regardless of its freshness."
),
0.0, 1.0, 0.25, 0.05 );

get_option( "MERGE_COMESTIBLES_THRESHOLD" ).setPrerequisites( "MERGE_COMESTIBLES", {"liquid", "all"} );
} );

add_empty_line();

add( "AUTO_PICKUP", general, translate_marker( "Auto pickup enabled" ),
translate_marker( "Enable item auto pickup. Change pickup rules with the Auto Pickup Manager." ),
false
Expand Down Expand Up @@ -3369,6 +3394,16 @@ void options_manager::cache_to_globals()
fov_3d_z_range = ::get_option<int>( "FOV_3D_Z_RANGE" );
static_z_effect = ::get_option<bool>( "STATICZEFFECT" );
PICKUP_RANGE = ::get_option<int>( "PICKUP_RANGE" );

merge_comestible_mode = ( [] {
const auto opt = ::get_option<std::string>( "MERGE_COMESTIBLES" );
return opt == "legacy" ? merge_comestible_t::merge_legacy
: opt == "liquid" ? merge_comestible_t::merge_liquid
: merge_comestible_t::merge_all;
} )();

similarity_threshold = ::get_option<float>( "MERGE_COMESTIBLES_THRESHOLD" );

#if defined(SDL_SOUND)
sounds::sound_enabled = ::get_option<bool>( "SOUND_ENABLED" );
#endif
Expand Down
100 changes: 99 additions & 1 deletion tests/item_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "math_defines.h"
#include "units.h"
#include "value_ptr.h"
#include "cached_item_options.h"

TEST_CASE( "item_volume", "[item]" )
{
Expand Down Expand Up @@ -69,7 +70,9 @@ TEST_CASE( "stacking_over_time", "[item]" )
item &A = *item::spawn_temporary( "bologna" );
item &B = *item::spawn_temporary( "bologna" );

GIVEN( "Two items with the same birthday" ) {
GIVEN( "Two items with the same birthday (stack mode: legacy)" ) {
merge_comestible_mode = merge_comestible_t::merge_legacy;

REQUIRE( A.stacks_with( B ) );
WHEN( "the items are aged different numbers of seconds" ) {
A.mod_rot( A.type->comestible->spoils - 1_turns );
Expand Down Expand Up @@ -159,8 +162,103 @@ TEST_CASE( "stacking_over_time", "[item]" )
}
}
}

GIVEN( "Two items with the same birthday (stack mode: all)" ) {
merge_comestible_mode = merge_comestible_t::merge_all;
similarity_threshold = 1.0f;

REQUIRE( A.stacks_with( B ) );
WHEN( "the items are aged different numbers of seconds" ) {
A.mod_rot( A.type->comestible->spoils - 1_turns );
B.mod_rot( B.type->comestible->spoils - 3_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged the same to the minute but different numbers of seconds" ) {
A.mod_rot( A.type->comestible->spoils - 5_minutes );
B.mod_rot( B.type->comestible->spoils - 5_minutes );
B.mod_rot( -5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged a few seconds different but different minutes" ) {
A.mod_rot( A.type->comestible->spoils - 5_minutes );
B.mod_rot( B.type->comestible->spoils - 5_minutes );
B.mod_rot( 5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged the same to the hour but different numbers of minutes" ) {
A.mod_rot( A.type->comestible->spoils - 5_hours );
B.mod_rot( B.type->comestible->spoils - 5_hours );
B.mod_rot( -5_minutes );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged a few seconds different but different hours" ) {
A.mod_rot( A.type->comestible->spoils - 5_hours );
B.mod_rot( B.type->comestible->spoils - 5_hours );
B.mod_rot( 5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged the same to the day but different numbers of seconds" ) {
A.mod_rot( A.type->comestible->spoils - 3_days );
B.mod_rot( B.type->comestible->spoils - 3_days );
B.mod_rot( -5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged a few seconds different but different days" ) {
A.mod_rot( A.type->comestible->spoils - 3_days );
B.mod_rot( B.type->comestible->spoils - 3_days );
B.mod_rot( 5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged the same to the week but different numbers of seconds" ) {
A.mod_rot( A.type->comestible->spoils - 7_days );
B.mod_rot( B.type->comestible->spoils - 7_days );
B.mod_rot( -5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged a few seconds different but different weeks" ) {
A.mod_rot( A.type->comestible->spoils - 7_days );
B.mod_rot( B.type->comestible->spoils - 7_days );
B.mod_rot( 5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged the same to the season but different numbers of seconds" ) {
A.mod_rot( A.type->comestible->spoils - calendar::season_length() );
B.mod_rot( B.type->comestible->spoils - calendar::season_length() );
B.mod_rot( -5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
WHEN( "the items are aged a few seconds different but different seasons" ) {
A.mod_rot( A.type->comestible->spoils - calendar::season_length() );
B.mod_rot( B.type->comestible->spoils - calendar::season_length() );
B.mod_rot( 5_turns );
THEN( "they stack" ) {
CHECK( A.stacks_with( B ) );
}
}
}
}


TEST_CASE( "magazine_copyfrom_extends", "[item]" )
{
item gun( "glock_19" );
Expand Down
Loading