Files
fs2open.github.com/code/mission/missioncampaign.cpp
Taylor Richards 0e55c36877 update netgame_descript_packet()
(based on PR 2109 by Cyborg17)

 - support compaign descriptions (Mantis 1826)
 - allow sending multiple packets to get entire description
 - update description of selected game on join screen if needed
2020-12-24 09:38:23 -05:00

1890 lines
53 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 "pilotfile/pilotfile.h"
#include "playerman/player.h"
#include "popup/popup.h"
#include "scripting/scripting.h"
#include "ship/ship.h"
#include "starfield/supernova.h"
#include "ui/ui.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;
int Campaign_file_missing;
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
*/
int mission_campaign_get_info(const char *filename, char *name, int *type, int *max_players, char **desc, char **first_mission)
{
int i, success = 0;
char campaign_type[NAME_LENGTH], fname[MAX_FILENAME_LEN];
Assert( name != NULL );
Assert( type != NULL );
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);
*type = -1;
do {
try
{
read_file_text(fname);
reset_parse();
required_string("$Name:");
stuff_string(name, F_NAME, NAME_LENGTH);
if (name == NULL) {
nprintf(("Warning", "No name found for campaign file %s\n", filename));
break;
}
required_string("$Type:");
stuff_string(campaign_type, F_NAME, NAME_LENGTH);
for (i = 0; i < MAX_CAMPAIGN_TYPES; i++) {
if (!stricmp(campaign_type, campaign_types[i])) {
*type = i;
}
}
if (name == NULL) {
Warning(LOCATION, "Invalid campaign type \"%s\"\n", campaign_type);
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 = 1;
}
}
catch (const parse::ParseException& e)
{
mprintf(("MISSIONCAMPAIGN: Unable to parse '%s'! Error message = %s.\n", fname, e.what()));
break;
}
} while (0);
Assert(success);
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)
{
char name[NAME_LENGTH];
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);
if (MC_desc)
Campaign_descs[Num_campaigns] = desc;
Num_campaigns++;
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];
memset(Campaign.ships_allowed, 0, sizeof(Campaign.ships_allowed));
memset(Campaign.weapons_allowed, 0, sizeof(Campaign.weapons_allowed));
if (optional_string("+Starting Ships:")) {
count = stuff_int_list(ship_list, MAX_SHIP_CLASSES, 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 = stuff_int_list(weapon_list, MAX_WEAPON_TYPES, 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, player* pl, int load_savefile, bool reset_stats)
{
int i;
char name[NAME_LENGTH], type[NAME_LENGTH], temp[NAME_LENGTH];
if (campaign_is_ignored(filename)) {
Campaign_file_missing = 1;
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;
}
// read the mission file and get the list of mission filenames
try
{
// 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();
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( filename );
reset_parse();
// copy filename to campaign structure minus the extension
auto len = strlen(filename) - 4;
Assert(len < MAX_FILENAME_LEN);
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);
Campaign.desc = NULL;
if (optional_string("+Description:"))
Campaign.desc = stuff_and_malloc_string(F_MULTITEXT, NULL);
// if the type is multiplayer -- get the number of players
Campaign.num_players = 0;
if ( Campaign.type != CAMPAIGN_TYPE_SINGLE) {
required_string("+Num players:");
stuff_int( &(Campaign.num_players) );
}
// parse flags - Goober5000
Campaign.flags = CF_DEFAULT_VALUE;
if (optional_string("$Flags:"))
{
stuff_int( &(Campaign.flags) );
}
// parse the optional ship/weapon information
mission_campaign_get_sw_info();
// parse the mission file and actually read in the mission stuff
Campaign.num_missions = 0;
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";
}
// 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)
if (optional_string("+Substitute Main Hall:")) {
stuff_string(temp, F_RAW, 32);
// see if this main hall exists
main_hall_defines *mhd = main_hall_get_pointer(temp);
if (mhd != NULL) {
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:") ) {
cm->mission_branch_brief_anim = stuff_and_malloc_string(F_MULTITEXT, NULL);
}
cm->mission_branch_brief_sound = NULL;
if ( optional_string("+Mission Loop Brief Sound:") || optional_string("+Mission Fork Brief Sound:") ) {
cm->mission_branch_brief_sound = stuff_and_malloc_string(F_MULTITEXT, NULL);
}
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;
if (Fred_running) {
cm->num_goals = -1;
cm->num_events = -1;
cm->num_variables = -1;
} else {
cm->num_goals = 0;
cm->num_events = 0;
cm->num_variables = 0;
}
// it's possible to have data already loaded from the pilotfile
// if so free it to avoid memory leaks
if (cm->goals != nullptr) {
vm_free(cm->goals);
cm->goals = nullptr;
}
if (cm->events != nullptr) {
vm_free(cm->events);
cm->events = nullptr;
}
if (cm->variables != nullptr) {
vm_free(cm->variables);
cm->variables = nullptr;
}
Campaign.num_missions++;
}
}
catch (const parse::ParseException& e)
{
mprintf(("Error parsing '%s'\r\nError message = %s.\r\n", filename, e.what()));
Campaign.filename[0] = 0;
Campaign.num_missions = 0;
if (!Fred_running && !(Game_mode & GM_MULTIPLAYER)) {
Campaign_file_missing = 1;
Campaign_load_failure = CAMPAIGN_ERROR_MISSING;
return CAMPAIGN_ERROR_MISSING;
}
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.
Campaign.next_mission = 0;
Campaign.prev_mission = -1;
Campaign.current_mission = -1;
Campaign.loop_mission = CAMPAIGN_LOOP_MISSION_UNINITIALIZED;
Campaign.num_missions_completed = 0;
// 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;
} else {
// make sure we initialize red alert data for the new CSG
red_alert_clear();
// and reset stats when requested
if (reset_stats) {
pl->stats.init();
}
Pilot.save_savefile();
}
}
}
// all is good here, move along
Campaign_file_missing = 0;
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 = 0;
player_loadout_init();
}
/**
* Fill in the root of the campaign save filename
*/
void mission_campaign_savefile_generate_root(char *filename, player *pl)
{
char base[_MAX_FNAME];
Assert ( strlen(Campaign.filename) != 0 ); //-V805
if (pl == NULL) {
Assert((Player_num >= 0) && (Player_num < MAX_PLAYERS));
pl = &Players[Player_num];
}
Assert( pl != NULL );
// build up the filename for the save file. There could be a problem with filename length,
// but this problem can get fixed in several ways -- ignore the problem for now though.
_splitpath( Campaign.filename, NULL, NULL, base, NULL );
Assert ( (strlen(base) + strlen(pl->callsign) + 1) < _MAX_FNAME );
sprintf( filename, NOX("%s.%s."), pl->callsign, base );
}
// 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], base[_MAX_FNAME];
_splitpath( cfilename, NULL, NULL, base, NULL );
if ( Player->flags & PLAYER_FLAGS_IS_MULTI ) {
return; // no such thing as a multiplayer campaign savefile
}
// 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);
}
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 copy_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 reset Campaign.next_mission to proper value
}
Campaign.loop_mission = Campaign.next_mission;
Campaign.next_mission = copy_next_mission;
}
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()
{
char *name;
int cur, i;
cmission *mission_obj;
cur = Campaign.current_mission;
name = Campaign.missions[cur].name;
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
if ( mission_obj->goals != NULL ) {
vm_free( mission_obj->goals );
mission_obj->goals = NULL;
}
mission_obj->num_goals = Num_goals;
if ( mission_obj->num_goals > 0 ) {
mission_obj->goals = (mgoal *)vm_malloc( sizeof(mgoal) * Num_goals );
Assert( mission_obj->goals != NULL );
}
// copy the needed info from the Mission_goal struct to our internal structure
for (i = 0; i < Num_goals; i++ ) {
if (Mission_goals[i].name[0] == '\0') {
char goal_name[NAME_LENGTH];
sprintf(goal_name, NOX("Goal #%d"), i);
strcpy_s( mission_obj->goals[i].name, goal_name);
} else
strcpy_s( mission_obj->goals[i].name, Mission_goals[i].name );
Assert ( Mission_goals[i].satisfied != GOAL_INCOMPLETE ); // should be true or false at this point!!!
mission_obj->goals[i].status = (char)Mission_goals[i].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
if ( mission_obj->events != NULL ) {
vm_free( mission_obj->events );
mission_obj->events = NULL;
}
mission_obj->num_events = Num_mission_events;
if ( mission_obj->num_events > 0 ) {
mission_obj->events = (mevent *)vm_malloc( sizeof(mevent) * Num_mission_events );
Assert( mission_obj->events != NULL );
}
// copy the needed info from the Mission_goal struct to our internal structure
for (i = 0; i < Num_mission_events; i++ ) {
if (Mission_events[i].name[0] == '\0') {
char event_name[NAME_LENGTH];
sprintf(event_name, NOX("Event #%d"), i);
nprintf(("Warning", "Mission goal in mission %s must have a +Name field! using %s for campaign save file\n", mission_obj->name, name));
strcpy_s( mission_obj->events[i].name, event_name);
} else
strcpy_s( mission_obj->events[i].name, Mission_events[i].name );
// 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
if ( Mission_events[i].formula == -1 ) {
if ( Mission_events[i].result )
mission_obj->events[i].status = EVENT_SATISFIED;
else
mission_obj->events[i].status = EVENT_FAILED;
} else
Int3();
}
}
void mission_campaign_store_variables(int persistence_type, bool store_red_alert)
{
int cur, i, j;
cmission *mission_obj;
cur = Campaign.current_mission;
mission_obj = &Campaign.missions[cur];
// handle variables that are saved on mission victory -------------------------------------
if (mission_obj->variables != NULL) {
vm_free( mission_obj->variables );
mission_obj->variables = NULL;
}
int num_mission_variables = sexp_campaign_file_variable_count();
if (num_mission_variables > 0) {
if (store_red_alert) {
for (auto& current_rav : Campaign.red_alert_variables) {
Campaign.persistent_variables.push_back(current_rav);
}
}
for (i = 0; i < sexp_variable_count(); i++) {
if (!(Sexp_variables[i].type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE)) {
if (Sexp_variables[i].type & persistence_type) {
bool add_it = true;
// see if we already have a variable with this name
for (j = 0; j < (int)Campaign.persistent_variables.size(); j++) {
if (!(stricmp(Sexp_variables[i].variable_name, Campaign.persistent_variables[j].variable_name))) {
add_it = false;
Campaign.persistent_variables[j].type = Sexp_variables[i].type;
strcpy_s(Campaign.persistent_variables[j].text, Sexp_variables[i].text);
break;
}
}
// new variable
if (add_it) {
Campaign.persistent_variables.push_back(Sexp_variables[i]);
}
}
}
// we might need to save some eternal variables
else if ((persistence_type & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS) && (Sexp_variables[i].type & persistence_type) && (Sexp_variables[i].type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE)) {
bool add_it = true;
for (j = 0; j < (int)Player->variables.size(); j++) {
if (!(stricmp(Sexp_variables[i].variable_name, Player->variables[j].variable_name))) {
Player->variables[j] = Sexp_variables[i];
add_it = false;
break;
}
}
// if not found then add new entry
if (add_it) {
Player->variables.push_back(Sexp_variables[i]);
}
}
}
}
}
void mission_campaign_store_goals_and_events_and_variables()
{
mission_campaign_store_goals_and_events();
mission_campaign_store_variables(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS);
}
/**
* 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;
}
// Goober5000 - player-persistent variables are handled when the mission is
// over, not necessarily when the mission is accepted
Player->failures_this_session = 0;
// 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
Script_system.RunCondition(CHA_CMISSIONACCEPT);
} 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.
if (mission_obj->goals != NULL) {
vm_free( mission_obj->goals );
mission_obj->goals = NULL;
}
mission_obj->num_goals = 0;
if (mission_obj->events != NULL) {
vm_free( mission_obj->events );
mission_obj->events = NULL;
}
mission_obj->num_events = 0;
// Goober5000
if (mission_obj->variables != NULL) {
vm_free( mission_obj->variables );
mission_obj->variables = NULL;
}
mission_obj->num_variables = 0;
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
}
/**
* 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;
if (Campaign.desc != NULL) {
vm_free(Campaign.desc);
Campaign.desc = NULL;
}
// 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++ ) {
if ( Campaign.missions[i].name != NULL ) {
vm_free(Campaign.missions[i].name);
Campaign.missions[i].name = NULL;
}
if (Campaign.missions[i].notes != NULL) {
vm_free(Campaign.missions[i].notes);
Campaign.missions[i].notes = NULL;
}
if ( Campaign.missions[i].goals != NULL ) {
vm_free ( Campaign.missions[i].goals );
Campaign.missions[i].goals = NULL;
}
if ( Campaign.missions[i].events != NULL ) {
vm_free ( Campaign.missions[i].events );
Campaign.missions[i].events = NULL;
}
// Goober5000
if ( Campaign.missions[i].variables != NULL ) {
vm_free ( Campaign.missions[i].variables );
Campaign.missions[i].variables = NULL;
}
// the next three are strdup'd return values from parselo.cpp - taylor
if (Campaign.missions[i].mission_branch_desc != NULL) {
vm_free(Campaign.missions[i].mission_branch_desc);
Campaign.missions[i].mission_branch_desc = NULL;
}
if (Campaign.missions[i].mission_branch_brief_anim != NULL) {
vm_free(Campaign.missions[i].mission_branch_brief_anim);
Campaign.missions[i].mission_branch_brief_anim = NULL;
}
if (Campaign.missions[i].mission_branch_brief_sound != NULL) {
vm_free(Campaign.missions[i].mission_branch_brief_sound);
Campaign.missions[i].mission_branch_brief_sound = NULL;
}
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].num_goals = 0;
Campaign.missions[i].num_events = 0;
Campaign.missions[i].num_variables = 0; // Goober5000
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].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.flags = 0;
Campaign.num_missions = 0;
Campaign.num_missions_completed = 0;
Campaign.current_mission = -1;
Campaign.next_mission = -1;
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();
}
/**
* 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], goals[MAX_GOALS][NAME_LENGTH];
char events[MAX_MISSION_EVENTS][NAME_LENGTH];
int i, z, event_count, count = 0;
filename = Campaign.missions[num].name;
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);
if (Campaign.missions[num].notes){
vm_free(Campaign.missions[num].notes);
}
Campaign.missions[num].notes = (char *)vm_malloc(strlen(notes) + 1);
strcpy(Campaign.missions[num].notes, notes);
}
}
event_count = 0;
// skip to events section in the mission file. Events come before goals, so we process them first
if (skip_to_string("#Events")) {
while (1) {
if (skip_to_string("$Formula:", "#Goals") != 1){
break;
}
z = skip_to_string("+Name:", "$Formula:");
if (!z){
break;
}
if (z == 1){
stuff_string(events[event_count], F_NAME, NAME_LENGTH);
}
else {
sprintf(events[event_count], NOX("Event #%d"), event_count + 1);
}
event_count++;
if (event_count > MAX_MISSION_EVENTS) {
Warning(LOCATION, "Maximum number of events exceeded (%d)!", MAX_MISSION_EVENTS);
event_count = MAX_MISSION_EVENTS;
break;
}
}
}
count = 0;
if (skip_to_string("#Goals")) {
while (1) {
if (skip_to_string("$Type:", "#End") != 1){
break;
}
z = skip_to_string("+Name:", "$Type:");
if (!z){
break;
}
if (z == 1){
stuff_string(goals[count], F_NAME, NAME_LENGTH);
}
else {
sprintf(goals[count], NOX("Goal #%d"), count + 1);
}
count++;
if (count > MAX_GOALS) {
Warning(LOCATION, "Maximum number of goals exceeded (%d)!", MAX_GOALS);
count = MAX_GOALS;
break;
}
}
}
Campaign.missions[num].num_goals = count;
if (count) {
Campaign.missions[num].goals = (mgoal *)vm_malloc(count * sizeof(mgoal));
Assert(Campaign.missions[num].goals); // make sure we got the memory
memset(Campaign.missions[num].goals, 0, count * sizeof(mgoal));
for (i = 0; i < count; i++){
strcpy_s(Campaign.missions[num].goals[i].name, goals[i]);
}
}
// copy the events
Campaign.missions[num].num_events = event_count;
if (event_count) {
Campaign.missions[num].events = (mevent *)vm_malloc(event_count * sizeof(mevent));
Assert(Campaign.missions[num].events);
memset(Campaign.missions[num].events, 0, event_count * sizeof(mevent));
for (i = 0; i < event_count; i++){
strcpy_s(Campaign.missions[num].events[i].name, events[i]);
}
}
// 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!
cutscene_mark_viewable( filename );
}
/**
* Return the type of campaign of the passed filename
*/
int mission_campaign_parse_is_multi(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);
std::transform(filename_no_ext.begin(), filename_no_ext.end(), filename_no_ext.begin(),
[](char c) { return (char)::tolower(c); });
for (auto &ii: Ignored_campaigns) {
if (ii == filename_no_ext) {
return true;
}
}
return false;
}
// returns 0: loaded, !0: error
int mission_load_up_campaign( player *pl )
{
int rc = -1, idx;
if ( pl == NULL )
pl = Player;
// find best campaign to use:
// 1) last used
// 2) builtin
// 3) anything else
// last used...
if ( strlen(pl->current_campaign) ) {
if (!campaign_is_ignored(pl->current_campaign)) {
return mission_campaign_load(pl->current_campaign, pl);
}
else {
Campaign_file_missing = 1;
}
}
// builtin...
rc = mission_campaign_load(Default_campaign_file_name, pl);
// everything else...
if (rc < 0) {
// 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], 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()
{
// 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_status == SUPERNOVA_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 by default. pass 0 to override that
*/
void mission_campaign_skip_to_next(int start_game)
{
// 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();
// mark mission as skipped
Campaign.missions[Campaign.current_mission].flags |= CMISSION_FLAG_SKIPPED;
// store
mission_campaign_store_goals_and_events_and_variables();
// now set the next mission
mission_campaign_eval_next_mission();
// clear out relevant player vars
Player->failures_this_session = 0;
Player->show_skip_popup = 1;
if (start_game) {
// 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();
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
*/
void mission_campaign_jump_to_mission(const char* name, bool no_skip)
{
int i = 0, mission_num = -1;
char dest_name[64], *p;
// load in the campaign junk
mission_load_up_campaign();
// tack the .fs2 onto the input name
strcpy_s(dest_name, name);
p = strchr(dest_name, '.');
if (p != NULL)
*p = '\0';
strcat_s(dest_name, ".fs2");
// search for our mission
for (i = 0; i < Campaign.num_missions; i++) {
if ((Campaign.missions[i].name != NULL) && !stricmp(Campaign.missions[i].name, dest_name)) {
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
// restart the campaign
mission_campaign_savefile_delete(Campaign.filename);
mission_campaign_load(Campaign.filename);
} else {
for (SCP_vector<ship_info>::iterator 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);
}
}
// Goober5000
void mission_campaign_save_on_close_variables()
{
int i;
// make sure we are actually playing a campaign
if (!(Game_mode & GM_CAMPAIGN_MODE))
return;
// make sure this is a single-player campaign
if (!(Campaign.type == CAMPAIGN_TYPE_SINGLE))
return;
// now save variables
for (i = 0; i < sexp_variable_count(); i++) {
// we only want the on mission close type. On campaign progress type are dealt with elsewhere
if ( !(Sexp_variables[i].type & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE) ) {
continue;
}
bool found = false;
// deal with eternals
if ((Sexp_variables[i].type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE)) {
// check if variable already exists and updated it
for (auto& current_variable : Player->variables) {
if (!(stricmp(Sexp_variables[i].variable_name, current_variable.variable_name))) {
current_variable = Sexp_variables[i];
found = true;
break;
}
}
// if not found then add new entry
if (!found) {
Player->variables.push_back(Sexp_variables[i]);
}
}
}
// store any non-eternal on mission close variables
mission_campaign_store_variables(SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE, false);
}
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;
}