diff --git a/code/menuui/readyroom.cpp b/code/menuui/readyroom.cpp index 899b427f828..ec08bd4cc0b 100644 --- a/code/menuui/readyroom.cpp +++ b/code/menuui/readyroom.cpp @@ -1575,10 +1575,22 @@ void campaign_room_scroll_info_down() void campaign_reset(const SCP_string& campaign_file) { - // note: we do not toss all-time stats from player's performance in campaign up till now - mission_campaign_savefile_delete(campaign_file.c_str()); + Assert(Player != nullptr); + + // if the caller is resetting a campaign other than the active one (only reachable via the Lua resetCampaign API), + // we can't go through mission_campaign_load(): that would swap the global Campaign struct over to the target + // campaign and overwrite Player->stats with the wrong running totals. Just delete the CSG; the next time the + // player activates this campaign, mission_campaign_load() will recreate a fresh one. + if (stricmp(campaign_file.c_str(), Campaign.filename) != 0) { + mission_campaign_savefile_delete(campaign_file.c_str()); + return; + } - const int load_status = mission_campaign_load(campaign_file.c_str(), nullptr, nullptr, 1); + // note: just as in retail, we do not toss all-time stats from player's performance in campaign up till now; + // reset_accumulated_stats=false tells mission_campaign_load() (and through it, csg_reset_data()) + // to leave Player->stats alone when it recreates the CSG, so accumulated score/rank/kills/medals carry over + mission_campaign_savefile_delete(campaign_file.c_str()); + const int load_status = mission_campaign_load(campaign_file.c_str(), nullptr, nullptr, true, false); // see if we successfully loaded this campaign if (load_status == 0) { diff --git a/code/mission/missioncampaign.cpp b/code/mission/missioncampaign.cpp index 4c60527038f..6612d881c6e 100644 --- a/code/mission/missioncampaign.cpp +++ b/code/mission/missioncampaign.cpp @@ -437,7 +437,7 @@ void mission_campaign_get_sw_info() * this file. If you change the format of the campaign file, you should be sure these related * functions work properly and update them if it breaks them. */ -int mission_campaign_load(const char* filename, const char* full_path, player* pl, int load_savefile) +int mission_campaign_load(const char* filename, const char* full_path, player* pl, bool load_savefile, bool reset_accumulated_stats) { int i; char name[NAME_LENGTH], type[NAME_LENGTH], temp[NAME_LENGTH]; @@ -709,7 +709,11 @@ int mission_campaign_load(const char* filename, const char* full_path, player* p } // start with fresh new campaign data else { - Pilot.clear_savefile(false); // don't reset ships and weapons because they are currently the campaign's starting ones + // don't reset ships and weapons because they have just been set to the campaign's starting ones; + // don't reset score/rank because it tracks all-time score/rank and we want to stay consistent; + // reset_accumulated_stats is false when the caller is resetting an existing campaign and wants to + // keep the player's accumulated totals + Pilot.clear_savefile(false, reset_accumulated_stats, false); Campaign.next_mission = 0; Pilot.save_savefile(); } diff --git a/code/mission/missioncampaign.h b/code/mission/missioncampaign.h index 6e9e6658e48..232e16da1ba 100644 --- a/code/mission/missioncampaign.h +++ b/code/mission/missioncampaign.h @@ -175,7 +175,7 @@ void player_loadout_init(); void mission_campaign_init( void ); // load up and initialize a new campaign -int mission_campaign_load(const char* filename, const char* full_path = nullptr, player* pl = nullptr, int load_savefile = 1); +int mission_campaign_load(const char* filename, const char* full_path = nullptr, player* pl = nullptr, bool load_savefile = true, bool reset_accumulated_stats = true); bool campaign_is_ignored(const char *filename); diff --git a/code/pilotfile/csg.cpp b/code/pilotfile/csg.cpp index 6b0f20933cc..c9c82dfe042 100644 --- a/code/pilotfile/csg.cpp +++ b/code/pilotfile/csg.cpp @@ -1594,7 +1594,7 @@ void pilotfile::csg_write_container(const sexp_container &container) } } -void pilotfile::csg_reset_data(bool reset_ships_and_weapons) +void pilotfile::csg_reset_data(bool reset_ships_and_weapons, bool reset_accumulated_stats, bool reset_score_and_rank) { int idx; cmission *missionp; @@ -1605,8 +1605,12 @@ void pilotfile::csg_reset_data(bool reset_ships_and_weapons) m_data_invalid = false; - // init stats - p->stats.init(); + // init stats (unless the caller wants to keep the player's accumulated totals, + // e.g. for a campaign reset) + if (reset_accumulated_stats) { + // even when we do clear stats, we might not clear score or rank + p->stats.init(reset_score_and_rank); + } // zero out allowed ships/weapons if (reset_ships_and_weapons) { @@ -1723,7 +1727,7 @@ bool pilotfile::load_savefile(player *_p, const char *campaign) mprintf(("CSG => Loading '%s' with version %d...\n", filename.c_str(), (int)csg_ver)); - csg_reset_data(true); + csg_reset_data(true, true, true); // the point of all this: read in the CSG contents while ( !cfeof(cfp) ) { @@ -1948,7 +1952,7 @@ bool pilotfile::save_savefile() return true; } -void pilotfile::clear_savefile(bool reset_ships_and_weapons) +void pilotfile::clear_savefile(bool reset_ships_and_weapons, bool reset_accumulated_stats, bool reset_score_and_rank) { if (Game_mode & GM_MULTIPLAYER) { return; @@ -1958,7 +1962,7 @@ void pilotfile::clear_savefile(bool reset_ships_and_weapons) Assert((Player_num >= 0) && (Player_num < MAX_PLAYERS)); p = &Players[Player_num]; - csg_reset_data(reset_ships_and_weapons); + csg_reset_data(reset_ships_and_weapons, reset_accumulated_stats, reset_score_and_rank); } /* diff --git a/code/pilotfile/pilotfile.h b/code/pilotfile/pilotfile.h index c3b0616acc7..35d6b13aeb9 100644 --- a/code/pilotfile/pilotfile.h +++ b/code/pilotfile/pilotfile.h @@ -93,7 +93,7 @@ class pilotfile { bool save_player(player *_p = nullptr); bool save_savefile(); - void clear_savefile(bool reset_ships_and_weapons); + void clear_savefile(bool reset_ships_and_weapons, bool reset_accumulated_stats, bool reset_score_and_rank); // updating stats, multi and/or all-time void update_stats(scoring_struct *stats, bool training = false); @@ -215,7 +215,7 @@ class pilotfile { // -------------------------------------------------------------------- // CSG specific // -------------------------------------------------------------------- - void csg_reset_data(bool reset_ships_and_weapons); + void csg_reset_data(bool reset_ships_and_weapons, bool reset_accumulated_stats, bool reset_score_and_rank); void csg_close(); void csg_read_flags(); diff --git a/code/playerman/player.h b/code/playerman/player.h index 2bf6f6098a3..9518d40839e 100644 --- a/code/playerman/player.h +++ b/code/playerman/player.h @@ -123,7 +123,7 @@ class player int objnum; // object number for this player button_info bi; // structure that holds bit vectors for button presses control_info ci; // control info structure for this player - scoring_struct stats; // scoring and stats info for the player (points to multi_stats or single_stats) + scoring_struct stats; // scoring and stats info for the player that are currently accumulating; loaded from multi_stats or all_time_stats int friendly_hits; // Number of times hit a friendly ship this mission. float friendly_damage; // Total friendly damage done in mission. Diminishes over time. diff --git a/code/scripting/api/objs/player.cpp b/code/scripting/api/objs/player.cpp index 797b3f913cd..9b542f23297 100644 --- a/code/scripting/api/objs/player.cpp +++ b/code/scripting/api/objs/player.cpp @@ -66,7 +66,7 @@ player_h& player_h::operator=(player_h&& other) noexcept ADE_OBJ(l_Player, player_h, "player", "Player handle"); -ADE_VIRTVAR(Stats, l_Player, "scoring_stats stats", "The scoring stats of this player (read-only)", "scoring_stats", "The player stats or invalid handle") { +ADE_VIRTVAR(Stats, l_Player, "scoring_stats stats", "The scoring stats of this player that are currently accumulating, e.g. from a campaign or multiplayer session; read-only", "scoring_stats", "The player stats or invalid handle") { player_h* plr; if (!ade_get_args(L, "o", l_Player.GetPtr(&plr))) return ade_set_error(L, "o", l_ScoringStats.Set(scoring_stats_h())); @@ -74,6 +74,9 @@ ADE_VIRTVAR(Stats, l_Player, "scoring_stats stats", "The scoring stats of this p if (!plr->isValid()) return ade_set_error(L, "o", l_ScoringStats.Set(scoring_stats_h())); + if (ADE_SETTING_VAR) + LuaError(L, "This property is read only."); + return ade_set_args(L, "o", l_ScoringStats.Set(scoring_stats_h(plr->get()->stats, plr->get()))); } diff --git a/code/stats/scoring.cpp b/code/stats/scoring.cpp index 6b8542da3d6..0e9acfba475 100644 --- a/code/stats/scoring.cpp +++ b/code/stats/scoring.cpp @@ -408,13 +408,16 @@ void traitor_init() } // initialize a nice blank scoring element -void scoring_struct::init() +void scoring_struct::init(bool reset_score_and_rank) { flags = 0; - score = 0; - rank = 0; - medal_counts.assign((int)Medals.size(), 0); + if (reset_score_and_rank) { + score = 0; + rank = 0; + } + + medal_counts.assign(Medals.size(), 0); memset(kills, 0, sizeof(kills)); assists = 0; diff --git a/code/stats/scoring.h b/code/stats/scoring.h index ac9d3dc787f..27667ee0c82 100644 --- a/code/stats/scoring.h +++ b/code/stats/scoring.h @@ -142,7 +142,7 @@ class scoring_struct scoring_struct(const scoring_struct &s) { assign(s); } scoring_struct& operator=(const scoring_struct &s) { assign(s); return *this; } - void init(); + void init(bool reset_score_and_rank = true); void assign(const scoring_struct &s); bool operator==(const scoring_struct& rhs) const; diff --git a/fred2/campaigneditordlg.cpp b/fred2/campaigneditordlg.cpp index 2b31c02810f..f6694de0837 100644 --- a/fred2/campaigneditordlg.cpp +++ b/fred2/campaigneditordlg.cpp @@ -201,7 +201,7 @@ void campaign_editor::load_campaign(const char *filename, const char *full_path) else m_current_campaign_path = _T(""); - auto result = mission_campaign_load(filename, full_path, nullptr, 0); + auto result = mission_campaign_load(filename, full_path, nullptr, false); if (result != 0) { if (result == CAMPAIGN_ERROR_CORRUPT) MessageBox("Requested campaign file is corrupt.", "Could not load campaign file"); diff --git a/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp b/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp index 21196c3e58d..fb319e7b98f 100644 --- a/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/CampaignEditorDialogModel.cpp @@ -423,7 +423,7 @@ void CampaignEditorDialogModel::loadCampaignFromFile(const SCP_string& filename) // Attempt to load the selected file into the global Campaign struct. // We pass the full path via the filename argument now. - if (mission_campaign_load(filename.c_str(), filename.c_str(), nullptr, 0) != 0) { + if (mission_campaign_load(filename.c_str(), filename.c_str(), nullptr, false) != 0) { // Load failed. Reset the model to a clean "new campaign" state. initializeData(nullptr); clearCampaignGlobal(); // Ensure cleanup after failed load