mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
This uses the Discord Rich Presence API for improving the integration of FSO into Discord. At the moment this includes state notifications for being in the main hall, the command and mission briefing and in-mission. When in-mission it also includes the elapsed time of the mission with support for time compression. The details part of the notification includes the campaign name and mission name if possible.
1961 lines
54 KiB
C++
1961 lines
54 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 "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;
|
|
|
|
|
|
bool campaign_is_ignored(const char *filename);
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
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);
|
|
}
|
|
|
|
// 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];
|
|
|
|
// set allowable ships to the SIF_PLAYER_SHIPs
|
|
memset(Campaign.ships_allowed, 0, sizeof(Campaign.ships_allowed));
|
|
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;
|
|
}
|
|
|
|
for (i = 0; i < MAX_WEAPON_TYPES; i++)
|
|
Campaign.weapons_allowed[i] = 1;
|
|
|
|
if (optional_string("+Starting Ships:")) {
|
|
for (i = 0; i < static_cast<int>(Ship_info.size()); i++)
|
|
Campaign.ships_allowed[i] = 0;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (optional_string("+Starting Weapons:")) {
|
|
for (i = 0; i < MAX_WEAPON_TYPES; i++)
|
|
Campaign.weapons_allowed[i] = 0;
|
|
|
|
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++)
|
|
Campaign.weapons_allowed[weapon_list[i]] = 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( 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;
|
|
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;
|
|
}
|
|
|
|
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(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;
|
|
}
|
|
|
|
/**
|
|
* Loads up a freespace campaign given the filename.
|
|
* This routine is used to load up campaigns when a pilot file is loaded.
|
|
* Generally, the filename will probably be the freespace campaign file, but not necessarily.
|
|
*/
|
|
int mission_campaign_load_by_name( char *filename )
|
|
{
|
|
char name[NAME_LENGTH],test[5];
|
|
int type,max_players;
|
|
|
|
// make sure to tack on .fsc on the end if its not there already
|
|
if(filename[0] != '\0'){
|
|
if(strlen(filename) > 4){
|
|
strcpy_s(test,filename+(strlen(filename)-4));
|
|
if(strcmp(test, FS_CAMPAIGN_FILE_EXT)!=0){
|
|
strcat(filename, FS_CAMPAIGN_FILE_EXT);
|
|
}
|
|
} else {
|
|
strcat(filename, FS_CAMPAIGN_FILE_EXT);
|
|
}
|
|
} else {
|
|
Error(LOCATION,"Tried to load campaign file with illegal length/extension!");
|
|
}
|
|
|
|
if (!mission_campaign_get_info(filename, name, &type, &max_players)){
|
|
return -1;
|
|
}
|
|
|
|
Num_campaigns = 0;
|
|
Campaign_file_names[Num_campaigns] = vm_strdup(filename);
|
|
Campaign_names[Num_campaigns] = vm_strdup(name);
|
|
Num_campaigns++;
|
|
mission_campaign_load(filename);
|
|
return 0;
|
|
}
|
|
|
|
int mission_campaign_load_by_name_csfe( char *filename, char *callsign )
|
|
{
|
|
Game_mode |= GM_NORMAL;
|
|
strcpy_s(Player->callsign, callsign);
|
|
return mission_campaign_load_by_name( filename);
|
|
}
|
|
|
|
/*
|
|
* 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 following function always only ever ever ever called by CSFE!!!!!
|
|
*/
|
|
int campaign_savefile_save(char *pname)
|
|
{
|
|
if (Campaign.type == CAMPAIGN_TYPE_SINGLE)
|
|
Game_mode &= ~GM_MULTIPLAYER;
|
|
else
|
|
Game_mode |= GM_MULTIPLAYER;
|
|
|
|
strcpy_s(Player->callsign, pname);
|
|
|
|
return (int)Pilot.save_savefile();
|
|
}
|
|
|
|
|
|
// 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( 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 );
|
|
}
|
|
|
|
void campaign_delete_save( char *cfn, char *pname)
|
|
{
|
|
strcpy_s(Player->callsign, pname);
|
|
mission_campaign_savefile_delete(cfn);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The following code only ever called by CSFE!!!!
|
|
*/
|
|
void campaign_savefile_load(char *fname, char *pname)
|
|
{
|
|
if (Campaign.type == CAMPAIGN_TYPE_SINGLE) {
|
|
Game_mode &= ~GM_MULTIPLAYER;
|
|
Game_mode &= GM_NORMAL;
|
|
}
|
|
else
|
|
Game_mode |= GM_MULTIPLAYER;
|
|
|
|
strcpy_s(Player->callsign, pname);
|
|
|
|
Pilot.load_savefile(fname);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
} 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( 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);
|
|
|
|
for (SCP_vector<SCP_string>::iterator ii = Ignored_campaigns.begin(); ii != Ignored_campaigns.end(); ++ii) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
rc = mission_campaign_load(Default_campaign_file_name, pl);
|
|
|
|
// builtin...
|
|
if (rc < 0) {
|
|
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(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.num_missions_completed = i;
|
|
}
|
|
}
|
|
|
|
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 < Num_weapon_types; 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;
|
|
}
|