diff --git a/data/json/scores.json b/data/json/scores.json index 94fd0453b17fd..961089f99bc72 100644 --- a/data/json/scores.json +++ b/data/json/scores.json @@ -36,7 +36,8 @@ "id": "num_avatar_zombie_kills", "type": "event_statistic", "stat_type": "count", - "event_transformation": "avatar_zombie_kills" + "event_transformation": "avatar_zombie_kills", + "description": "Number of zombies killed" }, { "id": "score_kills", @@ -44,6 +45,12 @@ "description": "Number of monsters killed: %s", "statistic": "num_avatar_kills" }, + { + "id": "achievement_kill_zombie", + "type": "achievement", + "description": "One down, billions to go\u2026", + "requirements": [ { "event_statistic": "num_avatar_zombie_kills", "is": ">=", "target": 1 } ] + }, { "id": "moves_not_mounted", "type": "event_transformation", diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index d0105612d1285..71efc7b44446e 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -1053,7 +1053,7 @@ request](https://github.com/CleverRaven/Cataclysm-DDA/pull/36657) and the } ``` -### Scores +### Scores and achievements Scores are defined in two or three steps based on *events*. To see what events exist and what data they contain, read [`event.h`](../src/event.h). @@ -1178,6 +1178,11 @@ given field for that unique event: "field": "avatar_id" ``` +Regardless of `stat_type`, each `event_statistic` can also have: +```C++ +"description": "Number of things" // Intended for use in describing achievement requirements. +``` + #### `score` Scores simply associate a description to an event for formatting in tabulations @@ -1194,6 +1199,28 @@ Note that even though most statistics yield an integer, you should still use "statistic": "avatar_num_headshots" ``` +#### `achievement` + +Achievements are goals for the player to aspire to, in the usual sense of the +term as popularised in other games. + +An achievement is specified via requirements, each of which is a constraint on +an `event_statistic`. For example: + +```C++ +{ + "id": "achievement_kill_zombie", + "type": "achievement", + // The achievement description is used for the UI. + "description": "One down, billions to go\u2026", + "requirements": [ + // Each requirement must specify the statistic being constrained, and the + // constraint in terms of a comparison against some target value. + { "event_statistic": "num_avatar_zombie_kills", "is": ">=", "target": 1 } + ] +}, +``` + ### Skills ```C++ diff --git a/lang/extract_json_strings.py b/lang/extract_json_strings.py index 432666ea80501..3cd2975a9e288 100755 --- a/lang/extract_json_strings.py +++ b/lang/extract_json_strings.py @@ -112,6 +112,7 @@ def warning_supressed(filename): # "sound" member # "messages" member containing an array of translatable strings automatically_convertible = { + "achievement", "activity_type", "AMMO", "ammunition_type", @@ -125,6 +126,7 @@ def warning_supressed(filename): "CONTAINER", "dream", "ENGINE", + "event_statistic", "faction", "furniture", "GENERIC", diff --git a/src/achievement.cpp b/src/achievement.cpp new file mode 100644 index 0000000000000..2de0dd9c20c2c --- /dev/null +++ b/src/achievement.cpp @@ -0,0 +1,330 @@ +#include "achievement.h" + +#include "avatar.h" +#include "event_statistics.h" +#include "generic_factory.h" +#include "stats_tracker.h" + +// Some details about how achievements work +// ======================================== +// +// Achievements are built on the stats_tracker, which is in turn built on the +// event_bus. Each of these layers involves subscription / callback style +// interfaces, so the code flow may now be obvious. Here's a quick outline of +// the execution flow to help clarify how it all fits together. +// +// * Various core game code paths generate events via the event bus. +// * The stats_traecker subscribes to the event bus, and receives these events. +// * Events contribute to event_multisets managed by the stats_tracker. +// * (In the docs, these event_multisets are described as "event streams"). +// * (Optionally) event_transformations transform these event_multisets into +// other event_multisets based on json-defined transformation rules. These +// are also managed by stats_tracker. +// * event_statistics monitor these event_multisets and summarize them into +// single values. These are also managed by stats_tracker. +// * Each achievement requirement has a corresponding requirement_watcher which +// is alerted to statistic changes via the stats_tracker's watch interface. +// * Each requirement_watcher notifies its achievement_tracker (of which there +// is one per achievement) of the requirement's status on each change to a +// statistic. +// * The achievement_tracker keeps track of which requirements are currently +// satisfied and which are not. +// * When all the requirements are satisfied, the achievement_tracker tells the +// achievement_tracker (only one of these exists per game). +// * The achievement_tracker calls the achievement_attained_callback it was +// given at construction time. This hooks into the actual game logic (e.g. +// telling the player they just got an achievement). + +namespace +{ + +generic_factory achievement_factory( "achievement" ); + +} // namespace + +enum class achievement_comparison { + greater_equal, + last, +}; + +namespace io +{ + +template<> +std::string enum_to_string( achievement_comparison data ) +{ + switch( data ) { + // *INDENT-OFF* + case achievement_comparison::greater_equal: return ">="; + // *INDENT-ON* + case achievement_comparison::last: + break; + } + debugmsg( "Invalid achievement_comparison" ); + abort(); +} + +} // namespace io + +template<> +struct enum_traits { + static constexpr achievement_comparison last = achievement_comparison::last; +}; + +struct achievement_requirement { + string_id statistic; + achievement_comparison comparison; + int target; + + void deserialize( JsonIn &jin ) { + const JsonObject &jo = jin.get_object(); + if( !( jo.read( "event_statistic", statistic ) && + jo.read( "is", comparison ) && + jo.read( "target", target ) ) ) { + jo.throw_error( "Mandatory field missing for achievement requirement" ); + } + } + + void check( const string_id &id ) const { + if( !statistic.is_valid() ) { + debugmsg( "score %s refers to invalid statistic %s", id.str(), statistic.str() ); + } + } + + bool satisifed_by( const cata_variant &v ) const { + int value = v.get(); + switch( comparison ) { + case achievement_comparison::greater_equal: + return value >= target; + case achievement_comparison::last: + break; + } + debugmsg( "Invalid achievement_requirement comparison value" ); + abort(); + } +}; + + +void achievement::load_achievement( const JsonObject &jo, const std::string &src ) +{ + achievement_factory.load( jo, src ); +} + +void achievement::check_consistency() +{ + achievement_factory.check(); +} + +const std::vector &achievement::get_all() +{ + return achievement_factory.get_all(); +} + +void achievement::reset() +{ + achievement_factory.reset(); +} + +void achievement::load( const JsonObject &jo, const std::string & ) +{ + mandatory( jo, was_loaded, "description", description_ ); + mandatory( jo, was_loaded, "requirements", requirements_ ); +} + +void achievement::check() const +{ + for( const achievement_requirement &req : requirements_ ) { + req.check( id ); + } +} + +class requirement_watcher : stat_watcher +{ + public: + requirement_watcher( achievement_tracker &tracker, const achievement_requirement &req, + stats_tracker &stats ) : + tracker_( &tracker ), + requirement_( &req ) { + stats.add_watcher( req.statistic, this ); + } + + void new_value( const cata_variant &new_value, stats_tracker & ) override; + + bool is_satisfied( stats_tracker &stats ) { + return requirement_->satisifed_by( requirement_->statistic->value( stats ) ); + } + private: + achievement_tracker *tracker_; + const achievement_requirement *requirement_; +}; + +class achievement_tracker +{ + public: + // Non-movable because requirement_watcher stores a pointer to us + achievement_tracker( const achievement_tracker & ) = delete; + achievement_tracker &operator=( const achievement_tracker & ) = delete; + + achievement_tracker( const achievement &a, achievements_tracker &tracker, + stats_tracker &stats ) : + achievement_( &a ), + tracker_( &tracker ) { + for( const achievement_requirement &req : a.requirements() ) { + watchers_.push_back( std::make_unique( *this, req, stats ) ); + } + + for( const std::unique_ptr &watcher : watchers_ ) { + bool is_satisfied = watcher->is_satisfied( stats ); + sorted_watchers_[is_satisfied].insert( watcher.get() ); + } + } + + void set_requirement( requirement_watcher *watcher, bool is_satisfied ) { + if( !sorted_watchers_[is_satisfied].insert( watcher ).second ) { + // No change + return; + } + + // Remove from other; check for completion. + sorted_watchers_[!is_satisfied].erase( watcher ); + assert( sorted_watchers_[0].size() + sorted_watchers_[1].size() == watchers_.size() ); + + if( sorted_watchers_[false].empty() ) { + tracker_->report_achievement( achievement_, achievement_completion::completed ); + } + } + private: + const achievement *achievement_; + achievements_tracker *tracker_; + std::vector> watchers_; + + // sorted_watchers_ maintains two sets of watchers, categorised by + // whether they watch a satisfied or unsatisfied requirement. This + // allows us to check whether the achievment is met on each new stat + // value in O(1) time. + std::array, 2> sorted_watchers_; +}; + +void requirement_watcher::new_value( const cata_variant &new_value, stats_tracker & ) +{ + tracker_->set_requirement( this, requirement_->satisifed_by( new_value ) ); +} + +namespace io +{ +template<> +std::string enum_to_string( achievement_completion data ) +{ + switch( data ) { + // *INDENT-OFF* + case achievement_completion::pending: return "pending"; + case achievement_completion::completed: return "completed"; + // *INDENT-ON* + case achievement_completion::last: + break; + } + debugmsg( "Invalid achievement_completion" ); + abort(); +} + +} // namespace io + +void achievement_state::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + jsout.member_as_string( "completion", completion ); + jsout.member( "last_state_change", last_state_change ); + jsout.end_object(); +} + +void achievement_state::deserialize( JsonIn &jsin ) +{ + JsonObject jo = jsin.get_object(); + jo.read( "completion", completion ); + jo.read( "last_state_change", last_state_change ); +} + +achievements_tracker::achievements_tracker( + stats_tracker &stats, + const std::function &achievement_attained_callback ) : + stats_( &stats ), + achievement_attained_callback_( achievement_attained_callback ) +{} + +achievements_tracker::~achievements_tracker() = default; + +std::vector achievements_tracker::valid_achievements() const +{ + std::vector result; + for( const achievement &ach : achievement::get_all() ) { + if( initial_achievements_.count( ach.id ) ) { + result.push_back( &ach ); + } + } + return result; +} + +void achievements_tracker::report_achievement( const achievement *a, achievement_completion comp ) +{ + auto it = achievements_status_.find( a->id ); + achievement_completion existing_comp = + ( it == achievements_status_.end() ) ? achievement_completion::pending + : it->second.completion; + if( existing_comp == comp ) { + return; + } + achievement_state new_state{ + comp, + calendar::turn + }; + if( it == achievements_status_.end() ) { + achievements_status_.emplace( a->id, new_state ); + } else { + it->second = new_state; + } + if( comp == achievement_completion::completed ) { + achievement_attained_callback_( a ); + } +} + +void achievements_tracker::clear() +{ + watchers_.clear(); + initial_achievements_.clear(); + achievements_status_.clear(); +} + +void achievements_tracker::notify( const cata::event &e ) +{ + if( e.type() == event_type::game_start ) { + assert( initial_achievements_.empty() ); + for( const achievement &ach : achievement::get_all() ) { + initial_achievements_.insert( ach.id ); + } + init_watchers(); + } +} + +void achievements_tracker::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + jsout.member( "initial_achievements", initial_achievements_ ); + jsout.member( "achievements_status", achievements_status_ ); + jsout.end_object(); +} + +void achievements_tracker::deserialize( JsonIn &jsin ) +{ + JsonObject jo = jsin.get_object(); + jo.read( "initial_achievements", initial_achievements_ ); + jo.read( "achievements_status", achievements_status_ ); + + init_watchers(); +} + +void achievements_tracker::init_watchers() +{ + for( const achievement *a : valid_achievements() ) { + watchers_.emplace_back( *a, *this, *stats_ ); + } +} diff --git a/src/achievement.h b/src/achievement.h new file mode 100644 index 0000000000000..dfe2d9527c40d --- /dev/null +++ b/src/achievement.h @@ -0,0 +1,97 @@ +#ifndef CATA_ACHIEVEMENT_H +#define CATA_ACHIEVEMENT_H + +#include +#include +#include +#include +#include +#include + +#include "event_bus.h" +#include "string_id.h" +#include "translations.h" + +class JsonObject; +struct achievement_requirement; +class achievement_tracker; +class stats_tracker; + +class achievement +{ + public: + achievement() = default; + + void load( const JsonObject &, const std::string & ); + void check() const; + static void load_achievement( const JsonObject &, const std::string & ); + static void check_consistency(); + static const std::vector &get_all(); + static void reset(); + + string_id id; + bool was_loaded = false; + + const translation &description() const { + return description_; + } + + const std::vector &requirements() const { + return requirements_; + } + private: + translation description_; + std::vector requirements_; +}; + +enum class achievement_completion { + pending, + completed, + last +}; + +template<> +struct enum_traits { + static constexpr achievement_completion last = achievement_completion::last; +}; + +struct achievement_state { + achievement_completion completion; + time_point last_state_change; + + void serialize( JsonOut & ) const; + void deserialize( JsonIn & ); +}; + +class achievements_tracker : public event_subscriber +{ + public: + // Non-movable because achievement_tracker stores a pointer to us + achievements_tracker( const achievements_tracker & ) = delete; + achievements_tracker &operator=( const achievements_tracker & ) = delete; + + achievements_tracker( stats_tracker &, + const std::function &achievement_attained_callback ); + ~achievements_tracker() override; + + // Return all scores which are valid now and existed at game start + std::vector valid_achievements() const; + + void report_achievement( const achievement *, achievement_completion ); + + void clear(); + void notify( const cata::event & ) override; + + void serialize( JsonOut & ) const; + void deserialize( JsonIn & ); + private: + void init_watchers(); + + stats_tracker *stats_ = nullptr; + std::function achievement_attained_callback_; + std::list watchers_; + std::unordered_set> initial_achievements_; + std::unordered_map, achievement_state> achievements_status_; +}; + +#endif // CATA_ACHIEVEMENT_H diff --git a/src/event_statistics.cpp b/src/event_statistics.cpp index 5388254c74fb1..45f6f96c2057d 100644 --- a/src/event_statistics.cpp +++ b/src/event_statistics.cpp @@ -539,12 +539,16 @@ struct event_statistic_unique_value : event_statistic::impl { struct state : stats_tracker_state, event_multiset_watcher { state( const event_statistic_unique_value *s, stats_tracker &stats ) : - stat( s ), - count( stats.get_events( s->type_ ).count() ), - value( s->value( stats ) ) { + stat( s ) { + init( stats ); stats.add_watcher( stat->type_, this ); } + void init( stats_tracker &stats ) { + count = stats.get_events( stat->type_ ).count(); + value = stat->value( stats ); + } + void event_added( const cata::event &e, stats_tracker &stats ) override { ++count; if( count == 1 ) { @@ -558,7 +562,7 @@ struct event_statistic_unique_value : event_statistic::impl { } void events_reset( const event_multiset &, stats_tracker &stats ) override { - *this = state( stat, stats ); + init( stats ); stats.stat_value_changed( stat->id_, value ); } @@ -600,6 +604,8 @@ void event_statistic::load( const JsonObject &jo, const std::string & ) std::string type; mandatory( jo, was_loaded, "stat_type", type ); + optional( jo, was_loaded, "description", description_ ); + if( type == "count" ) { impl_ = std::make_unique( id, event_source( jo ) ); } else if( type == "total" ) { diff --git a/src/event_statistics.h b/src/event_statistics.h index 271997910be73..8cf8ccad4e303 100644 --- a/src/event_statistics.h +++ b/src/event_statistics.h @@ -72,8 +72,13 @@ class event_statistic string_id id; bool was_loaded = false; + const std::string &description() const { + return description_; + } + class impl; private: + std::string description_; cata::clone_ptr impl_; }; diff --git a/src/game.cpp b/src/game.cpp index 2ba4ebb564305..ed201fa48b352 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -26,6 +26,7 @@ #include #include +#include "achievement.h" #include "action.h" #include "activity_handlers.h" #include "artifact.h" @@ -245,10 +246,17 @@ bool is_valid_in_w_terrain( const point &p ) return p.x >= 0 && p.x < TERRAIN_WINDOW_WIDTH && p.y >= 0 && p.y < TERRAIN_WINDOW_HEIGHT; } +static void achievement_attained( const achievement *a ) +{ + g->u.add_msg_if_player( m_good, _( "You completed the achievement \"%s\"." ), + a->description() ); +} + // This is the main game set-up process. game::game() : liveview( *liveview_ptr ), scent_ptr( *this ), + achievements_tracker_ptr( *stats_tracker_ptr, achievement_attained ), m( *map_ptr ), u( *u_ptr ), scent( *scent_ptr ), @@ -273,6 +281,7 @@ game::game() : events().subscribe( &*stats_tracker_ptr ); events().subscribe( &*kill_tracker_ptr ); events().subscribe( &*memorial_logger_ptr ); + events().subscribe( &*achievements_tracker_ptr ); events().subscribe( &*spell_events_ptr ); world_generator = std::make_unique(); // do nothing, everything that was in here is moved to init_data() which is called immediately after g = new game; in main.cpp @@ -662,6 +671,7 @@ void game::setup() stats().clear(); // reset kill counts kill_tracker_ptr->clear(); + achievements_tracker_ptr->clear(); // reset follower list follower_ids.clear(); scent.reset(); diff --git a/src/game.h b/src/game.h index 3a184978cf352..0ab7492fb81d1 100644 --- a/src/game.h +++ b/src/game.h @@ -94,6 +94,7 @@ enum target_mode : int; struct special_game; using itype_id = std::string; +class achievements_tracker; class avatar; class event_bus; class kill_tracker; @@ -926,6 +927,7 @@ class game pimpl timed_event_manager_ptr; pimpl event_bus_ptr; pimpl stats_tracker_ptr; + pimpl achievements_tracker_ptr; pimpl kill_tracker_ptr; pimpl memorial_logger_ptr; pimpl spell_events_ptr; diff --git a/src/init.cpp b/src/init.cpp index fa7a4c1d1421f..391bd3a8ef6eb 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -11,6 +11,7 @@ #include #include +#include "achievement.h" #include "activity_type.h" #include "ammo.h" #include "anatomy.h" @@ -392,6 +393,7 @@ void DynamicDataLoader::initialize() add( "event_transformation", &event_transformation::load_transformation ); add( "event_statistic", &event_statistic::load_statistic ); add( "score", &score::load_score ); + add( "achievement", &achievement::load_achievement ); #if defined(TILES) add( "mod_tileset", &load_mod_tileset ); #else @@ -540,6 +542,7 @@ void DynamicDataLoader::unload_data() event_transformation::reset(); event_statistic::reset(); score::reset(); + achievement::reset(); scent_type::reset(); // TODO: @@ -700,6 +703,7 @@ void DynamicDataLoader::check_consistency( loading_ui &ui ) { _( "Statistics" ), &event_statistic::check_consistency }, { _( "Scent types" ), &scent_type::check_scent_consistency }, { _( "Scores" ), &score::check_consistency }, + { _( "Achivements" ), &achievement::check_consistency }, { _( "Disease types" ), &disease_type::check_disease_consistency }, { _( "Factions" ), &faction_template::check_consistency }, } diff --git a/src/json.h b/src/json.h index e4faca27de5f5..6ec59c517e183 100644 --- a/src/json.h +++ b/src/json.h @@ -720,6 +720,10 @@ class JsonOut member( name ); write( value ); } + template void member_as_string( const std::string &name, const T &value ) { + member( name ); + write_as_string( value ); + } }; /* JsonObject diff --git a/src/savegame.cpp b/src/savegame.cpp index 9652152968fa2..c3936ef615531 100644 --- a/src/savegame.cpp +++ b/src/savegame.cpp @@ -11,6 +11,7 @@ #include #include +#include "achievement.h" #include "avatar.h" #include "coordinate_conversions.h" #include "creature_tracker.h" @@ -101,6 +102,7 @@ void game::serialize( std::ostream &fout ) // save stats. json.member( "kill_tracker", *kill_tracker_ptr ); json.member( "stats_tracker", *stats_tracker_ptr ); + json.member( "achievements_tracker", *achievements_tracker_ptr ); json.member( "player", u ); Messages::serialize( json ); @@ -234,6 +236,7 @@ void game::unserialize( std::istream &fin ) data.read( "player", u ); data.read( "stats_tracker", *stats_tracker_ptr ); + data.read( "achievements_tracker", *achievements_tracker_ptr ); Messages::deserialize( data ); } catch( const JsonError &jsonerr ) { diff --git a/src/stats_tracker.cpp b/src/stats_tracker.cpp index dd3e0001cf210..e877e06a59b6b 100644 --- a/src/stats_tracker.cpp +++ b/src/stats_tracker.cpp @@ -1,5 +1,7 @@ #include "stats_tracker.h" +#include + #include "event_statistics.h" static bool event_data_matches( const cata::event::data_type &data, @@ -66,15 +68,42 @@ void event_multiset::add( const cata::event &e ) counts_[e.data()]++; } -stat_watcher::~stat_watcher() = default; -event_multiset_watcher::~event_multiset_watcher() = default; -stats_tracker_state::~stats_tracker_state() = default; - void event_multiset::add( const counts_type::value_type &e ) { counts_[e.first] += e.second; } +base_watcher::~base_watcher() +{ + if( subscribed_to ) { + subscribed_to->unwatch( this ); + } +} + +void base_watcher::on_subscribe( stats_tracker *s ) +{ + if( subscribed_to ) { + debugmsg( "Subscribing a single base_watcher multiple times is not supported" ); + } + subscribed_to = s; +} + +void base_watcher::on_unsubscribe( stats_tracker *s ) +{ + if( subscribed_to != s ) { + debugmsg( "Unexpected notification of unsubscription from wrong stats_tracker" ); + } else { + subscribed_to = nullptr; + } +} + +stats_tracker_state::~stats_tracker_state() = default; + +stats_tracker::~stats_tracker() +{ + unwatch_all(); +} + event_multiset &stats_tracker::get_events( event_type type ) { return data.emplace( type, event_multiset( type ) ).first->second; @@ -94,12 +123,14 @@ cata_variant stats_tracker::value_of( const string_id &stat ) void stats_tracker::add_watcher( event_type type, event_multiset_watcher *watcher ) { event_type_watchers[type].push_back( watcher ); + watcher->on_subscribe( this ); } void stats_tracker::add_watcher( const string_id &id, event_multiset_watcher *watcher ) { event_transformation_watchers[id].push_back( watcher ); + watcher->on_subscribe( this ); std::unique_ptr &state = event_transformation_states[ id ]; if( !state ) { state = id->watch( *this ); @@ -109,12 +140,38 @@ void stats_tracker::add_watcher( const string_id &id, void stats_tracker::add_watcher( const string_id &id, stat_watcher *watcher ) { stat_watchers[id].push_back( watcher ); + watcher->on_subscribe( this ); std::unique_ptr &state = stat_states[ id ]; if( !state ) { state = id->watch( *this ); } } +void stats_tracker::unwatch( base_watcher *watcher ) +{ + // Use a slow O(n) approach for now; if it proves problematic we can build + // an index, but that seems over-complex. + auto erase_from = [watcher]( auto & map_of_vectors ) { + for( auto &p : map_of_vectors ) { + auto &vector = p.second; + auto it = std::find( vector.begin(), vector.end(), watcher ); + if( it != vector.end() ) { + vector.erase( it ); + return true; + } + } + return false; + }; + + + if( erase_from( event_type_watchers ) || + erase_from( event_transformation_watchers ) || + erase_from( stat_watchers ) ) { + return; + } + debugmsg( "unwatch for a watcher not found" ); +} + void stats_tracker::transformed_set_changed( const string_id &id, const cata::event &new_element ) { @@ -161,7 +218,27 @@ std::vector stats_tracker::valid_scores() const void stats_tracker::clear() { + unwatch_all(); data.clear(); + event_transformation_states.clear(); + stat_states.clear(); + initial_scores.clear(); +} + +void stats_tracker::unwatch_all() +{ + auto unsub_all = [&]( auto & map_of_vectors ) { + for( auto const &p : map_of_vectors ) { + const auto &vector = p.second; + for( base_watcher *watcher : vector ) { + watcher->on_unsubscribe( this ); + } + } + map_of_vectors.clear(); + }; + unsub_all( event_type_watchers ); + unsub_all( event_transformation_watchers ); + unsub_all( stat_watchers ); } void stats_tracker::notify( const cata::event &e ) @@ -177,6 +254,7 @@ void stats_tracker::notify( const cata::event &e ) } if( e.type() == event_type::game_start ) { + assert( initial_scores.empty() ); for( const score &scr : score::get_all() ) { initial_scores.insert( scr.id ); } diff --git a/src/stats_tracker.h b/src/stats_tracker.h index b6c591f980ae6..481b3eeb93a79 100644 --- a/src/stats_tracker.h +++ b/src/stats_tracker.h @@ -62,17 +62,30 @@ class event_multiset counts_type counts_; }; -class stat_watcher +class base_watcher +{ + public: + base_watcher() = default; + base_watcher( const base_watcher & ) = delete; + base_watcher &operator=( const base_watcher & ) = delete; + protected: + virtual ~base_watcher(); + private: + friend class stats_tracker; + void on_subscribe( stats_tracker * ); + void on_unsubscribe( stats_tracker * ); + stats_tracker *subscribed_to = nullptr; +}; + +class stat_watcher : public base_watcher { public: - virtual ~stat_watcher() = 0; virtual void new_value( const cata_variant &, stats_tracker & ) = 0; }; -class event_multiset_watcher +class event_multiset_watcher : public base_watcher { public: - virtual ~event_multiset_watcher() = 0; virtual void event_added( const cata::event &, stats_tracker & ) = 0; virtual void events_reset( const event_multiset &, stats_tracker & ) = 0; }; @@ -86,6 +99,8 @@ class stats_tracker_state class stats_tracker : public event_subscriber { public: + ~stats_tracker() override; + event_multiset &get_events( event_type ); event_multiset get_events( const string_id & ); @@ -95,6 +110,8 @@ class stats_tracker : public event_subscriber void add_watcher( const string_id &, event_multiset_watcher * ); void add_watcher( const string_id &, stat_watcher * ); + void unwatch( base_watcher * ); + void transformed_set_changed( const string_id &, const cata::event &new_element ); void transformed_set_changed( const string_id &, @@ -111,6 +128,8 @@ class stats_tracker : public event_subscriber void serialize( JsonOut & ) const; void deserialize( JsonIn & ); private: + void unwatch_all(); + std::unordered_map data; std::unordered_map> event_type_watchers; diff --git a/tests/stats_tracker_test.cpp b/tests/stats_tracker_test.cpp index 932807cc02b77..eaa1603d6579c 100644 --- a/tests/stats_tracker_test.cpp +++ b/tests/stats_tracker_test.cpp @@ -1,5 +1,6 @@ #include "catch/catch.hpp" +#include "achievement.h" #include "avatar.h" #include "event_statistics.h" #include "game.h" @@ -216,6 +217,31 @@ TEST_CASE( "stats_tracker_watchers", "[stats]" ) } } +TEST_CASE( "achievments_tracker", "[stats]" ) +{ + const achievement *achievement_completed = nullptr; + event_bus b; + stats_tracker s; + b.subscribe( &s ); + achievements_tracker a( s, [&]( const achievement * a ) { + achievement_completed = a; + } ); + b.subscribe( &a ); + + SECTION( "kills" ) { + const character_id u_id = g->u.getID(); + const mtype_id mon_zombie( "mon_zombie" ); + const cata::event avatar_zombie_kill = + cata::event::make( u_id, mon_zombie ); + + b.send( u_id ); + CHECK( achievement_completed == nullptr ); + b.send( avatar_zombie_kill ); + REQUIRE( achievement_completed != nullptr ); + CHECK( achievement_completed->id.str() == "achievement_kill_zombie" ); + } +} + TEST_CASE( "stats_tracker_in_game", "[stats]" ) { g->stats().clear();