diff --git a/src/achievement.cpp b/src/achievement.cpp index 500ae5cd656c5..44618856c8c74 100644 --- a/src/achievement.cpp +++ b/src/achievement.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "cata_assert.h" #include "color.h" @@ -842,6 +843,25 @@ bool achievements_tracker::is_hidden( const achievement *ach ) const return false; } +void achievements_tracker::write_json_achievements( std::ostream &achievement_file ) const +{ + JsonOut jsout( achievement_file, true, 2 ); + jsout.start_object(); + jsout.member( "achievement_version", 0 ); + + std::vector ach_ids; + + for( const auto &kv : achievements_status_ ) { + if( kv.second.completion == achievement_completion::completed ) { + ach_ids.push_back( kv.first ); + } + } + + jsout.member( "achievements", ach_ids ); + + jsout.end_object(); +} + std::string achievements_tracker::ui_text_for( const achievement *ach ) const { auto state_it = achievements_status_.find( ach->id ); diff --git a/src/achievement.h b/src/achievement.h index 376ace5e228b5..2abf490d5cef4 100644 --- a/src/achievement.h +++ b/src/achievement.h @@ -209,6 +209,8 @@ class achievements_tracker : public event_subscriber void report_achievement( const achievement *, achievement_completion ); + void write_json_achievements( std::ostream &achievement_file ) const; + achievement_completion is_completed( const achievement_id & ) const; bool is_hidden( const achievement * ) const; std::string ui_text_for( const achievement * ) const; diff --git a/src/debug_menu.cpp b/src/debug_menu.cpp index 766137a420980..c631092bd4fe1 100644 --- a/src/debug_menu.cpp +++ b/src/debug_menu.cpp @@ -3336,7 +3336,7 @@ void debug() break; case debug_menu_index::UNLOCK_ALL: if( query_yn( - _( "Activating this will add the Arcade Mode achievement unlocking all starting scenarios and professions for all worlds. The character who performs this action will need to die for it to be recorded. Achievements are tracked from the memorial folder if you need to get rid of this. Activating this will spoil factions and situations you may otherwise stumble upon naturally while playing. Some scenarios are frustrating for the uninitiated, and some professions skip portions of the game's content. If new to the game progression would otherwise help you be introduced to mechanics at a reasonable pace." ) ) ) { + _( "Activating this will add the Arcade Mode achievement unlocking all starting scenarios and professions for all worlds. You will need to save the character in order to record this. Achievements are tracked from the save/achievements/ folder if you need to get rid of this. Activating this will spoil factions and situations you may otherwise stumble upon naturally while playing. Some scenarios are frustrating for the uninitiated, and some professions skip portions of the game's content. If new to the game progression would otherwise help you be introduced to mechanics at a reasonable pace." ) ) ) { get_achievements().report_achievement( &achievement_achievement_arcade_mode.obj(), achievement_completion::completed ); } diff --git a/src/do_turn.cpp b/src/do_turn.cpp index ea577db574ea2..73a085e12a8a3 100644 --- a/src/do_turn.cpp +++ b/src/do_turn.cpp @@ -84,6 +84,9 @@ bool cleanup_at_end() // and the overmap, and the local map. g->save_maps(); //Omap also contains the npcs who need to be saved. + //save achievements entry + g->save_achievements(); + g->death_screen(); std::chrono::seconds time_since_load = std::chrono::duration_cast( diff --git a/src/game.cpp b/src/game.cpp index 9640fa25ff69c..3761cc6539945 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -92,6 +92,7 @@ #include "gates.h" #include "get_version.h" #include "harvest.h" +#include "help.h" #include "iexamine.h" #include "init.h" #include "input.h" @@ -143,6 +144,7 @@ #include "overmapbuffer.h" #include "panels.h" #include "past_games_info.h" +#include "past_achievements_info.h" #include "path_info.h" #include "pathfinding.h" #include "pickup.h" @@ -3333,6 +3335,57 @@ bool game::save_player_data() ; } + +bool game::save_achievements() +{ + const std::string &achievement_dir = PATH_INFO::achievementdir(); + + //Check if achievement dir exists + if( !assure_dir_exist( achievement_dir ) ) { + dbg( D_ERROR ) << "game:save_achievements: Unable to make achievement directory."; + debugmsg( "Could not make '%s' directory", achievement_dir ); + return false; + } + + // This sets the maximum length for the filename + constexpr size_t suffix_len = 24 + 1; + constexpr size_t max_name_len = FILENAME_MAX - suffix_len; + + const size_t name_len = u.name.size(); + // Here -1 leaves space for the ~ + const size_t truncated_name_len = ( name_len >= max_name_len ) ? ( max_name_len - 1 ) : name_len; + + std::ostringstream achievement_file_path; + + achievement_file_path << achievement_dir; + + if( get_options().has_option( "ENCODING_CONV" ) && !get_option( "ENCODING_CONV" ) ) { + // Use the default locale to replace non-printable characters with _ in the player name. + std::locale locale{ "C" }; + std::replace_copy_if( std::begin( u.name ), std::begin( u.name ) + truncated_name_len, + std::ostream_iterator( achievement_file_path ), + [&]( const char c ) { + return !std::isgraph( c, locale ); + }, '_' ); + } else { + achievement_file_path << u.name; + } + + // Add a ~ if the player name was actually truncated. + achievement_file_path << ( ( truncated_name_len != name_len ) ? "~-" : "-" ); + const int character_id = get_player_character().getID().get_value(); + const std::string json_path_string = achievement_file_path.str() + std::to_string( + character_id ) + ".json"; + + // Clear past achievements so that it will be reloaded + clear_past_achievements(); + + return write_to_file( json_path_string, [&]( std::ostream & fout ) { + get_achievements().write_json_achievements( fout ); + }, _( "player achievements" ) ); + +} + event_bus &game::events() { return *event_bus_ptr; @@ -3392,6 +3445,7 @@ bool game::save() events().send( time_since_load, total_time_played ); try { if( !save_player_data() || + !save_achievements() || !save_factions_missions_npcs() || !save_maps() || !get_auto_pickup().save_character() || diff --git a/src/game.h b/src/game.h index 794f7a919491b..09dffee0c0864 100644 --- a/src/game.h +++ b/src/game.h @@ -1055,6 +1055,7 @@ class game void move_save_to_graveyard(); bool save_player_data(); + bool save_achievements(); // ########################## DATA ################################ // May be a bit hacky, but it's probably better than the header spaghetti pimpl map_ptr; // NOLINT(cata-serialize) diff --git a/src/past_achievements_info.cpp b/src/past_achievements_info.cpp new file mode 100644 index 0000000000000..24e6a2c8ea7c0 --- /dev/null +++ b/src/past_achievements_info.cpp @@ -0,0 +1,81 @@ +#include + +#include "past_achievements_info.h" +#include "past_games_info.h" +#include "achievement.h" +#include "json.h" +#include "json_loader.h" + +void past_achievements_info::clear() +{ + *this = past_achievements_info(); +} + +bool past_achievements_info::is_completed( const achievement_id &ach ) const +{ + auto ach_it = completed_achievements_.find( ach ); + return ach_it != completed_achievements_.end(); +} + +/* + * This is for when the player copies their memorial folder + * in and expects those achievements to still work for scenarios/professions + * + * It will create a save/achievements/ entry called memorial_achievements.json, + * this file contains the achievement ids from the players memorials. + */ +bool past_achievements_info::migrate_memorial() +{ + const std::string &oldachievements_file = PATH_INFO::oldachievements(); + if( memorial_loaded_ ) { + return false; + } + memorial_loaded_ = true; + + std::ostringstream oldachievements_file_path; + oldachievements_file_path << oldachievements_file; + + return write_to_file( oldachievements_file, [&]( std::ostream & fout ) { + get_past_games().write_json_achievements( fout ); + }, _( "player achievements" ) ); +} + +void past_achievements_info::load() +{ + if( loaded_ ) { + return; + } + loaded_ = true; + const cata_path &achievement_dir = PATH_INFO::achievementdir_path(); + assure_dir_exist( achievement_dir ); + + migrate_memorial(); + std::vector filenames = get_files_from_path( ".json", achievement_dir, true, true ); + + for( const cata_path &filename : filenames ) { + int version; + std::vector achievements; + JsonValue jsin = json_loader::from_path( filename ); + JsonObject jo = jsin.get_object(); + jo.read( "achievement_version", version ); + if( version != 0 ) { + continue; + } + jo.read( "achievements", achievements ); + completed_achievements_.insert( achievements.cbegin(), achievements.cend() ); + } +} + +static past_achievements_info past_achievements; +past_achievements_info::past_achievements_info() = default; + +const past_achievements_info &get_past_achievements() +{ + past_achievements.load(); + return past_achievements; +} + +void clear_past_achievements() +{ + past_achievements.clear(); +} diff --git a/src/past_achievements_info.h b/src/past_achievements_info.h new file mode 100644 index 0000000000000..f86abf30edc4a --- /dev/null +++ b/src/past_achievements_info.h @@ -0,0 +1,29 @@ +#pragma once +#ifndef CATA_SRC_PAST_ACHIEVEMENTS_INFO_H +#define CATA_SRC_PAST_ACHIEVEMENTS_INFO_H + +#include + +#include "achievement.h" + +class past_achievements_info +{ + public: + past_achievements_info(); + + bool migrate_memorial(); + void load(); + void clear(); + bool is_completed( const achievement_id & ) const; + private: + + bool loaded_ = false; + bool memorial_loaded_ = false; + std::set completed_achievements_; +}; + + +const past_achievements_info &get_past_achievements(); +void clear_past_achievements(); + +#endif // CATA_SRC_PAST_ACHIEVEMENTS_INFO_H diff --git a/src/past_games_info.cpp b/src/past_games_info.cpp index b67d4901f0e0d..67eba2c4a54a2 100644 --- a/src/past_games_info.cpp +++ b/src/past_games_info.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include "achievement.h" @@ -90,6 +92,25 @@ const achievement_completion_info *past_games_info::achievement( const achieveme return ach_it == completed_achievements_.end() ? nullptr : &ach_it->second; } +void past_games_info::write_json_achievements( std::ostream &achievement_file ) const +{ + JsonOut jsout( achievement_file, true, 2 ); + jsout.start_object(); + jsout.member( "achievement_version", 0 ); + + std::unordered_set ach_id_strings; + + for( achievement_id kv : ach_ids_ ) { + if( achievement( kv ) ) { + ach_id_strings.insert( kv.c_str() ); + } + } + + jsout.member( "achievements", ach_id_strings ); + + jsout.end_object(); +} + void past_games_info::ensure_loaded() { if( loaded_ ) { @@ -165,6 +186,7 @@ void past_games_info::ensure_loaded() continue; } achievement_id ach = ach_it->second.get(); + ach_ids_.push_back( ach ); completed_achievements_[ach].games_completed.push_back( &game ); } inp_mngr.pump_events(); diff --git a/src/past_games_info.h b/src/past_games_info.h index 2c3a8f00883a8..fa6c584afa76d 100644 --- a/src/past_games_info.h +++ b/src/past_games_info.h @@ -48,6 +48,7 @@ class past_games_info public: past_games_info(); + void write_json_achievements( std::ostream &achievement_file ) const; void ensure_loaded(); void clear(); const achievement_completion_info *achievement( const achievement_id & ) const; @@ -56,6 +57,7 @@ class past_games_info bool loaded_ = false; std::unordered_map completed_achievements_; std::vector info_; + std::vector ach_ids_; }; const past_games_info &get_past_games(); diff --git a/src/path_info.cpp b/src/path_info.cpp index affab13464048..a6b2a9bc7dc19 100644 --- a/src/path_info.cpp +++ b/src/path_info.cpp @@ -42,6 +42,8 @@ static std::string autonote_value; static std::string keymap_value; static std::string options_value; static std::string memorialdir_value; +static std::string achievementdir_value; +static std::string oldachievements_value; static std::string langdir_value; static cata_path autonote_path_value; @@ -53,6 +55,8 @@ static cata_path gfxdir_path_value; static cata_path keymap_path_value; static cata_path langdir_path_value; static cata_path memorialdir_path_value; +static cata_path achievementdir_path_value; +static cata_path oldachievements_path_value; static cata_path motd_path_value; static cata_path options_path_value; static cata_path savedir_path_value; @@ -142,6 +146,10 @@ void PATH_INFO::set_standard_filenames() savedir_path_value = cata_path{ cata_path::root_path::save, fs::path{} }; memorialdir_value = user_dir_value + "memorial/"; memorialdir_path_value = user_dir_path_value / "memorial"; + achievementdir_value = savedir_value + "achievement/"; + achievementdir_path_value = savedir_path_value / "achievement"; + oldachievements_value = achievementdir_value + "memorial_achievements.json"; + oldachievements_path_value = achievementdir_path_value / "memorial_achievements.json"; #if defined(USE_XDG_DIR) const char *user_dir; @@ -327,10 +335,26 @@ std::string PATH_INFO::memorialdir() { return memorialdir_value; } +std::string PATH_INFO::achievementdir() +{ + return achievementdir_value; +} +std::string PATH_INFO::oldachievements() +{ + return oldachievements_value; +} cata_path PATH_INFO::memorialdir_path() { return memorialdir_path_value; } +cata_path PATH_INFO::achievementdir_path() +{ + return achievementdir_path_value; +} +cata_path PATH_INFO::oldachievements_path() +{ + return oldachievements_path_value; +} cata_path PATH_INFO::jsondir() { return datadir_path_value / "core"; diff --git a/src/path_info.h b/src/path_info.h index 013249eb3aee4..448f8a9752dfb 100644 --- a/src/path_info.h +++ b/src/path_info.h @@ -35,6 +35,8 @@ std::string user_font(); std::string graveyarddir(); std::string keymap(); std::string memorialdir(); +std::string achievementdir(); +std::string oldachievements(); std::string player_base_save_path(); std::string savedir(); std::string sokoban(); @@ -75,6 +77,8 @@ cata_path langdir_path(); cata_path lastworld(); cata_path legacy_fontdata(); cata_path memorialdir_path(); +cata_path achievementdir_path(); +cata_path oldachievements_path(); cata_path moddir(); cata_path mods_dev_default(); cata_path mods_user_default(); diff --git a/src/profession.cpp b/src/profession.cpp index ae28ad69f101f..df7420a8b5721 100644 --- a/src/profession.cpp +++ b/src/profession.cpp @@ -24,6 +24,7 @@ #include "mutation.h" #include "options.h" #include "past_games_info.h" +#include "past_achievements_info.h" #include "pimpl.h" #include "translations.h" #include "type_id.h" @@ -671,19 +672,15 @@ ret_val profession::can_afford( const Character &you, const int points ) c ret_val profession::can_pick() const { // if meta progression is disabled then skip this - if( get_past_games().achievement( achievement_achievement_arcade_mode ) || + if( get_past_achievements().is_completed( achievement_achievement_arcade_mode ) || !get_option( "META_PROGRESS" ) ) { return ret_val::make_success(); } if( _requirement ) { - const achievement_completion_info *other_games = get_past_games().achievement( - _requirement.value()->id ); - if( !other_games ) { - return ret_val::make_failure( - _( "You must complete the achievement \"%s\" to unlock this profession." ), - _requirement.value()->name() ); - } else if( other_games->games_completed.empty() ) { + const bool has_req = get_past_achievements().is_completed( + _requirement.value()->id ); + if( !has_req ) { return ret_val::make_failure( _( "You must complete the achievement \"%s\" to unlock this profession." ), _requirement.value()->name() ); diff --git a/src/scenario.cpp b/src/scenario.cpp index b91686cce8c4d..b11ae2efac885 100644 --- a/src/scenario.cpp +++ b/src/scenario.cpp @@ -12,6 +12,7 @@ #include "mutation.h" #include "options.h" #include "past_games_info.h" +#include "past_achievements_info.h" #include "profession.h" #include "rng.h" #include "start_location.h" @@ -654,19 +655,15 @@ ret_val scenario::can_afford( const scenario ¤t_scenario, const int ret_val scenario::can_pick() const { // if meta progression is disabled then skip this - if( get_past_games().achievement( achievement_achievement_arcade_mode ) || + if( get_past_achievements().is_completed( achievement_achievement_arcade_mode ) || !get_option( "META_PROGRESS" ) ) { return ret_val::make_success(); } if( _requirement ) { - const achievement_completion_info *other_games = get_past_games().achievement( - _requirement.value()->id ); - if( !other_games ) { - return ret_val::make_failure( - _( "You must complete the achievement \"%s\" to unlock this scenario." ), - _requirement.value()->name() ); - } else if( other_games->games_completed.empty() ) { + const bool has_req = get_past_achievements().is_completed( + _requirement.value()->id ); + if( !has_req ) { return ret_val::make_failure( _( "You must complete the achievement \"%s\" to unlock this scenario." ), _requirement.value()->name() );