Skip to content

Commit

Permalink
feat: stack comestibles regardless of rot (#4320)
Browse files Browse the repository at this point in the history
* feat: stack comestibles regardless of rot

* feat: configurable merge strategy

* perf: cache option

* fix: take account of liquid only option

* feat: display item content freshness

* fix: averaged rot in liquid transfer

* test: test legacy and all merging

* fix: fallthrough in switch statement

* fix: explicit fallthrough

* feat: change to similarity based merging
  • Loading branch information
scarf005 authored Mar 10, 2024
1 parent 52eb39d commit e253538
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 12 deletions.
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

0 comments on commit e253538

Please sign in to comment.