Skip to content

Commit

Permalink
Revamp achievement requirement tracking
Browse files Browse the repository at this point in the history
Fixes #39879.

We now stop tracking requirement values when the achievement is
completed or fails.  Moreover, fixes a bug where a completed achievement
could be un-completed by further changes to the underlying statistics.
  • Loading branch information
jbytheway committed Apr 27, 2020
1 parent 7a0d2aa commit e118ece
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 59 deletions.
180 changes: 125 additions & 55 deletions src/achievement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,25 @@ void achievement::check() const
}
}

static std::string text_for_requirement( const achievement_requirement &req,
const cata_variant &current_value )
{
bool is_satisfied = req.satisifed_by( current_value );
nc_color c = is_satisfied ? c_green : c_yellow;
int current = current_value.get<int>();
int target;
std::string result;
if( req.comparison == achievement_comparison::anything ) {
target = 1;
result = string_format( _( "Triggered by " ) );
} else {
target = req.target;
result = string_format( _( "%s/%s " ), current, target );
}
result += req.statistic->description().translated( target );
return colorize( result, c );
}

class requirement_watcher : stat_watcher
{
public:
Expand All @@ -335,6 +354,10 @@ class requirement_watcher : stat_watcher
stats.add_watcher( req.statistic, this );
}

const cata_variant &current_value() const {
return current_value_;
}

const achievement_requirement &requirement() const {
return *requirement_;
}
Expand All @@ -346,20 +369,7 @@ class requirement_watcher : stat_watcher
}

std::string ui_text() const {
bool is_satisfied = requirement_->satisifed_by( current_value_ );
nc_color c = is_satisfied ? c_green : c_yellow;
int current = current_value_.get<int>();
int target;
std::string result;
if( requirement_->comparison == achievement_comparison::anything ) {
target = 1;
result = string_format( _( "Triggered by " ) );
} else {
target = requirement_->target;
result = string_format( _( "%s/%s " ), current, target );
}
result += requirement_->statistic->description().translated( target );
return colorize( result, c );
return text_for_requirement( *requirement_, current_value_ );
}
private:
cata_variant current_value_;
Expand All @@ -369,8 +379,12 @@ class requirement_watcher : stat_watcher

void requirement_watcher::new_value( const cata_variant &new_value, stats_tracker & )
{
current_value_ = new_value;
tracker_->set_requirement( this, requirement_->satisifed_by( new_value ) );
if( !tracker_->time_is_expired() ) {
current_value_ = new_value;
}
// set_requirement can result in this being deleted, so it must be the last
// thing in this function
tracker_->set_requirement( this, requirement_->satisifed_by( current_value_ ) );
}

namespace io
Expand All @@ -393,6 +407,40 @@ std::string enum_to_string<achievement_completion>( achievement_completion data

} // namespace io

std::string achievement_state::ui_text( const achievement *ach ) const
{
// First: the achievement name and description
nc_color c = color_from_completion( completion );
std::string result = colorize( ach->name(), c ) + "\n";
if( !ach->description().empty() ) {
result += " " + colorize( ach->description(), c ) + "\n";
}

if( completion == achievement_completion::completed ) {
std::string message = string_format(
_( "Completed %s" ), to_string( last_state_change ) );
result += " " + colorize( message, c ) + "\n";
} else {
// Next: the time constraint
if( ach->time_constraint() ) {
result += " " + ach->time_constraint()->ui_text() + "\n";
}
}

// Next: the requirements
const std::vector<achievement_requirement> &reqs = ach->requirements();
// If these two vectors are of different sizes then the definition must
// have changed since it was complated / failed, so we don't print any
// requirements info.
if( final_values.size() == reqs.size() ) {
for( size_t i = 0; i < final_values.size(); ++i ) {
result += " " + text_for_requirement( reqs[i], final_values[i] ) + "\n";
}
}

return result;
}

void achievement_state::serialize( JsonOut &jsout ) const
{
jsout.start_object();
Expand Down Expand Up @@ -431,44 +479,62 @@ void achievement_tracker::set_requirement( requirement_watcher *watcher, bool is
assert( sorted_watchers_[0].size() + sorted_watchers_[1].size() == watchers_.size() );
}

achievement_completion time_comp = achievement_->time_constraint() ?
achievement_->time_constraint()->completed() : achievement_completion::completed;
achievement_completion time_comp =
achievement_->time_constraint() ?
achievement_->time_constraint()->completed() : achievement_completion::completed;

if( sorted_watchers_[false].empty() && time_comp == achievement_completion::completed ) {
// report_achievement can result in this being deleted, so it must be
// the last thing in the function
tracker_->report_achievement( achievement_, achievement_completion::completed );
return;
}

if( time_comp == achievement_completion::failed ||
( !is_satisfied && watcher->requirement().becomes_false ) ) {
// report_achievement can result in this being deleted, so it must be
// the last thing in the function
tracker_->report_achievement( achievement_, achievement_completion::failed );
}
}

