mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
1940 lines
57 KiB
C++
1940 lines
57 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 "cmdline/cmdline.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
|
|
};
|
|
|
|
// module-local sets of new ships/weapons granted to the player during the current mission
|
|
static SCP_set<int> Granted_ships;
|
|
static SCP_set<int> Granted_weapons;
|
|
|
|
// 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()
|
|
{
|
|
if (optional_string("+Starting Ships:")) {
|
|
SCP_vector<int> ship_list;
|
|
stuff_int_list(ship_list, ParseLookupType::SHIP_INFO_TYPE);
|
|
|
|
// now note which ships we are allowed
|
|
for (int idx : ship_list) {
|
|
if (Ship_info[idx].flags[Ship::Info_Flags::Player_ship])
|
|
Campaign.ships_allowed.insert(idx);
|
|
}
|
|
} 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.insert(static_cast<int>(std::distance(Ship_info.cbegin(), it)));
|
|
}
|
|
}
|
|
|
|
if (optional_string("+Starting Weapons:")) {
|
|
SCP_vector<int> weapon_list;
|
|
stuff_int_list(weapon_list, ParseLookupType::WEAPON_POOL_TYPE);
|
|
|
|
// now note which weapons we are allowed
|
|
for (int idx : weapon_list) {
|
|
if (Weapon_info[idx].wi_flags[Weapon::Info_Flags::Player_allowed])
|
|
Campaign.weapons_allowed.insert(idx);
|
|
}
|
|
} 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.insert(static_cast<int>(std::distance(Weapon_info.cbegin(), it)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
|
|
Player_loadout.ship_pool.clear();
|
|
Player_loadout.weapon_pool.clear();
|
|
|
|
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 persistent ships and weapons for the next campaign mission
|
|
Granted_ships.clear();
|
|
Granted_weapons.clear();
|
|
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 );
|
|
Granted_ships.clear();
|
|
Granted_weapons.clear();
|
|
|
|
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;
|
|
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
|
|
Campaign.ships_allowed.insert(Granted_ships.begin(), Granted_ships.end());
|
|
Campaign.weapons_allowed.insert(Granted_weapons.begin(), Granted_weapons.end());
|
|
|
|
// 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;
|
|
Campaign.ships_allowed.clear();
|
|
Campaign.weapons_allowed.clear();
|
|
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 ) {
|
|
Granted_ships.insert(sindex);
|
|
} else if ( type == CAMPAIGN_PERSISTENT_WEAPON ) {
|
|
Granted_weapons.insert(sindex);
|
|
} 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 (i = 0; i < ship_info_size(); i++) {
|
|
Campaign.ships_allowed.insert(i);
|
|
}
|
|
for (i = 0; i < weapon_info_size(); i++) {
|
|
Campaign.weapons_allowed.insert(i);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|