From ede6aea27f0090dc7e2ef3f8b41537a67daf8af6 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Mon, 9 Sep 2019 09:14:17 -0400 Subject: [PATCH 01/10] Add runtime access to event fields It's already possible to extract the required fields and their types for an event at compile time. Make it possible to also do so at runtime. --- src/event.cpp | 28 ++++++++++++++++++++++++++++ src/event.h | 2 ++ 2 files changed, 30 insertions(+) diff --git a/src/event.cpp b/src/event.cpp index 6d86b37746587..672ed89932517 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -136,4 +136,32 @@ DEFINE_EVENT_FIELDS( teleports_into_wall ) } // namespace event_detail +template +static void get_fields_if_match( event_type type, std::map &out ) +{ + if( Type == type ) { + out = { event_detail::event_spec::fields.begin(), + event_detail::event_spec::fields.end() + }; + } +} + +template +static std::map +get_fields_helper( event_type type, std::integer_sequence ) +{ + std::map result; + bool discard[] = { + ( get_fields_if_match( I )>( type, result ), true )... + }; + ( void ) discard; + return result; +} + +std::map event::get_fields( event_type type ) +{ + return get_fields_helper( + type, std::make_integer_sequence( event_type::num_event_types )> {} ); +} + } // namespace cata diff --git a/src/event.h b/src/event.h index c0af70a063972..7d65e9ad66418 100644 --- a/src/event.h +++ b/src/event.h @@ -547,6 +547,8 @@ class event > ()( calendar::turn, std::forward( args )... ); } + static std::map get_fields( event_type ); + event_type type() const { return type_; } From 2472c22b2c7d7b37a85cc7aa70fe2145113eb9a7 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Mon, 9 Sep 2019 09:16:07 -0400 Subject: [PATCH 02/10] Add avatar id to game_start event --- src/event.cpp | 1 + src/event.h | 9 +++++++-- src/game.cpp | 2 +- tests/memorial_test.cpp | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/event.cpp b/src/event.cpp index 672ed89932517..d213c561ad048 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -124,6 +124,7 @@ DEFINE_EVENT_FIELDS( gains_addiction ) DEFINE_EVENT_FIELDS( gains_mutation ) DEFINE_EVENT_FIELDS( gains_skill_level ) DEFINE_EVENT_FIELDS( game_over ) +DEFINE_EVENT_FIELDS( game_start ) DEFINE_EVENT_FIELDS( installs_cbm ) DEFINE_EVENT_FIELDS( installs_faulty_cbm ) DEFINE_EVENT_FIELDS( launches_nuke ) diff --git a/src/event.h b/src/event.h index 7d65e9ad66418..9a521af7e4b91 100644 --- a/src/event.h +++ b/src/event.h @@ -398,7 +398,7 @@ struct event_spec { }; template<> -struct event_spec : event_spec_empty { +struct event_spec { static constexpr std::array, 2> fields = {{ { "is_suicide", cata_variant_type::bool_ }, { "last_words", cata_variant_type::string }, @@ -407,7 +407,12 @@ struct event_spec : event_spec_empty { }; template<> -struct event_spec : event_spec_empty {}; +struct event_spec { + static constexpr std::array, 1> fields = {{ + { "avatar_id", cata_variant_type::character_id }, + } + }; +}; template<> struct event_spec { diff --git a/src/game.cpp b/src/game.cpp index b7508afb9bbd8..1f09e52390ced 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -869,7 +869,7 @@ bool game::start_game() mission->assign( u ); } - g->events().send(); + g->events().send( u.getID() ); return true; } diff --git a/tests/memorial_test.cpp b/tests/memorial_test.cpp index 175661a36c63e..adca2e9f50c11 100644 --- a/tests/memorial_test.cpp +++ b/tests/memorial_test.cpp @@ -176,7 +176,7 @@ TEST_CASE( "memorials" ) m, b, u_name + " was killed.\nLast words: last_words", false, "last_words" ); check_memorial( - m, b, u_name + " began their journey into the Cataclysm." ); + m, b, u_name + " began their journey into the Cataclysm.", ch ); check_memorial( m, b, "Installed bionic: Alarm System.", ch, cbm ); From bcb56663d1adf150e81f332447126aa2c7d743ef Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Sun, 8 Sep 2019 16:55:04 -0400 Subject: [PATCH 03/10] Scores defined in json Change event_tracker into event_multiset, because we want it to be a more generic object used for more than just tracking events. It now represents any collection of events of the same type. Introduce three new json-defined objects: - event_transformation, which converts one multiset of events to another. For example, it can filter all kill events to just kill events attributed to the avatar. - event_statistic, which converts a multiset of events into a single value. The simplest case, for example, is counting the number of events. - score, which uses an event_statistic to produce a score, which can be saved as a permanent record of a game. Currently scores' only purpose is to be written to the memorial log. --- data/json/scores.json | 61 ++++++ src/event_statistics.cpp | 355 +++++++++++++++++++++++++++++++++++ src/event_statistics.h | 81 ++++++++ src/init.cpp | 12 +- src/savegame_json.cpp | 27 +-- src/stats_tracker.cpp | 53 +++++- src/stats_tracker.h | 31 ++- tests/stats_tracker_test.cpp | 45 +++++ 8 files changed, 636 insertions(+), 29 deletions(-) create mode 100644 data/json/scores.json create mode 100644 src/event_statistics.cpp create mode 100644 src/event_statistics.h diff --git a/data/json/scores.json b/data/json/scores.json new file mode 100644 index 0000000000000..f9d45fd8068e8 --- /dev/null +++ b/data/json/scores.json @@ -0,0 +1,61 @@ +[ + { + "id": "avatar_id", + "type": "event_statistic", + "stat_type": "unique_value", + "event_type": "game_start", + "field": "avatar_id" + }, + { + "id": "avatar_kills", + "type": "event_transformation", + "event_type": "character_kills_monster", + "value_constraints": { + "killer": { "equals_statistic": "avatar_id" } + } + }, + { + "id": "num_avatar_kills", + "type": "event_statistic", + "stat_type": "count", + "event_transformation": "avatar_kills" + }, + { + "id": "score_kills", + "type": "score", + "name": "Number of monsters killed", + "statistic": "num_avatar_kills" + }, + { + "id": "moves_not_mounted", + "type": "event_transformation", + "event_type": "avatar_moves", + "value_constraints": { + "mount": { "equals": "" } + } + }, + { + "id": "num_moves", + "type": "event_statistic", + "stat_type": "count", + "event_type": "avatar_moves" + }, + { + "id": "num_moves_not_mounted", + "type": "event_statistic", + "stat_type": "count", + "event_transformation": "moves_not_mounted" + }, + { + "id": "score_moves", + "type": "score", + "name": "Distance moved", + "statistic": "num_moves" + }, + { + "id": "score_walked", + "type": "score", + "name": "Distance walked", + "statistic": "num_moves_not_mounted" + } +] diff --git a/src/event_statistics.cpp b/src/event_statistics.cpp new file mode 100644 index 0000000000000..9ffd2ad6bf867 --- /dev/null +++ b/src/event_statistics.cpp @@ -0,0 +1,355 @@ +#include "event_statistics.h" + +#include "event.h" +#include "generic_factory.h" +#include "stats_tracker.h" + +namespace +{ + +generic_factory event_transformation_factory( "event_transformation" ); +generic_factory event_statistic_factory( "event_statistic" ); +generic_factory score_factory( "score" ); + +} // namespace + +template<> +const event_transformation &string_id::obj() const +{ + return event_transformation_factory.obj( *this ); +} + +template<> +bool string_id::is_valid() const +{ + return event_transformation_factory.is_valid( *this ); +} + +void event_transformation::load_transformation( JsonObject &jo, const std::string &src ) +{ + event_transformation_factory.load( jo, src ); +} + +void event_transformation::check_consistency() +{ + event_transformation_factory.check(); +} + +void event_transformation::reset() +{ + event_transformation_factory.reset(); +} + +template<> +const event_statistic &string_id::obj() const +{ + return event_statistic_factory.obj( *this ); +} + +template<> +bool string_id::is_valid() const +{ + return event_statistic_factory.is_valid( *this ); +} + +void event_statistic::load_statistic( JsonObject &jo, const std::string &src ) +{ + event_statistic_factory.load( jo, src ); +} + +void event_statistic::check_consistency() +{ + event_statistic_factory.check(); +} + +void event_statistic::reset() +{ + event_statistic_factory.reset(); +} + +template<> +const score &string_id::obj() const +{ + return score_factory.obj( *this ); +} + +template<> +bool string_id::is_valid() const +{ + return score_factory.is_valid( *this ); +} + +void score::load_score( JsonObject &jo, const std::string &src ) +{ + score_factory.load( jo, src ); +} + +void score::check_consistency() +{ + score_factory.check(); +} + +void score::reset() +{ + score_factory.reset(); +} + +class event_transformation::impl +{ + public: + virtual ~impl() = default; + virtual event_multiset initialize( stats_tracker & ) const = 0; + virtual void check( const std::string &/*name*/ ) const {} + virtual std::unique_ptr clone() const = 0; +}; + +class event_statistic::impl +{ + public: + virtual ~impl() = default; + virtual cata_variant value( stats_tracker & ) const = 0; + virtual void check( const std::string &/*name*/ ) const {} + virtual std::unique_ptr clone() const = 0; +}; + +struct value_constraint { + cata::optional equals_; + cata::optional equals_string_; + cata::optional> equals_statistic_; + + bool permits( const cata_variant &v, stats_tracker &stats ) const { + if( equals_ && *equals_ != v ) { + return false; + } + if( equals_string_ && *equals_string_ != v.get_string() ) { + return false; + } + if( equals_statistic_ && stats.value_of( *equals_statistic_ ) != v ) { + return false; + } + return true; + } + + void deserialize( JsonIn &jsin ) { + JsonObject jo = jsin.get_object(); + int equals_int; + if( jo.read( "equals", equals_int, false ) ) { + equals_ = cata_variant::make( equals_int ); + } + + std::string equals_string; + if( jo.read( "equals", equals_string, false ) ) { + equals_string_ = equals_string; + } + + string_id stat; + if( jo.read( "equals_statistic", stat ) ) { + equals_statistic_ = stat; + } + + if( !equals_ && !equals_string_ && !equals_statistic_ ) { + jo.throw_error( "No valid value constraint found" ); + } + } + + void check( const std::string &name ) const { + if( equals_statistic_ && !equals_statistic_->is_valid() ) { + debugmsg( "constraint for event_transformation %s refers to invalid statistic %s", + name, equals_statistic_->str() ); + } + } +}; + +struct event_transformation_match : public event_transformation::impl { + template + event_transformation_match( event_type type, const Constriants &constriants ) : + type_( type ), + constraints_( constriants.begin(), constriants.end() ) + {} + + event_type type_; + std::vector> constraints_; + + bool matches( const cata::event::data_type &data, stats_tracker &stats ) const { + for( const std::pair &p : constraints_ ) { + const std::string &field = p.first; + const value_constraint &constraint = p.second; + const auto it = data.find( field ); + if( it == data.end() || !constraint.permits( it->second, stats ) ) { + return false; + } + } + return true; + } + + event_multiset initialize( stats_tracker &stats ) const override { + const event_multiset::counts_type &input = stats.get_events( type_ ).counts(); + event_multiset result( type_ ); + + for( const std::pair &p : input ) { + if( matches( p.first, stats ) ) { + result.add( p ); + } + } + return result; + } + + void check( const std::string &name ) const override { + for( const std::pair &p : constraints_ ) { + p.second.check( name ); + } + } + + std::unique_ptr clone() const override { + return std::make_unique( *this ); + } +}; + +event_multiset event_transformation::initialize( stats_tracker &stats ) const +{ + return impl_->initialize( stats ); +} + +void event_transformation::load( JsonObject &jo, const std::string & ) +{ + event_type type = event_type::num_event_types; + mandatory( jo, was_loaded, "event_type", type ); + std::map constraints; + mandatory( jo, was_loaded, "value_constraints", constraints ); + + impl_ = std::make_unique( type, constraints ); +} + +void event_transformation::check() const +{ + impl_->check( id.str() ); +} + +struct event_statistic_count_type : event_statistic::impl { + event_statistic_count_type( event_type t ) : type( t ) {} + + event_type type; + + cata_variant value( stats_tracker &stats ) const override { + int count = stats.get_events( type ).count(); + return cata_variant::make( count ); + } + + std::unique_ptr clone() const override { + return std::make_unique( *this ); + } +}; + +struct event_statistic_count_transformation : event_statistic::impl { + event_statistic_count_transformation( const string_id &transformation ) : + transformation_( transformation ) + {} + + string_id transformation_; + + cata_variant value( stats_tracker &stats ) const override { + int count = stats.get_events( transformation_ ).count(); + return cata_variant::make( count ); + } + + std::unique_ptr clone() const override { + return std::make_unique( *this ); + } +}; + +struct event_statistic_unique_value : event_statistic::impl { + event_statistic_unique_value( event_type type, const std::string &field ) : + type_( type ), field_( field ) + {} + + event_type type_; + std::string field_; + + cata_variant value( stats_tracker &stats ) const override { + const event_multiset::counts_type &counts = stats.get_events( type_ ).counts(); + if( counts.size() != 1 ) { + return cata_variant(); + } + + const cata::event::data_type &d = counts.begin()->first; + auto it = d.find( field_ ); + if( it == d.end() ) { + return cata_variant(); + } + return it->second; + } + + void check( const std::string &name ) const override { + std::map event_fields = cata::event::get_fields( type_ ); + auto it = event_fields.find( field_ ); + if( it == event_fields.end() ) { + debugmsg( "event_statistic %s refers to field %s in event_type %s, but that type has " + "no such field", name, field_, io::enum_to_string( type_ ) ); + } + } + + std::unique_ptr clone() const override { + return std::make_unique( *this ); + } +}; + +cata_variant event_statistic::value( stats_tracker &stats ) const +{ + return impl_->value( stats ); +} + +void event_statistic::load( JsonObject &jo, const std::string & ) +{ + std::string type; + mandatory( jo, was_loaded, "stat_type", type ); + + if( type == "count" ) { + event_type event_t = event_type::num_event_types; + optional( jo, was_loaded, "event_type", event_t, event_type::num_event_types ); + string_id event_sub; + optional( jo, was_loaded, "event_transformation", event_sub ); + + if( ( event_t == event_type::num_event_types ) == event_sub.is_empty() ) { + jo.throw_error( "Must specify exactly one of 'event_type' or 'event_transformation' in " + "event_statistic of type 'count'" ); + } + + if( event_sub.is_empty() ) { + impl_ = std::make_unique( event_t ); + } else { + impl_ = std::make_unique( event_sub ); + } + } else if( type == "unique_value" ) { + event_type event_t = event_type::num_event_types; + mandatory( jo, was_loaded, "event_type", event_t ); + std::string field; + mandatory( jo, was_loaded, "field", field ); + + impl_ = std::make_unique( event_t, field ); + } else { + jo.throw_error( "Invalid stat_type '" + type + "'" ); + } +} + +void event_statistic::check() const +{ + impl_->check( id.str() ); +} + +cata_variant score::value( stats_tracker &stats ) const +{ + return stats.value_of( stat ); +} + +void score::load( JsonObject &jo, const std::string & ) +{ + mandatory( jo, was_loaded, "name", name ); + mandatory( jo, was_loaded, "statistic", stat ); +} + +void score::check() const +{ + if( !stat.is_valid() ) { + debugmsg( "score %s refers to invalid statistic %s", id.str(), stat.str() ); + } +} diff --git a/src/event_statistics.h b/src/event_statistics.h new file mode 100644 index 0000000000000..e0f818d92e92d --- /dev/null +++ b/src/event_statistics.h @@ -0,0 +1,81 @@ +#pragma once +#ifndef CATA_EVENT_STATISTICS_H +#define CATA_EVENT_STATISTICS_H + +#include +#include + +#include "clone_ptr.h" +#include "string_id.h" + +class cata_variant; +namespace cata +{ +class event; +} // namespace cata +class event_multiset; +enum class event_type : int; +class JsonObject; +class stats_tracker; + +// A transformation from one multiset of events to another +class event_transformation +{ + public: + event_multiset initialize( stats_tracker & ) const; + + void load( JsonObject &, const std::string & ); + void check() const; + static void load_transformation( JsonObject &, const std::string & ); + static void check_consistency(); + static void reset(); + + string_id id; + bool was_loaded = false; + + class impl; + private: + cata::clone_ptr impl_; +}; + +// A value computed from events somehow +class event_statistic +{ + public: + cata_variant value( stats_tracker & ) const; + + void load( JsonObject &, const std::string & ); + void check() const; + static void load_statistic( JsonObject &, const std::string & ); + static void check_consistency(); + static void reset(); + + string_id id; + bool was_loaded = false; + + class impl; + private: + cata::clone_ptr impl_; +}; + +class score +{ + public: + score() = default; + std::string tname() const; + cata_variant value( stats_tracker &stats ) const; + + void load( JsonObject &, const std::string & ); + void check() const; + static void load_score( JsonObject &, const std::string & ); + static void check_consistency(); + static void reset(); + + string_id id; + bool was_loaded = false; + private: + std::string name; + string_id stat; +}; + +#endif // CATA_EVENT_STATISTICS_H diff --git a/src/init.cpp b/src/init.cpp index 5736edd91993e..614de00a33d6b 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -22,6 +22,7 @@ #include "dialogue.h" #include "effect.h" #include "emit.h" +#include "event_statistics.h" #include "faction.h" #include "fault.h" #include "filesystem.h" @@ -375,6 +376,9 @@ void DynamicDataLoader::initialize() add( "SPELL", &spell_type::load_spell ); add( "clothing_mod", &clothing_mods::load ); add( "ter_furn_transform", &ter_furn_transform::load_transform ); + add( "event_transformation", &event_transformation::load_transformation ); + add( "event_statistic", &event_statistic::load_statistic ); + add( "score", &score::load_score ); #if defined(TILES) add( "mod_tileset", &load_mod_tileset ); #else @@ -519,6 +523,9 @@ void DynamicDataLoader::unload_data() VehicleGroup::reset(); VehiclePlacement::reset(); VehicleSpawn::reset(); + event_transformation::reset(); + event_statistic::reset(); + score::reset(); // TODO: // Name::clear(); @@ -667,7 +674,10 @@ void DynamicDataLoader::check_consistency( loading_ui &ui ) { _( "NPC templates" ), &npc_template::check_consistency }, { _( "Body parts" ), &body_part_struct::check_consistency }, { _( "Anatomies" ), &anatomy::check_consistency }, - { _( "Spells" ), &spell_type::check_consistency } + { _( "Spells" ), &spell_type::check_consistency }, + { _( "Transformations" ), &event_transformation::check_consistency }, + { _( "Statistics" ), &event_statistic::check_consistency }, + { _( "Scores" ), &score::check_consistency } } }; diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index ffcb4461eae5e..f17cd2dee4d31 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -3306,30 +3306,20 @@ void cata_variant::deserialize( JsonIn &jsin ) jsin.end_array(); } -void event_tracker::serialize( JsonOut &jsout ) const +void event_multiset::serialize( JsonOut &jsout ) const { jsout.start_object(); - using value_type = decltype( event_counts )::value_type; - std::vector copy( event_counts.begin(), event_counts.end() ); + std::vector copy( counts_.begin(), counts_.end() ); jsout.member( "event_counts", copy ); jsout.end_object(); } -void event_tracker::deserialize( JsonIn &jsin ) +void event_multiset::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(); - } - } + JsonObject jo = jsin.get_object(); + std::vector> copy; + jo.read( "event_counts", copy ); + counts_ = { copy.begin(), copy.end() }; } void stats_tracker::serialize( JsonOut &jsout ) const @@ -3343,6 +3333,9 @@ void stats_tracker::deserialize( JsonIn &jsin ) { JsonObject jo = jsin.get_object(); jo.read( "data", data ); + for( std::pair &d : data ) { + d.second.set_type( d.first ); + } } void submap::store( JsonOut &jsout ) const diff --git a/src/stats_tracker.cpp b/src/stats_tracker.cpp index 336a185b628d3..1a2c5a835ef34 100644 --- a/src/stats_tracker.cpp +++ b/src/stats_tracker.cpp @@ -1,5 +1,7 @@ #include "stats_tracker.h" +#include "event_statistics.h" + static bool event_data_matches( const cata::event::data_type &data, const cata::event::data_type &criteria ) { @@ -12,10 +14,26 @@ static bool event_data_matches( const cata::event::data_type &data, return true; } -int event_tracker::count( const cata::event::data_type &criteria ) const +void event_multiset::set_type( event_type type ) +{ + // Used during stats_tracker deserialization to set the type + assert( type_ == event_type::num_event_types ); + type_ = type; +} + +int event_multiset::count() const { int total = 0; - for( const auto &pair : event_counts ) { + for( const auto &pair : counts_ ) { + total += pair.second; + } + return total; +} + +int event_multiset::count( const cata::event::data_type &criteria ) const +{ + int total = 0; + for( const auto &pair : counts_ ) { if( event_data_matches( pair.first, criteria ) ) { total += pair.second; } @@ -23,10 +41,10 @@ int event_tracker::count( const cata::event::data_type &criteria ) const return total; } -int event_tracker::total( const std::string &field, const cata::event::data_type &criteria ) const +int event_multiset::total( const std::string &field, const cata::event::data_type &criteria ) const { int total = 0; - for( const auto &pair : event_counts ) { + for( const auto &pair : counts_ ) { auto it = pair.first.find( field ); if( it == pair.first.end() ) { continue; @@ -38,9 +56,14 @@ int event_tracker::total( const std::string &field, const cata::event::data_type return total; } -void event_tracker::add( const cata::event &e ) +void event_multiset::add( const cata::event &e ) +{ + counts_[e.data()]++; +} + +void event_multiset::add( const counts_type::value_type &e ) { - event_counts[e.data()]++; + counts_[e.first] += e.second; } int stats_tracker::count( const cata::event &e ) const @@ -57,6 +80,17 @@ int stats_tracker::count( event_type type, const cata::event::data_type &criteri return it->second.count( criteria ); } +event_multiset &stats_tracker::get_events( event_type type ) +{ + return data.emplace( type, event_multiset( type ) ).first->second; +} + +event_multiset stats_tracker::get_events( + const string_id &transform_id ) +{ + return transform_id->initialize( *this ); +} + int stats_tracker::total( event_type type, const std::string &field, const cata::event::data_type &criteria ) const { @@ -67,6 +101,11 @@ int stats_tracker::total( event_type type, const std::string &field, return it->second.total( field, criteria ); } +cata_variant stats_tracker::value_of( const string_id &stat ) +{ + return stat->value( *this ); +} + void stats_tracker::clear() { data.clear(); @@ -74,5 +113,5 @@ void stats_tracker::clear() void stats_tracker::notify( const cata::event &e ) { - data[e.type()].add( e ); + get_events( e.type() ).add( e ); } diff --git a/src/stats_tracker.h b/src/stats_tracker.h index b7d724188bd9b..d07a9d817113a 100644 --- a/src/stats_tracker.h +++ b/src/stats_tracker.h @@ -4,26 +4,45 @@ #include "event_bus.h" #include "hash_utils.h" +class event_statistic; +class event_transformation; + // The stats_tracker is intended to keep a summary of events that have occured. -// For each event_type it stores an event_tracker. +// For each event_type it stores an event_multiset. // 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 +class event_multiset { public: + using counts_type = std::unordered_map; + + // Default constructor for deserialization deliberately uses invalid + // type + event_multiset() : type_( event_type::num_event_types ) {} + event_multiset( event_type type ) : type_( type ) {} + + void set_type( event_type ); + + const counts_type &counts() const { + return counts_; + } + + int count() const; 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 add( const counts_type::value_type & ); void serialize( JsonOut & ) const; void deserialize( JsonIn & ); private: - std::unordered_map event_counts; + event_type type_; + counts_type counts_; }; class stats_tracker : public event_subscriber @@ -47,6 +66,10 @@ class stats_tracker : public event_subscriber 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; + event_multiset &get_events( event_type ); + event_multiset get_events( const string_id & ); + + cata_variant value_of( const string_id & ); void clear(); void notify( const cata::event & ) override; @@ -54,7 +77,7 @@ class stats_tracker : public event_subscriber void serialize( JsonOut & ) const; void deserialize( JsonIn & ); private: - std::unordered_map data; + std::unordered_map data; }; #endif // CATA_STATS_TRACKER_H diff --git a/tests/stats_tracker_test.cpp b/tests/stats_tracker_test.cpp index a1663f3da185e..1522046b68106 100644 --- a/tests/stats_tracker_test.cpp +++ b/tests/stats_tracker_test.cpp @@ -1,6 +1,7 @@ #include "catch/catch.hpp" #include "avatar.h" +#include "event_statistics.h" #include "game.h" #include "stats_tracker.h" @@ -58,6 +59,50 @@ TEST_CASE( "stats_tracker_total_events", "[stats]" ) CHECK( s.total( event_type::character_takes_damage, "damage", damage_to_any ) == 35 ); } +TEST_CASE( "stats_tracker_with_event_statistics", "[stats]" ) +{ + stats_tracker s; + event_bus b; + b.subscribe( &s ); + + const mtype_id no_monster; + const mtype_id horse( "mon_horse" ); + const cata::event walk = cata::event::make( no_monster ); + const cata::event ride = cata::event::make( horse ); + const string_id score_moves( "score_moves" ); + const string_id score_walked( "score_walked" ); + + CHECK( score_walked->value( s ) == cata_variant( 0 ) ); + CHECK( score_moves->value( s ) == cata_variant( 0 ) ); + b.send( walk ); + CHECK( score_walked->value( s ) == cata_variant( 1 ) ); + CHECK( score_moves->value( s ) == cata_variant( 1 ) ); + b.send( ride ); + CHECK( score_walked->value( s ) == cata_variant( 1 ) ); + CHECK( score_moves->value( s ) == cata_variant( 2 ) ); + + const character_id u_id = g->u.getID(); + character_id other_id = u_id; + ++other_id; + const mtype_id mon( "mon_zombie" ); + const cata::event avatar_kill = + cata::event::make( u_id, mon ); + const cata::event other_kill = + cata::event::make( other_id, mon ); + const string_id avatar_id( "avatar_id" ); + const string_id num_avatar_kills( "num_avatar_kills" ); + const string_id score_kills( "score_kills" ); + + b.send( u_id ); + CHECK( avatar_id->value( s ) == cata_variant( u_id ) ); + CHECK( score_kills->value( s ).get() == 0 ); + b.send( avatar_kill ); + CHECK( num_avatar_kills->value( s ).get() == 1 ); + CHECK( score_kills->value( s ).get() == 1 ); + b.send( other_kill ); + CHECK( score_kills->value( s ).get() == 1 ); +} + TEST_CASE( "stats_tracker_in_game", "[stats]" ) { g->stats().clear(); From afb099cc1f48ab5e3304ce3e9c8ef1e1d410fcc1 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 12 Sep 2019 07:36:26 -0400 Subject: [PATCH 04/10] Use descriptions rather than names for scores The new "description" field is expected to include a '%s' field for the value of the score to be inserted into. --- data/json/scores.json | 6 +++--- src/event_statistics.cpp | 15 ++++++++++----- src/event_statistics.h | 9 +++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/data/json/scores.json b/data/json/scores.json index f9d45fd8068e8..79b0d536d341c 100644 --- a/data/json/scores.json +++ b/data/json/scores.json @@ -23,7 +23,7 @@ { "id": "score_kills", "type": "score", - "name": "Number of monsters killed", + "description": "Number of monsters killed: %s", "statistic": "num_avatar_kills" }, { @@ -49,13 +49,13 @@ { "id": "score_moves", "type": "score", - "name": "Distance moved", + "description": "Distance moved: %s squares", "statistic": "num_moves" }, { "id": "score_walked", "type": "score", - "name": "Distance walked", + "description": "Distance walked: %s squares", "statistic": "num_moves_not_mounted" } ] diff --git a/src/event_statistics.cpp b/src/event_statistics.cpp index 9ffd2ad6bf867..8a00a07b0649d 100644 --- a/src/event_statistics.cpp +++ b/src/event_statistics.cpp @@ -336,20 +336,25 @@ void event_statistic::check() const impl_->check( id.str() ); } +std::string score::description( stats_tracker &stats ) const +{ + return string_format( _( description_ ), value( stats ).get_string() ); +} + cata_variant score::value( stats_tracker &stats ) const { - return stats.value_of( stat ); + return stats.value_of( stat_ ); } void score::load( JsonObject &jo, const std::string & ) { - mandatory( jo, was_loaded, "name", name ); - mandatory( jo, was_loaded, "statistic", stat ); + mandatory( jo, was_loaded, "description", description_ ); + mandatory( jo, was_loaded, "statistic", stat_ ); } void score::check() const { - if( !stat.is_valid() ) { - debugmsg( "score %s refers to invalid statistic %s", id.str(), stat.str() ); + if( !stat_.is_valid() ) { + debugmsg( "score %s refers to invalid statistic %s", id.str(), stat_.str() ); } } diff --git a/src/event_statistics.h b/src/event_statistics.h index e0f818d92e92d..2569f58fa0229 100644 --- a/src/event_statistics.h +++ b/src/event_statistics.h @@ -62,8 +62,9 @@ class score { public: score() = default; - std::string tname() const; - cata_variant value( stats_tracker &stats ) const; + // Returns translated description including value + std::string description( stats_tracker & ) const; + cata_variant value( stats_tracker & ) const; void load( JsonObject &, const std::string & ); void check() const; @@ -74,8 +75,8 @@ class score string_id id; bool was_loaded = false; private: - std::string name; - string_id stat; + std::string description_; + string_id stat_; }; #endif // CATA_EVENT_STATISTICS_H From 382022e615f4c8e63bb5f3ef6fe592653e537507 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 12 Sep 2019 07:38:06 -0400 Subject: [PATCH 05/10] Add a new statistic type "total" This is used for things like total damage received in the game. --- src/event_statistics.cpp | 65 ++++++++++++++++++++++++++++------------ src/stats_tracker.cpp | 5 ++++ src/stats_tracker.h | 1 + 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/event_statistics.cpp b/src/event_statistics.cpp index 8a00a07b0649d..4549d528810a1 100644 --- a/src/event_statistics.cpp +++ b/src/event_statistics.cpp @@ -225,35 +225,54 @@ void event_transformation::check() const impl_->check( id.str() ); } -struct event_statistic_count_type : event_statistic::impl { - event_statistic_count_type( event_type t ) : type( t ) {} +// Helper struct to abstract the two possible sources of event_multisets: +// event_types and event_transformations +struct event_source { + event_source( event_type t ) : type( t ) {} + event_source( const string_id &t ) : transformation( t ) {} - event_type type; + event_type type = event_type::num_event_types; + string_id transformation; + + event_multiset get( stats_tracker &stats ) const { + if( transformation.is_empty() ) { + return stats.get_events( type ); + } else { + return stats.get_events( transformation ); + } + } +}; + +struct event_statistic_count : event_statistic::impl { + event_statistic_count( const event_source &s ) : source( s ) {} + + event_source source; cata_variant value( stats_tracker &stats ) const override { - int count = stats.get_events( type ).count(); + int count = source.get( stats ).count(); return cata_variant::make( count ); } std::unique_ptr clone() const override { - return std::make_unique( *this ); + return std::make_unique( *this ); } }; -struct event_statistic_count_transformation : event_statistic::impl { - event_statistic_count_transformation( const string_id &transformation ) : - transformation_( transformation ) +struct event_statistic_total : event_statistic::impl { + event_statistic_total( const event_source &s, const std::string &f ) : + source( s ), field( f ) {} - string_id transformation_; + event_source source; + std::string field; cata_variant value( stats_tracker &stats ) const override { - int count = stats.get_events( transformation_ ).count(); - return cata_variant::make( count ); + int total = source.get( stats ).total( field ); + return cata_variant::make( total ); } std::unique_ptr clone() const override { - return std::make_unique( *this ); + return std::make_unique( *this ); } }; @@ -303,22 +322,30 @@ void event_statistic::load( JsonObject &jo, const std::string & ) std::string type; mandatory( jo, was_loaded, "stat_type", type ); - if( type == "count" ) { + auto get_event_source = [&]() { event_type event_t = event_type::num_event_types; optional( jo, was_loaded, "event_type", event_t, event_type::num_event_types ); - string_id event_sub; - optional( jo, was_loaded, "event_transformation", event_sub ); + string_id event_transformation; + optional( jo, was_loaded, "event_transformation", event_transformation ); - if( ( event_t == event_type::num_event_types ) == event_sub.is_empty() ) { + if( ( event_t == event_type::num_event_types ) == event_transformation.is_empty() ) { jo.throw_error( "Must specify exactly one of 'event_type' or 'event_transformation' in " "event_statistic of type 'count'" ); } - if( event_sub.is_empty() ) { - impl_ = std::make_unique( event_t ); + if( event_transformation.is_empty() ) { + return event_source( event_t ); } else { - impl_ = std::make_unique( event_sub ); + return event_source( event_transformation ); } + }; + + if( type == "count" ) { + impl_ = std::make_unique( get_event_source() ); + } else if( type == "total" ) { + std::string field; + mandatory( jo, was_loaded, "field", field ); + impl_ = std::make_unique( get_event_source(), field ); } else if( type == "unique_value" ) { event_type event_t = event_type::num_event_types; mandatory( jo, was_loaded, "event_type", event_t ); diff --git a/src/stats_tracker.cpp b/src/stats_tracker.cpp index 1a2c5a835ef34..69539761f46d3 100644 --- a/src/stats_tracker.cpp +++ b/src/stats_tracker.cpp @@ -41,6 +41,11 @@ int event_multiset::count( const cata::event::data_type &criteria ) const return total; } +int event_multiset::total( const std::string &field ) const +{ + return total( field, {} ); +} + int event_multiset::total( const std::string &field, const cata::event::data_type &criteria ) const { int total = 0; diff --git a/src/stats_tracker.h b/src/stats_tracker.h index d07a9d817113a..933653f802022 100644 --- a/src/stats_tracker.h +++ b/src/stats_tracker.h @@ -33,6 +33,7 @@ class event_multiset int count() const; int count( const cata::event::data_type &criteria ) const; + int total( const std::string &field ) const; int total( const std::string &field, const cata::event::data_type &criteria ) const; void add( const cata::event & ); From ad40ca8b9eb9435c1e933c42efbb3f0909e63efe Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 12 Sep 2019 07:39:13 -0400 Subject: [PATCH 06/10] Convert memorial_logger stats to use scores Rather than the hardcoded scores we previously had, just iterate over all the json-defined scores. Also, add new json scores to cover all the stats that were previously listed. --- data/json/scores.json | 62 ++++++++++++++++++++++++++++++++++++++++ src/event_statistics.cpp | 5 ++++ src/event_statistics.h | 2 ++ src/memorial_logger.cpp | 18 ++++-------- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/data/json/scores.json b/data/json/scores.json index 79b0d536d341c..317b07b853769 100644 --- a/data/json/scores.json +++ b/data/json/scores.json @@ -57,5 +57,67 @@ "type": "score", "description": "Distance walked: %s squares", "statistic": "num_moves_not_mounted" + }, + { + "id": "avatar_takes_damage", + "type": "event_transformation", + "event_type": "character_takes_damage", + "value_constraints": { + "character": { "equals_statistic": "avatar_id" } + } + }, + { + "id": "avatar_damage_taken", + "type": "event_statistic", + "stat_type": "total", + "field": "damage", + "event_transformation": "avatar_takes_damage" + }, + { + "id": "score_damage_taken", + "type": "score", + "description": "Damage taken: %s damage", + "statistic": "avatar_damage_taken" + }, + { + "id": "avatar_heals_damage", + "type": "event_transformation", + "event_type": "character_heals_damage", + "value_constraints": { + "character": { "equals_statistic": "avatar_id" } + } + }, + { + "id": "avatar_damage_healed", + "type": "event_statistic", + "stat_type": "total", + "field": "damage", + "event_transformation": "avatar_heals_damage" + }, + { + "id": "score_damage_healed", + "type": "score", + "description": "Damage healed: %s damage", + "statistic": "avatar_damage_healed" + }, + { + "id": "avatar_headshots", + "type": "event_transformation", + "event_type": "character_gets_headshot", + "value_constraints": { + "character": { "equals_statistic": "avatar_id" } + } + }, + { + "id": "avatar_num_headshots", + "type": "event_statistic", + "stat_type": "count", + "event_transformation": "avatar_headshots" + }, + { + "id": "score_headshots", + "type": "score", + "description": "Headshots: %s", + "statistic": "avatar_num_headshots" } ] diff --git a/src/event_statistics.cpp b/src/event_statistics.cpp index 4549d528810a1..627d45da9f189 100644 --- a/src/event_statistics.cpp +++ b/src/event_statistics.cpp @@ -89,6 +89,11 @@ void score::check_consistency() score_factory.check(); } +const std::vector &score::get_all() +{ + return score_factory.get_all(); +} + void score::reset() { score_factory.reset(); diff --git a/src/event_statistics.h b/src/event_statistics.h index 2569f58fa0229..89a08d2b6ed7a 100644 --- a/src/event_statistics.h +++ b/src/event_statistics.h @@ -4,6 +4,7 @@ #include #include +#include #include "clone_ptr.h" #include "string_id.h" @@ -70,6 +71,7 @@ class score void check() const; static void load_score( JsonObject &, const std::string & ); static void check_consistency(); + static const std::vector &get_all(); static void reset(); string_id id; diff --git a/src/memorial_logger.cpp b/src/memorial_logger.cpp index baa7cc412cf45..0ab9a8b5c866d 100644 --- a/src/memorial_logger.cpp +++ b/src/memorial_logger.cpp @@ -6,6 +6,7 @@ #include "avatar.h" #include "bionics.h" #include "effect.h" +#include "event_statistics.h" #include "filesystem.h" #include "game.h" #include "get_version.h" @@ -328,18 +329,11 @@ void memorial_logger::write( std::ostream &file, const std::string &epitaph ) co file << eol; //Lifetime stats - file << _( "Lifetime Stats" ) << 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 << _( "Lifetime Stats and Scores" ) << eol; + + for( const score &scr : score::get_all() ) { + file << indent << scr.description( g->stats() ) << eol; + } file << eol; //History From 2072dc522c6bffc26b2178a1e84404ebd2c88904 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 12 Sep 2019 07:40:16 -0400 Subject: [PATCH 07/10] Update the stats tests Want a test for the new "total" stat feature. Rearrange the other tests into SECTIONs at the same time. --- tests/stats_tracker_test.cpp | 82 +++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/tests/stats_tracker_test.cpp b/tests/stats_tracker_test.cpp index 1522046b68106..c762736ac2364 100644 --- a/tests/stats_tracker_test.cpp +++ b/tests/stats_tracker_test.cpp @@ -65,42 +65,58 @@ TEST_CASE( "stats_tracker_with_event_statistics", "[stats]" ) event_bus b; b.subscribe( &s ); - const mtype_id no_monster; - const mtype_id horse( "mon_horse" ); - const cata::event walk = cata::event::make( no_monster ); - const cata::event ride = cata::event::make( horse ); - const string_id score_moves( "score_moves" ); - const string_id score_walked( "score_walked" ); + SECTION( "movement" ) { + const mtype_id no_monster; + const mtype_id horse( "mon_horse" ); + const cata::event walk = cata::event::make( no_monster ); + const cata::event ride = cata::event::make( horse ); + const string_id score_moves( "score_moves" ); + const string_id score_walked( "score_walked" ); - CHECK( score_walked->value( s ) == cata_variant( 0 ) ); - CHECK( score_moves->value( s ) == cata_variant( 0 ) ); - b.send( walk ); - CHECK( score_walked->value( s ) == cata_variant( 1 ) ); - CHECK( score_moves->value( s ) == cata_variant( 1 ) ); - b.send( ride ); - CHECK( score_walked->value( s ) == cata_variant( 1 ) ); - CHECK( score_moves->value( s ) == cata_variant( 2 ) ); + CHECK( score_walked->value( s ) == cata_variant( 0 ) ); + CHECK( score_moves->value( s ) == cata_variant( 0 ) ); + b.send( walk ); + CHECK( score_walked->value( s ) == cata_variant( 1 ) ); + CHECK( score_moves->value( s ) == cata_variant( 1 ) ); + b.send( ride ); + CHECK( score_walked->value( s ) == cata_variant( 1 ) ); + CHECK( score_moves->value( s ) == cata_variant( 2 ) ); + } - const character_id u_id = g->u.getID(); - character_id other_id = u_id; - ++other_id; - const mtype_id mon( "mon_zombie" ); - const cata::event avatar_kill = - cata::event::make( u_id, mon ); - const cata::event other_kill = - cata::event::make( other_id, mon ); - const string_id avatar_id( "avatar_id" ); - const string_id num_avatar_kills( "num_avatar_kills" ); - const string_id score_kills( "score_kills" ); + SECTION( "kills" ) { + const character_id u_id = g->u.getID(); + character_id other_id = u_id; + ++other_id; + const mtype_id mon( "mon_zombie" ); + const cata::event avatar_kill = + cata::event::make( u_id, mon ); + const cata::event other_kill = + cata::event::make( other_id, mon ); + const string_id avatar_id( "avatar_id" ); + const string_id num_avatar_kills( "num_avatar_kills" ); + const string_id score_kills( "score_kills" ); + + b.send( u_id ); + CHECK( avatar_id->value( s ) == cata_variant( u_id ) ); + CHECK( score_kills->value( s ).get() == 0 ); + b.send( avatar_kill ); + CHECK( num_avatar_kills->value( s ).get() == 1 ); + CHECK( score_kills->value( s ).get() == 1 ); + b.send( other_kill ); + CHECK( score_kills->value( s ).get() == 1 ); + } + + SECTION( "damage" ) { + const character_id u_id = g->u.getID(); + const cata::event avatar_2_damage = + cata::event::make( u_id, 2 ); + const string_id damage_taken( "score_damage_taken" ); - b.send( u_id ); - CHECK( avatar_id->value( s ) == cata_variant( u_id ) ); - CHECK( score_kills->value( s ).get() == 0 ); - b.send( avatar_kill ); - CHECK( num_avatar_kills->value( s ).get() == 1 ); - CHECK( score_kills->value( s ).get() == 1 ); - b.send( other_kill ); - CHECK( score_kills->value( s ).get() == 1 ); + b.send( u_id ); + CHECK( damage_taken->value( s ).get() == 0 ); + b.send( avatar_2_damage ); + CHECK( damage_taken->value( s ).get() == 2 ); + } } TEST_CASE( "stats_tracker_in_game", "[stats]" ) From ed46d9aba2907c2517c3497a93082c7b88d96503 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 12 Sep 2019 23:38:35 -0400 Subject: [PATCH 08/10] Document the new json structures --- doc/JSON_INFO.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index 05aaff21d1a20..abf734b449227 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -49,6 +49,7 @@ Here's a quick summary of what each of the JSON files contain, broken down by fo | regional_map_settings.json | settings for the entire map generation | road_vehicles.json | vehicle spawn information for roads | rotatable_symbols.json | rotatable symbols - do not edit +| scores.json | statistics, scores, and achievements | skills.json | skill descriptions and ID's | snippets.json | flier/poster descriptions | species.json | monster species @@ -261,7 +262,7 @@ When adding a new bionic, if it's not included with another one, you must also a ### Item Groups -Item groups have been expanded, look at doc/ITEM_SPAWN.md to their new description. +Item groups have been expanded, look at [the detailed docs](ITEM_SPAWN.md) to their new description. The syntax listed here is still valid. | Identifier | Description @@ -650,7 +651,83 @@ Mods can modify this via `add:traits` and `remove:traits`. "post_terrain": "t_pit_spiked" // Terrain type after construction is complete ``` -## Skills +### Scores + +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). + +* First, optionally, define an `event_transformation` which converts events as + generated in-game into a format more suitable for your purpose. +* Second, define an `event_statistic` which summarizes a collection of events + into a single value (usually a number, but other types of value are + possible). +* Third, define a `score` which uses such a statistic. + +#### `event_transformation` + +Currently the only available transformation is to filter the set of events +based on certain constraints. + +```C++ +"id": "moves_on_horse", +"type": "event_transformation", +"event_type" : "avatar_moves", // An event type. The transformation will act on events of this type +"value_constraints" : { // A dictionary of constraints + // Each key is the field to which the constraint applies + // The value specifies the constraint. + // "equals" can be used to specify a constant string value the field must take. + // "equals_statistic" specifies that the value must match the value of some statistic (see below) + "mount" : { "equals": "mon_horse" } +} +``` + +#### `event_statistic` + +A statistic must specify a source of events via one of the following: + +```C++ +"event_type" : "avatar_moves" // Consider all moves of this type +"event_transformation" : "moves_on_horse" // Consider moves resulting from this transformation +``` + +Then it specifies a particular `stat_type` and potentially additional details +as follows: + +The number of events: +```C++ +"stat_type" : "count" +``` + +The sum of the numeric value in the specified field across all events: +```C++ +"stat_type" : "total" +"field" : "damage" +``` + +Assume there is only a single event to consider, and take the value of the +given field for that unique event: +```C++ +"stat_type": "unique_value", +"field": "avatar_id" +``` + +#### `score` + +Scores simply associate a description to an event for formatting in tabulations +of scores. The `description` specifies a string which is expected to contain a +`%s` format specifier where the value of the statistic will be inserted. + +Note that even though most statistics yield an integer, you should still use +`%s`. + +```C++ +"id": "score_headshots", +"type": "score", +"description": "Headshots: %s", +"statistic": "avatar_num_headshots" +``` + +### Skills ```C++ "ident" : "smg", // Unique ID. Must be one continuous word, use underscores if necessary From 8f3072a0114c60cc200c869713a4e7fd43c0bb43 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Thu, 12 Sep 2019 23:50:28 -0400 Subject: [PATCH 09/10] Translation support for new json structures --- data/json/scores.json | 2 +- doc/TRANSLATING.md | 1 + lang/extract_json_strings.py | 3 +++ src/event_statistics.cpp | 2 +- src/event_statistics.h | 3 ++- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/data/json/scores.json b/data/json/scores.json index 317b07b853769..98fc0de6253ae 100644 --- a/data/json/scores.json +++ b/data/json/scores.json @@ -117,7 +117,7 @@ { "id": "score_headshots", "type": "score", - "description": "Headshots: %s", + "description": { "ctxt": "score description", "str": "Headshots: %s" }, "statistic": "avatar_num_headshots" } ] diff --git a/doc/TRANSLATING.md b/doc/TRANSLATING.md index 057108feb3d9b..f3c95e5c19813 100644 --- a/doc/TRANSLATING.md +++ b/doc/TRANSLATING.md @@ -250,6 +250,7 @@ new syntax "name" would be a `dict`, which may break unmigrated script. | Mutation names/descriptions | NPC class names/descriptions | Tool quality names +| Score descriptions | Skill names/descriptions | Bionic names/descriptions | Terrain bash sound descriptions diff --git a/lang/extract_json_strings.py b/lang/extract_json_strings.py index c477c6397b52b..59fc989392f87 100755 --- a/lang/extract_json_strings.py +++ b/lang/extract_json_strings.py @@ -68,6 +68,8 @@ def warning_supressed(filename): "colordef", "emit", "enchantment", + "event_transformation", + "event_statistic", "EXTERNAL_OPTION", "GAME_OPTION", "ITEM_BLACKLIST", @@ -147,6 +149,7 @@ def warning_supressed(filename): "overmap_land_use_code", "overmap_terrain", "PET_ARMOR", + "score", "skill", "snippet", "speech", diff --git a/src/event_statistics.cpp b/src/event_statistics.cpp index 627d45da9f189..085851b133ac7 100644 --- a/src/event_statistics.cpp +++ b/src/event_statistics.cpp @@ -370,7 +370,7 @@ void event_statistic::check() const std::string score::description( stats_tracker &stats ) const { - return string_format( _( description_ ), value( stats ).get_string() ); + return string_format( description_.translated(), value( stats ).get_string() ); } cata_variant score::value( stats_tracker &stats ) const diff --git a/src/event_statistics.h b/src/event_statistics.h index 89a08d2b6ed7a..b7d64c2a2c55f 100644 --- a/src/event_statistics.h +++ b/src/event_statistics.h @@ -8,6 +8,7 @@ #include "clone_ptr.h" #include "string_id.h" +#include "translations.h" class cata_variant; namespace cata @@ -77,7 +78,7 @@ class score string_id id; bool was_loaded = false; private: - std::string description_; + translation description_; string_id stat_; }; From 370630aad4e4708d85bfe4c42bca60a7b4e10d33 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Fri, 13 Sep 2019 00:13:39 -0400 Subject: [PATCH 10/10] json styling --- data/json/scores.json | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/data/json/scores.json b/data/json/scores.json index 98fc0de6253ae..7e571951d35fc 100644 --- a/data/json/scores.json +++ b/data/json/scores.json @@ -10,9 +10,7 @@ "id": "avatar_kills", "type": "event_transformation", "event_type": "character_kills_monster", - "value_constraints": { - "killer": { "equals_statistic": "avatar_id" } - } + "value_constraints": { "killer": { "equals_statistic": "avatar_id" } } }, { "id": "num_avatar_kills", @@ -30,9 +28,7 @@ "id": "moves_not_mounted", "type": "event_transformation", "event_type": "avatar_moves", - "value_constraints": { - "mount": { "equals": "" } - } + "value_constraints": { "mount": { "equals": "" } } }, { "id": "num_moves", @@ -62,9 +58,7 @@ "id": "avatar_takes_damage", "type": "event_transformation", "event_type": "character_takes_damage", - "value_constraints": { - "character": { "equals_statistic": "avatar_id" } - } + "value_constraints": { "character": { "equals_statistic": "avatar_id" } } }, { "id": "avatar_damage_taken", @@ -83,9 +77,7 @@ "id": "avatar_heals_damage", "type": "event_transformation", "event_type": "character_heals_damage", - "value_constraints": { - "character": { "equals_statistic": "avatar_id" } - } + "value_constraints": { "character": { "equals_statistic": "avatar_id" } } }, { "id": "avatar_damage_healed", @@ -104,9 +96,7 @@ "id": "avatar_headshots", "type": "event_transformation", "event_type": "character_gets_headshot", - "value_constraints": { - "character": { "equals_statistic": "avatar_id" } - } + "value_constraints": { "character": { "equals_statistic": "avatar_id" } } }, { "id": "avatar_num_headshots",