std::string achievement_tracker::ui_text( const achievement_state *state ) const
bool achievement_tracker::time_is_expired() const
{
return achievement_->time_constraint() &&
achievement_->time_constraint()->completed() == achievement_completion::failed;
}

std::vector<cata_variant> achievement_tracker::current_values() const
{
std::vector<cata_variant> result;
result.reserve( watchers_.size() );
for( const std::unique_ptr<requirement_watcher> &watcher : watchers_ ) {
result.push_back( watcher->current_value() );
}
return result;
}

std::string achievement_tracker::ui_text() const
{
// Determine overall achievement status
achievement_completion comp = state ? state->completion : achievement_completion::pending;
if( comp == achievement_completion::pending && achievement_->time_constraint() &&
achievement_->time_constraint()->completed() == achievement_completion::failed ) {
comp = achievement_completion::failed;
if( time_is_expired() ) {
return achievement_state{
achievement_completion::failed,
achievement_->time_constraint()->target(),
current_values()
}.ui_text( achievement_ );
}

// First: the achievement description
nc_color c = color_from_completion( comp );
// First: the achievement name and description
nc_color c = color_from_completion( achievement_completion::pending );
std::string result = colorize( achievement_->name(), c ) + "\n";
if( !achievement_->description().empty() ) {
result += " " + colorize( achievement_->description(), c ) + "\n";
}

if( comp == achievement_completion::completed ) {
std::string message = string_format(
_( "Completed %s" ), to_string( state->last_state_change ) );
result += " " + colorize( message, c ) + "\n";
} else {
// Next: the time constraint
if( achievement_->time_constraint() ) {
result += " " + achievement_->time_constraint()->ui_text() + "\n";
}
// Next: the time constraint
if( achievement_->time_constraint() ) {
result += " " + achievement_->time_constraint()->ui_text() + "\n";
}

// Next: the requirements
Expand Down Expand Up @@ -501,31 +567,33 @@ std::vector<const achievement *> achievements_tracker::valid_achievements() cons

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{
assert( comp != achievement_completion::pending );
assert( !achievements_status_.count( a->id ) );

auto tracker_it = trackers_.find( a->id );
achievements_status_.emplace(
a->id,
achievement_state{
comp,
calendar::turn
};
if( it == achievements_status_.end() ) {
achievements_status_.emplace( a->id, new_state );
} else {
it->second = new_state;
calendar::turn,
tracker_it->second.current_values()
}
);
if( comp == achievement_completion::completed ) {
achievement_attained_callback_( a );
}
trackers_.erase( tracker_it );
}

achievement_completion achievements_tracker::is_completed( const string_id<achievement> &id ) const
{
auto it = achievements_status_.find( id );
if( it == achievements_status_.end() ) {
// It might still have failed; check for time expiry
auto tracker_it = trackers_.find( id );
if( tracker_it != trackers_.end() && tracker_it->second.time_is_expired() ) {
return achievement_completion::failed;
}
return achievement_completion::pending;
}
return it->second.completion;
Expand All @@ -534,21 +602,20 @@ achievement_completion achievements_tracker::is_completed( const string_id<achie
std::string achievements_tracker::ui_text_for( const achievement *ach ) const
{
auto state_it = achievements_status_.find( ach->id );
const achievement_state *state = nullptr;
if( state_it != achievements_status_.end() ) {
state = &state_it->second;
return state_it->second.ui_text( ach );
}
auto watcher_it = watchers_.find( ach->id );
if( watcher_it == watchers_.end() ) {
auto tracker_it = trackers_.find( ach->id );
if( tracker_it == trackers_.end() ) {
return colorize( ach->description() + _( "\nInternal error: achievement lacks watcher." ),
c_red );
}
return watcher_it->second.ui_text( state );
return tracker_it->second.ui_text();
}

void achievements_tracker::clear()
{
watchers_.clear();
trackers_.clear();
initial_achievements_.clear();
achievements_status_.clear();
}
Expand Down Expand Up @@ -584,7 +651,10 @@ void achievements_tracker::deserialize( JsonIn &jsin )
void achievements_tracker::init_watchers()
{
for( const achievement *a : valid_achievements() ) {
watchers_.emplace(
if( achievements_status_.count( a->id ) ) {
continue;
}
trackers_.emplace(
std::piecewise_construct, std::forward_as_tuple( a->id ),
std::forward_as_tuple( *a, *this, *stats_ ) );
}
Expand Down
24 changes: 20 additions & 4 deletions src/achievement.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,20 @@ struct enum_traits<achievement::time_bound::epoch> {
static constexpr achievement::time_bound::epoch last = achievement::time_bound::epoch::last;
};

// Once an achievement is either completed or failed it is stored as an
// achievement_state
struct achievement_state {
// The final state
achievement_completion completion;

// When it became that state
time_point last_state_change;

// The values for each requirement at the time of completion or failure
std::vector<cata_variant> final_values;

std::string ui_text( const achievement * ) const;

void serialize( JsonOut & ) const;
void deserialize( JsonIn & );
};
Expand All @@ -127,7 +137,9 @@ class achievement_tracker

void set_requirement( requirement_watcher *watcher, bool is_satisfied );

std::string ui_text( const achievement_state * ) const;
bool time_is_expired() const;
std::vector<cata_variant> current_values() const;
std::string ui_text() const;
private:
const achievement *achievement_;
achievements_tracker *tracker_;
Expand All @@ -147,8 +159,9 @@ class achievements_tracker : public event_subscriber
achievements_tracker( const achievements_tracker & ) = delete;
achievements_tracker &operator=( const achievements_tracker & ) = delete;

achievements_tracker( stats_tracker &,
const std::function<void( const achievement * )> &achievement_attained_callback );
achievements_tracker(
stats_tracker &,
const std::function<void( const achievement * )> &achievement_attained_callback );
~achievements_tracker() override;

// Return all scores which are valid now and existed at game start
Expand All @@ -169,8 +182,11 @@ class achievements_tracker : public event_subscriber

stats_tracker *stats_ = nullptr;
std::function<void( const achievement * )> achievement_attained_callback_;
std::unordered_map<string_id<achievement>, achievement_tracker> watchers_;
std::unordered_set<string_id<achievement>> initial_achievements_;

// Class invariant: each valid achievement has exactly one of a watcher
// (if it's pending) or a status (if it's completed or failed).
std::unordered_map<string_id<achievement>, achievement_tracker> trackers_;
std::unordered_map<string_id<achievement>, achievement_state> achievements_status_;
};

Expand Down
25 changes: 25 additions & 0 deletions tests/stats_tracker_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,32 @@ TEST_CASE( "achievments_tracker", "[stats]" )
CHECK( a.ui_text_for( &*a_kill_in_first_minute ) ==
"<color_c_light_gray>Rude awakening</color>\n"
" <color_c_light_gray>Within 1 minute of start of game (passed)</color>\n"
" <color_c_yellow>0/1 monster killed</color>\n" );
}

// Advance a minute and kill again
calendar::turn += 1_minutes;
b.send( avatar_zombie_kill );

if( time_since_game_start < 1_minutes ) {
CHECK( a.ui_text_for( achievements_completed.at( a_kill_zombie ) ) ==
"<color_c_light_green>One down, billions to go…</color>\n"
" <color_c_light_green>Completed Year 1, Spring, day 1 0000.30</color>\n"
" <color_c_green>1/1 zombie killed</color>\n" );
CHECK( a.ui_text_for( achievements_completed.at( a_kill_in_first_minute ) ) ==
"<color_c_light_green>Rude awakening</color>\n"
" <color_c_light_green>Completed Year 1, Spring, day 1 0000.30</color>\n"
" <color_c_green>1/1 monster killed</color>\n" );
} else {
CHECK( a.ui_text_for( achievements_completed.at( a_kill_zombie ) ) ==
"<color_c_light_green>One down, billions to go…</color>\n"
" <color_c_light_green>Completed Year 1, Spring, day 1 0010.00</color>\n"
" <color_c_green>1/1 zombie killed</color>\n" );
CHECK( !achievements_completed.count( a_kill_in_first_minute ) );
CHECK( a.ui_text_for( &*a_kill_in_first_minute ) ==
"<color_c_light_gray>Rude awakening</color>\n"
" <color_c_light_gray>Within 1 minute of start of game (passed)</color>\n"
" <color_c_yellow>0/1 monster killed</color>\n" );
}
}

Expand Down

0 comments on commit e118ece

Please sign in to comment.