From 4baf4ebbe7b3e9d0c51b018cc9b05ec6d733c4b4 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Wed, 4 Sep 2019 02:30:21 -0400 Subject: [PATCH] Add stats tracker (#33782) * Added auto_hash and range_hash range_hash is a hash for using e.g. containers as keys in unordered_maps. auto_hash is a hash function that chooses automatically between std::hash and cata::tuple_hash depending on the hashed type. * Add event::data_type This is a typedef for the map type used to store the event data. Adding it because the underlying type seems likely to change in the future. * Add a simple stats_tracker that counts events * Add stats_tracker to game * Serialize stats_tracker * Add hp_part case for cata_variant * Add (and use) a character_takes_damage event * Add stats_tracker::total This sums the value of a particular field in events satisfying a criterion. * Replace lifetime_stats with stats_tracker Previously each player object contained some lifetime_stats about tiles moved, etc. Replace all that with the newly added stats_tracker functionality. One side-effect is that muscle-powered bionics now recharge at random intervals, rather than regularly, but should have the same average charge rates. * Some comments to help explain stats_tracker * Add std::hash specialization for event_type This is supposed to be in the standard library in C++14 but gcc 5.3 lacks it. --- src/bodypart.cpp | 19 +++++++++ src/cata_variant.cpp | 1 + src/cata_variant.h | 7 +++- src/event.cpp | 9 ++++- src/event.h | 57 ++++++++++++++++++++++++-- src/game.cpp | 22 +++++++--- src/game.h | 5 ++- src/hash_utils.h | 26 ++++++++++++ src/memorial_logger.cpp | 26 ++++++++---- src/player.cpp | 28 +++++-------- src/player.h | 14 ------- src/pldata.h | 5 +++ src/ranged.cpp | 6 ++- src/savegame.cpp | 7 +++- src/savegame_json.cpp | 63 ++++++++++++++++++----------- src/stats_tracker.cpp | 78 ++++++++++++++++++++++++++++++++++++ src/stats_tracker.h | 60 +++++++++++++++++++++++++++ tests/stats_tracker_test.cpp | 67 +++++++++++++++++++++++++++++++ 18 files changed, 422 insertions(+), 78 deletions(-) create mode 100644 src/stats_tracker.cpp create mode 100644 src/stats_tracker.h create mode 100644 tests/stats_tracker_test.cpp diff --git a/src/bodypart.cpp b/src/bodypart.cpp index 21d37963705ac..fcc77a0527440 100644 --- a/src/bodypart.cpp +++ b/src/bodypart.cpp @@ -48,6 +48,25 @@ std::string enum_to_string( side data ) abort(); } +template<> +std::string enum_to_string( hp_part data ) +{ + switch( data ) { + // *INDENT-OFF* + case hp_part::hp_head: return "head"; + case hp_part::hp_torso: return "torso"; + case hp_part::hp_arm_l: return "arm_l"; + case hp_part::hp_arm_r: return "arm_r"; + case hp_part::hp_leg_l: return "leg_l"; + case hp_part::hp_leg_r: return "leg_r"; + // *INDENT-ON* + case hp_part::num_hp_parts: + break; + } + debugmsg( "Invalid hp_part" ); + abort(); +} + } // namespace io namespace diff --git a/src/cata_variant.cpp b/src/cata_variant.cpp index e82ee7681130e..21831bf63d587 100644 --- a/src/cata_variant.cpp +++ b/src/cata_variant.cpp @@ -17,6 +17,7 @@ std::string enum_to_string( cata_variant_type type ) case cata_variant_type::bool_: return "bool"; case cata_variant_type::character_id: return "character_id"; case cata_variant_type::efftype_id: return "efftype_id"; + case cata_variant_type::hp_part: return "hp_part"; case cata_variant_type::int_: return "int"; case cata_variant_type::itype_id: return "itype_id"; case cata_variant_type::matype_id: return "matype_id"; diff --git a/src/cata_variant.h b/src/cata_variant.h index 5cadb430ef6f9..c38e90d883999 100644 --- a/src/cata_variant.h +++ b/src/cata_variant.h @@ -16,6 +16,7 @@ enum add_type : int; enum body_part : int; enum class mutagen_technique : int; +enum hp_part : int; using itype_id = std::string; @@ -30,6 +31,7 @@ enum class cata_variant_type : int { bool_, character_id, efftype_id, + hp_part, int_, itype_id, matype_id, @@ -143,7 +145,7 @@ struct convert_enum { }; // These are the specializations of convert for each value type. -static_assert( static_cast( cata_variant_type::num_types ) == 18, +static_assert( static_cast( cata_variant_type::num_types ) == 19, "This assert is a reminder to add conversion support for any new types to the " "below specializations" ); @@ -186,6 +188,9 @@ struct convert { template<> struct convert : convert_string_id {}; +template<> +struct convert : convert_enum {}; + template<> struct convert { using type = int; diff --git a/src/event.cpp b/src/event.cpp index a4696f6c6bc71..6d86b37746587 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -12,15 +12,19 @@ std::string enum_to_string( event_type data ) case event_type::activates_mininuke: return "activates_mininuke"; case event_type::administers_mutagen: return "administers_mutagen"; case event_type::angers_amigara_horrors: return "angers_amigara_horrors"; + case event_type::avatar_moves: return "avatar_moves"; case event_type::awakes_dark_wyrms: return "awakes_dark_wyrms"; case event_type::becomes_wanted: return "becomes_wanted"; case event_type::broken_bone_mends: return "broken_bone_mends"; case event_type::buries_corpse: return "buries_corpse"; case event_type::causes_resonance_cascade: return "causes_resonance_cascade"; case event_type::character_gains_effect: return "character_gains_effect"; + case event_type::character_gets_headshot: return "character_gets_headshot"; + case event_type::character_heals_damage: return "character_heals_damage"; case event_type::character_kills_character: return "character_kills_character"; case event_type::character_kills_monster: return "character_kills_monster"; case event_type::character_loses_effect: return "character_loses_effect"; + case event_type::character_takes_damage: return "character_takes_damage"; case event_type::character_triggers_trap: return "character_triggers_trap"; case event_type::consumes_marloss_item: return "consumes_marloss_item"; case event_type::crosses_marloss_threshold: return "crosses_marloss_threshold"; @@ -87,7 +91,7 @@ constexpr std::array, constexpr std::array, event_spec_character::fields.size()> event_spec_character::fields; -static_assert( static_cast( event_type::num_event_types ) == 57, +static_assert( static_cast( event_type::num_event_types ) == 61, "This static_assert is a reminder to add a definition below when you add a new " "event_type. If your event_spec specialization inherits from another struct for " "its fields definition then you probably don't need a definition here." ); @@ -99,12 +103,15 @@ static_assert( static_cast( event_type::num_event_types ) == 57, DEFINE_EVENT_FIELDS( activates_artifact ) DEFINE_EVENT_FIELDS( administers_mutagen ) +DEFINE_EVENT_FIELDS( avatar_moves ) DEFINE_EVENT_FIELDS( broken_bone_mends ) DEFINE_EVENT_FIELDS( buries_corpse ) DEFINE_EVENT_FIELDS( character_gains_effect ) +DEFINE_EVENT_FIELDS( character_heals_damage ) DEFINE_EVENT_FIELDS( character_kills_character ) DEFINE_EVENT_FIELDS( character_kills_monster ) DEFINE_EVENT_FIELDS( character_loses_effect ) +DEFINE_EVENT_FIELDS( character_takes_damage ) DEFINE_EVENT_FIELDS( character_triggers_trap ) DEFINE_EVENT_FIELDS( consumes_marloss_item ) DEFINE_EVENT_FIELDS( crosses_mutation_threshold ) diff --git a/src/event.h b/src/event.h index aef93c57c95bd..c0af70a063972 100644 --- a/src/event.h +++ b/src/event.h @@ -22,15 +22,19 @@ enum class event_type { activates_mininuke, administers_mutagen, angers_amigara_horrors, + avatar_moves, awakes_dark_wyrms, becomes_wanted, broken_bone_mends, buries_corpse, causes_resonance_cascade, character_gains_effect, + character_gets_headshot, + character_heals_damage, character_kills_character, character_kills_monster, character_loses_effect, + character_takes_damage, character_triggers_trap, consumes_marloss_item, crosses_marloss_threshold, @@ -91,6 +95,18 @@ std::string enum_to_string( event_type data ); } // namespace io +namespace std +{ + +template<> +struct hash { + size_t operator()( const event_type v ) const noexcept { + return static_cast( v ); + } +}; + +} // namespace std + namespace cata { @@ -117,7 +133,7 @@ struct event_spec_character { }; }; -static_assert( static_cast( event_type::num_event_types ) == 57, +static_assert( static_cast( event_type::num_event_types ) == 61, "This static_assert is to remind you to add a specialization for your new " "event_type below" ); @@ -145,6 +161,14 @@ struct event_spec { template<> struct event_spec : event_spec_empty {}; +template<> +struct event_spec { + static constexpr std::array, 1> fields = {{ + { "mount", cata_variant_type::mtype_id }, + } + }; +}; + template<> struct event_spec : event_spec_empty {}; @@ -182,6 +206,18 @@ struct event_spec { }; }; +template<> +struct event_spec : event_spec_character {}; + +template<> +struct event_spec { + static constexpr std::array, 2> fields = {{ + { "character", cata_variant_type::character_id }, + { "damage", cata_variant_type::int_ }, + } + }; +}; + template<> struct event_spec { static constexpr std::array, 2> fields = {{ @@ -210,6 +246,15 @@ struct event_spec { }; }; +template<> +struct event_spec { + static constexpr std::array, 2> fields = {{ + { "character", cata_variant_type::character_id }, + { "damage", cata_variant_type::int_ }, + } + }; +}; + template<> struct event_spec { static constexpr std::array, 2> fields = {{ @@ -476,7 +521,9 @@ struct make_event_helper; class event { public: - event( event_type type, time_point time, std::map &&data ) + using data_type = std::map; + + event( event_type type, time_point time, data_type &&data ) : type_( type ) , time_( time ) , data_( std::move( data ) ) @@ -526,10 +573,14 @@ class event auto get( const std::string &key ) const { return get_variant( key ).get(); } + + const data_type &data() const { + return data_; + } private: event_type type_; time_point time_; - std::map data_; + data_type data_; }; namespace event_detail diff --git a/src/game.cpp b/src/game.cpp index 6939faca4660e..280e886e9cd72 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -48,7 +48,6 @@ #include "dependency_tree.h" #include "editmap.h" #include "enums.h" -#include "timed_event.h" #include "faction.h" #include "filesystem.h" #include "game_constants.h" @@ -104,9 +103,11 @@ #include "sdltiles.h" #include "sounds.h" #include "start_location.h" +#include "stats_tracker.h" #include "string_formatter.h" #include "string_input_popup.h" #include "submap.h" +#include "timed_event.h" #include "translations.h" #include "trap.h" #include "uistate.h" @@ -279,6 +280,7 @@ game::game() : { player_was_sleeping = false; reset_light_level(); + events().subscribe( &*stats_tracker_ptr ); events().subscribe( &*kill_tracker_ptr ); events().subscribe( &*memorial_logger_ptr ); world_generator = std::make_unique(); @@ -662,6 +664,7 @@ void game::setup() SCT.vSCT.clear(); //Delete pending messages + stats().clear(); // reset kill counts kill_tracker_ptr->clear(); // reset follower list @@ -2856,6 +2859,11 @@ event_bus &game::events() return *event_bus_ptr; } +stats_tracker &game::stats() +{ + return *stats_tracker_ptr; +} + memorial_logger &game::memorial() { return *memorial_logger_ptr; @@ -9073,8 +9081,12 @@ bool game::walk_move( const tripoint &dest_loc ) add_msg( m_good, _( "You are hiding in the %s." ), m.name( dest_loc ) ); } - if( dest_loc != u.pos() && !u.is_mounted() ) { - u.lifetime_stats.squares_walked++; + if( dest_loc != u.pos() ) { + mtype_id mount_type; + if( u.is_mounted() ) { + mount_type = u.mounted_creature->type->id; + } + g->events().send( mount_type ); } tripoint oldpos = u.pos(); @@ -9725,7 +9737,7 @@ void game::on_move_effects() // TODO: Move this to a character method if( !u.is_mounted() ) { const item muscle( "muscle" ); - if( u.lifetime_stats.squares_walked % 8 == 0 ) {// active power gen + if( one_in( 8 ) ) {// active power gen if( u.has_active_bionic( bionic_id( "bio_torsionratchet" ) ) ) { u.charge_power( 1 ); } @@ -9735,7 +9747,7 @@ void game::on_move_effects() } } } - if( u.lifetime_stats.squares_walked % 160 == 0 ) { // passive power gen + if( one_in( 160 ) ) {// passive power gen if( u.has_bionic( bionic_id( "bio_torsionratchet" ) ) ) { u.charge_power( 1 ); } diff --git a/src/game.h b/src/game.h index e8f3972fefad1..5eaaf30c48bac 100644 --- a/src/game.h +++ b/src/game.h @@ -90,8 +90,9 @@ class map; class memorial_logger; class faction_manager; class new_faction_manager; -class player; class npc; +class player; +class stats_tracker; class vehicle; class Creature_tracker; class scenario; @@ -887,6 +888,7 @@ class game pimpl scent_ptr; pimpl timed_event_manager_ptr; pimpl event_bus_ptr; + pimpl stats_tracker_ptr; pimpl kill_tracker_ptr; pimpl memorial_logger_ptr; @@ -898,6 +900,7 @@ class game timed_event_manager &timed_events; event_bus &events(); + stats_tracker &stats(); memorial_logger &memorial(); pimpl critter_tracker; diff --git a/src/hash_utils.h b/src/hash_utils.h index 138c4f6241cf8..98aba822cccec 100644 --- a/src/hash_utils.h +++ b/src/hash_utils.h @@ -60,6 +60,32 @@ struct tuple_hash { } }; +// auto_hash will use std::hash for most types but tuple_hash for pair or +// tuple. +template +struct auto_hash : std::hash {}; + +template +struct auto_hash> : tuple_hash {}; + +template +struct auto_hash> : tuple_hash {}; + +struct range_hash { + template + std::size_t operator()( const Range &range ) const noexcept { + using value_type = typename Range::value_type; + using hash_type = auto_hash; + hash_type hash; + + std::size_t seed = range.size(); + for( const auto &value : range ) { + hash_combine( seed, value, hash ); + } + return seed; + } +}; + } // namespace cata #endif // CATA_TUPLE_HASH_H diff --git a/src/memorial_logger.cpp b/src/memorial_logger.cpp index 27edbee39e81c..414b54ee4a90d 100644 --- a/src/memorial_logger.cpp +++ b/src/memorial_logger.cpp @@ -19,6 +19,7 @@ #include "overmapbuffer.h" #include "profession.h" #include "skill.h" +#include "stats_tracker.h" static const efftype_id effect_adrenaline( "adrenaline" ); static const efftype_id effect_datura( "datura" ); @@ -328,14 +329,17 @@ void memorial_logger::write( std::ostream &file, const std::string &epitaph ) co //Lifetime stats file << _( "Lifetime Stats" ) << eol; - file << indent << string_format( _( "Distance walked: %d squares" ), - u.lifetime_stats.squares_walked ) << eol; - file << indent << string_format( _( "Damage taken: %d damage" ), - u.lifetime_stats.damage_taken ) << eol; - file << indent << string_format( _( "Damage healed: %d damage" ), - u.lifetime_stats.damage_healed ) << eol; - file << indent << string_format( _( "Headshots: %d" ), - u.lifetime_stats.headshots ) << eol; + cata::event::data_type not_mounted = { { "mount", cata_variant( mtype_id() ) } }; + int moves = g->stats().count( event_type::avatar_moves, not_mounted ); + cata::event::data_type is_u = { { "character", cata_variant( u.getID() ) } }; + int damage_taken = g->stats().total( event_type::character_takes_damage, "damage", is_u ); + int damage_healed = g->stats().total( event_type::character_heals_damage, "damage", is_u ); + int headshots = g->stats().count( event_type::character_gets_headshot, is_u ); + + file << indent << string_format( _( "Distance walked: %d squares" ), moves ) << eol; + file << indent << string_format( _( "Damage taken: %d damage" ), damage_taken ) << eol; + file << indent << string_format( _( "Damage healed: %d damage" ), damage_healed ) << eol; + file << indent << string_format( _( "Headshots: %d" ), headshots ) << eol; file << eol; //History @@ -996,6 +1000,12 @@ void memorial_logger::notify( const cata::event &e ) pgettext( "memorial_female", "Set off an alarm." ) ); break; } + // All the events for which we have no memorial log are here + case event_type::avatar_moves: + case event_type::character_gets_headshot: + case event_type::character_heals_damage: + case event_type::character_takes_damage: + break; case event_type::num_event_types: { debugmsg( "Invalid event type" ); break; diff --git a/src/player.cpp b/src/player.cpp index 067cc899be566..cac589b19ca79 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -3225,11 +3225,10 @@ void player::apply_damage( Creature *source, body_part hurt, int dam, const bool mod_pain( dam / 2 ); - hp_cur[hurtpart] -= dam; - if( hp_cur[hurtpart] < 0 ) { - lifetime_stats.damage_taken += hp_cur[hurtpart]; - hp_cur[hurtpart] = 0; - } + const int dam_to_bodypart = std::min( dam, hp_cur[hurtpart] ); + + hp_cur[hurtpart] -= dam_to_bodypart; + g->events().send( getID(), dam_to_bodypart ); if( hp_cur[hurtpart] <= 0 && ( source == nullptr || !source->is_hallucination() ) ) { if( has_effect( effect_mending, hurt ) ) { @@ -3239,7 +3238,6 @@ void player::apply_damage( Creature *source, body_part hurt, int dam, const bool } } - lifetime_stats.damage_taken += dam; if( dam > get_painkiller() ) { on_hurt( source ); } @@ -3307,12 +3305,9 @@ void player::heal( body_part healed, int dam ) void player::heal( hp_part healed, int dam ) { if( hp_cur[healed] > 0 ) { - hp_cur[healed] += dam; - if( hp_cur[healed] > hp_max[healed] ) { - lifetime_stats.damage_healed -= hp_cur[healed] - hp_max[healed]; - hp_cur[healed] = hp_max[healed]; - } - lifetime_stats.damage_healed += dam; + int effective_heal = std::min( dam, hp_max[healed] - hp_cur[healed] ); + hp_cur[healed] += effective_heal; + g->events().send( getID(), effective_heal ); } } @@ -3333,12 +3328,9 @@ void player::hurtall( int dam, Creature *source, bool disturb /*= true*/ ) for( int i = 0; i < num_hp_parts; i++ ) { const hp_part bp = static_cast( i ); // Don't use apply_damage here or it will annoy the player with 6 queries - hp_cur[bp] -= dam; - lifetime_stats.damage_taken += dam; - if( hp_cur[bp] < 0 ) { - lifetime_stats.damage_taken += hp_cur[bp]; - hp_cur[bp] = 0; - } + const int dam_to_bodypart = std::min( dam, hp_cur[bp] ); + hp_cur[bp] -= dam_to_bodypart; + g->events().send( getID(), dam_to_bodypart ); } // Low pain: damage is spread all over the body, so not as painful as 6 hits in one part diff --git a/src/player.h b/src/player.h index 1455884ade435..59ed8fef5157f 100644 --- a/src/player.h +++ b/src/player.h @@ -140,18 +140,6 @@ class player_morale; // This corresponds to the level of accuracy of a "snap" or "hip" shot. extern const double MAX_RECOIL; -//Don't forget to add new stats counters -//to the save and load functions in savegame_json.cpp -struct stats { - int squares_walked = 0; - int damage_taken = 0; - int damage_healed = 0; - int headshots = 0; - - void serialize( JsonOut &json ) const; - void deserialize( JsonIn &jsin ); -}; - struct stat_mod { int strength = 0; int dexterity = 0; @@ -1647,8 +1635,6 @@ class player : public Character std::map bionic_installation_issues( const bionic_id &bioid ); std::set follower_ids; - //Record of player stats, for posterity only - stats lifetime_stats; void mod_stat( const std::string &stat, float modifier ) override; bool is_underwater() const override; diff --git a/src/pldata.h b/src/pldata.h index aac98fcf5d432..b1cfe6d9a1187 100644 --- a/src/pldata.h +++ b/src/pldata.h @@ -43,6 +43,11 @@ enum hp_part : int { num_hp_parts }; +template<> +struct enum_traits { + static constexpr hp_part last = num_hp_parts; +}; + class addiction { public: diff --git a/src/ranged.cpp b/src/ranged.cpp index b1e4a7b5d5fb4..5b3d10e09842f 100644 --- a/src/ranged.cpp +++ b/src/ranged.cpp @@ -20,6 +20,7 @@ #include "cata_utility.h" #include "debug.h" #include "dispersion.h" +#include "event_bus.h" #include "game.h" #include "gun_mode.h" #include "input.h" @@ -442,7 +443,8 @@ int player::fire_gun( const tripoint &target, int shots, item &gun ) } if( shot.missed_by <= .1 ) { - lifetime_stats.headshots++; // TODO: check head existence for headshot + // TODO: check head existence for headshot + g->events().send( getID() ); } if( shot.hit_critter ) { @@ -715,7 +717,7 @@ dealt_projectile_attack player::throw_item( const tripoint &target, const item & if( missed_by <= 0.1 && dealt_attack.hit_critter != nullptr ) { practice( skill_used, final_xp_mult, MAX_SKILL ); // TODO: Check target for existence of head - lifetime_stats.headshots++; + g->events().send( getID() ); } else if( dealt_attack.hit_critter != nullptr && missed_by > 0.0f ) { practice( skill_used, final_xp_mult / ( 1.0f + missed_by ), MAX_SKILL ); } else { diff --git a/src/savegame.cpp b/src/savegame.cpp index 71871c4f689a5..0c6e0d065369e 100644 --- a/src/savegame.cpp +++ b/src/savegame.cpp @@ -16,6 +16,7 @@ #include "creature_tracker.h" #include "debug.h" #include "faction.h" +#include "int_id.h" #include "io.h" #include "kill_tracker.h" #include "map.h" @@ -35,7 +36,7 @@ #include "omdata.h" #include "overmap_types.h" #include "regional_settings.h" -#include "int_id.h" +#include "stats_tracker.h" #include "string_id.h" #if defined(__ANDROID__) @@ -94,8 +95,9 @@ void game::serialize( std::ostream &fout ) json.member( "active_monsters", *critter_tracker ); json.member( "stair_monsters", coming_to_stairs ); - // save killcounts. + // save stats. json.member( "kill_tracker", *kill_tracker_ptr ); + json.member( "stats_tracker", *stats_tracker_ptr ); json.member( "player", u ); Messages::serialize( json ); @@ -236,6 +238,7 @@ void game::unserialize( std::istream &fin ) } data.read( "player", u ); + data.read( "stats_tracker", *stats_tracker_ptr ); Messages::deserialize( data ); } catch( const JsonError &jsonerr ) { diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 3bc7a989a5eae..6876d9c141844 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -89,6 +89,7 @@ #include "magic_teleporter_list.h" #include "point.h" #include "requirements.h" +#include "stats_tracker.h" #include "vpart_position.h" struct oter_type_t; @@ -898,8 +899,6 @@ void avatar::store( JsonOut &json ) const json.member( "completed_missions", mission::to_uid_vector( completed_missions ) ); json.member( "failed_missions", mission::to_uid_vector( failed_missions ) ); - json.member( "player_stats", lifetime_stats ); - json.member( "show_map_memory", show_map_memory ); json.member( "assigned_invlet" ); @@ -1088,8 +1087,6 @@ void avatar::load( JsonObject &data ) } } - data.read( "player_stats", lifetime_stats ); - //Load from legacy map_memory save location (now in its own file .mm) if( data.has_member( "map_memory_tiles" ) || data.has_member( "map_memory_curses" ) ) { player_map_memory.load( data ); @@ -3104,25 +3101,6 @@ void addiction::deserialize( JsonIn &jsin ) jo.read( "sated", sated ); } -void stats::serialize( JsonOut &json ) const -{ - json.start_object(); - json.member( "squares_walked", squares_walked ); - json.member( "damage_taken", damage_taken ); - json.member( "damage_healed", damage_healed ); - json.member( "headshots", headshots ); - json.end_object(); -} - -void stats::deserialize( JsonIn &jsin ) -{ - JsonObject jo = jsin.get_object(); - jo.read( "squares_walked", squares_walked ); - jo.read( "damage_taken", damage_taken ); - jo.read( "damage_healed", damage_healed ); - jo.read( "headshots", headshots ); -} - void serialize( const recipe_subset &value, JsonOut &jsout ) { jsout.start_array(); @@ -3315,6 +3293,45 @@ void cata_variant::deserialize( JsonIn &jsin ) jsin.end_array(); } +void event_tracker::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + using value_type = decltype( event_counts )::value_type; + std::vector copy( event_counts.begin(), event_counts.end() ); + jsout.member( "event_counts", copy ); + jsout.end_object(); +} + +void event_tracker::deserialize( JsonIn &jsin ) +{ + jsin.start_object(); + while( !jsin.end_object() ) { + std::string name = jsin.get_member_name(); + if( name == "event_counts" ) { + std::vector> copy; + if( !jsin.read( copy ) ) { + jsin.error( "Failed to read event_counts" ); + } + event_counts = { copy.begin(), copy.end() }; + } else { + jsin.skip_value(); + } + } +} + +void stats_tracker::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + jsout.member( "data", data ); + jsout.end_object(); +} + +void stats_tracker::deserialize( JsonIn &jsin ) +{ + JsonObject jo = jsin.get_object(); + jo.read( "data", data ); +} + void submap::store( JsonOut &jsout ) const { jsout.member( "turn_last_touched", last_touched ); diff --git a/src/stats_tracker.cpp b/src/stats_tracker.cpp new file mode 100644 index 0000000000000..336a185b628d3 --- /dev/null +++ b/src/stats_tracker.cpp @@ -0,0 +1,78 @@ +#include "stats_tracker.h" + +static bool event_data_matches( const cata::event::data_type &data, + const cata::event::data_type &criteria ) +{ + for( const auto &criterion : criteria ) { + auto it = data.find( criterion.first ); + if( it == data.end() || it->second != criterion.second ) { + return false; + } + } + return true; +} + +int event_tracker::count( const cata::event::data_type &criteria ) const +{ + int total = 0; + for( const auto &pair : event_counts ) { + if( event_data_matches( pair.first, criteria ) ) { + total += pair.second; + } + } + return total; +} + +int event_tracker::total( const std::string &field, const cata::event::data_type &criteria ) const +{ + int total = 0; + for( const auto &pair : event_counts ) { + auto it = pair.first.find( field ); + if( it == pair.first.end() ) { + continue; + } + if( event_data_matches( pair.first, criteria ) ) { + total += pair.second * it->second.get(); + } + } + return total; +} + +void event_tracker::add( const cata::event &e ) +{ + event_counts[e.data()]++; +} + +int stats_tracker::count( const cata::event &e ) const +{ + return count( e.type(), e.data() ); +} + +int stats_tracker::count( event_type type, const cata::event::data_type &criteria ) const +{ + auto it = data.find( type ); + if( it == data.end() ) { + return 0; + } + return it->second.count( criteria ); +} + +int stats_tracker::total( event_type type, const std::string &field, + const cata::event::data_type &criteria ) const +{ + auto it = data.find( type ); + if( it == data.end() ) { + return 0; + } + return it->second.total( field, criteria ); +} + +void stats_tracker::clear() +{ + data.clear(); +} + +void stats_tracker::notify( const cata::event &e ) +{ + data[e.type()].add( e ); +} diff --git a/src/stats_tracker.h b/src/stats_tracker.h new file mode 100644 index 0000000000000..b7d724188bd9b --- /dev/null +++ b/src/stats_tracker.h @@ -0,0 +1,60 @@ +#ifndef CATA_STATS_TRACKER_H +#define CATA_STATS_TRACKER_H + +#include "event_bus.h" +#include "hash_utils.h" + +// The stats_tracker is intended to keep a summary of events that have occured. +// For each event_type it stores an event_tracker. +// Within the event_tracker, counts are kept. The events are partitioned +// according to their data (an event::data_type object, which is a map of keys +// to values). +// The stats_tracker can be queried in various ways to get summary statistics +// about events that have occured. + +class event_tracker +{ + public: + int count( const cata::event::data_type &criteria ) const; + int total( const std::string &field, const cata::event::data_type &criteria ) const; + + void add( const cata::event & ); + + void serialize( JsonOut & ) const; + void deserialize( JsonIn & ); + private: + std::unordered_map event_counts; +}; + +class stats_tracker : public event_subscriber +{ + public: + // count returns the number of events matching given criteria that have + // occured. + // total returns the sum of some integer-valued field across every + // event satisfying certain criteria. + // For example, count might return the number of times the avatar has + // taken damage, while total might return the total damage taken in all + // those cases. + // The criteria have two parts: + // - The event_type + // - An event::data_type map specifying some values that must be + // matched in the events of that type. You can provide just a subset + // of the relevant keys from the event_type in your criteria. + // The first count overload combines these criteria into a single event + // object for convenience since that contains the two pieces necessary. + int count( const cata::event & ) const; + int count( event_type, const cata::event::data_type &criteria ) const; + int total( event_type, const std::string &field, + const cata::event::data_type &criteria ) const; + + void clear(); + void notify( const cata::event & ) override; + + void serialize( JsonOut & ) const; + void deserialize( JsonIn & ); + private: + std::unordered_map data; +}; + +#endif // CATA_STATS_TRACKER_H diff --git a/tests/stats_tracker_test.cpp b/tests/stats_tracker_test.cpp new file mode 100644 index 0000000000000..a1663f3da185e --- /dev/null +++ b/tests/stats_tracker_test.cpp @@ -0,0 +1,67 @@ +#include "catch/catch.hpp" + +#include "avatar.h" +#include "game.h" +#include "stats_tracker.h" + +TEST_CASE( "stats_tracker_count_events", "[stats]" ) +{ + stats_tracker s; + event_bus b; + b.subscribe( &s ); + + const character_id u_id = g->u.getID(); + const mtype_id mon1( "mon_zombie" ); + const mtype_id mon2( "mon_zombie_brute" ); + const cata::event kill1 = cata::event::make( u_id, mon1 ); + const cata::event kill2 = cata::event::make( u_id, mon2 ); + const cata::event::data_type char_is_player{ { "killer", cata_variant( u_id ) } }; + + CHECK( s.count( kill1 ) == 0 ); + CHECK( s.count( kill2 ) == 0 ); + CHECK( s.count( event_type::character_kills_monster, char_is_player ) == 0 ); + b.send( kill1 ); + CHECK( s.count( kill1 ) == 1 ); + CHECK( s.count( kill2 ) == 0 ); + CHECK( s.count( event_type::character_kills_monster, char_is_player ) == 1 ); + b.send( kill2 ); + CHECK( s.count( kill1 ) == 1 ); + CHECK( s.count( kill2 ) == 1 ); + CHECK( s.count( event_type::character_kills_monster, char_is_player ) == 2 ); +} + +TEST_CASE( "stats_tracker_total_events", "[stats]" ) +{ + stats_tracker s; + event_bus b; + b.subscribe( &s ); + + const character_id u_id = g->u.getID(); + character_id other_id = u_id; + ++other_id; + const cata::event::data_type damage_to_any{}; + const cata::event::data_type damage_to_u{ { "character", cata_variant( u_id ) } }; + + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_u ) == 0 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_any ) == 0 ); + b.send( u_id, 10 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_u ) == 10 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_any ) == 10 ); + b.send( other_id, 10 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_u ) == 10 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_any ) == 20 ); + b.send( u_id, 10 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_u ) == 20 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_any ) == 30 ); + b.send( u_id, 5 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_u ) == 25 ); + CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_any ) == 35 ); +} + +TEST_CASE( "stats_tracker_in_game", "[stats]" ) +{ + g->stats().clear(); + cata::event e = cata::event::make(); + g->events().send( e ); + CHECK( g->stats().count( e ) == 1 ); +}