Files
fs2open.github.com/code/mission/missioncampaign.cpp
T
Goober5000andClaude Opus 4.8 bed228d264 avoid undefined behavior from UNREACHABLE on reachable code paths (#7555)
* avoid undefined behavior from UNREACHABLE on reachable code paths

UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.

Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.

A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.

Where the converted site was a simple guard of the form
    if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
    Assertion(!x, ...);
    if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* addendum

After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible.  UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error".  Also fix a few other sites and change a couple to Warnings.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-09 11:20:25 +09:00

1957 lines
58 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include <cstdio>
#include <cstring>
#include <csetjmp>
#include <cerrno>
#ifdef _WIN32
#include <direct.h>
#include <io.h>
#endif
#ifdef SCP_UNIX
#include <sys/stat.h>
#include <glob.h>
#endif
#include "freespace.h"
#include "missioncampaign.h"
#include "cfile/cfile.h"
#include "cutscene/cutscenes.h"
#include "cutscene/movie.h"
#include "gamesequence/gamesequence.h"
#include "gamesnd/eventmusic.h"
#include "localization/localize.h"
#include "menuui/mainhallmenu.h"
#include "menuui/techmenu.h"
#include "mission/missioncampaign.h"
#include "mission/missiongoals.h"
#include "missionui/missionscreencommon.h"
#include "missionui/redalert.h"
#include "parse/parselo.h"
#include "parse/sexp.h"
#include "parse/sexp_container.h"
#include "pilotfile/pilotfile.h"
#include "playerman/player.h"
#include "popup/popup.h"
#include "scripting/global_hooks.h"
#include "scripting/scripting.h"
#include "ship/ship.h"
#include "starfield/supernova.h"
#include "ui/ui.h"
#include "utils/string_utils.h"
#include "weapon/weapon.h"
// campaign wasn't ended
int Campaign_ending_via_supernova = 0;
// stuff for selecting campaigns. We need to keep both arrays around since we display the
// list of campaigns by name, but must load campaigns by filename
char *Campaign_names[MAX_CAMPAIGNS] = { NULL };
char *Campaign_file_names[MAX_CAMPAIGNS] = { NULL };
char *Campaign_descs[MAX_CAMPAIGNS] = { NULL };
int Num_campaigns;
bool Campaign_file_missing = false;
int Campaign_load_failure = 0;
int Campaign_names_inited = 0;
SCP_vector<SCP_string> Ignored_campaigns;
char Default_campaign_file_name[MAX_FILENAME_LEN - 4] = { 0 };
// stuff used for campaign list building
static bool MC_desc = false;
static bool MC_multiplayer = false;
const char *campaign_types[MAX_CAMPAIGN_TYPES] =
{
//XSTR:OFF
"single",
"multi coop",
"multi teams"
//XSTR:ON
};
// modules local variables to deal with getting new ships/weapons available to the player
int Num_granted_ships, Num_granted_weapons; // per mission counts of new ships and weapons
int Granted_ships[MAX_SHIP_CLASSES];
int Granted_weapons[MAX_WEAPON_TYPES];
// variables to control the UI stuff for loading campaigns
LOCAL UI_WINDOW Campaign_window;
LOCAL UI_LISTBOX Campaign_listbox;
LOCAL UI_BUTTON Campaign_okb, Campaign_cancelb;
// the campaign!!!!!
campaign Campaign;
/**
* Returns a string (which is malloced in this routine) of the name of the given freespace campaign file.
* In the type field, we return if the campaign is a single player or multiplayer campaign.
* The type field will only be valid if the name returned is non-NULL
*/
bool mission_campaign_get_info(const char *filename, SCP_string &name, int *type, int *max_players, char **desc, char **first_mission)
{
int i, success = false;
SCP_string campaign_type;
char fname[MAX_FILENAME_LEN];
Assert( type != NULL );
// make sure outputs always have sane values
name.clear();
*type = -1;
if (max_players) {
*max_players = 0;
}
if (desc) {
*desc = nullptr;
}
if (first_mission) {
*first_mission = nullptr;
}
strncpy(fname, filename, MAX_FILENAME_LEN - 1);
auto fname_len = strlen(fname);
if ((fname_len < 4) || stricmp(fname + fname_len - 4, FS_CAMPAIGN_FILE_EXT) != 0){
strcat_s(fname, FS_CAMPAIGN_FILE_EXT);
fname_len += 4;
}
Assert(fname_len < MAX_FILENAME_LEN);
do {
try
{
read_file_text(fname);
reset_parse();
required_string("$Name:");
stuff_string(name, F_NAME);
if (name.empty()) {
nprintf(("Warning", "No name found for campaign file %s\n", filename));
break;
}
required_string("$Type:");
stuff_string(campaign_type, F_NAME);
for (i = 0; i < MAX_CAMPAIGN_TYPES; i++) {
if (!stricmp(campaign_type.c_str(), campaign_types[i])) {
*type = i;
}
}
if (*type < 0) {
Warning(LOCATION, "Invalid campaign type \"%s\"\n", campaign_type.c_str());
break;
}
if (desc) {
if (optional_string("+Description:")) {
*desc = stuff_and_malloc_string(F_MULTITEXT, NULL);
}
else {
*desc = NULL;
}
}
// if this is a multiplayer campaign, get the max players
if ((*type) != CAMPAIGN_TYPE_SINGLE) {
skip_to_string("+Num Players:");
stuff_int(max_players);
// Cyborg17 - and the first mission name if we want it, too
if (first_mission) {
skip_to_string("$Mission:");
*first_mission = stuff_and_malloc_string(F_NAME, nullptr);
}
}
// if we found a valid campaign type
if ((*type) >= 0) {
success = true;
}
}
catch (const parse::ParseException& e)
{
mprintf(("MISSIONCAMPAIGN: Unable to parse '%s'! Error message = %s.\n", fname, e.what()));
break;
}
} while (0);
return success;
}
/**
* Parses campaign and returns a list of missions in it.
* @return Number of missions added to the 'list', and up to 'max' missions may be added to 'list'.
* @return Negative on error.
*/
int mission_campaign_get_mission_list(const char *filename, char **list, int max)
{
int i, num = 0;
char name[MAX_FILENAME_LEN];
filename = cf_add_ext(filename, FS_CAMPAIGN_FILE_EXT);
// read the campaign file and get the list of mission filenames
try
{
read_file_text(filename);
reset_parse();
while (skip_to_string("$Mission:") > 0) {
stuff_string(name, F_NAME, MAX_FILENAME_LEN);
if (num < max)
list[num++] = vm_strdup(name);
else
Warning(LOCATION, "Maximum number of missions exceeded (%d)!", max);
}
}
catch (const parse::ParseException& e)
{
mprintf(("MISSIONCAMPAIGN: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
// since we can't return count of allocated elements, free them instead
for (i = 0; i<num; i++)
vm_free(list[i]);
num = -1;
}
return num;
}
void mission_campaign_free_list()
{
int i;
if ( !Campaign_names_inited )
return;
for (i = 0; i < Num_campaigns; i++) {
if (Campaign_names[i] != NULL) {
vm_free(Campaign_names[i]);
Campaign_names[i] = NULL;
}
if (Campaign_file_names[i] != NULL) {
vm_free(Campaign_file_names[i]);
Campaign_file_names[i] = NULL;
}
if (Campaign_descs[i] != NULL) {
vm_free(Campaign_descs[i]);
Campaign_descs[i] = NULL;
}
}
Num_campaigns = 0;
Campaign_names_inited = 0;
}
int mission_campaign_maybe_add(const char *filename)
{
SCP_string name;
char *desc = NULL;
int type, max_players;
// don't add ignored campaigns
if (campaign_is_ignored(filename)) {
return 0;
}
if ( mission_campaign_get_info( filename, name, &type, &max_players, &desc) ) {
if ( !MC_multiplayer && (type == CAMPAIGN_TYPE_SINGLE) ) {
Campaign_names[Num_campaigns] = vm_strdup(name.c_str());
if (MC_desc)
Campaign_descs[Num_campaigns] = desc;
Num_campaigns++;
// Note that we're not freeing desc here because the pointer is getting copied to Campaign_descs which is freed later.
return 1;
}
}
if (desc != NULL)
vm_free(desc);
return 0;
}
/**
* Builds up the list of campaigns that the user might be able to pick from.
* It uses the multiplayer flag to tell if we should display a list of single or multiplayer campaigns.
* This routine sets the Num_campaigns and Campaign_names global variables
*/
void mission_campaign_build_list(bool desc, bool sort, bool multiplayer)
{
char wild_card[10];
int i, j, incr = 0;
char *t = NULL;
int rc = 0;
if (Campaign_names_inited)
return;
MC_desc = desc;
MC_multiplayer = multiplayer;
memset(wild_card, 0, sizeof(wild_card));
strcpy_s(wild_card, NOX("*"));
strcat_s(wild_card, FS_CAMPAIGN_FILE_EXT);
// if we have already been loaded then free everything and reload
if (Num_campaigns != 0)
mission_campaign_free_list();
// set filter for cf_get_file_list() if there isn't one set already (the simroom has a special one)
if (Get_file_list_filter == NULL)
Get_file_list_filter = mission_campaign_maybe_add;
// now get the list of all mission names
// NOTE: we don't do sorting here, but we assume CF_SORT_NAME, and do it manually below
rc = cf_get_file_list(MAX_CAMPAIGNS, Campaign_file_names, CF_TYPE_MISSIONS, wild_card, CF_SORT_NONE);
Assert( rc == Num_campaigns );
// now sort everything, if we are supposed to
if (sort) {
incr = Num_campaigns / 2;
while (incr > 0) {
for (i = incr; i < Num_campaigns; i++) {
j = i - incr;
while (j >= 0) {
char *name1 = Campaign_names[j];
char *name2 = Campaign_names[j + incr];
// if we hit this then a coder probably did something dumb (like not needing to sort)
if ( (name1 == NULL) || (name2 == NULL) ) {
Int3();
break;
}
if ( !strnicmp(name1, "the ", 4) )
name1 += 4;
if ( !strnicmp(name2, "the ", 4) )
name2 += 4;
if (stricmp(name1, name2) > 0) {
// first, do filenames
t = Campaign_file_names[j];
Campaign_file_names[j] = Campaign_file_names[j + incr];
Campaign_file_names[j + incr] = t;
// next, actual names
t = Campaign_names[j];
Campaign_names[j] = Campaign_names[j + incr];
Campaign_names[j + incr] = t;
// finally, do descriptions
if (desc) {
t = Campaign_descs[j];
Campaign_descs[j] = Campaign_descs[j + incr];
Campaign_descs[j + incr] = t;
}
j -= incr;
} else {
break;
}
}
}
incr /= 2;
}
}
// Done!
Campaign_names_inited = 1;
}
/**
* Gets optional ship/weapon information
*/
void mission_campaign_get_sw_info()
{
int i, count, ship_list[MAX_SHIP_CLASSES], weapon_list[MAX_WEAPON_TYPES];
if (optional_string("+Starting Ships:")) {
count = sz2i(stuff_int_list(ship_list, MAX_SHIP_CLASSES, ParseLookupType::SHIP_INFO_TYPE));
// now set the array elements stating which ships we are allowed
for (i = 0; i < count; i++) {
if (Ship_info[ship_list[i]].flags[Ship::Info_Flags::Player_ship])
Campaign.ships_allowed[ship_list[i]] = 1;
}
}
else {
// set allowable ships to the SIF_PLAYER_SHIPs
for (auto it = Ship_info.cbegin(); it != Ship_info.cend(); ++it) {
if (it->flags[Ship::Info_Flags::Player_ship])
Campaign.ships_allowed[std::distance(Ship_info.cbegin(), it)] = 1;
}
}
if (optional_string("+Starting Weapons:")) {
count = sz2i(stuff_int_list(weapon_list, MAX_WEAPON_TYPES, ParseLookupType::WEAPON_POOL_TYPE));
// now set the array elements stating which ships we are allowed
for (i = 0; i < count; i++) {
if (Weapon_info[weapon_list[i]].wi_flags[Weapon::Info_Flags::Player_allowed])
Campaign.weapons_allowed[weapon_list[i]] = 1;
}
}
else {
// set allowable weapons to the player-allowed ones
for (auto it = Weapon_info.cbegin(); it != Weapon_info.cend(); ++it) {
if (it->wi_flags[Weapon::Info_Flags::Player_allowed])
Campaign.weapons_allowed[std::distance(Weapon_info.cbegin(), it)] = 1;
}
}
}
/**
* Starts a new campaign. It reads in the mission information in the campaign file
* It also sets up all variables needed inside of the game to deal with starting mission numbers, etc
*
* Note: Due to difficulties in generalizing this function, parts of it are duplicated throughout
* 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 i;
char name[NAME_LENGTH], type[NAME_LENGTH], temp[NAME_LENGTH];
if (campaign_is_ignored(filename)) {
Campaign_file_missing = true;
Campaign_load_failure = CAMPAIGN_ERROR_IGNORED;
return CAMPAIGN_ERROR_IGNORED;
}
filename = cf_add_ext(filename, FS_CAMPAIGN_FILE_EXT);
if ( pl == NULL )
pl = Player;
if ( !Fred_running && load_savefile && (pl == NULL) ) {
Int3();
load_savefile = 0;
}
// be sure to remove all old malloced strings of Mission_names
// we must also free any goal stuff that was from a previous campaign
// this also frees sexpressions so the next call to init_sexp will be able to reclaim
// nodes previously used by another campaign.
mission_campaign_clear();
// read the mission file and get the list of mission filenames
try
{
strcpy_s( Campaign.filename, filename );
// only initialize the sexpression stuff when Fred isn't running. It'll screw things up major
// if it does
if ( !Fred_running ){
init_sexp(); // must initialize the sexpression stuff
}
read_file_text( full_path ? full_path : filename );
reset_parse();
// copy filename to campaign structure minus the extension
auto len = strlen(filename) - 4;
Assert(len + 1 < CF_MAX_PATHNAME_LENGTH);
strncpy(Campaign.filename, filename, len);
Campaign.filename[len] = '\0';
required_string("$Name:");
stuff_string( name, F_NAME, NAME_LENGTH );
//Store campaign name in the global struct
strcpy_s( Campaign.name, name );
required_string( "$Type:" );
stuff_string( type, F_NAME, NAME_LENGTH );
for (i = 0; i < MAX_CAMPAIGN_TYPES; i++ ) {
if ( !stricmp(type, campaign_types[i]) ) {
Campaign.type = i;
break;
}
}
if ( i == MAX_CAMPAIGN_TYPES )
Error(LOCATION, "Unknown campaign type %s!", type);
if (optional_string("+Description:"))
stuff_string(Campaign.description, F_MULTITEXT);
// if the type is multiplayer -- get the number of players
if ( Campaign.type != CAMPAIGN_TYPE_SINGLE) {
required_string("+Num players:");
stuff_int( &(Campaign.num_players) );
}
// parse flags - Goober5000
if (optional_string("$Flags:"))
{
stuff_int( &(Campaign.flags) );
}
if (optional_string("$begin_custom_data_map")) {
parse_string_map(Campaign.custom_data, "$end_custom_data_map", "+Val:");
}
// parse the optional ship/weapon information
mission_campaign_get_sw_info();
// parse the mission file and actually read in the mission stuff
while ( required_string_either("#End", "$Mission:") ) {
cmission *cm;
required_string("$Mission:");
stuff_string(name, F_NAME, NAME_LENGTH);
cm = &Campaign.missions[Campaign.num_missions];
cm->name = vm_strdup(name);
cm->notes = NULL;
cm->briefing_cutscene[0] = 0;
if ( optional_string("+Briefing Cutscene:") )
stuff_string( cm->briefing_cutscene, F_NAME, NAME_LENGTH );
cm->flags = 0;
if (optional_string("+Flags:"))
stuff_int(&cm->flags);
cm->main_hall = "0";
// deal with previous campaign versions
if (cm->flags & CMISSION_FLAG_BASTION) {
cm->main_hall = "1";
}
// clear any other flag bits to prevent bogus values causing surprises
cm->flags &= CMISSION_EXTERNAL_FLAG_MASK;
// Goober5000 - new main hall stuff!
// Updated by CommanderDJ
if (optional_string("+Main Hall:")) {
stuff_string(temp, F_RAW, 32);
cm->main_hall = temp;
}
// Goober5000 - substitute main hall (like substitute music)
cm->substitute_main_hall = "";
if (optional_string("+Substitute Main Hall:")) {
stuff_string(temp, F_RAW, 32);
cm->substitute_main_hall = temp;
// if we're running FRED, keep the halls separate (so we can save the campaign file),
// but if we're running FS, replace the main hall with the substitute right now
if (!Fred_running) {
// see if this main hall exists
main_hall_defines* mhd = main_hall_get_pointer(temp);
if (mhd != nullptr) {
cm->main_hall = temp;
} else {
mprintf(("Substitute main hall '%s' not found\n", temp));
}
}
}
// Goober5000 - new debriefing persona stuff!
cm->debrief_persona_index = 0;
if (optional_string("+Debriefing Persona Index:"))
stuff_ubyte(&cm->debrief_persona_index);
cm->formula = -1;
if ( optional_string("+Formula:") ) {
cm->formula = get_sexp_main();
if ( !Fred_running ) {
Assert ( cm->formula != -1 );
sexp_mark_persistent( cm->formula );
} else {
if ( cm->formula == -1 ){
Campaign_load_failure = CAMPAIGN_ERROR_SEXP_EXHAUSTED;
return CAMPAIGN_ERROR_SEXP_EXHAUSTED;
}
}
}
// Do mission branching stuff
if ( optional_string("+Mission Loop:") ) {
cm->flags |= CMISSION_FLAG_HAS_LOOP;
} else if ( optional_string("+Mission Fork:") ) {
cm->flags |= CMISSION_FLAG_HAS_FORK;
}
cm->mission_branch_desc = NULL;
if ( optional_string("+Mission Loop Text:") || optional_string("+Mission Fork Text:") ) {
cm->mission_branch_desc = stuff_and_malloc_string(F_MULTITEXT, NULL);
}
cm->mission_branch_brief_anim = NULL;
if ( optional_string("+Mission Loop Brief Anim:") || optional_string("+Mission Fork Brief Anim:") ) {
ignore_white_space(); // it might be on the next line
cm->mission_branch_brief_anim = stuff_and_malloc_string(F_FILESPEC, nullptr);
(void)optional_string("$end_multi_text"); // consume the unneeded ending token
}
cm->mission_branch_brief_sound = NULL;
if ( optional_string("+Mission Loop Brief Sound:") || optional_string("+Mission Fork Brief Sound:") ) {
ignore_white_space(); // it might be on the next line
cm->mission_branch_brief_sound = stuff_and_malloc_string(F_FILESPEC, nullptr);
(void)optional_string("$end_multi_text"); // consume the unneeded ending token
}
cm->mission_loop_formula = -1;
if ( optional_string("+Formula:") ) {
cm->mission_loop_formula = get_sexp_main();
if ( !Fred_running ) {
Assert ( cm->mission_loop_formula != -1 );
sexp_mark_persistent( cm->mission_loop_formula );
} else {
if ( cm->mission_loop_formula == -1 ){
Campaign_load_failure = CAMPAIGN_ERROR_SEXP_EXHAUSTED;
return CAMPAIGN_ERROR_SEXP_EXHAUSTED;
}
}
}
cm->level = 0;
if (optional_string("+Level:")) {
stuff_int( &cm->level );
if ( cm->level == 0 ) // check if the top (root) of the whole tree
Campaign.next_mission = Campaign.num_missions;
} else
Campaign.realign_required = 1;
cm->pos = 0;
if (optional_string("+Position:"))
stuff_int( &cm->pos );
else
Campaign.realign_required = 1;
cm->goals.clear();
cm->events.clear();
cm->variables.clear();
cm->flags |= CMISSION_FLAG_FRED_LOAD_PENDING;
Campaign.num_missions++;
}
if ((Game_mode & GM_MULTIPLAYER) && Campaign.num_missions > UINT8_MAX)
throw parse::ParseException("Number of campaign missions is too high and breaks multi!");
}
catch (const parse::FileOpenException& foe)
{
mprintf(("Error opening '%s'\r\nError message = %s.\r\n", filename, foe.what()));
Campaign.filename[0] = 0;
Campaign.num_missions = 0;
Campaign_file_missing = true;
Campaign_load_failure = CAMPAIGN_ERROR_MISSING;
return CAMPAIGN_ERROR_MISSING;
}
catch (const parse::ParseException& pe)
{
mprintf(("Error parsing '%s'\r\nError message = %s.\r\n", filename, pe.what()));
Campaign.filename[0] = 0;
Campaign.num_missions = 0;
Campaign_file_missing = true;
Campaign_load_failure = CAMPAIGN_ERROR_CORRUPT;
return CAMPAIGN_ERROR_CORRUPT;
}
// set up the other variables for the campaign stuff. After initializing, we must try and load
// the campaign save file for this player. Since all campaign loads go through this routine, I
// think this place should be the only necessary place to load the campaign save stuff. The campaign
// save file will get written when a mission has ended by player choice.
// loading the campaign will get us to the current and next mission that the player must fly
// plus load all of the old goals that future missions might rely on.
if (!Fred_running && load_savefile && (Campaign.type == CAMPAIGN_TYPE_SINGLE)) {
// savefile can fail to load for numerous otherwise non-fatal reasons
// if it doesn't load in that case then it will be (re)created at save
if ( !Pilot.load_savefile(pl, Campaign.filename) ) {
// but if the data is invalid for the savefile then it is fatal
if ( Pilot.is_invalid() ) {
Campaign.filename[0] = 0;
Campaign.num_missions = 0;
Campaign_load_failure = CAMPAIGN_ERROR_SAVEFILE;
return CAMPAIGN_ERROR_SAVEFILE;
}
// 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
Campaign.next_mission = 0;
Pilot.save_savefile();
}
}
}
// all is good here, move along
Campaign_file_missing = false;
return 0;
}
/*
* initialise Player_loadout with default values
*/
void player_loadout_init()
{
int i = 0, j = 0;
memset(Player_loadout.filename, 0, sizeof(Player_loadout.filename));
memset(Player_loadout.last_modified, 0, sizeof(Player_loadout.last_modified));
for ( i = 0; i < MAX_SHIP_CLASSES; i++ ) {
Player_loadout.ship_pool[i] = 0;
}
for ( i = 0; i < MAX_WEAPON_TYPES; i++ ) {
Player_loadout.weapon_pool[i] = 0;
}
for ( i = 0; i < MAX_WSS_SLOTS; i++ ) {
Player_loadout.unit_data[i].ship_class = -1;
for ( j = 0; j < MAX_SHIP_WEAPONS; j++ ) {
Player_loadout.unit_data[i].wep[j] = 0;
Player_loadout.unit_data[i].wep_count[j] = 0;
}
}
}
/**
* Initializes some variables then loads the default FreeSpace single player campaign.
*/
void mission_campaign_init()
{
mission_campaign_clear();
Campaign_file_missing = false;
player_loadout_init();
}
// the below two functions is internal to this module. It is here so that I can group the save/load
// functions together.
//
/**
* Deletes any save file in the players directory for the given
* campaign filename
*/
void mission_campaign_savefile_delete(const char* cfilename)
{
char filename[_MAX_FNAME];
if ( Player->flags & PLAYER_FLAGS_IS_MULTI ) {
return; // no such thing as a multiplayer campaign savefile
}
auto base = util::get_file_part(cfilename);
// do a sanity check, but don't arbitrarily drop any extension in case the filename contains a period
Assertion(!stristr(base, FS_CAMPAIGN_FILE_EXT), "The campaign should not have an extension at this point!");
// only support the new filename here - taylor
sprintf_safe( filename, NOX("%s.%s.csg"), Player->callsign, base );
cf_delete(filename, CF_TYPE_PLAYERS, CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
}
/**
* Deletes all the save files for this particular pilot.
* Just call cfile function which will delete multiple files
*
* @param pilot_name Name of pilot
*/
void mission_campaign_delete_all_savefiles(char *pilot_name)
{
int dir_type, num_files, i;
char file_spec[MAX_FILENAME_LEN + 2];
char filename[1024];
int(*filter_save)(const char *filename);
SCP_vector<SCP_string> names;
auto ext = NOX(".csg");
dir_type = CF_TYPE_PLAYERS;
sprintf(file_spec, NOX("%s.*%s"), pilot_name, ext);
// HACK HACK HACK HACK!!!! cf_get_file_list is not reentrant. Pretty dumb because it should
// be. I have to save any file filters
filter_save = Get_file_list_filter;
Get_file_list_filter = NULL;
num_files = cf_get_file_list(names, dir_type, file_spec, CF_SORT_NONE, nullptr,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
Get_file_list_filter = filter_save;
for (i = 0; i < num_files; i++) {
strcpy_s(filename, names[i].c_str());
strcat_s(filename, ext);
cf_delete(filename, dir_type, CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
}
}
/**
* Sets up the internal veriables of the campaign structure so the player can play the next mission.
* If there are no more missions available in the campaign, this function returns -1, else 0 if the mission was
* set successfully
*/
int mission_campaign_next_mission()
{
if ((Campaign.next_mission == -1) || (Campaign.name[0] == '\0')) // will be set to -1 when there is no next mission
return -1;
if (Campaign.num_missions < 1)
return -2;
Campaign.current_mission = Campaign.next_mission;
strcpy_s(Game_current_mission_filename, Campaign.missions[Campaign.current_mission].name);
// check for end of loop.
if (Campaign.current_mission == Campaign.loop_reentry) {
Campaign.loop_enabled = 0;
}
// reset the number of persistent ships and weapons for the next campaign mission
Num_granted_ships = 0;
Num_granted_weapons = 0;
return 0;
}
/**
* Called to go to the previous mission in the campaign. Used only for Red Alert missions
*/
int mission_campaign_previous_mission()
{
if (!(Game_mode & GM_CAMPAIGN_MODE))
return 0;
if (Campaign.prev_mission == -1)
return 0;
Campaign.current_mission = Campaign.prev_mission;
Campaign.prev_mission = -1;
Campaign.next_mission = Campaign.current_mission;
Campaign.num_missions_completed--;
Campaign.missions[Campaign.next_mission].completed = 0;
// copy backed up variables over
for (auto& ra_variable : Campaign.red_alert_variables) {
Campaign.persistent_variables.push_back(ra_variable);
}
// copy backed up containers over
for (const auto& ra_container : Campaign.red_alert_containers) {
Campaign.persistent_containers.emplace_back(ra_container);
}
Pilot.save_savefile();
// reset the player stats to be the stats from this level
Player->stats.assign( Campaign.missions[Campaign.current_mission].stats );
strcpy_s( Game_current_mission_filename, Campaign.missions[Campaign.current_mission].name );
Num_granted_ships = 0;
Num_granted_weapons = 0;
return 1;
}
/**
* Evaluate next campaign mission - set as Campaign.next_mission. Also set Campaign.loop_mission
*/
void mission_campaign_eval_next_mission()
{
Campaign.next_mission = -1;
int cur = Campaign.current_mission;
// evaluate next mission (straight path)
if (Campaign.missions[cur].formula != -1) {
flush_sexp_tree(Campaign.missions[cur].formula); // force formula to be re-evaluated
eval_sexp(Campaign.missions[cur].formula); // this should reset Campaign.next_mission to proper value
}
// evaluate mission loop mission (if any) so it can be used if chosen
if ( Campaign.missions[cur].flags & CMISSION_FLAG_HAS_LOOP ) {
int saved_next_mission = Campaign.next_mission;
// Set temporarily to -1 so we know if loop formula fails to assign
Campaign.next_mission = -1;
if (Campaign.missions[cur].mission_loop_formula != -1) {
flush_sexp_tree(Campaign.missions[cur].mission_loop_formula); // force formula to be re-evaluated
eval_sexp(Campaign.missions[cur].mission_loop_formula); // this should set Campaign.next_mission to the loop mission
}
Campaign.loop_mission = Campaign.next_mission;
Campaign.next_mission = saved_next_mission;
// If the loop mission and the next mission are the same, then don't do the loop. This could be the case if the campaign
// only allows us to proceed to the loop mission if certain conditions are met.
if (Campaign.loop_mission == Campaign.next_mission) {
Campaign.loop_mission = -1;
}
}
if (Campaign.next_mission == -1) {
nprintf(("allender", "No next mission to proceed to.\n"));
} else {
nprintf(("allender", "Next mission is number %d [%s]\n", Campaign.next_mission, Campaign.missions[Campaign.next_mission].name));
}
}
/**
* Store mission's goals and events in Campaign struct
*/
void mission_campaign_store_goals_and_events()
{
if (!(Game_mode & GM_CAMPAIGN_MODE) || (Campaign.current_mission < 0))
return;
int cur = Campaign.current_mission;
auto mission_obj = &Campaign.missions[cur];
// first we must save the status of the current missions goals in the campaign mission structure.
// After that, we can determine which mission is tagged as the next mission. Finally, we
// can save the campaign save file
// we might have goal and event status if the player replayed a mission
mission_obj->goals.clear();
// copy the needed info from the Mission_goal struct to our internal structure
for (const auto& goal: Mission_goals) {
mission_obj->goals.emplace_back();
auto& stored_goal = mission_obj->goals.back();
if (goal.name.empty())
sprintf(stored_goal.name, NOX("Goal #" SIZE_T_ARG), &goal - &Mission_goals[0] + 1);
else
strncpy_s(stored_goal.name, goal.name.c_str(), NAME_LENGTH - 1);
Assert ( goal.satisfied != GOAL_INCOMPLETE ); // should be true or false at this point!!!
stored_goal.status = (char)goal.satisfied;
}
// do the same thing for events as we did for goals
// we might have goal and event status if the player replayed a mission
mission_obj->events.clear();
// copy the needed info from the Mission_goal struct to our internal structure
for (const auto& event: Mission_events) {
mission_obj->events.emplace_back();
auto& stored_event = mission_obj->events.back();
if (event.name.empty()) {
sprintf(stored_event.name, NOX("Event #" SIZE_T_ARG), &event - &Mission_events[0] + 1);
nprintf(("Warning", "Mission event in mission %s must have a +Name field! using %s for campaign save file\n", mission_obj->name, stored_event.name));
} else
strncpy_s(stored_event.name, event.name.c_str(), NAME_LENGTH - 1);
// Old method:
// getting status for the events is a little different. If the formula value for the event entry
// is -1, then we know the value of the result field will never change. If the formula is
// not -1 (i.e. still being evaluated at mission end time), we will write "incomplete" for the
// event evaluation
// New method: check a flag. Also, even with the old method, events are always
// forced satisfied or failed at the end of a mission
if (event.flags & MEF_EVENT_IS_DONE) {
if ( event.result )
stored_event.status = static_cast<int>(EventStatus::SATISFIED);
else
stored_event.status = static_cast<int>(EventStatus::FAILED);
} else
Assertion(false, "Mission event formula should be marked MEF_EVENT_IS_DONE at end-of-mission");
}
}
void mission_campaign_store_variables(int persistence_type, bool store_red_alert)
{
if (!(Game_mode & GM_CAMPAIGN_MODE) || (Campaign.current_mission < 0))
return;
int cur = Campaign.current_mission;
auto mission_obj = &Campaign.missions[cur];
mission_obj->variables.clear();
int num_sexp_variables = sexp_variable_count();
for (int i = 0; i < num_sexp_variables; i++) {
if (!(Sexp_variables[i].type & persistence_type)) {
continue;
}
// player-persistent (aka "eternal")
if (Sexp_variables[i].type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) {
// see if we already have a variable with this name
int j = find_item_with_string(Player->variables, &sexp_variable::variable_name, Sexp_variables[i].variable_name);
if (j >= 0) {
Player->variables[j].type = Sexp_variables[i].type;
strcpy_s(Player->variables[j].text, Sexp_variables[i].text);
}
// new variable
else {
Player->variables.push_back(Sexp_variables[i]);
}
}
// campaign-persistent
else {
// see if we already have a variable with this name
int j = find_item_with_string(Campaign.persistent_variables, &sexp_variable::variable_name, Sexp_variables[i].variable_name);
if (j >= 0) {
Campaign.persistent_variables[j].type = Sexp_variables[i].type;
strcpy_s(Campaign.persistent_variables[j].text, Sexp_variables[i].text);
}
// new variable
else {
Campaign.persistent_variables.push_back(Sexp_variables[i]);
}
}
}
if (store_red_alert) {
for (const auto& current_rav : Campaign.red_alert_variables) {
if (find_item_with_string(Campaign.persistent_variables, &sexp_variable::variable_name, current_rav.variable_name) < 0) {
Campaign.persistent_variables.push_back(current_rav);
} else {
Warning(LOCATION, "A red alert variable has the same name as a persistent variable!");
}
}
}
}
// jg18 - adapted from mission_campaign_store_variables()
void mission_campaign_store_containers(ContainerType persistence_type, bool store_red_alert)
{
if (!(Game_mode & GM_CAMPAIGN_MODE) || (Campaign.current_mission < 0))
return;
for (const auto &container : get_all_sexp_containers()) {
if (none(container.type & persistence_type)) {
continue;
}
// player-persistent (aka "eternal")
if (container.is_eternal()) {
// see if we already have a container with this name
auto ppc_it = std::find_if(Player->containers.begin(),
Player->containers.end(),
[&container](const sexp_container& ppc) {
return ppc.name_matches(container);
});
if (ppc_it != Player->containers.end()) {
*ppc_it = container;
} else {
// new player-persistent container
Player->containers.push_back(container);
}
}
// campaign-persistent
else {
// see if we already have a container with this name
auto cpc_it = std::find_if(Campaign.persistent_containers.begin(),
Campaign.persistent_containers.end(),
[&container](const sexp_container& cpc) {
return cpc.name_matches(container);
});
if (cpc_it != Campaign.persistent_containers.end()) {
*cpc_it = container;
} else {
// new container
Campaign.persistent_containers.push_back(container);
}
}
}
if (store_red_alert) {
for (const auto& container : Campaign.red_alert_containers) {
// see if we already have a container with this name
auto cpc_it = std::find_if(Campaign.persistent_containers.begin(),
Campaign.persistent_containers.end(),
[&container](const sexp_container& cpc) {
return cpc.name_matches(container);
});
if (cpc_it == Campaign.persistent_containers.end()) {
Campaign.persistent_containers.push_back(container);
} else {
Warning(LOCATION, "A red alert container has the same name as a persistent container!");
}
}
}
}
void mission_campaign_store_goals_and_events_and_variables(bool store_red_alert_data)
{
mission_campaign_store_goals_and_events();
mission_campaign_store_variables(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS, store_red_alert_data);
mission_campaign_store_containers(ContainerType::SAVE_ON_MISSION_PROGRESS, store_red_alert_data);
}
/**
* Called when the player's mission is over. It updates the internal store of goals
* and their status then saves the state of the campaign in the campaign file.
* This gets called after player accepts mission results in debriefing.
*/
void mission_campaign_mission_over(bool do_next_mission)
{
int mission_num, i;
cmission *mission_obj;
// I don't think that we should have a record for these -- maybe we might?????? If we do,
// then we should free them
if ( !(Game_mode & GM_CAMPAIGN_MODE) ){
return;
}
mission_num = Campaign.current_mission;
Assert( mission_num != -1 );
mission_obj = &Campaign.missions[mission_num];
// determine if any ships/weapons were granted this mission
for ( i=0; i<Num_granted_ships; i++ ){
Campaign.ships_allowed[Granted_ships[i]] = 1;
}
for ( i=0; i<Num_granted_weapons; i++ ){
Campaign.weapons_allowed[Granted_weapons[i]] = 1;
}
// update campaign.mission stats (used to allow backout inRedAlert)
// .. but we don't do this if we are inside of the prev/current loop hack
if ( Campaign.prev_mission != Campaign.current_mission ) {
mission_obj->stats.assign( Player->stats );
if(!(Game_mode & GM_MULTIPLAYER)){
scoring_backout_accept( &mission_obj->stats );
}
}
// if we are moving to a new mission, then change our data. If we are staying on the same mission,
// then we don't want to do anything. Remove information about goals/events
if ( Campaign.next_mission != mission_num ) {
Campaign.prev_mission = mission_num;
Campaign.current_mission = -1;
// very minor support for non-linear campaigns - taylor
if (Campaign.missions[mission_num].completed != 1) {
Campaign.num_missions_completed++;
Campaign.missions[mission_num].completed = 1;
}
// save the scoring values from the previous mission at the start of this mission -- for red alert
// save the state of the campaign in the campaign save file and move to the end_game state
if (Campaign.type == CAMPAIGN_TYPE_SINGLE) {
Pilot.save_savefile();
}
// runs the new scripting conditional hook, "On Campaign Mission Accept" --wookieejedi
scripting::hooks::OnCampaignMissionAccept->run(
scripting::hook_param_list(scripting::hook_param("Mission", 's', mission_obj->name)
));
} else {
// free up the goals and events which were just malloced. It's kind of like erasing any fact
// that the player played this mission in the campaign.
mission_obj->goals.clear();
mission_obj->events.clear();
mission_obj->variables.clear();
Sexp_nodes[mission_obj->formula].value = SEXP_UNKNOWN;
}
if (do_next_mission)
mission_campaign_next_mission(); // sets up whatever needs to be set to actually play next mission
}
void mission_campaign_free_mission_strings(cmission &cm)
{
if (cm.name != nullptr) {
vm_free(cm.name);
cm.name = nullptr;
}
if (cm.notes != nullptr) {
vm_free(cm.notes);
cm.notes = nullptr;
}
// the next three are strdup'd return values from parselo.cpp - taylor
if (cm.mission_branch_desc != nullptr) {
vm_free(cm.mission_branch_desc);
cm.mission_branch_desc = nullptr;
}
if (cm.mission_branch_brief_anim != nullptr) {
vm_free(cm.mission_branch_brief_anim);
cm.mission_branch_brief_anim = nullptr;
}
if (cm.mission_branch_brief_sound != nullptr) {
vm_free(cm.mission_branch_brief_sound);
cm.mission_branch_brief_sound = nullptr;
}
}
/**
* Called when the game closes -- to get rid of memory errors for Bounds checker
* also called at campaign init and campaign load
*/
void mission_campaign_clear()
{
int i;
Campaign.description.clear();
// be sure to remove all old malloced strings of Mission_names
// we must also free any goal stuff that was from a previous campaign
for ( i=0; i<Campaign.num_missions; i++ ) {
mission_campaign_free_mission_strings(Campaign.missions[i]);
Campaign.missions[i].goals.clear();
Campaign.missions[i].events.clear();
Campaign.missions[i].variables.clear();
if ( !Fred_running ){
sexp_unmark_persistent(Campaign.missions[i].formula); // free any sexpression nodes used by campaign.
}
memset(Campaign.missions[i].briefing_cutscene, 0, NAME_LENGTH);
Campaign.missions[i].formula = 0;
Campaign.missions[i].completed = 0;
Campaign.missions[i].mission_loop_formula = 0;
Campaign.missions[i].level = 0;
Campaign.missions[i].pos = 0;
Campaign.missions[i].flags = 0;
Campaign.missions[i].main_hall = "";
Campaign.missions[i].substitute_main_hall = "";
Campaign.missions[i].debrief_persona_index = 0;
Campaign.missions[i].stats.init();
}
memset(Campaign.name, 0, NAME_LENGTH);
memset(Campaign.filename, 0, MAX_FILENAME_LEN);
Campaign.type = 0;
Campaign.custom_data.clear();
Campaign.flags = CF_DEFAULT_VALUE;
Campaign.num_missions = 0;
Campaign.num_missions_completed = 0;
Campaign.current_mission = -1;
Campaign.next_mission = 0;
Campaign.prev_mission = -1;
Campaign.loop_enabled = 0;
Campaign.loop_mission = CAMPAIGN_LOOP_MISSION_UNINITIALIZED;
Campaign.loop_reentry = 0;
Campaign.realign_required = 0;
Campaign.num_players = 0;
memset( Campaign.ships_allowed, 0, sizeof(Campaign.ships_allowed) );
memset( Campaign.weapons_allowed, 0, sizeof(Campaign.weapons_allowed) );
Campaign.persistent_variables.clear();
Campaign.red_alert_variables.clear();
Campaign.persistent_containers.clear();
Campaign.red_alert_containers.clear();
}
/**
* Extract the mission filenames for a campaign.
*
* @param filename Name of campaign file
* @param dest Storage for the mission filename, must be already allocated
* @param num Output parameter for the number of mission filenames in the campaign
*
* note that dest should allocate at least dest[MAX_CAMPAIGN_MISSIONS][NAME_LENGTH]
*/
int mission_campaign_get_filenames(char *filename, char dest[][NAME_LENGTH], int *num)
{
// read the mission file and get the list of mission filenames
try
{
Assert( strlen(filename) < MAX_FILENAME_LEN ); // make sure no overflow
read_file_text(filename);
reset_parse();
required_string("$Name:");
advance_to_eoln(NULL);
required_string( "$Type:" );
advance_to_eoln(NULL);
// parse the mission file and actually read in the mission stuff
*num = 0;
while ( skip_to_string("$Mission:") == 1 ) {
stuff_string(dest[*num], F_NAME, NAME_LENGTH);
(*num)++;
}
}
catch (const parse::ParseException& e)
{
mprintf(("MISSIONCAMPAIGN: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
return 1;
}
return 0;
}
SCP_string mission_campaign_get_name(const char* filename)
{
// read the mission file and only read the name entry
SCP_string filename_str = filename;
filename_str += FS_CAMPAIGN_FILE_EXT;
try {
Assertion(filename_str.size() < MAX_FILENAME_LEN,
"Filename (%s) is too long. Is " SIZE_T_ARG " bytes long but maximum is %d.", filename_str.c_str(),
filename_str.size(), MAX_FILENAME_LEN); // make sure no overflow
read_file_text(filename_str.c_str());
reset_parse();
required_string("$Name:");
SCP_string res;
stuff_string(res, F_NAME);
return res;
} catch (const parse::ParseException& e) {
mprintf(("MISSIONCAMPAIGN: Unable to parse '%s'! Error message = %s.\n", filename_str.c_str(), e.what()));
return SCP_string();
}
}
/**
* Read the goals and events from a mission in a campaign file and store that information
* in the campaign structure for use in the campaign editor, error checking, etc
*/
void read_mission_goal_list(int num)
{
char *filename, notes[NOTES_LENGTH];
int z;
Assertion(num >= 0 && num < Campaign.num_missions, "mission number out of range!");
filename = Campaign.missions[num].name;
if (Campaign.missions[num].notes) {
vm_free(Campaign.missions[num].notes);
Campaign.missions[num].notes = nullptr;
}
Campaign.missions[num].events.clear();
Campaign.missions[num].goals.clear();
Campaign.missions[num].variables.clear();
try
{
read_file_text(filename);
reset_parse();
init_sexp();
// first, read the mission notes for this mission. Used in campaign editor
if (skip_to_string("#Mission Info")) {
if (skip_to_string("$Notes:")) {
stuff_string(notes, F_NOTES, NOTES_LENGTH);
Campaign.missions[num].notes = (char *)vm_malloc(strlen(notes) + 1);
strcpy(Campaign.missions[num].notes, notes);
}
}
// skip to events section in the mission file. Events come before goals, so we process them first
if (skip_to_string("#Events")) {
while (true) {
if (skip_to_string("$Formula:", "#Goals") != 1){
break;
}
z = skip_to_string("+Name:", "$Formula:");
if (!z){
break;
}
Campaign.missions[num].events.emplace_back();
auto& stored_event = Campaign.missions[num].events.back();
if (z == 1)
stuff_string(stored_event.name, F_NAME, NAME_LENGTH);
else
sprintf(stored_event.name, NOX("Event #" SIZE_T_ARG), &stored_event - &Campaign.missions[num].events[0] + 1);
}
}
if (skip_to_string("#Goals")) {
while (true) {
if (skip_to_string("$Type:", "#End") != 1){
break;
}
z = skip_to_string("+Name:", "$Type:");
if (!z){
break;
}
Campaign.missions[num].goals.emplace_back();
auto& stored_goal = Campaign.missions[num].goals.back();
if (z == 1)
stuff_string(stored_goal.name, F_NAME, NAME_LENGTH);
else
sprintf(stored_goal.name, NOX("Goal #" SIZE_T_ARG), &stored_goal - &Campaign.missions[num].goals[0] + 1);
}
}
// Goober5000 - variables do not need to be read here
}
catch (const parse::ParseException& e)
{
mprintf(("MISSIONCAMPAIGN: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
return;
}
}
/**
* Return index into Campaign's list of missions of the mission with the given
* filename.
*
* This function tried to be a little smart about filename looking for the .fsm
* extension since filenames are stored with the extension in the campaign file.
*
* @return index of mission in campaign structure. -1 if mission name not found.
*/
int mission_campaign_find_mission( const char *name )
{
int i;
char realname[_MAX_PATH];
if (name == NULL)
return -1;
// look for an extension on the file. If no extension, add default ".fsm" onto the
// end of the filename
strcpy_s(realname, name );
if ( strchr(name, '.') == NULL ){
sprintf(realname, NOX("%s%s"), name, FS_MISSION_FILE_EXT );
}
for (i = 0; i < Campaign.num_missions; i++ ) {
if ( !stricmp(realname, Campaign.missions[i].name) ){
return i;
}
}
return -1;
}
void mission_campaign_maybe_play_movie(int type)
{
int mission_idx;
char *filename;
// only support pre mission movies for now.
Assert ( type == CAMPAIGN_MOVIE_PRE_MISSION );
if ( !(Game_mode & GM_CAMPAIGN_MODE) )
return;
mission_idx = Campaign.current_mission;
Assert( mission_idx != -1 );
// get a possible filename for a movie to play.
filename = NULL;
switch( type ) {
case CAMPAIGN_MOVIE_PRE_MISSION:
if ( strlen(Campaign.missions[mission_idx].briefing_cutscene) )
filename = Campaign.missions[mission_idx].briefing_cutscene;
break;
default:
Int3();
break;
}
// no filename, no movie!
if ( !filename )
return;
movie::play(filename); //Play the movie!
}
/**
* Return the type of campaign of the passed filename
*/
int mission_campaign_parse_is_multi(const char *filename, char *name)
{
int i;
char temp[NAME_LENGTH];
try
{
read_file_text(filename);
reset_parse();
required_string("$Name:");
stuff_string(temp, F_NAME, NAME_LENGTH);
if (name)
strcpy(name, temp);
required_string("$Type:");
stuff_string(temp, F_NAME, NAME_LENGTH);
for (i = 0; i < MAX_CAMPAIGN_TYPES; i++) {
if (!stricmp(temp, campaign_types[i])) {
return i;
}
}
Error(LOCATION, "Unknown campaign type %s", temp);
return -1;
}
catch (const parse::ParseException& e)
{
mprintf(("MISSIONCAMPAIGN: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
return -1;
}
}
/**
* Save persistent information during a mission -- which then might or might not get
* saved out when the mission is over depending on whether player replays mission or commits.
*/
void mission_campaign_save_persistent( int type, int sindex )
{
// based on the type of information, save it off for possible saving into the campsign
// savefile when the mission is over
if ( type == CAMPAIGN_PERSISTENT_SHIP ) {
Assert( Num_granted_ships < MAX_SHIP_CLASSES );
Granted_ships[Num_granted_ships] = sindex;
Num_granted_ships++;
} else if ( type == CAMPAIGN_PERSISTENT_WEAPON ) {
Assert( Num_granted_weapons < MAX_WEAPON_TYPES );
Granted_weapons[Num_granted_weapons] = sindex;
Num_granted_weapons++;
} else
Int3();
}
bool campaign_is_ignored(const char *filename)
{
SCP_string filename_no_ext = filename;
drop_extension(filename_no_ext);
SCP_tolower(filename_no_ext);
for (auto &ii: Ignored_campaigns) {
if (ii == filename_no_ext) {
return true;
}
}
return false;
}
static bool campaign_reportable_error(int val)
{
return val != 0 && val != CAMPAIGN_ERROR_MISSING && val != CAMPAIGN_ERROR_IGNORED;
}
// returns 0: loaded, !0: error
int mission_load_up_campaign(bool fall_back_from_current)
{
int rc = -1, idx;
auto pl = Player;
// find best campaign to use:
// 1) cmdline
// 2) last used
// 3) builtin
// 4) anything else
// Note that in each step we only fall back when the error is benign, e.g. ignored or missing;
// if there's some other real error with the campaign file, we report it.
// Also note that if we *have* a current campaign, we shouldn't fall back *at all*, lest we repeatedly
// reset what the current campaign is, *unless* we are starting a brand new session or loading a new pilot.
// cmdline...
if ( Cmdline_campaign != nullptr && strlen(Cmdline_campaign) ) {
char* campaign = Cmdline_campaign;
// Clear cmdline value
// * Only set campaign once from cmdline.
// * Prevent subsequent load failures.
// * On success, campaign becomes "last used".
Cmdline_campaign = nullptr;
bool has_last_used_campaign = strlen(pl->current_campaign) > 0;
bool campaign_already_set = has_last_used_campaign
&& (stricmp(campaign, pl->current_campaign) == 0);
if (has_last_used_campaign) {
mprintf(("Current campaign is '%s'\n", pl->current_campaign));
}
if (!campaign_already_set) {
rc = mission_campaign_load(campaign, nullptr, pl);
if (rc == 0) {
// update pilot with the new current campaign (becomes "last used")
strcpy_s(pl->current_campaign, Campaign.filename);
mprintf(("Set current campaign to '%s'\n", campaign));
return rc;
} else {
mprintf(("Failed to set current campaign to '%s'\n", campaign));
}
}
}
// last used...
if ( strlen(pl->current_campaign) ) {
rc = mission_campaign_load(pl->current_campaign, nullptr, pl);
if (rc == 0 || !fall_back_from_current || campaign_reportable_error(rc)) {
return rc;
}
}
// builtin...
rc = mission_campaign_load(Default_campaign_file_name, nullptr, pl);
// everything else...
if (rc < 0 && !campaign_reportable_error(rc)) {
// no descriptions, no sorting
mission_campaign_build_list(false, false);
for (idx = 0; (idx < Num_campaigns) && (rc < 0); idx++) {
if ( (Campaign_file_names[idx] == NULL) || !strlen(Campaign_file_names[idx]) ) {
continue;
}
// skip current and builtin since they already didn't work
if ( !stricmp(Campaign_file_names[idx], pl->current_campaign) ) {
continue;
}
if ( !stricmp(Campaign_file_names[idx], BUILTIN_CAMPAIGN) ) {
continue;
}
// try to load it, whatever "it" is
rc = mission_campaign_load(Campaign_file_names[idx], nullptr, pl);
}
mission_campaign_free_list();
}
// update pilot with the new current campaign
if (rc == 0) {
strcpy_s(pl->current_campaign, Campaign.filename);
}
return rc;
}
/**
* For end of campaign in the single player game. Called when the end of campaign state is
* entered, which is triggered when the end-campaign sexpression is hit
*/
void mission_campaign_end_init()
{
// no need to do any initialization.
}
void mission_campaign_end_do()
{
mission_campaign_store_variables(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS, false);
mission_campaign_store_containers(ContainerType::SAVE_ON_MISSION_PROGRESS, false);
// close out the mission
event_music_level_close();
mission_goal_fail_incomplete();
scoring_level_close();
mission_campaign_mission_over();
// play the movies
// eventually we'll want to play one of two options (good ending or bad ending)
// this is specific to the FreeSpace 2 single-player campaign
if (!stricmp(Campaign.filename, "freespace2")) {
// did the supernova blow?
if (supernova_stage() >= SUPERNOVA_STAGE::HIT) {
movie::play_two("endpart1.mve", "endprt2b.mve"); // bad ending
} else {
movie::play_two("endpart1.mve", "endprt2a.mve"); // good ending
}
} else {
common_maybe_play_cutscene(MOVIE_END_CAMPAIGN);
}
gameseq_post_event( GS_EVENT_MAIN_MENU );
}
void mission_campaign_end_close()
{
// no need to do any initialization.
}
/**
* Skip to the next mission in the campaign
* this also posts the state change
*/
void mission_campaign_skip_to_next()
{
// mark mission as skipped
Campaign.missions[Campaign.current_mission].flags |= CMISSION_FLAG_SKIPPED;
// mark all goals/events complete
// these do not really matter, since is-previous-event-* and is-previous-goal-* sexps check
// to see if the mission was skipped, and use defaults accordingly.
mission_goal_mark_objectives_complete();
mission_goal_mark_events_complete();
// store
mission_campaign_store_goals_and_events_and_variables(false);
// now set the next mission
mission_campaign_eval_next_mission();
// because all goals/events are marked true, it's possible for the campaign condition to unexpectedly *not* evaluate to the next mission
// (e.g. if is-previous-goal-false or is-previous-event-false is used without the optional argument), so this is a failsafe
if (Campaign.next_mission == Campaign.current_mission) {
Campaign.next_mission++;
if (Campaign.next_mission < Campaign.num_missions) {
Warning(LOCATION, "mission_campaign_skip_to_next() could not determine the next mission! Choosing the next-in-sequence mission as a failsafe...");
} else {
Warning(LOCATION, "mission_campaign_skip_to_next() could not determine the next mission!");
Campaign.next_mission = -1;
}
}
// clear out relevant player vars
Player->failures_this_session = 0;
Player->show_skip_popup = 1;
// proceed to next mission or main hall
if ((Campaign.missions[Campaign.current_mission].flags & CMISSION_FLAG_HAS_LOOP) && (Campaign.loop_mission != -1)) {
// go to loop solicitation
gameseq_post_event(GS_EVENT_LOOP_BRIEF);
} else {
// closes out mission stuff, sets up next one
mission_campaign_mission_over();
if ( Campaign.next_mission == -1 || (The_mission.flags[Mission::Mission_Flags::End_to_mainhall]) ) {
// go to main hall, either the campaign is over or the FREDer requested it.
gameseq_post_event(GS_EVENT_MAIN_MENU);
} else {
// go to next mission
gameseq_post_event(GS_EVENT_START_GAME);
}
}
}
/**
* Breaks your ass out of the loop
* this also posts the state change
*/
void mission_campaign_exit_loop()
{
// set campaign to loop reentry point
Campaign.next_mission = Campaign.loop_reentry;
Campaign.current_mission = -1;
Campaign.loop_enabled = 0;
// set things up for next mission
mission_campaign_next_mission();
if ( Campaign.next_mission == -1 || (The_mission.flags[Mission::Mission_Flags::End_to_mainhall]) ) {
gameseq_post_event( GS_EVENT_MAIN_MENU );
} else {
gameseq_post_event( GS_EVENT_START_GAME );
}
}
/**
* Used for jumping to a particular campaign mission
* all previous missions marked skipped
* this relies on correct mission ordering in the campaign file
*/
bool mission_campaign_jump_to_mission(const char* filename, bool no_skip, bool preserve_loadout)
{
int i = 0, mission_num = -1;
constexpr size_t dest_filename_size = 64;
char dest_filename[dest_filename_size], *p;
// load in the campaign junk
mission_load_up_campaign();
// tack the .fs2 onto the input name if necessary
strcpy_s(dest_filename, filename);
drop_white_space(dest_filename);
p = strchr(dest_filename, '.');
if ((p == nullptr || stricmp(p, ".fs2") != 0) && strlen(dest_filename) <= dest_filename_size - 4 - 1)
strcat_s(dest_filename, ".fs2");
// search for our mission
for (i = 0; i < Campaign.num_missions; i++) {
if ((Campaign.missions[i].name != nullptr) && !stricmp(Campaign.missions[i].name, dest_filename)) {
mission_num = i;
break;
} else if (!no_skip) {
Campaign.missions[i].flags |= CMISSION_FLAG_SKIPPED;
Campaign.missions[i].completed = 1;
Campaign.num_missions_completed = i + 1;
}
}
if (mission_num < 0) {
// if we got here, no match was found
// based on player feedback, let's NOT restart the campaign but rather fail gracefully
return false;
} else {
if (!preserve_loadout) {
for (auto it = Ship_info.begin(); it != Ship_info.end(); it++) {
i = static_cast<int>(std::distance(Ship_info.begin(), it));
Campaign.ships_allowed[i] = 1;
}
for (i = 0; i < weapon_info_size(); i++) {
Campaign.weapons_allowed[i] = 1;
}
}
Campaign.next_mission = mission_num;
Campaign.prev_mission = mission_num - 1;
mission_campaign_next_mission();
Game_mode |= GM_CAMPAIGN_MODE;
gameseq_post_event(GS_EVENT_START_GAME);
return true;
}
}
SCP_vector<SCP_string> mission_campaign_get_valid_next_missions()
{
SCP_vector<SCP_string> valid_missions;
// This can be queried from UI states outside active mission gameplay
// where GM_CAMPAIGN_MODE may not be set even though a campaign is loaded.
if (Campaign.name[0] == '\0' || Campaign.num_missions <= 0) {
return valid_missions;
}
// During normal campaign flow, current_mission is set to -1 after accepting a mission
// and before the next mission actually starts. In that window, prev_mission is the
// mission whose branching formula produced the current next_mission.
// Prefer prev_mission when available since branch selection outside missions is
// generally based on the mission just completed.
int branch_source_mission = Campaign.prev_mission;
if (branch_source_mission < 0) {
branch_source_mission = Campaign.current_mission;
}
if (branch_source_mission < 0 || branch_source_mission >= Campaign.num_missions) {
// Campaigns that haven't started yet can have both current_mission and prev_mission
// unset. In that case, next_mission is the only available entry point.
if (Campaign.next_mission >= 0 && Campaign.next_mission < Campaign.num_missions) {
valid_missions.emplace_back(Campaign.missions[Campaign.next_mission].name);
}
return valid_missions;
}
auto& current = Campaign.missions[branch_source_mission];
if (current.formula < 0) {
return valid_missions;
}
// This helper is intended to enumerate all valid branches from a cond formula.
// If the formula is not cond, return an empty list.
if (get_operator_const(CTEXT(current.formula)) != OP_COND) {
return valid_missions;
}
const int saved_next = Campaign.next_mission;
int clauses = CDR(current.formula);
while (clauses >= 0) {
const int clause = CAR(clauses);
if (clause < 0) {
clauses = CDR(clauses);
continue;
}
flush_sexp_tree(current.formula);
const int condition = CAR(clause);
const int condition_result = eval_sexp(condition);
if (condition_result == SEXP_TRUE) {
Campaign.next_mission = -1;
int actions = CDR(clause);
while (actions >= 0) {
const int exp = CAR(actions);
if (exp >= 0) {
eval_sexp(exp);
}
actions = CDR(actions);
}
if (Campaign.next_mission >= 0 && Campaign.next_mission < Campaign.num_missions && Campaign.next_mission != branch_source_mission) {
const auto& mission_name = Campaign.missions[Campaign.next_mission].name;
if (std::find(valid_missions.begin(), valid_missions.end(), mission_name) == valid_missions.end()) {
valid_missions.emplace_back(mission_name);
}
}
}
clauses = CDR(clauses);
}
Campaign.next_mission = saved_next;
return valid_missions;
}
void mission_campaign_load_failure_popup()
{
if (Campaign_load_failure == 0) {
return;
}
if (Campaign_load_failure == CAMPAIGN_ERROR_CORRUPT) {
popup(PF_TITLE_BIG | PF_TITLE_RED | PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, XSTR("Error!\n\nRequested campaign is corrupt and cannot be loaded.\n\n"
"Please select a different campaign in the Campaign Room.", 1614));
} else if (Campaign_load_failure == CAMPAIGN_ERROR_SEXP_EXHAUSTED) {
popup(PF_TITLE_BIG | PF_TITLE_RED | PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, XSTR("Error!\n\nRequested campaign requires too many SEXPs and cannot be loaded.\n\n"
"Please select a different campaign in the Campaign Room.", 1615));
} else if (Campaign_load_failure == CAMPAIGN_ERROR_MISSING) {
// if it's just the campaign missing, there's another popup to deal with that
;
} else if (Campaign_load_failure == CAMPAIGN_ERROR_SAVEFILE) {
popup(PF_TITLE_BIG | PF_TITLE_RED | PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, XSTR("Error!\n\nThe pilot savefile "
"for this campaign is invalid for the current mod.\n\nPlease select another campaign or switch to the correct "
"mod in order to use this campaign.", 1617));
}
Campaign_load_failure = 0;
}