mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
Mission flags are saved to the mission file as the full 64-bit flagset value, but three issues prevented anything larger than 32 bits from being saved as intended: * `stuff_flagset` parsed the value with `stuff_long`, where `atol` clamps anything above `LONG_MAX` * `flagset::from_u64` cast the incoming value to `unsigned long`, truncating bits 32-63 * `flagset::to_u64` used `bitset::to_ulong` where it should use `bitset::to_ullong` Fix these issues and add `static_assert` in the flagset functions to defend against future issues. Also use the proper UINT64_T_ARG where it should be used. This was first triggered by `Limited_support_rearm_pool`, the first mission flag to occupy bit 31. Only mission flags are affected; the other flagset serialization paths were audited and are safe. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
5451 lines
149 KiB
C++
5451 lines
149 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 "missionsave.h"
|
|
|
|
#include "globalincs/alphacolors.h"
|
|
#include "globalincs/linklist.h"
|
|
#include "globalincs/version.h"
|
|
|
|
#include "ai/aigoals.h"
|
|
#include "ai/ailua.h"
|
|
#include "asteroid/asteroid.h"
|
|
#include "cfile/cfile.h"
|
|
#include "gamesnd/eventmusic.h"
|
|
#include "hud/hudsquadmsg.h"
|
|
#include "iff_defs/iff_defs.h"
|
|
#include "jumpnode/jumpnode.h"
|
|
#include "lighting/lighting.h"
|
|
#include "lighting/lighting_profiles.h"
|
|
#include "localization/fhash.h"
|
|
#include "localization/localize.h"
|
|
#include "math/bitarray.h"
|
|
#include "math/vecmat.h"
|
|
#include "mission/missionbriefcommon.h"
|
|
#include "mission/missioncampaign.h"
|
|
#include "mission/missiongoals.h"
|
|
#include "mission/missionmessage.h"
|
|
#include "mission/missionparse.h"
|
|
#include "missioneditor/common.h"
|
|
#include "missioneditor/sexp_annotation_model.h"
|
|
#include "missionui/fictionviewer.h"
|
|
#include "missionui/missioncmdbrief.h"
|
|
#include "nebula/neb.h"
|
|
#include "object/objectdock.h"
|
|
#include "object/objectshield.h"
|
|
#include "osapi/osapi.h"
|
|
#include "parse/sexp.h"
|
|
#include "parse/sexp_container.h"
|
|
#include "prop/prop.h"
|
|
#include "sound/ds.h"
|
|
#include "sound/sound.h"
|
|
#include "starfield/nebula.h"
|
|
#include "starfield/starfield.h"
|
|
#include "starfield/starfield_flags.h"
|
|
#include "weapon/weapon.h"
|
|
|
|
#include <freespace.h>
|
|
|
|
#define FRED_ENSURE_PROPERTY_VERSION(property, comments, formatstr, ...) \
|
|
do { \
|
|
if (optional_string_fred(property)) \
|
|
parse_comments(comments); \
|
|
else \
|
|
fout_version("\n" property); \
|
|
fout(formatstr, __VA_ARGS__); \
|
|
} while (false)
|
|
#define FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT(property, comments, expected_version, default, formatstr, value) \
|
|
do { \
|
|
if (value != default) { \
|
|
FRED_ENSURE_PROPERTY_VERSION(property, comments, formatstr, value); \
|
|
} else \
|
|
bypass_comment(expected_version " " property); \
|
|
} while (false)
|
|
#define FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F(property, comments, expected_version, default, formatstr, value) \
|
|
do { \
|
|
if (!fl_equal(value, default)) { \
|
|
FRED_ENSURE_PROPERTY_VERSION(property, comments, formatstr, value); \
|
|
} else \
|
|
bypass_comment(expected_version " " property); \
|
|
} while (false)
|
|
#define FRED_ENSURE_PROPERTY_VERSION_IF(property, comments, expected_version, ifcase, formatstr, ...) \
|
|
do { \
|
|
if (ifcase) { \
|
|
FRED_ENSURE_PROPERTY_VERSION(property, comments, formatstr, __VA_ARGS__); \
|
|
} else \
|
|
bypass_comment(expected_version " " property); \
|
|
} while (false)
|
|
|
|
|
|
int Fred_mission_save::autosave_mission_file(char* pathname)
|
|
{
|
|
char backup_name[256], name2[256];
|
|
int i;
|
|
|
|
auto len = strlen(pathname);
|
|
strcpy_s(backup_name, pathname);
|
|
strcpy_s(name2, pathname);
|
|
sprintf(backup_name + len, ".%.3d", MISSION_BACKUP_DEPTH);
|
|
cf_delete(backup_name, CF_TYPE_MISSIONS);
|
|
for (i = MISSION_BACKUP_DEPTH; i > 1; i--) {
|
|
sprintf(backup_name + len, ".%.3d", i - 1);
|
|
sprintf(name2 + len, ".%.3d", i);
|
|
cf_rename(backup_name, name2, CF_TYPE_MISSIONS);
|
|
}
|
|
|
|
strcpy(backup_name + len, ".001");
|
|
|
|
save_mission_internal(backup_name);
|
|
|
|
return err;
|
|
}
|
|
|
|
void Fred_mission_save::bypass_comment(const char* comment, const char* end)
|
|
{
|
|
char* ch = strstr(raw_ptr, comment);
|
|
if (ch != nullptr) {
|
|
if (end != nullptr) {
|
|
char* ep = strstr(raw_ptr, end);
|
|
if (ep != nullptr && ep < ch)
|
|
return;
|
|
}
|
|
char* writep = ch;
|
|
char* readp = strchr(writep, '\n');
|
|
|
|
// copy all characters past it
|
|
while (*readp != '\0') {
|
|
*writep = *readp;
|
|
|
|
writep++;
|
|
readp++;
|
|
}
|
|
|
|
*writep = '\0';
|
|
}
|
|
}
|
|
|
|
void Fred_mission_save::convert_special_tags_to_retail(char* text, int max_len)
|
|
{
|
|
replace_all(text, "$quote", "''", max_len);
|
|
replace_all(text, "$semicolon", ",", max_len);
|
|
}
|
|
|
|
void Fred_mission_save::convert_special_tags_to_retail(SCP_string& text)
|
|
{
|
|
replace_all(text, "$quote", "''");
|
|
replace_all(text, "$semicolon", ",");
|
|
}
|
|
|
|
void Fred_mission_save::convert_special_tags_to_retail() const
|
|
{
|
|
int i, team, stage;
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL)
|
|
return;
|
|
|
|
for (team = 0; team < Num_teams; team++) {
|
|
// command briefing
|
|
for (stage = 0; stage < Cmd_briefs[team].num_stages; stage++) {
|
|
convert_special_tags_to_retail(Cmd_briefs[team].stage[stage].text);
|
|
}
|
|
|
|
// briefing
|
|
for (stage = 0; stage < Briefings[team].num_stages; stage++) {
|
|
convert_special_tags_to_retail(Briefings[team].stages[stage].text);
|
|
}
|
|
|
|
// debriefing
|
|
for (stage = 0; stage < Debriefings[team].num_stages; stage++) {
|
|
convert_special_tags_to_retail(Debriefings[team].stages[stage].text);
|
|
}
|
|
}
|
|
|
|
for (i = Num_builtin_messages; i < Num_messages; i++) {
|
|
convert_special_tags_to_retail(Messages[i].message, MESSAGE_LENGTH - 1);
|
|
}
|
|
}
|
|
|
|
int Fred_mission_save::fout(const char* format, ...)
|
|
{
|
|
// don't output anything if we're saving in retail and have version-specific comments active
|
|
if (save_config.save_format == MissionFormat::RETAIL && !fso_ver_comment.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
SCP_string str;
|
|
va_list args;
|
|
|
|
if (err) {
|
|
return err;
|
|
}
|
|
|
|
va_start(args, format);
|
|
vsprintf(str, format, args);
|
|
va_end(args);
|
|
|
|
cfputs(str.c_str(), fp);
|
|
return 0;
|
|
}
|
|
|
|
int Fred_mission_save::fout_ext(const char* pre_str, const char* format, ...)
|
|
{
|
|
// don't output anything if we're saving in retail and have version-specific comments active
|
|
if (save_config.save_format == MissionFormat::RETAIL && !fso_ver_comment.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
SCP_string str_scp;
|
|
SCP_string str_out_scp;
|
|
va_list args;
|
|
int str_id;
|
|
|
|
if (err) {
|
|
return err;
|
|
}
|
|
|
|
va_start(args, format);
|
|
vsprintf(str_scp, format, args);
|
|
va_end(args);
|
|
|
|
if (pre_str) {
|
|
str_out_scp = pre_str;
|
|
}
|
|
|
|
// lookup the string in the hash table
|
|
str_id = fhash_string_exists(str_scp.c_str());
|
|
|
|
// doesn't exist, so assign it an ID of -1 and stick it in the table
|
|
if (str_id <= -2) {
|
|
str_out_scp += " XSTR(\"";
|
|
str_out_scp += str_scp;
|
|
str_out_scp += "\", -1)";
|
|
|
|
// add the string to the table
|
|
fhash_add_str(str_scp.c_str(), -1);
|
|
}
|
|
// _does_ exist, so just write it out as it is
|
|
else {
|
|
str_out_scp += " XSTR(\"";
|
|
str_out_scp += str_scp;
|
|
str_out_scp += "\", ";
|
|
str_out_scp += std::to_string(str_id);
|
|
str_out_scp += ")";
|
|
}
|
|
|
|
char* str_out_c = vm_strdup(str_out_scp.c_str());
|
|
|
|
// this could be a multi-line string, so we've got to handle it all properly
|
|
if (!fso_ver_comment.empty()) {
|
|
bool first_line = true;
|
|
char* str_p = str_out_c;
|
|
|
|
char* ch = strchr(str_out_c, '\n');
|
|
|
|
// if we have something, and it's not just at the end, then process it specially
|
|
if ((ch != nullptr) && (*(ch + 1) != '\0')) {
|
|
do {
|
|
if (*(ch + 1) != '\0') {
|
|
*ch = '\0';
|
|
|
|
if (first_line) {
|
|
first_line = false;
|
|
} else {
|
|
cfputs(fso_ver_comment.back().c_str(), fp);
|
|
cfputs(" ", fp);
|
|
}
|
|
|
|
cfputs(str_p, fp);
|
|
cfputc('\n', fp);
|
|
|
|
str_p = ch + 1;
|
|
} else {
|
|
if (first_line) {
|
|
first_line = false;
|
|
} else {
|
|
cfputs(fso_ver_comment.back().c_str(), fp);
|
|
cfputs(" ", fp);
|
|
}
|
|
|
|
cfputs(str_p, fp);
|
|
|
|
str_p = ch + 1;
|
|
|
|
break;
|
|
}
|
|
} while ((ch = strchr(str_p, '\n')) != nullptr);
|
|
|
|
// be sure to account for any ending elements too
|
|
if (strlen(str_p)) {
|
|
cfputs(fso_ver_comment.back().c_str(), fp);
|
|
cfputs(" ", fp);
|
|
cfputs(str_p, fp);
|
|
}
|
|
|
|
vm_free(str_out_c);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
cfputs(str_out_c, fp);
|
|
|
|
vm_free(str_out_c);
|
|
return 0;
|
|
}
|
|
|
|
int Fred_mission_save::fout_version(const char* format, ...)
|
|
{
|
|
SCP_string str_scp;
|
|
char* ch = nullptr;
|
|
va_list args;
|
|
|
|
if (err) {
|
|
return err;
|
|
}
|
|
|
|
// if we're saving in retail format, we can completely skip anything with a version-specific comment
|
|
if (save_config.save_format == MissionFormat::RETAIL && !fso_ver_comment.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
// output the version first thing, but skip the special case where we use
|
|
// fout_version() for multiline value strings (typically indicated by an initial space)
|
|
if ((save_config.save_format == MissionFormat::COMPATIBILITY_MODE) && (*format != ' ') && !fso_ver_comment.empty()) {
|
|
while (*format == '\n') {
|
|
str_scp.append(1, *format);
|
|
format++;
|
|
}
|
|
|
|
str_scp.append(fso_ver_comment.back().c_str());
|
|
str_scp.append(" ");
|
|
|
|
cfputs(str_scp.c_str(), fp);
|
|
|
|
str_scp = "";
|
|
}
|
|
|
|
va_start(args, format);
|
|
vsprintf(str_scp, format, args);
|
|
va_end(args);
|
|
|
|
char* str_c = vm_strdup(str_scp.c_str());
|
|
|
|
// this could be a multi-line string, so we've got to handle it all properly
|
|
if ((save_config.save_format == MissionFormat::COMPATIBILITY_MODE) && !fso_ver_comment.empty()) {
|
|
bool first_line = true;
|
|
char* str_p = str_c;
|
|
|
|
ch = strchr(str_c, '\n');
|
|
|
|
// if we have something, and it's not just at the end, then process it specially
|
|
if ((ch != nullptr) && (*(ch + 1) != '\0')) {
|
|
do {
|
|
if (*(ch + 1) != '\0') {
|
|
*ch = '\0';
|
|
|
|
if (first_line) {
|
|
first_line = false;
|
|
} else {
|
|
cfputs(fso_ver_comment.back().c_str(), fp);
|
|
cfputs(" ", fp);
|
|
}
|
|
|
|
cfputs(str_p, fp);
|
|
cfputc('\n', fp);
|
|
|
|
str_p = ch + 1;
|
|
} else {
|
|
if (first_line) {
|
|
first_line = false;
|
|
} else {
|
|
cfputs(fso_ver_comment.back().c_str(), fp);
|
|
cfputs(" ", fp);
|
|
}
|
|
|
|
cfputs(str_p, fp);
|
|
|
|
str_p = ch + 1;
|
|
|
|
break;
|
|
}
|
|
} while ((ch = strchr(str_p, '\n')) != nullptr);
|
|
|
|
// be sure to account for any ending elements too
|
|
if (strlen(str_p)) {
|
|
cfputs(fso_ver_comment.back().c_str(), fp);
|
|
cfputs(" ", fp);
|
|
cfputs(str_p, fp);
|
|
}
|
|
|
|
vm_free(str_c);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
cfputs(str_c, fp);
|
|
|
|
vm_free(str_c);
|
|
return 0;
|
|
}
|
|
|
|
void Fred_mission_save::fout_raw_comment(const char* comment_start)
|
|
{
|
|
Assertion(comment_start <= raw_ptr, "This function assumes the beginning of the comment precedes the current raw pointer!");
|
|
|
|
// the current character is \n, so either set it to 0, or set the preceding \r (if there is one) to 0
|
|
if (*(raw_ptr - 1) == '\r') {
|
|
*(raw_ptr - 1) = '\0';
|
|
} else {
|
|
*raw_ptr = '\0';
|
|
}
|
|
|
|
// save the comment, which will write all characters up to the 0 we just set
|
|
fout("%s\n", comment_start);
|
|
|
|
// restore the overwritten character
|
|
if (*(raw_ptr - 1) == '\0') {
|
|
*(raw_ptr - 1) = '\r';
|
|
} else {
|
|
*raw_ptr = '\n';
|
|
}
|
|
}
|
|
|
|
void Fred_mission_save::parse_comments(int newlines)
|
|
{
|
|
char* comment_start = nullptr;
|
|
int state = 0, same_line = 0, first_comment = 1, tab = 0, flag = 0;
|
|
bool version_added = false;
|
|
|
|
if (newlines < 0) {
|
|
newlines = -newlines;
|
|
tab = 1;
|
|
}
|
|
|
|
if (newlines)
|
|
same_line = 1;
|
|
|
|
if (fred_parse_flag || !Token_found_flag || !token_found || (token_found && (*Parse_text_raw == '\0'))) {
|
|
while (newlines-- > 0)
|
|
fout("\n");
|
|
|
|
if (tab && token_found)
|
|
fout_version("\t%s", token_found);
|
|
else if (token_found)
|
|
fout_version("%s", token_found);
|
|
else if (tab)
|
|
fout("\t");
|
|
|
|
return;
|
|
}
|
|
|
|
while (*raw_ptr != '\0') {
|
|
// state values (as far as I could figure out):
|
|
// 0 - raw_ptr not inside comment
|
|
// 1 - raw_ptr inside /**/ comment
|
|
// 2 - raw_ptr inside ; (newline-delimited) comment
|
|
// 3,4 - raw_ptr inside ;; (FSO version) comment
|
|
if (!state) {
|
|
if (token_found && (*raw_ptr == *token_found))
|
|
if (!strnicmp(raw_ptr, token_found, strlen(token_found))) {
|
|
same_line = newlines - 1 + same_line;
|
|
while (same_line-- > 0)
|
|
fout("\n");
|
|
|
|
if (tab) {
|
|
fout_version("\t");
|
|
fout("%s", token_found);
|
|
} else {
|
|
fout_version("%s", token_found);
|
|
}
|
|
|
|
// If you have a bunch of lines that all start with the same token (like, say, "+Subsystem:"),
|
|
// this makes it so it won't just repeatedly match the first one. -MageKing17
|
|
raw_ptr++;
|
|
|
|
if (version_added)
|
|
fso_comment_pop();
|
|
|
|
return;
|
|
}
|
|
|
|
if ((*raw_ptr == '/') && (raw_ptr[1] == '*')) {
|
|
comment_start = raw_ptr;
|
|
state = 1;
|
|
}
|
|
|
|
if ((*raw_ptr == ';') && (raw_ptr[1] != '!')) {
|
|
comment_start = raw_ptr;
|
|
state = 2;
|
|
|
|
// check for a FSO version comment, but if we can't understand it then
|
|
// just handle it as a regular comment
|
|
if ((raw_ptr[1] == ';') && (raw_ptr[2] == 'F') && (raw_ptr[3] == 'S') && (raw_ptr[4] == 'O')) {
|
|
int major, minor, build, revis;
|
|
int s_num = scan_fso_version_string(raw_ptr, &major, &minor, &build, &revis);
|
|
|
|
// hack for releases
|
|
if (FS_VERSION_REVIS < 1000) {
|
|
s_num = 3;
|
|
}
|
|
|
|
if ((s_num == 3) && ((major < FS_VERSION_MAJOR) || ((major == FS_VERSION_MAJOR) && ((minor < FS_VERSION_MINOR) || ((minor == FS_VERSION_MINOR) && (build <= FS_VERSION_BUILD)))))) {
|
|
state = 3;
|
|
} else if ((s_num == 4) && ((major < FS_VERSION_MAJOR) || ((major == FS_VERSION_MAJOR) && ((minor < FS_VERSION_MINOR) || ((minor == FS_VERSION_MINOR) && ((build < FS_VERSION_BUILD) || ((build == FS_VERSION_BUILD) && (revis <= FS_VERSION_REVIS)))))))) {
|
|
state = 3;
|
|
} else {
|
|
state = 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (*raw_ptr == '\n')
|
|
flag = 1;
|
|
|
|
if (flag && state && !(state == 3))
|
|
fout("\n");
|
|
|
|
} else {
|
|
if (*raw_ptr == '\n') {
|
|
if (state == 2) {
|
|
if (first_comment && !flag)
|
|
fout("\t\t");
|
|
fout_raw_comment(comment_start);
|
|
|
|
state = first_comment = same_line = flag = 0;
|
|
} else if (state == 4) {
|
|
same_line = newlines - 2 + same_line;
|
|
while (same_line-- > 0)
|
|
fout("\n");
|
|
fout_raw_comment(comment_start);
|
|
|
|
state = first_comment = same_line = flag = 0;
|
|
}
|
|
}
|
|
|
|
if ((*raw_ptr == '*') && (raw_ptr[1] == '/') && (state == 1)) {
|
|
if (first_comment && !flag)
|
|
fout("\t\t");
|
|
|
|
const char tmp = raw_ptr[2];
|
|
raw_ptr[2] = 0;
|
|
fout("%s", comment_start);
|
|
raw_ptr[2] = tmp;
|
|
state = first_comment = flag = 0;
|
|
}
|
|
|
|
if ((*raw_ptr == ';') && (raw_ptr[1] == ';') && (state == 3)) {
|
|
const char tmp = raw_ptr[2];
|
|
raw_ptr[2] = 0;
|
|
if (version_added)
|
|
fso_comment_pop();
|
|
else
|
|
version_added = true;
|
|
fso_comment_push(comment_start);
|
|
raw_ptr[2] = tmp;
|
|
state = first_comment = flag = 0;
|
|
raw_ptr++;
|
|
}
|
|
}
|
|
|
|
raw_ptr++;
|
|
}
|
|
|
|
if (version_added)
|
|
fso_comment_pop();
|
|
|
|
return;
|
|
}
|
|
|
|
void Fred_mission_save::save_ai_goals(ai_goal* goalp, int ship)
|
|
{
|
|
const char* str = nullptr;
|
|
char buf[80];
|
|
int i, valid, flag = 1;
|
|
|
|
for (i = 0; i < MAX_AI_GOALS; i++) {
|
|
if (goalp[i].ai_mode == AI_GOAL_NONE)
|
|
continue;
|
|
|
|
if (flag) {
|
|
if (optional_string_fred("$AI Goals:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$AI Goals:");
|
|
|
|
fout(" ( goals ");
|
|
flag = 0;
|
|
}
|
|
|
|
if (goalp[i].ai_mode == AI_GOAL_CHASE_ANY) {
|
|
fout("( ai-chase-any %d ) ", goalp[i].priority);
|
|
|
|
} else if (goalp[i].ai_mode == AI_GOAL_UNDOCK) {
|
|
fout("( ai-undock %d ) ", goalp[i].priority);
|
|
|
|
} else if (goalp[i].ai_mode == AI_GOAL_KEEP_SAFE_DISTANCE) {
|
|
fout("( ai-keep-safe-distance %d ) ", goalp[i].priority);
|
|
|
|
} else if (goalp[i].ai_mode == AI_GOAL_PLAY_DEAD) {
|
|
fout("( ai-play-dead %d ) ", goalp[i].priority);
|
|
|
|
} else if (goalp[i].ai_mode == AI_GOAL_PLAY_DEAD_PERSISTENT) {
|
|
fout("( ai-play-dead-persistent %d ) ", goalp[i].priority);
|
|
|
|
} else if (goalp[i].ai_mode == AI_GOAL_WARP) {
|
|
fout("( ai-warp-out %d ) ", goalp[i].priority);
|
|
|
|
} else {
|
|
valid = 1;
|
|
if (!goalp[i].target_name) {
|
|
Warning(LOCATION, "Ai goal has no target where one is required");
|
|
|
|
} else {
|
|
sprintf(buf, "\"%s\"", goalp[i].target_name);
|
|
switch (goalp[i].ai_mode) {
|
|
case AI_GOAL_WAYPOINTS:
|
|
str = "ai-waypoints";
|
|
break;
|
|
|
|
case AI_GOAL_WAYPOINTS_ONCE:
|
|
str = "ai-waypoints-once";
|
|
break;
|
|
|
|
case AI_GOAL_DESTROY_SUBSYSTEM:
|
|
if (goalp[i].docker.index == -1 || !goalp[i].docker.index) {
|
|
valid = 0;
|
|
Warning(LOCATION, "AI destroy subsystem goal invalid subsystem name\n");
|
|
|
|
} else {
|
|
sprintf(buf, "\"%s\" \"%s\"", goalp[i].target_name, goalp[i].docker.name);
|
|
str = "ai-destroy-subsystem";
|
|
}
|
|
|
|
break;
|
|
|
|
case AI_GOAL_DOCK:
|
|
if (ship < 0) {
|
|
valid = 0;
|
|
Warning(LOCATION, "Wings aren't allowed to have a docking goal\n");
|
|
|
|
} else if (goalp[i].docker.index == -1 || !goalp[i].docker.index) {
|
|
valid = 0;
|
|
Warning(LOCATION,
|
|
"AI dock goal for \"%s\" has invalid docker point "
|
|
"(docking with \"%s\")\n",
|
|
Ships[ship].ship_name,
|
|
goalp[i].target_name);
|
|
|
|
} else if (goalp[i].dockee.index == -1 || !goalp[i].dockee.index) {
|
|
valid = 0;
|
|
Warning(LOCATION,
|
|
"AI dock goal for \"%s\" has invalid dockee point "
|
|
"(docking with \"%s\")\n",
|
|
Ships[ship].ship_name,
|
|
goalp[i].target_name);
|
|
|
|
} else {
|
|
sprintf(buf,
|
|
"\"%s\" \"%s\" \"%s\"",
|
|
goalp[i].target_name,
|
|
goalp[i].docker.name,
|
|
goalp[i].dockee.name);
|
|
|
|
str = "ai-dock";
|
|
}
|
|
break;
|
|
|
|
case AI_GOAL_CHASE:
|
|
str = "ai-chase";
|
|
break;
|
|
|
|
case AI_GOAL_CHASE_WING:
|
|
str = "ai-chase-wing";
|
|
break;
|
|
|
|
case AI_GOAL_CHASE_SHIP_CLASS:
|
|
str = "ai-chase-ship-class";
|
|
break;
|
|
|
|
case AI_GOAL_CHASE_SHIP_TYPE:
|
|
str = "ai-chase-ship-type";
|
|
break;
|
|
|
|
case AI_GOAL_GUARD:
|
|
str = "ai-guard";
|
|
break;
|
|
|
|
case AI_GOAL_GUARD_WING:
|
|
str = "ai-guard-wing";
|
|
break;
|
|
|
|
case AI_GOAL_DISABLE_SHIP:
|
|
str = "ai-disable-ship";
|
|
break;
|
|
|
|
case AI_GOAL_DISABLE_SHIP_TACTICAL:
|
|
str = "ai-disable-ship-tactical";
|
|
break;
|
|
|
|
case AI_GOAL_DISARM_SHIP:
|
|
str = "ai-disarm-ship";
|
|
break;
|
|
|
|
case AI_GOAL_DISARM_SHIP_TACTICAL:
|
|
str = "ai-disarm-ship-tactical";
|
|
break;
|
|
|
|
case AI_GOAL_IGNORE:
|
|
str = "ai-ignore";
|
|
break;
|
|
|
|
case AI_GOAL_IGNORE_NEW:
|
|
str = "ai-ignore-new";
|
|
break;
|
|
|
|
case AI_GOAL_EVADE_SHIP:
|
|
str = "ai-evade-ship";
|
|
break;
|
|
|
|
case AI_GOAL_STAY_NEAR_SHIP:
|
|
str = "ai-stay-near-ship";
|
|
break;
|
|
|
|
case AI_GOAL_FORM_ON_WING:
|
|
str = "ai-form-on-wing";
|
|
break;
|
|
|
|
case AI_GOAL_STAY_STILL:
|
|
str = "ai-stay-still";
|
|
break;
|
|
|
|
case AI_GOAL_REARM_REPAIR:
|
|
str = "ai-rearm-repair";
|
|
break;
|
|
|
|
case AI_GOAL_FLY_TO_SHIP:
|
|
str = "ai-fly-to-ship";
|
|
break;
|
|
|
|
default:
|
|
UNREACHABLE("Goal %d not handled!", goalp[i].ai_mode);
|
|
valid = 0;
|
|
break;
|
|
}
|
|
|
|
if (valid)
|
|
fout("( %s %s %d ) ", str, buf, goalp[i].priority);
|
|
}
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
if (!flag)
|
|
fout(")");
|
|
|
|
fso_comment_pop(true);
|
|
}
|
|
|
|
int Fred_mission_save::save_asteroid_fields()
|
|
{
|
|
int i;
|
|
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Asteroid Fields");
|
|
parse_comments(2);
|
|
|
|
for (i = 0; i < 1 /*MAX_ASTEROID_FIELDS*/; i++) {
|
|
if (!Asteroid_field.num_initial_asteroids)
|
|
continue;
|
|
|
|
required_string_fred("$Density:");
|
|
parse_comments(2);
|
|
fout(" %d", Asteroid_field.num_initial_asteroids);
|
|
|
|
// field type
|
|
if (optional_string_fred("+Field Type:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Field Type:");
|
|
}
|
|
fout(" %d", Asteroid_field.field_type);
|
|
|
|
// debris type
|
|
if (optional_string_fred("+Debris Genre:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Debris Genre:");
|
|
}
|
|
fout(" %d", Asteroid_field.debris_genre);
|
|
|
|
// field_debris_type (only if debris genre)
|
|
if (Asteroid_field.debris_genre == DG_DEBRIS) {
|
|
for (size_t idx = 0; idx < Asteroid_field.field_debris_type.size(); idx++) {
|
|
if (Asteroid_field.field_debris_type[idx] != -1) {
|
|
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
if (idx < MAX_RETAIL_DEBRIS_TYPES) { // Retail can only have 3!
|
|
if (optional_string_fred("+Field Debris Type:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Field Debris Type:");
|
|
}
|
|
fout(" %d", Asteroid_field.field_debris_type[idx]);
|
|
}
|
|
} else {
|
|
if (optional_string_fred("+Field Debris Type Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Field Debris Type Name:");
|
|
}
|
|
fout(" %s", Asteroid_info[Asteroid_field.field_debris_type[idx]].name);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (size_t idx = 0; idx < Asteroid_field.field_asteroid_type.size(); idx++) {
|
|
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
if (idx < MAX_RETAIL_DEBRIS_TYPES) { // Retail can only have 3!
|
|
if (optional_string_fred("+Field Debris Type:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Field Debris Type:");
|
|
}
|
|
fout(" %d", idx);
|
|
}
|
|
} else {
|
|
if (optional_string_fred("+Field Debris Type Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Field Debris Type Name:");
|
|
}
|
|
fout(" %s", Asteroid_field.field_asteroid_type[idx].c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
required_string_fred("$Average Speed:");
|
|
parse_comments();
|
|
fout(" %f", vm_vec_mag(&Asteroid_field.vel));
|
|
|
|
required_string_fred("$Minimum:");
|
|
parse_comments();
|
|
save_vector(Asteroid_field.min_bound);
|
|
|
|
required_string_fred("$Maximum:");
|
|
parse_comments();
|
|
save_vector(Asteroid_field.max_bound);
|
|
|
|
if (Asteroid_field.has_inner_bound) {
|
|
if (optional_string_fred("+Inner Bound:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Inner Bound:");
|
|
}
|
|
|
|
required_string_fred("$Minimum:");
|
|
parse_comments();
|
|
save_vector(Asteroid_field.inner_min_bound);
|
|
|
|
required_string_fred("$Maximum:");
|
|
parse_comments();
|
|
save_vector(Asteroid_field.inner_max_bound);
|
|
}
|
|
|
|
if (Asteroid_field.enhanced_visibility_checks) {
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (optional_string_fred("+Use Enhanced Checks")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Use Enhanced Checks");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Asteroid_field.target_names.empty()) {
|
|
fso_comment_push(";;FSO 22.0.0;;");
|
|
if (optional_string_fred("$Asteroid Targets:")) {
|
|
parse_comments();
|
|
fout(" (");
|
|
} else {
|
|
fout_version("\n$Asteroid Targets: (");
|
|
}
|
|
|
|
for (SCP_string& name : Asteroid_field.target_names) {
|
|
fout(" \"%s\"", name.c_str());
|
|
}
|
|
|
|
fout(" )");
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_bitmaps()
|
|
{
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Background bitmaps");
|
|
parse_comments(2);
|
|
fout("\t\t;! %d total\n", stars_get_num_bitmaps());
|
|
|
|
required_string_fred("$Num stars:");
|
|
parse_comments();
|
|
fout(" %d", Num_stars);
|
|
|
|
required_string_fred("$Ambient light level:");
|
|
parse_comments();
|
|
fout(" %d", The_mission.ambient_light_level);
|
|
|
|
// neb2 stuff
|
|
if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
|
|
fout("\n");
|
|
required_string_fred("+Neb2:");
|
|
parse_comments();
|
|
fout(" %s", Neb2_texture_name);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL &&
|
|
The_mission.flags[Mission::Mission_Flags::Neb2_fog_color_override]) {
|
|
if (optional_string_fred("+Neb2Color:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Neb2Color:");
|
|
}
|
|
fout(" (");
|
|
for (auto c : Neb2_fog_color) {
|
|
fout(" %d", c);
|
|
}
|
|
fout(" )");
|
|
}
|
|
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
if (optional_string_fred("+Neb2Flags:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Neb2Flags:");
|
|
}
|
|
int flags = bit_array_as_int(Neb2_poof_flags.get(), Poof_info.size());
|
|
fout(" %d", flags);
|
|
} else {
|
|
if (optional_string_fred("+Neb2 Poofs List:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Neb2 Poofs List:");
|
|
}
|
|
fout(" (");
|
|
for (size_t i = 0; i < Poof_info.size(); ++i) {
|
|
if (get_bit(Neb2_poof_flags.get(), i)) {
|
|
fout(" \"%s\" ", Poof_info[i].name);
|
|
}
|
|
}
|
|
fout(") ");
|
|
}
|
|
}
|
|
// neb 1 stuff
|
|
else {
|
|
if (Nebula_index >= 0) {
|
|
if (optional_string_fred("+Nebula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Nebula:");
|
|
}
|
|
fout(" %s", Nebula_filenames[Nebula_index]);
|
|
|
|
required_string_fred("+Color:");
|
|
parse_comments();
|
|
fout(" %s", Nebula_colors[Mission_palette]);
|
|
|
|
required_string_fred("+Pitch:");
|
|
parse_comments();
|
|
fout(" %d", Nebula_pitch);
|
|
|
|
required_string_fred("+Bank:");
|
|
parse_comments();
|
|
fout(" %d", Nebula_bank);
|
|
|
|
required_string_fred("+Heading:");
|
|
parse_comments();
|
|
fout(" %d", Nebula_heading);
|
|
}
|
|
}
|
|
|
|
fso_comment_pop();
|
|
|
|
// Goober5000 - save all but the lowest priority using the special comment tag
|
|
for (size_t i = 0; i < Backgrounds.size(); i++) {
|
|
bool tag = (i < Backgrounds.size() - 1);
|
|
background_t* background = &Backgrounds[i];
|
|
|
|
// each background should be preceded by this line so that the suns/bitmaps are partitioned correctly
|
|
fso_comment_push(";;FSO 3.6.9;;");
|
|
if (optional_string_fred("$Bitmap List:")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout_version("\n\n$Bitmap List:");
|
|
}
|
|
|
|
if (!tag) {
|
|
fso_comment_pop(true);
|
|
}
|
|
|
|
// save our flags
|
|
if (save_config.save_format == MissionFormat::STANDARD && background->flags.any_set()) {
|
|
if (optional_string_fred("+Flags:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Flags:");
|
|
}
|
|
fout(" (");
|
|
if (background->flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]) {
|
|
fout(" \"corrected angles\"");
|
|
}
|
|
fout(" )");
|
|
}
|
|
|
|
// save suns by filename
|
|
for (size_t j = 0; j < background->suns.size(); j++) {
|
|
starfield_list_entry* sle = &background->suns[j];
|
|
|
|
// filename
|
|
required_string_fred("$Sun:");
|
|
parse_comments();
|
|
fout(" %s", sle->filename);
|
|
|
|
// angles
|
|
required_string_fred("+Angles:");
|
|
parse_comments();
|
|
angles ang = sle->ang;
|
|
if (save_config.save_format != MissionFormat::STANDARD ||
|
|
!background->flags[Starfield::Background_Flags::Corrected_angles_in_mission_file])
|
|
stars_uncorrect_background_sun_angles(&ang);
|
|
fout(" %f %f %f", ang.p, ang.b, ang.h);
|
|
|
|
// scale
|
|
required_string_fred("+Scale:");
|
|
parse_comments();
|
|
fout(" %f", sle->scale_x);
|
|
}
|
|
|
|
// save background bitmaps by filename
|
|
for (size_t j = 0; j < background->bitmaps.size(); j++) {
|
|
starfield_list_entry* sle = &background->bitmaps[j];
|
|
|
|
// filename
|
|
required_string_fred("$Starbitmap:");
|
|
parse_comments();
|
|
fout(" %s", sle->filename);
|
|
|
|
// angles
|
|
required_string_fred("+Angles:");
|
|
parse_comments();
|
|
angles ang = sle->ang;
|
|
if (save_config.save_format != MissionFormat::STANDARD ||
|
|
!background->flags[Starfield::Background_Flags::Corrected_angles_in_mission_file])
|
|
stars_uncorrect_background_bitmap_angles(&ang);
|
|
fout(" %f %f %f", ang.p, ang.b, ang.h);
|
|
|
|
// scale
|
|
required_string_fred("+ScaleX:");
|
|
parse_comments();
|
|
fout(" %f", sle->scale_x);
|
|
required_string_fred("+ScaleY:");
|
|
parse_comments();
|
|
fout(" %f", sle->scale_y);
|
|
|
|
// div
|
|
required_string_fred("+DivX:");
|
|
parse_comments();
|
|
fout(" %d", sle->div_x);
|
|
required_string_fred("+DivY:");
|
|
parse_comments();
|
|
fout(" %d", sle->div_y);
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
// taylor's environment map thingy
|
|
if (strlen(The_mission.envmap_name) > 0) { //-V805
|
|
fso_comment_push(";;FSO 3.6.9;;");
|
|
if (optional_string_fred("$Environment Map:")) {
|
|
parse_comments(2);
|
|
fout(" %s", The_mission.envmap_name);
|
|
} else {
|
|
fout_version("\n\n$Environment Map: %s", The_mission.envmap_name);
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.9;; $Environment Map:");
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_briefing()
|
|
{
|
|
int i, j, k, nb;
|
|
SCP_string sexp_out;
|
|
brief_stage* bs;
|
|
brief_icon* bi;
|
|
|
|
for (nb = 0; nb < Num_teams; nb++) {
|
|
|
|
required_string_fred("#Briefing");
|
|
parse_comments(2);
|
|
|
|
required_string_fred("$start_briefing");
|
|
parse_comments();
|
|
|
|
save_custom_bitmap("$briefing_background_640:",
|
|
"$briefing_background_1024:",
|
|
Briefings[nb].background[GR_640],
|
|
Briefings[nb].background[GR_1024]);
|
|
save_custom_bitmap("$ship_select_background_640:",
|
|
"$ship_select_background_1024:",
|
|
Briefings[nb].ship_select_background[GR_640],
|
|
Briefings[nb].ship_select_background[GR_1024]);
|
|
save_custom_bitmap("$weapon_select_background_640:",
|
|
"$weapon_select_background_1024:",
|
|
Briefings[nb].weapon_select_background[GR_640],
|
|
Briefings[nb].weapon_select_background[GR_1024]);
|
|
|
|
Assert(Briefings[nb].num_stages <= MAX_BRIEF_STAGES);
|
|
required_string_fred("$num_stages:");
|
|
parse_comments();
|
|
fout(" %d", Briefings[nb].num_stages);
|
|
|
|
for (i = 0; i < Briefings[nb].num_stages; i++) {
|
|
bs = &Briefings[nb].stages[i];
|
|
|
|
required_string_fred("$start_stage");
|
|
parse_comments();
|
|
|
|
required_string_fred("$multi_text");
|
|
parse_comments();
|
|
|
|
// XSTR
|
|
fout_ext("\n", "%s", bs->text.c_str());
|
|
|
|
required_string_fred("$end_multi_text", "$start_stage");
|
|
parse_comments();
|
|
|
|
if (!drop_white_space(bs->voice)[0]) {
|
|
strcpy_s(bs->voice, "None");
|
|
}
|
|
|
|
required_string_fred("$voice:");
|
|
parse_comments();
|
|
fout(" %s", bs->voice);
|
|
|
|
required_string_fred("$camera_pos:");
|
|
parse_comments();
|
|
save_vector(bs->camera_pos);
|
|
|
|
required_string_fred("$camera_orient:");
|
|
parse_comments();
|
|
save_matrix(bs->camera_orient);
|
|
|
|
required_string_fred("$camera_time:");
|
|
parse_comments();
|
|
fout(" %d", bs->camera_time);
|
|
|
|
if (!bs->draw_grid) {
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
fout("\n$no_grid");
|
|
}
|
|
}
|
|
|
|
if (!gr_compare_color_values(bs->grid_color, Color_briefing_grid)) {
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
fout("\n$grid_color:");
|
|
fout("(%d, %d, %d, %d)",
|
|
bs->grid_color.red,
|
|
bs->grid_color.green,
|
|
bs->grid_color.blue,
|
|
bs->grid_color.alpha);
|
|
}
|
|
}
|
|
|
|
required_string_fred("$num_lines:");
|
|
parse_comments();
|
|
fout(" %d", bs->num_lines);
|
|
|
|
for (k = 0; k < bs->num_lines; k++) {
|
|
required_string_fred("$line_start:");
|
|
parse_comments();
|
|
fout(" %d", bs->lines[k].start_icon);
|
|
|
|
required_string_fred("$line_end:");
|
|
parse_comments();
|
|
fout(" %d", bs->lines[k].end_icon);
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
required_string_fred("$num_icons:");
|
|
parse_comments();
|
|
Assert(bs->num_icons <= MAX_STAGE_ICONS);
|
|
fout(" %d", bs->num_icons);
|
|
|
|
required_string_fred("$Flags:");
|
|
parse_comments();
|
|
fout(" %d", bs->flags);
|
|
|
|
required_string_fred("$Formula:");
|
|
parse_comments();
|
|
convert_sexp_to_string(sexp_out, bs->formula, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
for (j = 0; j < bs->num_icons; j++) {
|
|
bi = &bs->icons[j];
|
|
|
|
required_string_fred("$start_icon");
|
|
parse_comments();
|
|
|
|
required_string_fred("$type:");
|
|
parse_comments();
|
|
fout(" %d", bi->type);
|
|
|
|
required_string_fred("$team:");
|
|
parse_comments();
|
|
fout(" %s", Iff_info[bi->team].iff_name);
|
|
|
|
required_string_fred("$class:");
|
|
parse_comments();
|
|
if (bi->ship_class < 0)
|
|
bi->ship_class = 0;
|
|
|
|
fout(" %s", Ship_info[bi->ship_class].name);
|
|
|
|
required_string_fred("$pos:");
|
|
parse_comments();
|
|
save_vector(bi->pos);
|
|
|
|
if (drop_white_space(bi->label)[0]) {
|
|
if (optional_string_fred("$label:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$label:");
|
|
|
|
fout_ext(" ", "%s", bi->label);
|
|
}
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (drop_white_space(bi->closeup_label)[0]) {
|
|
if (optional_string_fred("$closeup label:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$closeup label:");
|
|
|
|
fout_ext(" ", "%s", bi->closeup_label);
|
|
}
|
|
}
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL && bi->scale_factor != 1.0f) {
|
|
if (optional_string_fred("$icon scale:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$icon scale:");
|
|
|
|
fout(" %d", static_cast<int>(bi->scale_factor * 100.0f));
|
|
}
|
|
|
|
if (optional_string_fred("+id:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+id:");
|
|
|
|
fout(" %d", bi->id);
|
|
|
|
required_string_fred("$hlight:");
|
|
parse_comments();
|
|
fout(" %d", (bi->flags & BI_HIGHLIGHT) ? 1 : 0);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (optional_string_fred("$mirror:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$mirror:");
|
|
|
|
fout(" %d", (bi->flags & BI_MIRROR_ICON) ? 1 : 0);
|
|
}
|
|
|
|
if ((save_config.save_format != MissionFormat::RETAIL) && (bi->flags & BI_USE_WING_ICON)) {
|
|
if (optional_string_fred("$use wing icon:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$use wing icon:");
|
|
|
|
fout(" %d", (bi->flags & BI_USE_WING_ICON) ? 1 : 0);
|
|
}
|
|
|
|
if ((save_config.save_format != MissionFormat::RETAIL) && (bi->flags & BI_USE_CARGO_ICON)) {
|
|
if (optional_string_fred("$use cargo icon:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$use cargo icon:");
|
|
|
|
fout(" %d", (bi->flags & BI_USE_CARGO_ICON) ? 1 : 0);
|
|
}
|
|
|
|
required_string_fred("$multi_text");
|
|
parse_comments();
|
|
|
|
// sprintf(out,"\n%s", bi->text);
|
|
// fout(out);
|
|
|
|
required_string_fred("$end_multi_text");
|
|
parse_comments();
|
|
|
|
required_string_fred("$end_icon");
|
|
parse_comments();
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
required_string_fred("$end_stage");
|
|
parse_comments();
|
|
|
|
fso_comment_pop();
|
|
}
|
|
required_string_fred("$end_briefing");
|
|
parse_comments();
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_cmd_brief()
|
|
{
|
|
int stage;
|
|
|
|
stage = 0;
|
|
required_string_fred("#Command Briefing");
|
|
parse_comments(2);
|
|
|
|
save_custom_bitmap("$Background 640:",
|
|
"$Background 1024:",
|
|
Cur_cmd_brief->background[GR_640],
|
|
Cur_cmd_brief->background[GR_1024],
|
|
1);
|
|
|
|
for (stage = 0; stage < Cur_cmd_brief->num_stages; stage++) {
|
|
required_string_fred("$Stage Text:");
|
|
parse_comments(2);
|
|
|
|
// XSTR
|
|
fout_ext("\n", "%s", Cur_cmd_brief->stage[stage].text.c_str());
|
|
|
|
required_string_fred("$end_multi_text", "$Stage Text:");
|
|
parse_comments();
|
|
|
|
if (!drop_white_space(Cur_cmd_brief->stage[stage].ani_filename)[0]) {
|
|
strcpy_s(Cur_cmd_brief->stage[stage].ani_filename, "<default>");
|
|
}
|
|
|
|
required_string_fred("$Ani Filename:");
|
|
parse_comments();
|
|
fout(" %s", Cur_cmd_brief->stage[stage].ani_filename);
|
|
|
|
if (!drop_white_space(Cur_cmd_brief->stage[stage].wave_filename)[0]) {
|
|
strcpy_s(Cur_cmd_brief->stage[stage].wave_filename, "None");
|
|
}
|
|
|
|
required_string_fred("+Wave Filename:", "$Ani Filename:");
|
|
parse_comments();
|
|
fout(" %s", Cur_cmd_brief->stage[stage].wave_filename);
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_cmd_briefs()
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < Num_teams; i++) {
|
|
Cur_cmd_brief = &Cmd_briefs[i];
|
|
save_cmd_brief();
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
void Fred_mission_save::fso_comment_push(const char* ver)
|
|
{
|
|
if (fso_ver_comment.empty()) {
|
|
fso_ver_comment.push_back(SCP_string(ver));
|
|
return;
|
|
}
|
|
|
|
const auto &before = fso_ver_comment.back();
|
|
|
|
int major, minor, build, revis;
|
|
int in_major, in_minor, in_build, in_revis;
|
|
int elem1, elem2;
|
|
|
|
elem1 = scan_fso_version_string(before.c_str(), &major, &minor, &build, &revis);
|
|
elem2 = scan_fso_version_string(ver, &in_major, &in_minor, &in_build, &in_revis);
|
|
|
|
// check consistency
|
|
if ((elem1 == 3 && elem2 == 4) || (elem1 == 4 && elem2 == 3)) {
|
|
elem1 = elem2 = 3;
|
|
} else if ((elem1 >= 3 && elem2 >= 3) && (revis < 1000 || in_revis < 1000)) {
|
|
elem1 = elem2 = 3;
|
|
}
|
|
|
|
if ((elem1 == 3) &&
|
|
((major > in_major) ||
|
|
((major == in_major) && ((minor > in_minor) || ((minor == in_minor) && (build > in_build)))))) {
|
|
// the push'd version is older than our current version, so just push a copy of the previous version
|
|
fso_ver_comment.push_back(before);
|
|
} else if ((elem1 == 4) && ((major > in_major) ||
|
|
((major == in_major) &&
|
|
((minor > in_minor) || ((minor == in_minor) &&
|
|
((build > in_build) || ((build == in_build) ||
|
|
(revis > in_revis)))))))) {
|
|
// the push'd version is older than our current version, so just push a copy of the previous version
|
|
fso_ver_comment.push_back(before);
|
|
} else {
|
|
fso_ver_comment.push_back(SCP_string(ver));
|
|
}
|
|
}
|
|
|
|
void Fred_mission_save::fso_comment_pop(bool pop_all)
|
|
{
|
|
if (fso_ver_comment.empty()) {
|
|
return;
|
|
}
|
|
|
|
if (pop_all) {
|
|
fso_ver_comment.clear();
|
|
return;
|
|
}
|
|
|
|
fso_ver_comment.pop_back();
|
|
}
|
|
|
|
// Per-ship field handling here MUST stay in sync with:
|
|
// parse_create_object_sub() in missionparse.cpp
|
|
// clone_ship_instance_data() in missioneditor/objectduplication.cpp
|
|
// When you add a new editable ship field, touch all three.
|
|
int Fred_mission_save::save_common_object_data(object* objp, ship* shipp)
|
|
{
|
|
int j, z;
|
|
ship_subsys* ptr = nullptr;
|
|
ship_info* sip = nullptr;
|
|
ship_weapon* wp = nullptr;
|
|
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
if (static_cast<int>(objp->phys_info.speed)) {
|
|
if (optional_string_fred("+Initial Velocity:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Initial Velocity:");
|
|
|
|
fout(" %d", static_cast<int>(objp->phys_info.speed));
|
|
}
|
|
|
|
if (fl2i(objp->hull_strength) != 100) {
|
|
if (optional_string_fred("+Initial Hull:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Initial Hull:");
|
|
|
|
fout(" %d", fl2i(objp->hull_strength));
|
|
}
|
|
|
|
if (fl2i(objp->shield_quadrant[0]) != 100) {
|
|
if (optional_string_fred("+Initial Shields:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Initial Shields:");
|
|
|
|
fout(" %d", fl2i(objp->shield_quadrant[0]));
|
|
}
|
|
|
|
// save normal ship weapons info
|
|
required_string_fred("+Subsystem:", "$Name:");
|
|
parse_comments();
|
|
fout(" Pilot");
|
|
|
|
wp = &shipp->weapons;
|
|
z = 0;
|
|
j = wp->num_primary_banks;
|
|
while (j-- && (j >= 0))
|
|
if (wp->primary_bank_weapons[j] != sip->primary_bank_weapons[j])
|
|
z = 1;
|
|
|
|
if (z) {
|
|
if (optional_string_fred("+Primary Banks:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Primary Banks:");
|
|
|
|
fout(" ( ");
|
|
for (j = 0; j < wp->num_primary_banks; j++) {
|
|
if (wp->primary_bank_weapons[j] != -1) { // Just in case someone has set a weapon bank to empty
|
|
fout("\"%s\" ", Weapon_info[wp->primary_bank_weapons[j]].name);
|
|
} else {
|
|
fout("\"\" ");
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
|
|
z = 0;
|
|
j = wp->num_secondary_banks;
|
|
while (j-- && (j >= 0))
|
|
if (wp->secondary_bank_weapons[j] != sip->secondary_bank_weapons[j])
|
|
z = 1;
|
|
|
|
if (z) {
|
|
if (optional_string_fred("+Secondary Banks:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Secondary Banks:");
|
|
|
|
fout(" ( ");
|
|
for (j = 0; j < wp->num_secondary_banks; j++) {
|
|
if (wp->secondary_bank_weapons[j] != -1) {
|
|
fout("\"%s\" ", Weapon_info[wp->secondary_bank_weapons[j]].name);
|
|
} else {
|
|
fout("\"\" ");
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
|
|
z = 0;
|
|
j = wp->num_secondary_banks;
|
|
while (j-- && (j >= 0))
|
|
if (wp->secondary_bank_ammo[j] != 100)
|
|
z = 1;
|
|
|
|
if (z) {
|
|
if (optional_string_fred("+Sbank Ammo:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Sbank Ammo:");
|
|
|
|
fout(" ( ");
|
|
for (j = 0; j < wp->num_secondary_banks; j++)
|
|
fout("%d ", wp->secondary_bank_ammo[j]);
|
|
|
|
fout(")");
|
|
}
|
|
|
|
ptr = GET_FIRST(&shipp->subsys_list);
|
|
Assert(ptr);
|
|
|
|
while (ptr != END_OF_LIST(&shipp->subsys_list) && ptr) {
|
|
// Crashing here!
|
|
if ((ptr->current_hits) || (ptr->system_info && ptr->system_info->type == SUBSYSTEM_TURRET) ||
|
|
(ptr->subsys_cargo_name > 0)) {
|
|
if (optional_string_fred("+Subsystem:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Subsystem:");
|
|
|
|
fout(" %s", ptr->system_info->subobj_name);
|
|
}
|
|
|
|
if (ptr->current_hits) {
|
|
if (optional_string_fred("$Damage:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$Damage:");
|
|
|
|
fout(" %d", static_cast<int>(ptr->current_hits));
|
|
}
|
|
|
|
if (ptr->subsys_cargo_name > 0) {
|
|
if (optional_string_fred("+Cargo Name:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Cargo Name:");
|
|
|
|
fout_ext(nullptr, "%s", Cargo_names[ptr->subsys_cargo_name]);
|
|
}
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (ptr->subsys_cargo_title[0] != '\0') {
|
|
if (optional_string_fred("+Cargo Title:", "$Name:", "+Subsystem:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Cargo Title:");
|
|
}
|
|
fout_ext(nullptr, "%s", ptr->subsys_cargo_title);
|
|
}
|
|
}
|
|
|
|
if (ptr->system_info->type == SUBSYSTEM_TURRET)
|
|
save_turret_info(ptr, SHIP_INDEX(shipp));
|
|
|
|
ptr = GET_NEXT(ptr);
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_containers()
|
|
{
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
return 0;
|
|
}
|
|
|
|
const auto& containers = get_all_sexp_containers();
|
|
|
|
if (containers.empty()) {
|
|
fso_comment_pop(true);
|
|
return 0;
|
|
}
|
|
|
|
required_string_fred("#Sexp_containers");
|
|
parse_comments(2);
|
|
|
|
bool list_found = false;
|
|
bool map_found = false;
|
|
|
|
// What types of container do we have?
|
|
for (const auto& container : containers) {
|
|
if (container.is_list()) {
|
|
list_found = true;
|
|
} else if (container.is_map()) {
|
|
map_found = true;
|
|
}
|
|
if (list_found && map_found) {
|
|
// no point in continuing to check
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (list_found) {
|
|
required_string_fred("$Lists");
|
|
parse_comments(2);
|
|
|
|
for (const auto& container : containers) {
|
|
if (container.is_list()) {
|
|
fout("\n$Name: %s", container.container_name.c_str());
|
|
if (any(container.type & ContainerType::STRING_DATA)) {
|
|
fout("\n$Data Type: String");
|
|
} else if (any(container.type & ContainerType::NUMBER_DATA)) {
|
|
fout("\n$Data Type: Number");
|
|
}
|
|
|
|
if (any(container.type & ContainerType::STRICTLY_TYPED_DATA)) {
|
|
fout("\n+Strictly Typed Data");
|
|
}
|
|
|
|
fout("\n$Data: ( ");
|
|
for (const auto& list_entry : container.list_data) {
|
|
fout("\"%s\" ", list_entry.c_str());
|
|
}
|
|
|
|
fout(")\n");
|
|
|
|
save_container_options(container);
|
|
}
|
|
}
|
|
|
|
required_string_fred("$End Lists");
|
|
parse_comments(1);
|
|
}
|
|
|
|
if (map_found) {
|
|
required_string_fred("$Maps");
|
|
parse_comments(2);
|
|
|
|
for (const auto& container : containers) {
|
|
if (container.is_map()) {
|
|
fout("\n$Name: %s", container.container_name.c_str());
|
|
if (any(container.type & ContainerType::STRING_DATA)) {
|
|
fout("\n$Data Type: String");
|
|
} else if (any(container.type & ContainerType::NUMBER_DATA)) {
|
|
fout("\n$Data Type: Number");
|
|
}
|
|
|
|
if (any(container.type & ContainerType::NUMBER_KEYS)) {
|
|
fout("\n$Key Type: Number");
|
|
} else {
|
|
fout("\n$Key Type: String");
|
|
}
|
|
|
|
if (any(container.type & ContainerType::STRICTLY_TYPED_KEYS)) {
|
|
fout("\n+Strictly Typed Keys");
|
|
}
|
|
|
|
if (any(container.type & ContainerType::STRICTLY_TYPED_DATA)) {
|
|
fout("\n+Strictly Typed Data");
|
|
}
|
|
|
|
SCP_vector<std::pair<SCP_string, SCP_string>> sorted_data(container.map_data.begin(),
|
|
container.map_data.end());
|
|
std::stable_sort(sorted_data.begin(),
|
|
sorted_data.end(),
|
|
[](const std::pair<SCP_string, SCP_string>& a, const std::pair<SCP_string, SCP_string>& b) {
|
|
return a.first < b.first;
|
|
});
|
|
|
|
fout("\n$Data: ( ");
|
|
for (const auto& map_entry : sorted_data) {
|
|
fout("\"%s\" \"%s\" ", map_entry.first.c_str(), map_entry.second.c_str());
|
|
}
|
|
|
|
fout(")\n");
|
|
|
|
save_container_options(container);
|
|
}
|
|
}
|
|
|
|
required_string_fred("$End Maps");
|
|
parse_comments(1);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
void Fred_mission_save::save_container_options(const sexp_container& container)
|
|
{
|
|
if (any(container.type & ContainerType::NETWORK)) {
|
|
fout("+Network Container\n");
|
|
}
|
|
|
|
if (container.is_eternal()) {
|
|
fout("+Eternal\n");
|
|
}
|
|
|
|
if (any(container.type & ContainerType::SAVE_ON_MISSION_CLOSE)) {
|
|
fout("+Save On Mission Close\n");
|
|
} else if (any(container.type & ContainerType::SAVE_ON_MISSION_PROGRESS)) {
|
|
fout("+Save On Mission Progress\n");
|
|
}
|
|
|
|
fout("\n");
|
|
}
|
|
|
|
void Fred_mission_save::save_custom_bitmap(const char* expected_string_640,
|
|
const char* expected_string_1024,
|
|
const char* string_field_640,
|
|
const char* string_field_1024,
|
|
int blank_lines)
|
|
{
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if ((*string_field_640 != '\0') || (*string_field_1024 != '\0')) {
|
|
while (blank_lines-- > 0)
|
|
fout("\n");
|
|
}
|
|
|
|
if (*string_field_640 != '\0') {
|
|
fout("\n%s %s", expected_string_640, string_field_640);
|
|
}
|
|
|
|
if (*string_field_1024 != '\0') {
|
|
fout("\n%s %s", expected_string_1024, string_field_1024);
|
|
}
|
|
}
|
|
}
|
|
|
|
int Fred_mission_save::save_cutscenes()
|
|
{
|
|
char type[NAME_LENGTH];
|
|
SCP_string sexp_out;
|
|
|
|
// Let's just assume it has them for now -
|
|
if (!(The_mission.cutscenes.empty())) {
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (optional_string_fred("#Cutscenes")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout_version("\n\n#Cutscenes");
|
|
}
|
|
fout("\n");
|
|
|
|
for (uint i = 0; i < The_mission.cutscenes.size(); i++) {
|
|
if (strlen(The_mission.cutscenes[i].filename)) {
|
|
// determine the name of this cutscene type
|
|
switch (The_mission.cutscenes[i].type) {
|
|
case MOVIE_PRE_FICTION:
|
|
strcpy_s(type, "$Fiction Viewer Cutscene:");
|
|
break;
|
|
case MOVIE_PRE_CMD_BRIEF:
|
|
strcpy_s(type, "$Command Brief Cutscene:");
|
|
break;
|
|
case MOVIE_PRE_BRIEF:
|
|
strcpy_s(type, "$Briefing Cutscene:");
|
|
break;
|
|
case MOVIE_PRE_GAME:
|
|
strcpy_s(type, "$Pre-game Cutscene:");
|
|
break;
|
|
case MOVIE_PRE_DEBRIEF:
|
|
strcpy_s(type, "$Debriefing Cutscene:");
|
|
break;
|
|
case MOVIE_POST_DEBRIEF:
|
|
strcpy_s(type, "$Post-debriefing Cutscene:");
|
|
break;
|
|
case MOVIE_END_CAMPAIGN:
|
|
strcpy_s(type, "$Campaign End Cutscene:");
|
|
break;
|
|
default:
|
|
Int3();
|
|
continue;
|
|
}
|
|
|
|
if (optional_string_fred(type)) {
|
|
parse_comments();
|
|
fout(" %s", The_mission.cutscenes[i].filename);
|
|
} else {
|
|
fout_version("\n%s %s", type, The_mission.cutscenes[i].filename);
|
|
}
|
|
|
|
required_string_fred("+formula:");
|
|
parse_comments();
|
|
convert_sexp_to_string(sexp_out, The_mission.cutscenes[i].formula, SEXP_SAVE_MODE);
|
|
fout(" %s\n", sexp_out.c_str());
|
|
}
|
|
}
|
|
required_string_fred("#end");
|
|
parse_comments();
|
|
} else {
|
|
SCP_string msg = "Warning: This mission contains cutscene data, but you are saving in the retail mission "
|
|
"format. This information will be lost.";
|
|
Message(os::dialogs::MESSAGEBOX_ERROR, msg.c_str());
|
|
}
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_debriefing()
|
|
{
|
|
int j, i;
|
|
SCP_string sexp_out;
|
|
|
|
for (j = 0; j < Num_teams; j++) {
|
|
|
|
Debriefing = &Debriefings[j];
|
|
|
|
required_string_fred("#Debriefing_info");
|
|
parse_comments(2);
|
|
|
|
save_custom_bitmap("$Background 640:",
|
|
"$Background 1024:",
|
|
Debriefing->background[GR_640],
|
|
Debriefing->background[GR_1024],
|
|
1);
|
|
|
|
required_string_fred("$Num stages:");
|
|
parse_comments(2);
|
|
fout(" %d", Debriefing->num_stages);
|
|
|
|
for (i = 0; i < Debriefing->num_stages; i++) {
|
|
required_string_fred("$Formula:");
|
|
parse_comments(2);
|
|
convert_sexp_to_string(sexp_out, Debriefing->stages[i].formula, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
// XSTR
|
|
required_string_fred("$Multi text");
|
|
parse_comments();
|
|
fout_ext("\n ", "%s", Debriefing->stages[i].text.c_str());
|
|
|
|
required_string_fred("$end_multi_text");
|
|
parse_comments();
|
|
|
|
if (!drop_white_space(Debriefing->stages[i].voice)[0]) {
|
|
strcpy_s(Debriefing->stages[i].voice, "None");
|
|
}
|
|
|
|
required_string_fred("$Voice:");
|
|
parse_comments();
|
|
fout(" %s", Debriefing->stages[i].voice);
|
|
|
|
// XSTR
|
|
required_string_fred("$Recommendation text:");
|
|
parse_comments();
|
|
fout_ext("\n ", "%s", Debriefing->stages[i].recommendation_text.c_str());
|
|
|
|
required_string_fred("$end_multi_text");
|
|
parse_comments();
|
|
|
|
fso_comment_pop();
|
|
}
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_events()
|
|
{
|
|
SCP_string sexp_out;
|
|
int i, j, add_flag;
|
|
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Events");
|
|
parse_comments(2);
|
|
fout("\t\t;! " SIZE_T_ARG " total\n", Mission_events.size());
|
|
|
|
for (i = 0; i < static_cast<int>(Mission_events.size()); i++) {
|
|
required_string_either_fred("$Formula:", "#Goals");
|
|
required_string_fred("$Formula:");
|
|
parse_comments(i ? 2 : 1);
|
|
convert_sexp_to_string(sexp_out, Mission_events[i].formula, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
if (!Mission_events[i].name.empty()) {
|
|
if (optional_string_fred("+Name:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Name:");
|
|
}
|
|
|
|
fout(" %s", Mission_events[i].name.c_str());
|
|
}
|
|
|
|
if (optional_string_fred("+Repeat Count:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Repeat Count:");
|
|
}
|
|
|
|
// if we have a trigger count but no repeat count, we want the event to loop until it has triggered enough times
|
|
if (Mission_events[i].repeat_count == 1 && Mission_events[i].trigger_count != 1) {
|
|
fout(" -1");
|
|
} else {
|
|
fout(" %d", Mission_events[i].repeat_count);
|
|
}
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL && Mission_events[i].trigger_count != 1) {
|
|
fso_comment_push(";;FSO 3.6.11;;");
|
|
if (optional_string_fred("+Trigger Count:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Trigger Count:");
|
|
}
|
|
fso_comment_pop();
|
|
|
|
fout(" %d", Mission_events[i].trigger_count);
|
|
}
|
|
|
|
if (optional_string_fred("+Interval:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Interval:");
|
|
}
|
|
|
|
fout(" %d", Mission_events[i].interval);
|
|
|
|
if (Mission_events[i].score != 0) {
|
|
if (optional_string_fred("+Score:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Score:");
|
|
}
|
|
fout(" %d", Mission_events[i].score);
|
|
}
|
|
|
|
if (Mission_events[i].chain_delay >= 0) {
|
|
if (optional_string_fred("+Chained:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Chained:");
|
|
}
|
|
|
|
fout(" %d", Mission_events[i].chain_delay);
|
|
}
|
|
|
|
// XSTR
|
|
if (!Mission_events[i].objective_text.empty()) {
|
|
if (optional_string_fred("+Objective:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Objective:");
|
|
}
|
|
|
|
fout_ext(" ", "%s", Mission_events[i].objective_text.c_str());
|
|
}
|
|
|
|
// XSTR
|
|
if (!Mission_events[i].objective_key_text.empty()) {
|
|
if (optional_string_fred("+Objective key:", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Objective key:");
|
|
}
|
|
|
|
fout_ext(" ", "%s", Mission_events[i].objective_key_text.c_str());
|
|
}
|
|
|
|
// save team
|
|
if (Mission_events[i].team >= 0) {
|
|
if (optional_string_fred("+Team:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Team:");
|
|
}
|
|
fout(" %d", Mission_events[i].team);
|
|
}
|
|
|
|
// save flags, if any
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
// we need to lazily-write the tag because we should only write it if there are also flags to write
|
|
// (some of the flags are transient, internal flags)
|
|
bool wrote_tag = false;
|
|
|
|
for (j = 0; j < Num_mission_event_flags; ++j) {
|
|
if (Mission_events[i].flags & Mission_event_flags[j].def) {
|
|
if (!wrote_tag) {
|
|
wrote_tag = true;
|
|
|
|
fso_comment_push(";;FSO 20.0.0;;");
|
|
if (optional_string_fred("+Event Flags: (", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Event Flags: (");
|
|
}
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fout(" \"%s\"", Mission_event_flags[j].name);
|
|
}
|
|
}
|
|
|
|
if (wrote_tag)
|
|
fout(" )");
|
|
}
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL && Mission_events[i].mission_log_flags != 0) {
|
|
fso_comment_push(";;FSO 3.6.11;;");
|
|
if (optional_string_fred("+Event Log Flags: (", "$Formula:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Event Log Flags: (");
|
|
}
|
|
fso_comment_pop();
|
|
|
|
for (j = 0; j < MAX_MISSION_EVENT_LOG_FLAGS; j++) {
|
|
add_flag = 1 << j;
|
|
if (Mission_events[i].mission_log_flags & add_flag) {
|
|
fout(" \"%s\"", Mission_event_log_flags[j]);
|
|
}
|
|
}
|
|
fout(" )");
|
|
}
|
|
|
|
// save event annotations
|
|
if (save_config.save_format != MissionFormat::RETAIL && !Event_annotations.empty()) {
|
|
bool at_least_one = false;
|
|
fso_comment_push(";;FSO 21.0.0;;");
|
|
event_annotation default_ea;
|
|
|
|
// see if there is an annotation for this event
|
|
for (const auto& ea : Event_annotations) {
|
|
if (ea.path.empty() || ea.path.front() != i)
|
|
continue;
|
|
|
|
if (!at_least_one) {
|
|
if (optional_string_fred("$Annotations Start", "$Formula:"))
|
|
parse_comments();
|
|
else
|
|
fout_version("\n$Annotations Start");
|
|
at_least_one = true;
|
|
}
|
|
|
|
if (ea.comment != default_ea.comment) {
|
|
if (optional_string_fred("+Comment:", "$Formula:"))
|
|
parse_comments();
|
|
else
|
|
fout_version("\n+Comment:");
|
|
|
|
auto copy = ea.comment;
|
|
lcl_fred_replace_stuff(copy);
|
|
fout(" %s", copy.c_str());
|
|
|
|
if (optional_string_fred("$end_multi_text", "$Formula:"))
|
|
parse_comments();
|
|
else
|
|
fout_version("\n$end_multi_text");
|
|
}
|
|
|
|
if (ea.r != default_ea.r || ea.g != default_ea.g || ea.b != default_ea.b) {
|
|
if (optional_string_fred("+Background Color:", "$Formula:"))
|
|
parse_comments();
|
|
else
|
|
fout_version("\n+Background Color:");
|
|
|
|
fout(" %d, %d, %d", ea.r, ea.g, ea.b);
|
|
}
|
|
|
|
if (ea.path.size() > 1) {
|
|
if (optional_string_fred("+Path:", "$Formula:"))
|
|
parse_comments();
|
|
else
|
|
fout_version("\n+Path:");
|
|
|
|
bool comma = false;
|
|
auto it = ea.path.begin();
|
|
for (++it; it != ea.path.end(); ++it) {
|
|
if (comma)
|
|
fout(",");
|
|
comma = true;
|
|
fout(" %d", *it);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (at_least_one) {
|
|
if (optional_string_fred("$Annotations End", "$Formula:"))
|
|
parse_comments();
|
|
else
|
|
fout_version("\n$Annotations End");
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_fiction()
|
|
{
|
|
if (mission_has_fiction()) {
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (optional_string_fred("#Fiction Viewer"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n\n#Fiction Viewer");
|
|
|
|
// we have multiple stages now, so save them all
|
|
for (auto& stage : Fiction_viewer_stages) {
|
|
fout("\n");
|
|
|
|
// save file
|
|
required_string_fred("$File:");
|
|
parse_comments();
|
|
fout(" %s", stage.story_filename);
|
|
|
|
// save font
|
|
if (strlen(stage.font_filename) > 0) //-V805
|
|
{
|
|
if (optional_string_fred("$Font:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$Font:");
|
|
fout(" %s", stage.font_filename);
|
|
}
|
|
|
|
// save voice
|
|
if (strlen(stage.voice_filename) > 0) //-V805
|
|
{
|
|
if (optional_string_fred("$Voice:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$Voice:");
|
|
fout(" %s", stage.voice_filename);
|
|
}
|
|
|
|
// save UI
|
|
if (strlen(stage.ui_name) > 0) {
|
|
if (optional_string_fred("$UI:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$UI:");
|
|
fout(" %s", stage.ui_name);
|
|
}
|
|
|
|
// save background
|
|
save_custom_bitmap("$Background 640:",
|
|
"$Background 1024:",
|
|
stage.background[GR_640],
|
|
stage.background[GR_1024]);
|
|
|
|
// save sexp formula if we have one
|
|
if (stage.formula >= 0 && stage.formula != Locked_sexp_true) {
|
|
SCP_string sexp_out;
|
|
convert_sexp_to_string(sexp_out, stage.formula, SEXP_SAVE_MODE);
|
|
|
|
if (optional_string_fred("$Formula:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$Formula:");
|
|
fout(" %s", sexp_out.c_str());
|
|
}
|
|
}
|
|
} else {
|
|
SCP_string msg = "Warning: This mission contains fiction viewer data, but you are saving in the retail "
|
|
"mission format. This information will be lost.";
|
|
Message(os::dialogs::MESSAGEBOX_ERROR, msg.c_str());
|
|
}
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_goals()
|
|
{
|
|
SCP_string sexp_out;
|
|
int i;
|
|
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Goals");
|
|
parse_comments(2);
|
|
fout("\t\t;! " SIZE_T_ARG " total\n", Mission_goals.size());
|
|
|
|
for (i = 0; i < static_cast<int>(Mission_goals.size()); i++) {
|
|
int type;
|
|
|
|
required_string_either_fred("$Type:", "#Waypoints");
|
|
required_string_fred("$Type:");
|
|
parse_comments(i ? 2 : 1);
|
|
|
|
type = Mission_goals[i].type & GOAL_TYPE_MASK;
|
|
fout(" %s", Goal_type_names[type]);
|
|
|
|
if (!Mission_goals[i].name.empty()) {
|
|
if (optional_string_fred("+Name:", "$Type:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Name:");
|
|
|
|
fout(" %s", Mission_goals[i].name.c_str());
|
|
}
|
|
|
|
// XSTR
|
|
required_string_fred("$MessageNew:");
|
|
parse_comments();
|
|
fout_ext(" ", "%s", Mission_goals[i].message.c_str());
|
|
fout("\n");
|
|
required_string_fred("$end_multi_text");
|
|
parse_comments(0);
|
|
|
|
required_string_fred("$Formula:");
|
|
parse_comments();
|
|
convert_sexp_to_string(sexp_out, Mission_goals[i].formula, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
if (Mission_goals[i].type & INVALID_GOAL) {
|
|
if (optional_string_fred("+Invalid", "$Type:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Invalid");
|
|
}
|
|
|
|
if (Mission_goals[i].flags & MGF_NO_MUSIC) {
|
|
if (optional_string_fred("+No music", "$Type:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+No music");
|
|
}
|
|
|
|
if (Mission_goals[i].score != 0) {
|
|
if (optional_string_fred("+Score:", "$Type:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Score:");
|
|
fout(" %d", Mission_goals[i].score);
|
|
}
|
|
|
|
if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) {
|
|
if (optional_string_fred("+Team:", "$Type:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Team:");
|
|
fout(" %d", Mission_goals[i].team);
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_matrix(matrix& m)
|
|
{
|
|
fout("\n\t%f, %f, %f,\n", m.vec.rvec.xyz.x, m.vec.rvec.xyz.y, m.vec.rvec.xyz.z);
|
|
fout("\t%f, %f, %f,\n", m.vec.uvec.xyz.x, m.vec.uvec.xyz.y, m.vec.uvec.xyz.z);
|
|
fout("\t%f, %f, %f", m.vec.fvec.xyz.x, m.vec.fvec.xyz.y, m.vec.fvec.xyz.z);
|
|
return 0;
|
|
}
|
|
|
|
int Fred_mission_save::save_messages()
|
|
{
|
|
int i;
|
|
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Messages");
|
|
parse_comments(2);
|
|
fout("\t\t;! %d total\n", Num_messages - Num_builtin_messages);
|
|
|
|
// Goober5000 - special Command info
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (stricmp(The_mission.command_sender, DEFAULT_COMMAND))
|
|
fout("\n$Command Sender: %s", The_mission.command_sender);
|
|
|
|
if (The_mission.command_persona != Default_command_persona)
|
|
fout("\n$Command Persona: %s", Personas[The_mission.command_persona].name);
|
|
}
|
|
|
|
for (i = Num_builtin_messages; i < Num_messages; i++) {
|
|
required_string_either_fred("$Name:", "#Reinforcements");
|
|
required_string_fred("$Name:");
|
|
parse_comments(2);
|
|
fout(" %s", Messages[i].name);
|
|
|
|
// team
|
|
required_string_fred("$Team:");
|
|
parse_comments(1);
|
|
if ((Messages[i].multi_team < 0) || (Messages[i].multi_team >= 2)) {
|
|
fout(" %d", -1);
|
|
} else {
|
|
fout(" %d", Messages[i].multi_team);
|
|
}
|
|
|
|
// XSTR
|
|
required_string_fred("$MessageNew:");
|
|
parse_comments();
|
|
fout_ext(" ", "%s", Messages[i].message);
|
|
fout("\n");
|
|
required_string_fred("$end_multi_text");
|
|
parse_comments(0);
|
|
|
|
if (Messages[i].persona_index != -1) {
|
|
if (optional_string_fred("+Persona:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Persona:");
|
|
|
|
fout(" %s", Personas[Messages[i].persona_index].name);
|
|
}
|
|
|
|
if (Messages[i].avi_info.name) {
|
|
if (optional_string_fred("+AVI Name:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+AVI Name:");
|
|
|
|
fout(" %s", Messages[i].avi_info.name);
|
|
}
|
|
|
|
if (Messages[i].wave_info.name) {
|
|
if (optional_string_fred("+Wave Name:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Wave Name:");
|
|
|
|
fout(" %s", Messages[i].wave_info.name);
|
|
}
|
|
|
|
if (Messages[i].note != "") {
|
|
if (optional_string_fred("+Note:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Note:");
|
|
|
|
auto copy = Messages[i].note;
|
|
lcl_fred_replace_stuff(copy);
|
|
fout(" %s", copy.c_str());
|
|
|
|
if (optional_string_fred("$end_multi_text", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout_version("\n$end_multi_text");
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_mission_file(const char* pathname)
|
|
{
|
|
char savepath[MAX_PATH_LEN];
|
|
|
|
strcpy_s(savepath, "");
|
|
auto p = strrchr(pathname, DIR_SEPARATOR_CHAR);
|
|
if (p) {
|
|
auto len = p - pathname;
|
|
strncpy(savepath, pathname, len);
|
|
savepath[len] = '\0';
|
|
strcat_s(savepath, DIR_SEPARATOR_STR);
|
|
}
|
|
strcat_s(savepath, "saving.xxx");
|
|
|
|
// only display this warning once, and only when the user explicitly saves, as opposed to autosave
|
|
static bool Displayed_retail_background_warning = false;
|
|
if (save_config.save_format != MissionFormat::STANDARD && !Displayed_retail_background_warning) {
|
|
for (const auto& bg : Backgrounds) {
|
|
if (bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]) {
|
|
SCP_string msg = "Background flags (including the fixed-angles-in-mission-file flag) are not supported in retail. "
|
|
"The sun and bitmap angles will be loaded differently by previous versions.";
|
|
Message(os::dialogs::MESSAGEBOX_ERROR, msg.c_str());
|
|
Displayed_retail_background_warning = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
save_mission_internal(savepath);
|
|
|
|
if (!err) {
|
|
if (save_config.create_bak_file) {
|
|
char backup_name[MAX_PATH_LEN];
|
|
|
|
strcpy_s(backup_name, pathname);
|
|
|
|
// drop extension
|
|
auto ext_ch = strrchr(backup_name, '.');
|
|
if (ext_ch != nullptr)
|
|
*ext_ch = 0;
|
|
|
|
strcat_s(backup_name, ".bak");
|
|
#ifdef _WIN32
|
|
cf_attrib(pathname, 0, FILE_ATTRIBUTE_READONLY, CF_TYPE_MISSIONS);
|
|
#endif
|
|
cf_delete(backup_name, CF_TYPE_MISSIONS);
|
|
cf_rename(pathname, backup_name, CF_TYPE_MISSIONS);
|
|
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
|
|
} else {
|
|
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_autosave_file(const char* pathname)
|
|
{
|
|
save_mission_internal(pathname);
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_template_file(const char* pathname)
|
|
{
|
|
char savepath[MAX_PATH_LEN];
|
|
|
|
strcpy_s(savepath, "");
|
|
auto p = strrchr(pathname, DIR_SEPARATOR_CHAR);
|
|
if (p) {
|
|
auto len = p - pathname;
|
|
strncpy(savepath, pathname, len);
|
|
savepath[len] = '\0';
|
|
strcat_s(savepath, DIR_SEPARATOR_STR);
|
|
}
|
|
strcat_s(savepath, "saving.xxx");
|
|
|
|
save_mission_internal(savepath);
|
|
|
|
if (!err) {
|
|
// Templates don't get .bak backups; just overwrite directly
|
|
cf_delete(pathname, CF_TYPE_MISSIONS);
|
|
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
void Fred_mission_save::save_template_info()
|
|
{
|
|
const auto& ti = save_config.template_info;
|
|
|
|
fout("#Template Info\n");
|
|
fout("\n$Template Title: %s", ti.title.c_str());
|
|
fout("\n$Template Author: %s", ti.author.c_str());
|
|
fout("\n$Template Tags: %s", ti.tags.c_str());
|
|
fout("\n$Template Description:\n%s\n$end_template_desc", ti.description.c_str());
|
|
fout("\n\n#End Template Info\n\n");
|
|
}
|
|
|
|
int Fred_mission_save::save_mission_info()
|
|
{
|
|
required_string_fred("#Mission Info");
|
|
parse_comments(0);
|
|
|
|
required_string_fred("$Version:");
|
|
parse_comments(2);
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
// All retail missions, both FS1 and FS2, have the same version
|
|
fout(" %d.%d", LEGACY_MISSION_VERSION.major, LEGACY_MISSION_VERSION.minor);
|
|
} else {
|
|
// Since previous versions of FreeSpace interpret this as a float, this can only have one decimal point
|
|
fout(" %d.%d", The_mission.required_fso_version.major, The_mission.required_fso_version.minor);
|
|
}
|
|
|
|
// XSTR
|
|
required_string_fred("$Name:");
|
|
parse_comments();
|
|
fout_ext(" ", "%s", The_mission.name.c_str());
|
|
|
|
required_string_fred("$Author:");
|
|
parse_comments();
|
|
fout(" %s", The_mission.author.c_str());
|
|
|
|
required_string_fred("$Created:");
|
|
parse_comments();
|
|
fout(" %s", The_mission.created);
|
|
|
|
required_string_fred("$Modified:");
|
|
parse_comments();
|
|
fout(" %s", The_mission.modified);
|
|
|
|
required_string_fred("$Notes:");
|
|
parse_comments();
|
|
fout("\n%s", The_mission.notes);
|
|
|
|
required_string_fred("$End Notes:");
|
|
parse_comments(0);
|
|
|
|
// XSTR
|
|
required_string_fred("$Mission Desc:");
|
|
parse_comments(2);
|
|
fout_ext("\n", "%s", The_mission.mission_desc);
|
|
fout("\n");
|
|
|
|
required_string_fred("$end_multi_text");
|
|
parse_comments(0);
|
|
|
|
#if 0
|
|
if (optional_string_fred("+Game Type:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n\n+Game Type:");
|
|
fout("\n%s", Game_types[The_mission.game_type]);
|
|
#endif
|
|
|
|
if (optional_string_fred("+Game Type Flags:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n+Game Type Flags:");
|
|
}
|
|
|
|
fout(" %d", The_mission.game_type);
|
|
|
|
if (optional_string_fred("+Flags:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n+Flags:");
|
|
}
|
|
|
|
fout(" " UINT64_T_ARG, The_mission.flags.to_u64());
|
|
|
|
// maybe write out Nebula values
|
|
if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
|
|
if (optional_string_fred("+NebAwacs:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n\n+NebAwacs:");
|
|
fout(" %f", Neb2_awacs);
|
|
|
|
// storm name
|
|
if (optional_string_fred("+Storm:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Storm:");
|
|
fout(" %s", Mission_parse_storm_name);
|
|
|
|
// write out the nebula clipping multipliers...
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
// legacy values
|
|
if (Neb2_fog_save_legacy_values)
|
|
{
|
|
if (optional_string_fred("+Fog Near Mult:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Fog Near Mult:");
|
|
fout(" %f", Neb2_fog_legacy_near_mult);
|
|
|
|
if (optional_string_fred("+Fog Far Mult:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Fog Far Mult:");
|
|
fout(" %f", Neb2_fog_legacy_far_mult);
|
|
}
|
|
// Modern fogging values - note that the writing of these values signals to the parser that this mission was created with a version of FSO that has the modern fogging fixes.
|
|
// Per Lafiel, these should never be only-if-non-default, since the fogging converter is controlled by their presence or absence.
|
|
else
|
|
{
|
|
if (optional_string_fred("+Fog 1000m Visibility:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Fog 1000m Visibility:");
|
|
fout(" %f", Neb2_fog_1000m_visibility);
|
|
|
|
if (optional_string_fred("+Fog Near Distance:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Fog Near Distance:");
|
|
fout(" %f", Neb2_fog_near_distance);
|
|
|
|
if (optional_string_fred("+Fog Skybox Clip Distance:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Fog Skybox Clip Distance:");
|
|
fout(" %f", Neb2_fog_skybox_clip_distance);
|
|
|
|
if (optional_string_fred("+Fog Clip Distance:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Fog Clip Distance:");
|
|
fout(" %f", Neb2_fog_clip_distance);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (The_mission.contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT) {
|
|
fout("\n$Contrail Speed Threshold: %d\n", The_mission.contrail_threshold);
|
|
}
|
|
|
|
if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default]) {
|
|
fout("\n+Large Ship Collision Group: %d\n", The_mission.large_ship_no_collide_collision_group);
|
|
}
|
|
}
|
|
|
|
{
|
|
bool hasVolumetricNoise = false;
|
|
|
|
if (The_mission.volumetrics) {
|
|
fso_comment_push(";;FSO 23.1.0;;");
|
|
|
|
if (optional_string_fred("+Volumetric Nebula:"))
|
|
parse_comments(2);
|
|
else
|
|
fout_version("\n+Volumetric Nebula:");
|
|
fout(" %s", The_mission.volumetrics->hullPof.c_str());
|
|
|
|
FRED_ENSURE_PROPERTY_VERSION("+Position:",
|
|
1,
|
|
" %f, %f, %f",
|
|
The_mission.volumetrics->pos.xyz.x,
|
|
The_mission.volumetrics->pos.xyz.y,
|
|
The_mission.volumetrics->pos.xyz.z);
|
|
FRED_ENSURE_PROPERTY_VERSION("+Color:",
|
|
1,
|
|
" (%d, %d, %d)",
|
|
static_cast<ubyte>(std::get<0>(The_mission.volumetrics->nebulaColor) * 255.0f),
|
|
static_cast<ubyte>(std::get<1>(The_mission.volumetrics->nebulaColor) * 255.0f),
|
|
static_cast<ubyte>(std::get<2>(The_mission.volumetrics->nebulaColor) * 255.0f));
|
|
FRED_ENSURE_PROPERTY_VERSION("+Visibility Opacity:", 1, " %f", The_mission.volumetrics->alphaLim);
|
|
FRED_ENSURE_PROPERTY_VERSION("+Visibility Distance:", 1, " %f", The_mission.volumetrics->opacityDistance);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Steps:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
15,
|
|
" %d",
|
|
The_mission.volumetrics->steps);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Resolution:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
6,
|
|
" %d",
|
|
The_mission.volumetrics->resolution);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Oversampling:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
2,
|
|
" %d",
|
|
The_mission.volumetrics->oversampling);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Smoothing:",
|
|
1,
|
|
";;FSO 25.0.0;;",
|
|
0.f,
|
|
" %f",
|
|
The_mission.volumetrics->smoothing);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Heyney Greenstein Coefficient:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
0.2f,
|
|
" %f",
|
|
The_mission.volumetrics->henyeyGreensteinCoeff);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Sun Falloff Factor:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
1.0f,
|
|
" %f",
|
|
The_mission.volumetrics->globalLightDistanceFactor);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Sun Steps:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
6,
|
|
" %d",
|
|
The_mission.volumetrics->globalLightSteps);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Emissive Light Spread:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
0.7f,
|
|
" %f",
|
|
The_mission.volumetrics->emissiveSpread);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Emissive Light Intensity:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
1.1f,
|
|
" %f",
|
|
The_mission.volumetrics->emissiveIntensity);
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Emissive Light Falloff:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
1.5f,
|
|
" %f",
|
|
The_mission.volumetrics->emissiveFalloff);
|
|
|
|
if (The_mission.volumetrics->noiseActive) {
|
|
hasVolumetricNoise = true;
|
|
|
|
if (optional_string_fred("+Noise:"))
|
|
parse_comments(1);
|
|
else
|
|
fout_version("\n+Noise:");
|
|
|
|
FRED_ENSURE_PROPERTY_VERSION("+Scale:",
|
|
1,
|
|
" (%f, %f)",
|
|
std::get<0>(The_mission.volumetrics->noiseScale),
|
|
std::get<1>(The_mission.volumetrics->noiseScale));
|
|
FRED_ENSURE_PROPERTY_VERSION("+Color:",
|
|
1,
|
|
" (%d, %d, %d)",
|
|
static_cast<ubyte>(std::get<0>(The_mission.volumetrics->noiseColor) * 255.0f),
|
|
static_cast<ubyte>(std::get<1>(The_mission.volumetrics->noiseColor) * 255.0f),
|
|
static_cast<ubyte>(std::get<2>(The_mission.volumetrics->noiseColor) * 255.0f));
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Intensity:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
1.0f,
|
|
" %f",
|
|
The_mission.volumetrics->noiseColorIntensity);
|
|
FRED_ENSURE_PROPERTY_VERSION_IF("+Function Base:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
The_mission.volumetrics->noiseColorFunc1,
|
|
" %s",
|
|
The_mission.volumetrics->noiseColorFunc1->c_str());
|
|
FRED_ENSURE_PROPERTY_VERSION_IF("+Function Sub:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
The_mission.volumetrics->noiseColorFunc2,
|
|
" %s",
|
|
The_mission.volumetrics->noiseColorFunc2->c_str());
|
|
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Resolution:",
|
|
1,
|
|
";;FSO 23.1.0;;",
|
|
5,
|
|
" %d",
|
|
The_mission.volumetrics->noiseResolution);
|
|
}
|
|
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 23.1.0;; +Volumetric Nebula:");
|
|
bypass_comment(";;FSO 23.1.0;; +Position:");
|
|
bypass_comment(";;FSO 23.1.0;; +Color:");
|
|
bypass_comment(";;FSO 23.1.0;; +Visibility Opacity:");
|
|
bypass_comment(";;FSO 23.1.0;; +Visibility Distance:");
|
|
bypass_comment(";;FSO 23.1.0;; +Steps:");
|
|
bypass_comment(";;FSO 23.1.0;; +Resolution:");
|
|
bypass_comment(";;FSO 23.1.0;; +Oversampling:");
|
|
bypass_comment(";;FSO 25.0.0;; +Smoothing:");
|
|
bypass_comment(";;FSO 23.1.0;; +Heyney Greenstein Coefficient:");
|
|
bypass_comment(";;FSO 23.1.0;; +Sun Falloff Factor:");
|
|
bypass_comment(";;FSO 23.1.0;; +Sun Steps:");
|
|
bypass_comment(";;FSO 23.1.0;; +Emissive Light Spread:");
|
|
bypass_comment(";;FSO 23.1.0;; +Emissive Light Intensity:");
|
|
bypass_comment(";;FSO 23.1.0;; +Emissive Light Falloff:");
|
|
}
|
|
|
|
if (!hasVolumetricNoise) {
|
|
bypass_comment(";;FSO 23.1.0;; +Noise:");
|
|
bypass_comment(";;FSO 23.1.0;; +Scale:");
|
|
bypass_comment(";;FSO 23.1.0;; +Color:");
|
|
bypass_comment(";;FSO 23.1.0;; +Intensity:");
|
|
bypass_comment(";;FSO 23.1.0;; +Function Base:");
|
|
bypass_comment(";;FSO 23.1.0;; +Function Sub:");
|
|
bypass_comment(";;FSO 23.1.0;; +Resolution:");
|
|
}
|
|
}
|
|
|
|
// For multiplayer missions -- write out the number of player starts and number of respawns
|
|
if (The_mission.game_type & MISSION_TYPE_MULTI) {
|
|
if (optional_string_fred("+Num Players:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n+Num Players:");
|
|
|
|
fout(" %d", Player_starts);
|
|
|
|
if (optional_string_fred("+Num Respawns:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n+Num Respawns:");
|
|
|
|
fout(" %d", The_mission.num_respawns);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
fso_comment_push(";;FSO 3.6.11;;");
|
|
if (optional_string_fred("+Max Respawn Time:"))
|
|
parse_comments(2);
|
|
else {
|
|
fout_version("\n+Max Respawn Time:");
|
|
}
|
|
fso_comment_pop();
|
|
|
|
fout(" %d", The_mission.max_respawn_delay);
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.11;; +Max Respawn Time:");
|
|
}
|
|
}
|
|
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
if (optional_string_fred("+Red Alert:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n+Red Alert:");
|
|
|
|
fout(" %d", (The_mission.flags[Mission::Mission_Flags::Red_alert]) ? 1 : 0);
|
|
}
|
|
|
|
if (save_config.save_format == MissionFormat::RETAIL) //-V581
|
|
{
|
|
if (optional_string_fred("+Scramble:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n+Scramble:");
|
|
|
|
fout(" %d", (The_mission.flags[Mission::Mission_Flags::Scramble]) ? 1 : 0);
|
|
}
|
|
|
|
if (optional_string_fred("+Disallow Support:")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n+Disallow Support:");
|
|
}
|
|
// this is compatible with non-SCP variants - Goober5000
|
|
fout(" %d", (The_mission.support_ships.max_support_ships == 0) ? 1 : 0);
|
|
|
|
// version-gated support options; see also check_for_25_1_data()
|
|
auto more_support_options_version = gameversion::version(25, 1);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (The_mission.required_fso_version >= more_support_options_version) {
|
|
if (optional_string_fred("+Disallow Support Rearm:")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n+Disallow Support Rearm:");
|
|
}
|
|
|
|
fout(" %d", The_mission.support_ships.disallow_rearm ? 1 : 0);
|
|
}
|
|
|
|
if (The_mission.required_fso_version >= more_support_options_version) {
|
|
if (optional_string_fred("+Allow Support Rearm Weapon Precedence:")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n+Allow Support Rearm Weapon Precedence:");
|
|
}
|
|
|
|
fout(" %d", The_mission.support_ships.allow_rearm_weapon_precedence ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
// here be WMCoolmon's hull and subsys repair stuff
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (optional_string_fred("+Hull Repair Ceiling:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n+Hull Repair Ceiling:");
|
|
}
|
|
fout(" %f", The_mission.support_ships.max_hull_repair_val);
|
|
|
|
if (optional_string_fred("+Subsystem Repair Ceiling:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n+Subsystem Repair Ceiling:");
|
|
}
|
|
fout(" %f", The_mission.support_ships.max_subsys_repair_val);
|
|
|
|
if (The_mission.required_fso_version >= more_support_options_version) {
|
|
if (optional_string_fred("+Support Rearm Pool From Loadout:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n+Support Rearm Pool From Loadout:");
|
|
}
|
|
fout(" %d", The_mission.support_ships.rearm_pool_from_loadout ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
if (Mission_all_attack) {
|
|
if (optional_string_fred("+All Teams Attack")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+All Teams Attack");
|
|
}
|
|
}
|
|
|
|
if (Entry_delay_time) {
|
|
if (optional_string_fred("+Player Entry Delay:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n\n+Player Entry Delay:");
|
|
|
|
fout("\n%f", f2fl(Entry_delay_time));
|
|
}
|
|
|
|
if (optional_string_fred("+Viewer pos:")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n\n+Viewer pos:");
|
|
}
|
|
|
|
save_vector(save_config.view_pos);
|
|
|
|
if (optional_string_fred("+Viewer orient:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Viewer orient:");
|
|
}
|
|
|
|
save_matrix(save_config.view_orient);
|
|
|
|
// squadron info
|
|
if (!(The_mission.game_type & MISSION_TYPE_MULTI) && (strlen(The_mission.squad_name) > 0)) { //-V805
|
|
// squad name
|
|
fout("\n+SquadReassignName: %s", The_mission.squad_name);
|
|
|
|
// maybe squad logo
|
|
if (strlen(The_mission.squad_filename) > 0) { //-V805
|
|
fout("\n+SquadReassignLogo: %s", The_mission.squad_filename);
|
|
}
|
|
}
|
|
|
|
// Goober5000 - special wing info
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
int i;
|
|
fout("\n");
|
|
|
|
// starting wings
|
|
if (strcmp(Starting_wing_names[0], "Alpha") || strcmp(Starting_wing_names[1], "Beta") ||
|
|
strcmp(Starting_wing_names[2], "Gamma")) {
|
|
fout("\n$Starting wing names: ( ");
|
|
|
|
for (i = 0; i < MAX_STARTING_WINGS; i++) {
|
|
fout("\"%s\" ", Starting_wing_names[i]);
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
|
|
// squadron wings
|
|
if (strcmp(Squadron_wing_names[0], "Alpha") || strcmp(Squadron_wing_names[1], "Beta") ||
|
|
strcmp(Squadron_wing_names[2], "Gamma") || strcmp(Squadron_wing_names[3], "Delta") ||
|
|
strcmp(Squadron_wing_names[4], "Epsilon")) {
|
|
fout("\n$Squadron wing names: ( ");
|
|
|
|
for (i = 0; i < MAX_SQUADRON_WINGS; i++) {
|
|
fout("\"%s\" ", Squadron_wing_names[i]);
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
|
|
// tvt wings
|
|
if (strcmp(TVT_wing_names[0], "Alpha") || strcmp(TVT_wing_names[1], "Zeta")) {
|
|
fout("\n$Team-versus-team wing names: ( ");
|
|
|
|
for (i = 0; i < MAX_TVT_WINGS; i++) {
|
|
fout("\"%s\" ", TVT_wing_names[i]);
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
}
|
|
|
|
save_custom_bitmap("$Load Screen 640:",
|
|
"$Load Screen 1024:",
|
|
The_mission.loading_screen[GR_640],
|
|
The_mission.loading_screen[GR_1024],
|
|
1);
|
|
|
|
// Phreak's skybox stuff
|
|
if (strlen(The_mission.skybox_model) > 0) //-V805
|
|
{
|
|
char out_str[NAME_LENGTH];
|
|
char* period;
|
|
|
|
// kill off any extension, we will add one here
|
|
strcpy_s(out_str, The_mission.skybox_model);
|
|
period = strrchr(out_str, '.');
|
|
if (period != NULL)
|
|
*period = 0;
|
|
|
|
fso_comment_push(";;FSO 3.6.0;;");
|
|
if (optional_string_fred("$Skybox Model:")) {
|
|
parse_comments(2);
|
|
fout(" %s.pof", out_str);
|
|
} else {
|
|
fout_version("\n\n$Skybox Model: %s.pof", out_str);
|
|
}
|
|
fso_comment_pop();
|
|
|
|
const auto& anim_triggers = The_mission.skybox_model_animations.getRegisteredAnimNames();
|
|
const auto& moveable_triggers = The_mission.skybox_model_animations.getRegisteredMoveables();
|
|
|
|
if (!anim_triggers.empty() || !moveable_triggers.empty()) {
|
|
fso_comment_push(";;FSO 23.0.0;;");
|
|
if (!anim_triggers.empty()) {
|
|
if (optional_string_fred("$Skybox Model Animations:")) {
|
|
parse_comments(1);
|
|
fout("( ");
|
|
for (const auto& animation : anim_triggers) {
|
|
fout("\"%s\" ", animation.c_str());
|
|
}
|
|
fout(")");
|
|
} else {
|
|
fout_version("\n$Skybox Model Animations:");
|
|
fout("( ");
|
|
for (const auto& animation : anim_triggers) {
|
|
fout("\"%s\" ", animation.c_str());
|
|
}
|
|
fout(")");
|
|
}
|
|
}
|
|
if (!moveable_triggers.empty()) {
|
|
if (optional_string_fred("$Skybox Model Moveables:")) {
|
|
parse_comments(1);
|
|
fout("( ");
|
|
for (const auto& moveable : moveable_triggers) {
|
|
fout("\"%s\" ", moveable.c_str());
|
|
}
|
|
fout(")");
|
|
} else {
|
|
fout_version("\n$Skybox Model Moveables:");
|
|
fout("( ");
|
|
for (const auto& moveable : moveable_triggers) {
|
|
fout("\"%s\" ", moveable.c_str());
|
|
}
|
|
fout(")");
|
|
}
|
|
}
|
|
fso_comment_pop();
|
|
}
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.0;; $Skybox Model:");
|
|
}
|
|
|
|
// orientation?
|
|
if ((strlen(The_mission.skybox_model) > 0) &&
|
|
!vm_matrix_same(&vmd_identity_matrix, &The_mission.skybox_orientation)) {
|
|
fso_comment_push(";;FSO 3.6.14;;");
|
|
if (optional_string_fred("+Skybox Orientation:")) {
|
|
parse_comments(1);
|
|
save_matrix(The_mission.skybox_orientation);
|
|
} else {
|
|
fout_version("\n+Skybox Orientation:");
|
|
save_matrix(The_mission.skybox_orientation);
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.14;; +Skybox Orientation:");
|
|
}
|
|
|
|
// are skybox flags in use?
|
|
if (The_mission.skybox_flags != DEFAULT_NMODEL_FLAGS) {
|
|
// char out_str[4096];
|
|
fso_comment_push(";;FSO 3.6.11;;");
|
|
if (optional_string_fred("+Skybox Flags:")) {
|
|
parse_comments(1);
|
|
fout(" %d", The_mission.skybox_flags);
|
|
} else {
|
|
fout_version("\n+Skybox Flags: %d", The_mission.skybox_flags);
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.11;; +Skybox Flags:");
|
|
}
|
|
|
|
// Goober5000's AI profile stuff
|
|
int profile_index = AI_PROFILES_INDEX(The_mission.ai_profile);
|
|
Assert(profile_index >= 0 && profile_index < MAX_AI_PROFILES);
|
|
|
|
fso_comment_push(";;FSO 3.6.9;;");
|
|
if (optional_string_fred("$AI Profile:")) {
|
|
parse_comments(2);
|
|
fout(" %s", The_mission.ai_profile->profile_name);
|
|
} else {
|
|
fout_version("\n\n$AI Profile: %s", The_mission.ai_profile->profile_name);
|
|
}
|
|
fso_comment_pop();
|
|
|
|
// EatThePath's lighting profiles
|
|
if (!The_mission.lighting_profile_name.empty() && The_mission.lighting_profile_name != lighting_profiles::default_name()) {
|
|
fso_comment_push(";;FSO 23.1.0;;");
|
|
if (optional_string_fred("$Lighting Profile:")) {
|
|
parse_comments(2);
|
|
fout(" %s", The_mission.lighting_profile_name.c_str());
|
|
} else {
|
|
fout_version("\n\n$Lighting Profile: %s", The_mission.lighting_profile_name.c_str());
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 23.1.0;; $Lighting Profile:");
|
|
}
|
|
|
|
// sound environment (EFX/EAX) - taylor
|
|
sound_env* m_env = &The_mission.sound_environment;
|
|
if ((m_env->id >= 0) && (m_env->id < static_cast<int>(EFX_presets.size()))) {
|
|
EFXREVERBPROPERTIES* efx_prop = &EFX_presets[m_env->id];
|
|
|
|
fso_comment_push(";;FSO 3.6.12;;");
|
|
|
|
fout_version("\n\n$Sound Environment: %s", efx_prop->name.c_str());
|
|
|
|
if (m_env->volume != efx_prop->flGain) {
|
|
fout_version("\n+Volume: %f", m_env->volume);
|
|
}
|
|
|
|
if (m_env->damping != efx_prop->flDecayHFRatio) {
|
|
fout_version("\n+Damping: %f", m_env->damping);
|
|
}
|
|
|
|
if (m_env->decay != efx_prop->flDecayTime) {
|
|
fout_version("\n+Decay Time: %f", m_env->decay);
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
void Fred_mission_save::save_mission_internal(const char* pathname)
|
|
{
|
|
time_t currentTime;
|
|
time(¤tTime);
|
|
auto timeinfo = localtime(¤tTime);
|
|
|
|
time_to_mission_info_string(timeinfo, The_mission.modified, DATE_TIME_LENGTH - 1);
|
|
|
|
// Migrate the version!
|
|
The_mission.required_fso_version = MISSION_VERSION;
|
|
|
|
// Additional incremental version update for some features
|
|
auto version_23_3 = gameversion::version(23, 3);
|
|
auto version_24_1 = gameversion::version(24, 1);
|
|
auto version_24_3 = gameversion::version(24, 3);
|
|
auto version_25_1 = gameversion::version(25, 1);
|
|
if (MISSION_VERSION >= version_25_1) {
|
|
Warning(LOCATION,
|
|
"Notify an SCP coder: now that the required mission version is at least 25.1, the check_for_25_1_data(), "
|
|
"check_for_24_3_data(), check_for_24_1_data(), and check_for_23_3_data() code can be removed");
|
|
} else if (check_for_25_1_data()) {
|
|
The_mission.required_fso_version = version_25_1;
|
|
} else if (MISSION_VERSION >= version_24_3) {
|
|
Warning(LOCATION, "Notify an SCP coder: now that the required mission version is at least 24.3, the "
|
|
"check_for_24_3_data(), check_for_24_1_data(), and check_for_23_3_data() code can be removed");
|
|
} else if (check_for_24_3_data()) {
|
|
The_mission.required_fso_version = version_24_3;
|
|
} else if (MISSION_VERSION >= version_24_1) {
|
|
Warning(LOCATION,
|
|
"Notify an SCP coder: now that the required mission version is at least 24.1, the check_for_24_1_data() "
|
|
"and check_for_23_3_data() code can be removed");
|
|
} else if (check_for_24_1_data()) {
|
|
The_mission.required_fso_version = version_24_1;
|
|
} else if (MISSION_VERSION >= version_23_3) {
|
|
Warning(LOCATION,
|
|
"Notify an SCP coder: now that the required mission version is at least 23.3, the check_for_23_3_data() "
|
|
"code can be removed");
|
|
} else if (check_for_23_3_data()) {
|
|
The_mission.required_fso_version = version_23_3;
|
|
}
|
|
|
|
reset_parse();
|
|
raw_ptr = Parse_text_raw;
|
|
fred_parse_flag = 0;
|
|
|
|
fp = cfopen(pathname, "wt", CF_TYPE_MISSIONS);
|
|
if (!fp) {
|
|
nprintf(("Error", "Can't open mission file to save.\n"));
|
|
err = -1;
|
|
return;
|
|
}
|
|
|
|
if (!save_config.template_info.title.empty())
|
|
save_template_info();
|
|
|
|
// Goober5000
|
|
convert_special_tags_to_retail();
|
|
|
|
if (save_mission_info()) {
|
|
err = -2;
|
|
} else if (save_plot_info()) {
|
|
err = -3;
|
|
} else if (save_variables()) {
|
|
err = -3;
|
|
} else if (save_containers()) {
|
|
err = -3;
|
|
// } else if (save_briefing_info()) {
|
|
// err = -4;
|
|
} else if (save_cutscenes()) {
|
|
err = -4;
|
|
} else if (save_fiction()) {
|
|
err = -3;
|
|
} else if (save_cmd_briefs()) {
|
|
err = -4;
|
|
} else if (save_briefing()) {
|
|
err = -4;
|
|
} else if (save_debriefing()) {
|
|
err = -5;
|
|
} else if (save_players()) {
|
|
err = -6;
|
|
} else if (save_objects()) {
|
|
err = -7;
|
|
} else if (save_wings()) {
|
|
err = -8;
|
|
} else if (save_props()) {
|
|
err = -18;
|
|
} else if (save_events()) {
|
|
err = -9;
|
|
} else if (save_goals()) {
|
|
err = -10;
|
|
} else if (save_waypoints()) {
|
|
err = -11;
|
|
} else if (save_messages()) {
|
|
err = -12;
|
|
} else if (save_reinforcements()) {
|
|
err = -13;
|
|
} else if (save_bitmaps()) {
|
|
err = -14;
|
|
} else if (save_asteroid_fields()) {
|
|
err = -15;
|
|
} else if (save_music()) {
|
|
err = -16;
|
|
} else if (save_custom_data()) {
|
|
err = -17;
|
|
} else {
|
|
required_string_fred("#End");
|
|
parse_comments(2);
|
|
token_found = NULL;
|
|
parse_comments();
|
|
fout("\n");
|
|
}
|
|
|
|
cfclose(fp);
|
|
if (err)
|
|
mprintf(("Mission saving error code #%d\n", err));
|
|
}
|
|
|
|
int Fred_mission_save::save_music()
|
|
{
|
|
required_string_fred("#Music");
|
|
parse_comments(2);
|
|
|
|
required_string_fred("$Event Music:");
|
|
parse_comments(2);
|
|
if (Current_soundtrack_num < 0)
|
|
fout(" None");
|
|
else
|
|
fout(" %s", Soundtracks[Current_soundtrack_num].name);
|
|
|
|
// Goober5000 - save using the special comment prefix
|
|
if (stricmp(The_mission.substitute_event_music_name, "None")) {
|
|
fso_comment_push(";;FSO 3.6.9;;");
|
|
if (optional_string_fred("$Substitute Event Music:")) {
|
|
parse_comments(1);
|
|
fout(" %s", The_mission.substitute_event_music_name);
|
|
} else {
|
|
fout_version("\n$Substitute Event Music: %s", The_mission.substitute_event_music_name);
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.9;; $Substitute Event Music:");
|
|
}
|
|
|
|
required_string_fred("$Briefing Music:");
|
|
parse_comments();
|
|
if (Mission_music[SCORE_BRIEFING] < 0)
|
|
fout(" None");
|
|
else
|
|
fout(" %s", Spooled_music[Mission_music[SCORE_BRIEFING]].name);
|
|
|
|
// Goober5000 - save using the special comment prefix
|
|
if (stricmp(The_mission.substitute_briefing_music_name, "None")) {
|
|
fso_comment_push(";;FSO 3.6.9;;");
|
|
if (optional_string_fred("$Substitute Briefing Music:")) {
|
|
parse_comments(1);
|
|
fout(" %s", The_mission.substitute_briefing_music_name);
|
|
} else {
|
|
fout_version("\n$Substitute Briefing Music: %s", The_mission.substitute_briefing_music_name);
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.9;; $Substitute Briefing Music:");
|
|
}
|
|
|
|
// avoid keeping the old one around
|
|
bypass_comment(";;FSO 3.6.8;; $Substitute Music:");
|
|
|
|
// old stuff
|
|
if (Mission_music[SCORE_DEBRIEFING_SUCCESS] != event_music_get_spooled_music_index("Success")) {
|
|
if (optional_string_fred("$Debriefing Success Music:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n$Debriefing Success Music:");
|
|
}
|
|
fout(" %s",
|
|
Mission_music[SCORE_DEBRIEFING_SUCCESS] < 0 ? "None"
|
|
: Spooled_music[Mission_music[SCORE_DEBRIEFING_SUCCESS]].name);
|
|
}
|
|
if (save_config.save_format != MissionFormat::RETAIL &&
|
|
Mission_music[SCORE_DEBRIEFING_AVERAGE] != event_music_get_spooled_music_index("Average")) {
|
|
if (optional_string_fred("$Debriefing Average Music:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n$Debriefing Average Music:");
|
|
}
|
|
fout(" %s",
|
|
Mission_music[SCORE_DEBRIEFING_AVERAGE] < 0 ? "None"
|
|
: Spooled_music[Mission_music[SCORE_DEBRIEFING_AVERAGE]].name);
|
|
}
|
|
if (Mission_music[SCORE_DEBRIEFING_FAILURE] != event_music_get_spooled_music_index("Failure")) {
|
|
if (optional_string_fred("$Debriefing Fail Music:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout("\n$Debriefing Fail Music:");
|
|
}
|
|
fout(" %s",
|
|
Mission_music[SCORE_DEBRIEFING_FAILURE] < 0 ? "None"
|
|
: Spooled_music[Mission_music[SCORE_DEBRIEFING_FAILURE]].name);
|
|
}
|
|
|
|
// Goober5000 - save using the special comment prefix
|
|
if (mission_has_fiction() && Mission_music[SCORE_FICTION_VIEWER] >= 0) {
|
|
fso_comment_push(";;FSO 3.6.11;;");
|
|
if (optional_string_fred("$Fiction Viewer Music:")) {
|
|
parse_comments(1);
|
|
fout(" %s", Spooled_music[Mission_music[SCORE_FICTION_VIEWER]].name);
|
|
} else {
|
|
fout_version("\n$Fiction Viewer Music: %s", Spooled_music[Mission_music[SCORE_FICTION_VIEWER]].name);
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.11;; $Fiction Viewer Music:");
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_custom_data()
|
|
{
|
|
if (save_config.save_format != MissionFormat::RETAIL &&
|
|
(!The_mission.custom_data.empty() || !The_mission.custom_strings.empty())) {
|
|
if (optional_string_fred("#Custom Data", "#End")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n\n#Custom Data");
|
|
}
|
|
|
|
if (The_mission.custom_data.size() > 0) {
|
|
if (optional_string_fred("$begin_data_map")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n\n$begin_data_map");
|
|
}
|
|
|
|
for (const auto& pair : The_mission.custom_data) {
|
|
fout("\n+Val: %s %s", pair.first.c_str(), pair.second.c_str());
|
|
}
|
|
|
|
if (optional_string_fred("$end_data_map")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n$end_data_map");
|
|
}
|
|
}
|
|
|
|
if (The_mission.custom_strings.size() > 0) {
|
|
required_string_fred("$begin_custom_strings");
|
|
parse_comments(2);
|
|
|
|
for (const auto& cs : The_mission.custom_strings) {
|
|
required_string_fred("$Name:");
|
|
parse_comments(2);
|
|
fout(" %s", cs.name.c_str());
|
|
|
|
required_string_fred("+Value:");
|
|
parse_comments();
|
|
fout(" %s", cs.value.c_str());
|
|
|
|
auto copy = cs.text;
|
|
lcl_fred_replace_stuff(copy);
|
|
required_string_fred("+String:");
|
|
parse_comments();
|
|
fout(" %s", copy.c_str());
|
|
|
|
required_string_fred("$end_multi_text");
|
|
parse_comments();
|
|
}
|
|
required_string_fred("$end_custom_strings");
|
|
parse_comments(2);
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_warp_params(WarpDirection direction, ship* shipp)
|
|
{
|
|
if (save_config.save_format == MissionFormat::RETAIL)
|
|
return err;
|
|
|
|
// for writing to file; c.f. parse_warp_params
|
|
const char* prefix = (direction == WarpDirection::WARP_IN) ? "$Warpin" : "$Warpout";
|
|
|
|
WarpParams *shipp_params, *sip_params;
|
|
if (direction == WarpDirection::WARP_IN) {
|
|
// if exactly the same params used, no need to output anything
|
|
if (shipp->warpin_params_index == Ship_info[shipp->ship_info_index].warpin_params_index)
|
|
return err;
|
|
|
|
shipp_params = &Warp_params[shipp->warpin_params_index];
|
|
sip_params = &Warp_params[Ship_info[shipp->ship_info_index].warpin_params_index];
|
|
} else {
|
|
// if exactly the same params used, no need to output anything
|
|
if (shipp->warpout_params_index == Ship_info[shipp->ship_info_index].warpout_params_index)
|
|
return err;
|
|
|
|
shipp_params = &Warp_params[shipp->warpout_params_index];
|
|
sip_params = &Warp_params[Ship_info[shipp->ship_info_index].warpout_params_index];
|
|
}
|
|
|
|
if (shipp_params->warp_type != sip_params->warp_type) {
|
|
// is it a fireball?
|
|
if (shipp_params->warp_type & WT_DEFAULT_WITH_FIREBALL) {
|
|
fout("\n%s type: %s", prefix, Fireball_info[shipp_params->warp_type & WT_FLAG_MASK].unique_id);
|
|
}
|
|
// probably a warp type
|
|
else if (shipp_params->warp_type >= 0 && shipp_params->warp_type < Num_warp_types) {
|
|
fout("\n%s type: %s", prefix, Warp_types[shipp_params->warp_type]);
|
|
}
|
|
}
|
|
|
|
if (shipp_params->snd_start != sip_params->snd_start) {
|
|
if (shipp_params->snd_start.isValid())
|
|
fout("\n%s Start Sound: %s", prefix, gamesnd_get_game_sound(shipp_params->snd_start)->name.c_str());
|
|
}
|
|
|
|
if (shipp_params->snd_end != sip_params->snd_end) {
|
|
if (shipp_params->snd_end.isValid())
|
|
fout("\n%s End Sound: %s", prefix, gamesnd_get_game_sound(shipp_params->snd_end)->name.c_str());
|
|
}
|
|
|
|
if (direction == WarpDirection::WARP_OUT && shipp_params->warpout_engage_time != sip_params->warpout_engage_time) {
|
|
if (shipp_params->warpout_engage_time > 0)
|
|
fout("\n%s engage time: %.2f", prefix, i2fl(shipp_params->warpout_engage_time) / 1000.0f);
|
|
}
|
|
|
|
if (shipp_params->speed != sip_params->speed) {
|
|
if (shipp_params->speed > 0.0f)
|
|
fout("\n%s speed: %.2f", prefix, shipp_params->speed);
|
|
}
|
|
|
|
if (shipp_params->time != sip_params->time) {
|
|
if (shipp_params->time > 0)
|
|
fout("\n%s time: %.2f", prefix, i2fl(shipp_params->time) / 1000.0f);
|
|
}
|
|
|
|
if (shipp_params->accel_exp != sip_params->accel_exp) {
|
|
if (shipp_params->accel_exp > 0.0f)
|
|
fout("\n%s %s exp: %.2f",
|
|
prefix,
|
|
direction == WarpDirection::WARP_IN ? "decel" : "accel",
|
|
shipp_params->accel_exp);
|
|
}
|
|
|
|
if (shipp_params->radius != sip_params->radius) {
|
|
if (shipp_params->radius > 0.0f)
|
|
fout("\n%s radius: %.2f", prefix, shipp_params->radius);
|
|
}
|
|
|
|
if (stricmp(shipp_params->anim, sip_params->anim) != 0) {
|
|
if (strlen(shipp_params->anim) > 0)
|
|
fout("\n%s animation: %s", prefix, shipp_params->anim);
|
|
}
|
|
|
|
if (shipp_params->supercap_warp_physics != sip_params->supercap_warp_physics) {
|
|
fout("\n$Supercap warp%s physics: %s",
|
|
direction == WarpDirection::WARP_IN ? "in" : "out",
|
|
shipp_params->supercap_warp_physics ? "YES" : "NO");
|
|
}
|
|
|
|
if (direction == WarpDirection::WARP_OUT &&
|
|
shipp_params->warpout_player_speed != sip_params->warpout_player_speed) {
|
|
if (shipp_params->warpout_player_speed > 0.0f)
|
|
fout("\n$Player warpout speed: %.2f", shipp_params->warpout_player_speed);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
// Per-ship field handling here MUST stay in sync with:
|
|
// parse_create_object_sub() in missionparse.cpp
|
|
// clone_ship_instance_data() in missioneditor/objectduplication.cpp
|
|
// When you add a new editable ship field, touch all three.
|
|
int Fred_mission_save::save_objects()
|
|
{
|
|
SCP_string sexp_out;
|
|
int i, z, count;
|
|
object* objp;
|
|
ship* shipp;
|
|
ship_info* sip;
|
|
|
|
required_string_fred("#Objects");
|
|
parse_comments(2);
|
|
fout("\t\t;! %d total\n", ship_get_num_ships());
|
|
|
|
for (i = count = 0; i < MAX_SHIPS; i++) {
|
|
if (Ships[i].objnum < 0) {
|
|
continue;
|
|
}
|
|
|
|
auto j = Objects[Ships[i].objnum].type;
|
|
if ((j != OBJ_SHIP) && (j != OBJ_START)) {
|
|
continue;
|
|
}
|
|
|
|
shipp = &Ships[i];
|
|
objp = &Objects[shipp->objnum];
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
required_string_either_fred("$Name:", "#Wings");
|
|
required_string_fred("$Name:");
|
|
parse_comments(count ? 2 : 1);
|
|
fout(" %s\t\t;! Object #%d", shipp->ship_name, i);
|
|
|
|
count++;
|
|
|
|
// Display name
|
|
// (If we are always saving display names, the "only/not written" comments do not apply)
|
|
// The display name is only written if it currently exists and the ship is not part of a wing, to avoid introducing inconsistencies
|
|
if (save_config.save_format != MissionFormat::RETAIL && ((save_config.always_save_display_names && shipp->wingnum < 0) || shipp->has_display_name())) {
|
|
char truncated_name[NAME_LENGTH];
|
|
strcpy_s(truncated_name, shipp->ship_name);
|
|
end_string_at_first_hash_symbol(truncated_name);
|
|
|
|
// Also, the display name is not written if it's just the truncation of the name at the hash
|
|
if ((save_config.always_save_display_names && shipp->wingnum < 0) || strcmp(shipp->get_display_name(), truncated_name) != 0) {
|
|
if (optional_string_fred("$Display Name:", "$Class:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n$Display Name:");
|
|
}
|
|
fout_ext(" ", "%s", shipp->get_display_name());
|
|
}
|
|
}
|
|
|
|
required_string_fred("\n$Class:");
|
|
parse_comments(0);
|
|
fout(" %s", Ship_info[shipp->ship_info_index].name);
|
|
|
|
// alt classes stuff
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
for (auto& ii : shipp->s_alt_classes) {
|
|
// is this a variable?
|
|
if (ii.variable_index != -1) {
|
|
fout("\n$Alt Ship Class: @%s", Sexp_variables[ii.variable_index].variable_name);
|
|
} else {
|
|
fout("\n$Alt Ship Class: \"%s\"", Ship_info[ii.ship_class].name);
|
|
}
|
|
|
|
// default class?
|
|
if (ii.default_to_this_class) {
|
|
fout("\n+Default Class:");
|
|
}
|
|
}
|
|
}
|
|
|
|
Assertion(save_config.fred_alt_names != nullptr, "save_config.fred_alt_names is null!");
|
|
|
|
// optional alternate type name
|
|
if (strlen(save_config.fred_alt_names[i])) {
|
|
if (optional_string_fred("$Alt:", "$Team:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n$Alt:");
|
|
}
|
|
fout(" %s", save_config.fred_alt_names[i]);
|
|
}
|
|
|
|
Assertion(save_config.fred_callsigns != nullptr, "save_config.fred_callsigns is null!");
|
|
|
|
// optional callsign
|
|
if (save_config.save_format != MissionFormat::RETAIL && strlen(save_config.fred_callsigns[i])) {
|
|
if (optional_string_fred("$Callsign:", "$Team:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n$Callsign:");
|
|
}
|
|
fout(" %s", save_config.fred_callsigns[i]);
|
|
}
|
|
|
|
required_string_fred("$Team:");
|
|
parse_comments();
|
|
fout(" %s", Iff_info[shipp->team].iff_name);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL && Ship_info[shipp->ship_info_index].uses_team_colors) {
|
|
required_string_fred("$Team Color Setting:");
|
|
parse_comments();
|
|
fout(" %s", shipp->team_name.c_str());
|
|
}
|
|
|
|
required_string_fred("$Location:");
|
|
parse_comments();
|
|
save_vector(Objects[shipp->objnum].pos);
|
|
|
|
required_string_fred("$Orientation:");
|
|
parse_comments();
|
|
save_matrix(Objects[shipp->objnum].orient);
|
|
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
required_string_fred("$IFF:");
|
|
parse_comments();
|
|
fout(" %s", "IFF 1");
|
|
|
|
required_string_fred("$AI Behavior:");
|
|
parse_comments();
|
|
fout(" %s", Ai_behavior_names[AIM_NONE]);
|
|
}
|
|
|
|
if (shipp->weapons.ai_class != Ship_info[shipp->ship_info_index].ai_class) {
|
|
if (optional_string_fred("+AI Class:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+AI Class:");
|
|
|
|
fout(" %s", Ai_class_names[shipp->weapons.ai_class]);
|
|
}
|
|
|
|
save_ai_goals(Ai_info[shipp->ai_index].goals, i);
|
|
|
|
// XSTR
|
|
required_string_fred("$Cargo 1:");
|
|
parse_comments();
|
|
fout_ext(" ", "%s", Cargo_names[shipp->cargo1]);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (shipp->cargo_title[0] != '\0') {
|
|
if (optional_string_fred("$Cargo Title:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n$Cargo Title:");
|
|
}
|
|
fout_ext(nullptr, "%s", shipp->cargo_title);
|
|
}
|
|
}
|
|
|
|
save_common_object_data(&Objects[shipp->objnum], &Ships[i]);
|
|
|
|
if (shipp->wingnum >= 0) {
|
|
shipp->arrival_location = ArrivalLocation::AT_LOCATION;
|
|
}
|
|
|
|
required_string_fred("$Arrival Location:");
|
|
parse_comments();
|
|
fout(" %s", Arrival_location_names[static_cast<int>(shipp->arrival_location)]);
|
|
|
|
if (shipp->arrival_location != ArrivalLocation::AT_LOCATION) {
|
|
if (optional_string_fred("+Arrival Distance:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Arrival Distance:");
|
|
}
|
|
|
|
fout(" %d", shipp->arrival_distance);
|
|
if (optional_string_fred("$Arrival Anchor:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n$Arrival Anchor:");
|
|
}
|
|
|
|
z = shipp->arrival_anchor.value();
|
|
if (z < 0) {
|
|
fout(" <error>");
|
|
} else if (z & ANCHOR_SPECIAL_ARRIVAL) {
|
|
// get name
|
|
char tmp[NAME_LENGTH + 15];
|
|
stuff_special_arrival_anchor_name(tmp, z, save_config.save_format == MissionFormat::RETAIL);
|
|
|
|
// save it
|
|
fout(" %s", tmp);
|
|
} else {
|
|
auto anchor_entry = ship_registry_get(z);
|
|
fout(" %s", anchor_entry ? anchor_entry->name : "<error>");
|
|
}
|
|
}
|
|
|
|
// Goober5000
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if ((shipp->arrival_location == ArrivalLocation::FROM_DOCK_BAY) && (shipp->arrival_path_mask > 0)) {
|
|
auto anchor_entry = ship_registry_get(shipp->arrival_anchor);
|
|
Assertion(anchor_entry, "Could not find arrival anchor for ship %s!", shipp->ship_name);
|
|
auto pm = model_get(anchor_entry->sip()->model_num);
|
|
|
|
fout("\n+Arrival Paths: ( ");
|
|
|
|
for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
|
|
if (shipp->arrival_path_mask & (1 << n)) {
|
|
fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
}
|
|
|
|
if (shipp->arrival_delay) {
|
|
if (optional_string_fred("+Arrival Delay:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Arrival Delay:");
|
|
|
|
fout(" %d", shipp->arrival_delay);
|
|
}
|
|
|
|
required_string_fred("$Arrival Cue:");
|
|
parse_comments();
|
|
convert_sexp_to_string(sexp_out, shipp->arrival_cue, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
if (shipp->wingnum >= 0) {
|
|
shipp->departure_location = DepartureLocation::AT_LOCATION;
|
|
}
|
|
|
|
required_string_fred("$Departure Location:");
|
|
parse_comments();
|
|
fout(" %s", Departure_location_names[static_cast<int>(shipp->departure_location)]);
|
|
|
|
if (shipp->departure_location != DepartureLocation::AT_LOCATION) {
|
|
required_string_fred("$Departure Anchor:");
|
|
parse_comments();
|
|
|
|
auto anchor_entry = ship_registry_get(shipp->departure_anchor);
|
|
fout(" %s", anchor_entry ? anchor_entry->name : "<error>");
|
|
}
|
|
|
|
// Goober5000
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if ((shipp->departure_location == DepartureLocation::TO_DOCK_BAY) && (shipp->departure_path_mask > 0)) {
|
|
auto anchor_entry = ship_registry_get(shipp->departure_anchor);
|
|
Assertion(anchor_entry, "Could not find departure anchor for ship %s!", shipp->ship_name);
|
|
auto pm = model_get(anchor_entry->sip()->model_num);
|
|
|
|
fout("\n+Departure Paths: ( ");
|
|
|
|
for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
|
|
if (shipp->departure_path_mask & (1 << n)) {
|
|
fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
}
|
|
|
|
if (shipp->departure_delay) {
|
|
if (optional_string_fred("+Departure delay:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Departure delay:");
|
|
|
|
fout(" %d", shipp->departure_delay);
|
|
}
|
|
|
|
required_string_fred("$Departure Cue:");
|
|
parse_comments();
|
|
convert_sexp_to_string(sexp_out, shipp->departure_cue, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
save_warp_params(WarpDirection::WARP_IN, shipp);
|
|
save_warp_params(WarpDirection::WARP_OUT, shipp);
|
|
|
|
required_string_fred("$Determination:");
|
|
parse_comments();
|
|
fout(" 10"); // dummy value for backwards compatibility
|
|
|
|
if (optional_string_fred("+Flags:", "$Name:")) {
|
|
parse_comments();
|
|
fout(" (");
|
|
} else
|
|
fout("\n+Flags: (");
|
|
|
|
if (shipp->flags[Ship::Ship_Flags::Cargo_revealed])
|
|
fout(" \"cargo-known\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Ignore_count])
|
|
fout(" \"ignore-count\"");
|
|
if (objp->flags[Object::Object_Flags::Protected])
|
|
fout(" \"protect-ship\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Reinforcement])
|
|
fout(" \"reinforcement\"");
|
|
if (objp->flags[Object::Object_Flags::No_shields] &&
|
|
!sip->flags[Ship::Info_Flags::Intrinsic_no_shields]) // don't save no-shields for intrinsic-no-shields ships
|
|
fout(" \"no-shields\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Escort])
|
|
fout(" \"escort\"");
|
|
if (objp->type == OBJ_START)
|
|
fout(" \"player-start\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_arrival_music])
|
|
fout(" \"no-arrival-music\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_arrival_warp])
|
|
fout(" \"no-arrival-warp\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_departure_warp])
|
|
fout(" \"no-departure-warp\"");
|
|
if (Objects[shipp->objnum].flags[Object::Object_Flags::Invulnerable])
|
|
fout(" \"invulnerable\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Hidden_from_sensors])
|
|
fout(" \"hidden-from-sensors\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Scannable])
|
|
fout(" \"scannable\"");
|
|
if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze])
|
|
fout(" \"kamikaze\"");
|
|
if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::No_dynamic])
|
|
fout(" \"no-dynamic\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Red_alert_store_status])
|
|
fout(" \"red-alert-carry\"");
|
|
if (objp->flags[Object::Object_Flags::Beam_protected])
|
|
fout(" \"beam-protect-ship\"");
|
|
if (objp->flags[Object::Object_Flags::Flak_protected])
|
|
fout(" \"flak-protect-ship\"");
|
|
if (objp->flags[Object::Object_Flags::Laser_protected])
|
|
fout(" \"laser-protect-ship\"");
|
|
if (objp->flags[Object::Object_Flags::Missile_protected])
|
|
fout(" \"missile-protect-ship\"");
|
|
if (shipp->ship_guardian_threshold != 0)
|
|
fout(" \"guardian\"");
|
|
if (objp->flags[Object::Object_Flags::Special_warpin])
|
|
fout(" \"special-warp\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Vaporize])
|
|
fout(" \"vaporize\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Stealth])
|
|
fout(" \"stealth\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Friendly_stealth_invis])
|
|
fout(" \"friendly-stealth-invisible\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Dont_collide_invis])
|
|
fout(" \"don't-collide-invisible\"");
|
|
// for compatibility reasons ship locked or weapons locked are saved as both locked in retail mode
|
|
if ((save_config.save_format == MissionFormat::RETAIL) &&
|
|
((shipp->flags[Ship::Ship_Flags::Ship_locked]) || (shipp->flags[Ship::Ship_Flags::Weapons_locked])))
|
|
fout(" \"locked\"");
|
|
fout(" )");
|
|
|
|
// flags2 added by Goober5000 --------------------------------
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (optional_string_fred("+Flags2:", "$Name:")) {
|
|
parse_comments();
|
|
fout(" (");
|
|
} else
|
|
fout("\n+Flags2: (");
|
|
|
|
if (shipp->flags[Ship::Ship_Flags::Primitive_sensors])
|
|
fout(" \"primitive-sensors\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_subspace_drive])
|
|
fout(" \"no-subspace-drive\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Navpoint_carry])
|
|
fout(" \"nav-carry-status\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Affected_by_gravity])
|
|
fout(" \"affected-by-gravity\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Toggle_subsystem_scanning])
|
|
fout(" \"toggle-subsystem-scanning\"");
|
|
if (objp->flags[Object::Object_Flags::Targetable_as_bomb])
|
|
fout(" \"targetable-as-bomb\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_builtin_messages])
|
|
fout(" \"no-builtin-messages\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Primaries_locked])
|
|
fout(" \"primaries-locked\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Secondaries_locked])
|
|
fout(" \"secondaries-locked\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_death_scream])
|
|
fout(" \"no-death-scream\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Always_death_scream])
|
|
fout(" \"always-death-scream\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Navpoint_needslink])
|
|
fout(" \"nav-needslink\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Hide_ship_name])
|
|
fout(" \"hide-ship-name\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Set_class_dynamically])
|
|
fout(" \"set-class-dynamically\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Lock_all_turrets_initially])
|
|
fout(" \"lock-all-turrets\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Afterburner_locked])
|
|
fout(" \"afterburners-locked\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Force_shields_on])
|
|
fout(" \"force-shields-on\"");
|
|
if (objp->flags[Object::Object_Flags::Dont_change_position])
|
|
fout(" \"don't-change-position\"");
|
|
if (objp->flags[Object::Object_Flags::Dont_change_orientation])
|
|
fout(" \"don't-change-orientation\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_ets])
|
|
fout(" \"no-ets\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Cloaked])
|
|
fout(" \"cloaked\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Ship_locked])
|
|
fout(" \"ship-locked\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Weapons_locked])
|
|
fout(" \"weapons-locked\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Scramble_messages])
|
|
fout(" \"scramble-messages\"");
|
|
if (!(objp->flags[Object::Object_Flags::Collides]))
|
|
fout(" \"no_collide\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_disabled_self_destruct])
|
|
fout(" \"no-disabled-self-destruct\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Same_arrival_warp_when_docked])
|
|
fout(" \"same-arrival-warp-when-docked\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Same_departure_warp_when_docked])
|
|
fout(" \"same-departure-warp-when-docked\"");
|
|
if (objp->flags[Object::Object_Flags::Attackable_if_no_collide])
|
|
fout(" \"ai-attackable-if-no-collide\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Fail_sound_locked_primary])
|
|
fout(" \"fail-sound-locked-primary\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Fail_sound_locked_secondary])
|
|
fout(" \"fail-sound-locked-secondary\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Aspect_immune])
|
|
fout(" \"aspect-immune\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Cannot_perform_scan_hide_cargo])
|
|
fout(" \"cannot-perform-scan-hide-cargo\"");
|
|
if (shipp->flags[Ship::Ship_Flags::Cannot_perform_scan_show_cargo])
|
|
fout(" \"cannot-perform-scan-show-cargo\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_targeting_limits])
|
|
fout(" \"no-targeting-limits\"");
|
|
if (shipp->flags[Ship::Ship_Flags::No_scanned_cargo])
|
|
fout(" \"no-scanned-cargo\"");
|
|
fout(" )");
|
|
}
|
|
// -----------------------------------------------------------
|
|
|
|
fout("\n+Respawn priority: %d", shipp->respawn_priority); // HA! Newline added by Goober5000
|
|
|
|
if (shipp->flags[Ship::Ship_Flags::Escort]) {
|
|
if (optional_string_fred("+Escort priority:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Escort priority:");
|
|
}
|
|
|
|
fout(" %d", shipp->escort_priority);
|
|
}
|
|
|
|
// Custom Guardian Thrshold
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (shipp->ship_guardian_threshold != 0) {
|
|
if (optional_string_fred("+Guardian Threshold:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Guardian Threshold:");
|
|
}
|
|
|
|
fout(" %d", shipp->ship_guardian_threshold);
|
|
}
|
|
}
|
|
|
|
// special explosions
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (shipp->use_special_explosion) {
|
|
fso_comment_push(";;FSO 3.6.13;;");
|
|
if (optional_string_fred("$Special Explosion:", "$Name:")) {
|
|
parse_comments();
|
|
|
|
required_string_fred("+Special Exp Damage:");
|
|
parse_comments();
|
|
fout(" %d", shipp->special_exp_damage);
|
|
|
|
required_string_fred("+Special Exp Blast:");
|
|
parse_comments();
|
|
fout(" %d", shipp->special_exp_blast);
|
|
|
|
required_string_fred("+Special Exp Inner Radius:");
|
|
parse_comments();
|
|
fout(" %d", shipp->special_exp_inner);
|
|
|
|
required_string_fred("+Special Exp Outer Radius:");
|
|
parse_comments();
|
|
fout(" %d", shipp->special_exp_outer);
|
|
|
|
if (shipp->use_shockwave && (shipp->special_exp_shockwave_speed > 0)) {
|
|
optional_string_fred("+Special Exp Shockwave Speed:");
|
|
parse_comments();
|
|
fout(" %d", shipp->special_exp_shockwave_speed);
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.13;; +Special Exp Shockwave Speed:", "$Name:");
|
|
}
|
|
|
|
if (shipp->special_exp_deathroll_time > 0) {
|
|
optional_string_fred("+Special Exp Death Roll Time:");
|
|
parse_comments();
|
|
fout(" %d", shipp->special_exp_deathroll_time);
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.13;; +Special Exp Death Roll Time:", "$Name:");
|
|
}
|
|
} else {
|
|
fout_version("\n$Special Explosion:");
|
|
|
|
fout_version("\n+Special Exp Damage:");
|
|
fout(" %d", shipp->special_exp_damage);
|
|
|
|
fout_version("\n+Special Exp Blast:");
|
|
fout(" %d", shipp->special_exp_blast);
|
|
|
|
fout_version("\n+Special Exp Inner Radius:");
|
|
fout(" %d", shipp->special_exp_inner);
|
|
|
|
fout_version("\n+Special Exp Outer Radius:");
|
|
fout(" %d", shipp->special_exp_outer);
|
|
|
|
if (shipp->use_shockwave && (shipp->special_exp_shockwave_speed > 0)) {
|
|
fout_version("\n+Special Exp Shockwave Speed:");
|
|
fout(" %d", shipp->special_exp_shockwave_speed);
|
|
}
|
|
|
|
if (shipp->special_exp_deathroll_time > 0) {
|
|
fout_version("\n+Special Exp Death Roll Time:");
|
|
fout(" %d", shipp->special_exp_deathroll_time);
|
|
}
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.13;; +Special Exp Shockwave Speed:", "$Name:");
|
|
bypass_comment(";;FSO 3.6.13;; +Special Exp Death Roll Time:", "$Name:");
|
|
}
|
|
}
|
|
// retail format special explosions
|
|
else {
|
|
if (shipp->use_special_explosion) {
|
|
int special_exp_index;
|
|
|
|
if (has_special_explosion_block_index(&Ships[i], &special_exp_index)) {
|
|
fout("\n+Special Exp index:");
|
|
fout(" %d", special_exp_index);
|
|
} else {
|
|
SCP_string msg = "You are saving in the retail mission format, but the mission has too many "
|
|
"special explosions defined.";
|
|
msg += shipp->ship_name;
|
|
msg += " has therefore lost any special explosion data that was defined for it. Either remove "
|
|
"special explosions or SEXP variables if you need it to have one.";
|
|
Message(os::dialogs::MESSAGEBOX_ERROR, msg.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Goober5000 ------------------------------------------------
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (shipp->special_hitpoints) {
|
|
fso_comment_push(";;FSO 3.6.13;;");
|
|
if (optional_string_fred("+Special Hitpoints:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Special Hitpoints:");
|
|
}
|
|
fso_comment_pop();
|
|
|
|
fout(" %d", shipp->special_hitpoints);
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.13;; +Special Hitpoints:", "$Name:");
|
|
}
|
|
|
|
if (shipp->special_shield >= 0) {
|
|
fso_comment_push(";;FSO 3.6.13;;");
|
|
if (optional_string_fred("+Special Shield Points:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Special Shield Points:");
|
|
}
|
|
fso_comment_pop();
|
|
|
|
fout(" %d", shipp->special_shield);
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.13;; +Special Shield Points:", "$Name:");
|
|
}
|
|
}
|
|
// -----------------------------------------------------------
|
|
|
|
if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze]) {
|
|
if (optional_string_fred("+Kamikaze Damage:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Kamikaze Damage:");
|
|
}
|
|
|
|
fout(" %d", Ai_info[shipp->ai_index].kamikaze_damage);
|
|
}
|
|
|
|
if (shipp->hotkey != -1) {
|
|
if (optional_string_fred("+Hotkey:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Hotkey:");
|
|
}
|
|
|
|
fout(" %d", shipp->hotkey);
|
|
}
|
|
|
|
// mwa -- new code to save off information about initially docked ships.
|
|
// Goober5000 - newer code to save off information about initially docked ships. ;)
|
|
if (object_is_docked(&Objects[shipp->objnum])) {
|
|
// possible incompatibility
|
|
if (save_config.save_format == MissionFormat::RETAIL && !dock_check_docked_one_on_one(&Objects[shipp->objnum])) {
|
|
static bool warned = false;
|
|
if (!warned) {
|
|
SCP_string msg = "You are saving in the retail mission format, but \"";
|
|
msg += shipp->ship_name;
|
|
msg += "\" is docked to more than one ship. If you wish to run this mission in retail, you should "
|
|
"remove the additional ships and save the mission again.";
|
|
Message(os::dialogs::MESSAGEBOX_ERROR, msg.c_str());
|
|
|
|
warned = true; // to avoid zillions of boxes
|
|
}
|
|
}
|
|
|
|
// save one-on-one groups as if they were retail
|
|
if (dock_check_docked_one_on_one(&Objects[shipp->objnum])) {
|
|
// retail format only saved information for non-leaders
|
|
if (!(shipp->flags[Ship::Ship_Flags::Dock_leader])) {
|
|
save_single_dock_instance(&Ships[i], Objects[shipp->objnum].dock_list);
|
|
}
|
|
}
|
|
// multiply docked
|
|
else {
|
|
// save all instances for all ships
|
|
for (dock_instance* dock_ptr = Objects[shipp->objnum].dock_list; dock_ptr != nullptr;
|
|
dock_ptr = dock_ptr->next) {
|
|
save_single_dock_instance(&Ships[i], dock_ptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// check the ship flag about killing off the ship before a mission starts. Write out the appropriate
|
|
// variable if necessary
|
|
if (shipp->flags[Ship::Ship_Flags::Kill_before_mission]) {
|
|
if (optional_string_fred("+Destroy At:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Destroy At: ");
|
|
|
|
fout(" %d", shipp->final_death_time);
|
|
}
|
|
|
|
// possibly write out the orders that this ship will accept. We'll only do it if the orders
|
|
// are not the default set of orders
|
|
if (shipp->orders_accepted != ship_get_default_orders_accepted(&Ship_info[shipp->ship_info_index])) {
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
if (optional_string_fred("+Orders Accepted:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Orders Accepted:");
|
|
|
|
int bitfield = 0;
|
|
for (size_t order : shipp->orders_accepted) {
|
|
if (order < 32)
|
|
bitfield |=
|
|
(1 << (order - 1)); // The first "true" order starts at idx 1, since 0 can be "no order"
|
|
}
|
|
fout(" %d\t\t;! note that this is a bitfield!!!", bitfield);
|
|
} else {
|
|
if (optional_string_fred("+Orders Accepted List:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Orders Accepted List:");
|
|
|
|
fout(" (");
|
|
for (size_t order_id : shipp->orders_accepted) {
|
|
const auto& order = Player_orders[order_id];
|
|
fout(" \"%s\"", order.parse_name.c_str());
|
|
}
|
|
fout(" )");
|
|
}
|
|
}
|
|
|
|
if (shipp->group >= 0) {
|
|
if (optional_string_fred("+Group:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Group:");
|
|
|
|
fout(" %d", shipp->group);
|
|
}
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL &&
|
|
!shipp->fred_layer.empty() &&
|
|
!lcase_equal(shipp->fred_layer, "Default")) {
|
|
if (optional_string_fred("+Layer:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Layer:");
|
|
|
|
fout(" %s", shipp->fred_layer.c_str());
|
|
}
|
|
|
|
// always write out the score to ensure backwards compatibility. If the score is the same as the value
|
|
// in the table write out a flag to tell the game to simply use whatever is in the table instead
|
|
if (Ship_info[shipp->ship_info_index].score == shipp->score) {
|
|
fso_comment_push(";;FSO 3.6.10;;");
|
|
if (optional_string_fred("+Use Table Score:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Use Table Score:");
|
|
}
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.10;; +Use Table Score:", "$Name:");
|
|
}
|
|
|
|
if (optional_string_fred("+Score:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Score:");
|
|
|
|
fout(" %d", shipp->score);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL && shipp->assist_score_pct != 0) {
|
|
fso_comment_push(";;FSO 3.6.10;;");
|
|
if (optional_string_fred("+Assist Score Percentage:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout_version("\n+Assist Score Percentage:");
|
|
}
|
|
fso_comment_pop();
|
|
|
|
fout(" %f", shipp->assist_score_pct);
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.10;; +Assist Score Percentage:", "$Name:");
|
|
}
|
|
|
|
// deal with the persona for this ship as well.
|
|
if (shipp->persona_index != -1) {
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
if (optional_string_fred("+Persona Index:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Persona Index:");
|
|
|
|
fout(" %d", shipp->persona_index);
|
|
} else {
|
|
if (optional_string_fred("+Persona Name:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Persona Name:");
|
|
|
|
fout(" %s", Personas[shipp->persona_index].name);
|
|
}
|
|
}
|
|
|
|
// Goober5000 - deal with texture replacement ----------------
|
|
if (!Fred_texture_replacements.empty()) {
|
|
bool needs_header = true;
|
|
fso_comment_push(";;FSO 3.6.8;;");
|
|
|
|
for (auto& ii : Fred_texture_replacements) {
|
|
if (!stricmp(shipp->ship_name, ii.ship_name) && !(ii.from_table)) {
|
|
if (needs_header) {
|
|
if (optional_string_fred("$Texture Replace:")) {
|
|
parse_comments(1);
|
|
} else {
|
|
fout_version("\n$Texture Replace:");
|
|
}
|
|
|
|
needs_header = false;
|
|
}
|
|
|
|
// write out this entry
|
|
if (optional_string_fred("+old:")) {
|
|
parse_comments(1);
|
|
fout(" %s", ii.old_texture);
|
|
} else {
|
|
fout_version("\n+old: %s", ii.old_texture);
|
|
}
|
|
|
|
if (optional_string_fred("+new:")) {
|
|
parse_comments(1);
|
|
fout(" %s", ii.new_texture);
|
|
} else {
|
|
fout_version("\n+new: %s", ii.new_texture);
|
|
}
|
|
}
|
|
}
|
|
|
|
fso_comment_pop();
|
|
} else {
|
|
bypass_comment(";;FSO 3.6.8;; $Texture Replace:", "$Name:");
|
|
}
|
|
|
|
// end of texture replacement -------------------------------
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_players()
|
|
{
|
|
bool wrote_fso_data = false;
|
|
int i, j;
|
|
int var_idx;
|
|
SCP_map<int, int> used_pool;
|
|
|
|
if (optional_string_fred("#Alternate Types:")) { // Make sure the parser doesn't get out of sync
|
|
required_string_fred("#end");
|
|
}
|
|
if (optional_string_fred("#Callsigns:")) {
|
|
required_string_fred("#end");
|
|
}
|
|
|
|
// write out alternate name list
|
|
if (Mission_alt_type_count > 0) {
|
|
fout("\n\n#Alternate Types:\n");
|
|
|
|
// write them all out
|
|
for (i = 0; i < Mission_alt_type_count; i++) {
|
|
fout("$Alt: %s\n", Mission_alt_types[i]);
|
|
}
|
|
|
|
// end
|
|
fout("\n#end\n");
|
|
}
|
|
|
|
// write out callsign list
|
|
if (save_config.save_format != MissionFormat::RETAIL && Mission_callsign_count > 0) {
|
|
fout("\n\n#Callsigns:\n");
|
|
|
|
// write them all out
|
|
for (i = 0; i < Mission_callsign_count; i++) {
|
|
fout("$Callsign: %s\n", Mission_callsigns[i]);
|
|
}
|
|
|
|
// end
|
|
fout("\n#end\n");
|
|
}
|
|
|
|
required_string_fred("#Players");
|
|
parse_comments(2);
|
|
fout("\t\t;! %d total\n", Player_starts);
|
|
|
|
SCP_vector<SCP_string> e_list = ai_lua_get_general_orders(true);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL && (!e_list.empty())) {
|
|
if (optional_string_fred("+General Orders Enabled:", "#Players"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+General Orders Enabled:");
|
|
|
|
fout(" (");
|
|
|
|
for (const SCP_string& order : e_list) {
|
|
fout(" \"%s\"", order.c_str());
|
|
}
|
|
fout(" )\n");
|
|
}
|
|
|
|
SCP_vector<SCP_string> v_list = ai_lua_get_general_orders(false, true);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL && (!v_list.empty())) {
|
|
if (optional_string_fred("+General Orders Valid:", "#Players"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+General Orders Valid:");
|
|
|
|
fout(" (");
|
|
|
|
for (const SCP_string& order : v_list) {
|
|
fout(" \"%s\"", order.c_str());
|
|
}
|
|
fout(" )\n");
|
|
}
|
|
|
|
for (i = 0; i < Num_teams; i++) {
|
|
required_string_fred("$Starting Shipname:");
|
|
parse_comments();
|
|
fout(" %s", (Player_start_shipnum >= 0) ? Ships[Player_start_shipnum].ship_name : "<none>");
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (Team_data[i].do_not_validate) {
|
|
required_string_fred("+Do Not Validate Loadout");
|
|
parse_comments();
|
|
}
|
|
}
|
|
|
|
required_string_fred("$Ship Choices:");
|
|
parse_comments();
|
|
fout(" (\n");
|
|
|
|
SCP_vector<SCP_string> ships_without_dogfight_weapons;
|
|
|
|
for (const auto &sc : Team_data[i].ship_choices) {
|
|
// Check to see if a variable name should be written for the class rather than a number
|
|
if (!sc.class_variable.empty()) {
|
|
var_idx = get_index_sexp_variable_name(sc.class_variable.c_str());
|
|
Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
|
|
wrote_fso_data = true;
|
|
|
|
fout("\t@%s\t", Sexp_variables[var_idx].variable_name);
|
|
} else {
|
|
fout("\t\"%s\"\t", Ship_info[sc.class_index].name);
|
|
}
|
|
|
|
// Now check if we should write a variable or a number for the amount of ships available
|
|
if (!sc.count_variable.empty()) {
|
|
var_idx = get_index_sexp_variable_name(sc.count_variable.c_str());
|
|
Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
|
|
wrote_fso_data = true;
|
|
|
|
fout("@%s\n", Sexp_variables[var_idx].variable_name);
|
|
} else {
|
|
fout("%d\n", sc.count);
|
|
}
|
|
|
|
// Check the weapons pool for at least one dogfight weapon for this ship type
|
|
// (variable-specified classes can't be resolved at save time, so skip them)
|
|
if (IS_MISSION_MULTI_DOGFIGHT && sc.class_index >= 0) {
|
|
bool found_dogfight_weapon = false;
|
|
for (const auto &wc : Team_data[i].weapon_choices) {
|
|
if (wc.class_index >= 0 &&
|
|
(Ship_info[sc.class_index].allowed_weapons[wc.class_index] & DOGFIGHT_WEAPON)) {
|
|
found_dogfight_weapon = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found_dogfight_weapon) {
|
|
ships_without_dogfight_weapons.emplace_back(Ship_info[sc.class_index].name);
|
|
}
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
|
|
// make sure we have at least one dogfight weapon for each ship type in a dogfight mission
|
|
if (IS_MISSION_MULTI_DOGFIGHT && !ships_without_dogfight_weapons.empty()) {
|
|
for (const auto& d_ship : ships_without_dogfight_weapons) {
|
|
mprintf(("Warning: Ship %s has no dogfight weapons allowed\n", d_ship.c_str()));
|
|
}
|
|
SCP_string msg =
|
|
"This mission is a dogfight mission but no dogfight weapons are available for at least one ship in the "
|
|
"loadout!\n\nA list of ships has been printed to the log. Please fix this problem before "
|
|
"distributing the mission.";
|
|
Message(os::dialogs::MESSAGEBOX_ERROR, msg.c_str());
|
|
}
|
|
|
|
if (optional_string_fred("+Weaponry Pool:", "$Starting Shipname:")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n\n+Weaponry Pool:");
|
|
}
|
|
|
|
fout(" (\n");
|
|
generate_weaponry_usage_list_team(i, used_pool);
|
|
|
|
for (const auto &wc : Team_data[i].weapon_choices) {
|
|
// first output the weapon name or a variable that sets it
|
|
if (!wc.class_variable.empty()) {
|
|
var_idx = get_index_sexp_variable_name(wc.class_variable.c_str());
|
|
Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
|
|
wrote_fso_data = true;
|
|
|
|
fout("\t@%s\t", Sexp_variables[var_idx].variable_name);
|
|
} else {
|
|
fout("\t\"%s\"\t", Weapon_info[wc.class_index].name);
|
|
}
|
|
|
|
// now output the amount of this weapon or a variable that sets it. If this weapon is in the used pool and
|
|
// isn't set by a variable we should add the amount of weapons used by the wings to it and zero the entry so
|
|
// we know that we have dealt with it
|
|
if (!wc.count_variable.empty()) {
|
|
var_idx = get_index_sexp_variable_name(wc.count_variable.c_str());
|
|
Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
|
|
wrote_fso_data = true;
|
|
|
|
fout("@%s\n", Sexp_variables[var_idx].variable_name);
|
|
} else {
|
|
if (!wc.class_variable.empty()) {
|
|
fout("%d\n", wc.count);
|
|
} else {
|
|
fout("%d\n", wc.count + used_pool.value_or(wc.class_index, 0));
|
|
used_pool.erase(wc.class_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// now we add anything left in the used pool as a static entry
|
|
if (!Team_data[i].do_not_validate) {
|
|
for (const auto &[weapon_class, count] : used_pool) {
|
|
if (count > 0) {
|
|
fout("\t\"%s\"\t%d\n", Weapon_info[weapon_class].name, count);
|
|
}
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
|
|
// version-gated support options; see also check_for_25_1_data()
|
|
auto more_support_options_version = gameversion::version(25, 1);
|
|
|
|
bool has_rearm_pool_entries = false;
|
|
if (save_config.save_format != MissionFormat::RETAIL && The_mission.required_fso_version >= more_support_options_version && !The_mission.support_ships.rearm_pool_from_loadout) {
|
|
for (j = 0; j < weapon_info_size(); j++) {
|
|
// mirror the skip conditions used when writing the pool below
|
|
if (!Weapon_info[j].wi_flags[Weapon::Info_Flags::Player_allowed] || Weapon_info[j].disallow_rearm) {
|
|
continue;
|
|
}
|
|
if (The_mission.support_ships.rearm_weapon_pool[i][j] != -1) {
|
|
has_rearm_pool_entries = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (has_rearm_pool_entries) {
|
|
if (optional_string_fred("+Support Rearm Pool:", "$Starting Shipname:")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n\n+Support Rearm Pool:");
|
|
}
|
|
|
|
fout(" (\n");
|
|
for (j = 0; j < weapon_info_size(); j++) {
|
|
if (!Weapon_info[j].wi_flags[Weapon::Info_Flags::Player_allowed]) {
|
|
continue;
|
|
}
|
|
// Default is -1 (unlimited). Skip disallow_rearm weapons too, parse normalizes them to 0 anyway.
|
|
if (Weapon_info[j].disallow_rearm) {
|
|
continue;
|
|
}
|
|
if (The_mission.support_ships.rearm_weapon_pool[i][j] != -1) {
|
|
fout("\t\"%s\"\t%d\n", Weapon_info[j].name, The_mission.support_ships.rearm_weapon_pool[i][j]);
|
|
}
|
|
}
|
|
fout(")");
|
|
}
|
|
|
|
// sanity check
|
|
if (save_config.save_format == MissionFormat::RETAIL && wrote_fso_data) {
|
|
// this is such an unlikely (and hard-to-fix) case that a warning should be sufficient
|
|
SCP_string msg = "This mission contains variable-based team loadout information, but you are saving in the "
|
|
"retail mission format. Retail FRED and FS2 will not be able to read this information.";
|
|
Message(os::dialogs::MESSAGEBOX_ERROR, msg.c_str());
|
|
}
|
|
|
|
// Goober5000 - mjn.mixael's required weapon feature
|
|
if (save_config.save_format != MissionFormat::RETAIL && !Team_data[i].required_weapons.empty()) {
|
|
if (optional_string_fred("+Required for mission:", "$Starting Shipname:"))
|
|
parse_comments(2);
|
|
else
|
|
fout("\n+Required for mission:");
|
|
|
|
fout(" (");
|
|
for (int weapon_class : Team_data[i].required_weapons) {
|
|
fout(" \"%s\"", Weapon_info[weapon_class].name);
|
|
}
|
|
fout(" )");
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_plot_info()
|
|
{
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
if (optional_string_fred("#Plot Info")) {
|
|
parse_comments(2);
|
|
|
|
// XSTR
|
|
required_string_fred("$Tour:");
|
|
parse_comments(2);
|
|
fout_ext(" ", "Blah");
|
|
|
|
required_string_fred("$Pre-Briefing Cutscene:");
|
|
parse_comments();
|
|
fout(" Blah");
|
|
|
|
required_string_fred("$Pre-Mission Cutscene:");
|
|
parse_comments();
|
|
fout(" Blah");
|
|
|
|
required_string_fred("$Next Mission Success:");
|
|
parse_comments();
|
|
fout(" Blah");
|
|
|
|
required_string_fred("$Next Mission Partial:");
|
|
parse_comments();
|
|
fout(" Blah");
|
|
|
|
required_string_fred("$Next Mission Failure:");
|
|
parse_comments();
|
|
fout(" Blah");
|
|
} else {
|
|
fout("\n\n#Plot Info\n\n");
|
|
|
|
fout("$Tour: ");
|
|
fout_ext(NULL, "Blah");
|
|
fout("\n");
|
|
fout("$Pre-Briefing Cutscene: Blah\n");
|
|
fout("$Pre-Mission Cutscene: Blah\n");
|
|
fout("$Next Mission Success: Blah\n");
|
|
fout("$Next Mission Partial: Blah\n");
|
|
fout("$Next Mission Failure: Blah\n");
|
|
|
|
fout("\n");
|
|
}
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_reinforcements()
|
|
{
|
|
int j, type;
|
|
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Reinforcements");
|
|
parse_comments(2);
|
|
fout("\t\t;! " SIZE_T_ARG " total\n", Reinforcements.size());
|
|
|
|
bool first_r = true;
|
|
for (const auto &reinforcement: Reinforcements) {
|
|
required_string_either_fred("$Name:", "#Background bitmaps");
|
|
required_string_fred("$Name:");
|
|
parse_comments(!first_r ? 2 : 1);
|
|
first_r = false;
|
|
fout(" %s", reinforcement.name);
|
|
|
|
type = TYPE_ATTACK_PROTECT;
|
|
for (j = 0; j < MAX_SHIPS; j++)
|
|
if ((Ships[j].objnum != -1) && !stricmp(Ships[j].ship_name, reinforcement.name)) {
|
|
if (Ship_info[Ships[j].ship_info_index].flags[Ship::Info_Flags::Support])
|
|
type = TYPE_REPAIR_REARM;
|
|
break;
|
|
}
|
|
|
|
required_string_fred("$Type:");
|
|
parse_comments();
|
|
fout(" %s", Reinforcement_type_names[type]);
|
|
|
|
required_string_fred("$Num times:");
|
|
parse_comments();
|
|
fout(" %d", reinforcement.uses);
|
|
|
|
if (optional_string_fred("+Arrival Delay:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Arrival Delay:");
|
|
fout(" %d", reinforcement.arrival_delay);
|
|
|
|
if (optional_string_fred("+No Messages:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+No Messages:");
|
|
fout(" (");
|
|
for (j = 0; j < MAX_REINFORCEMENT_MESSAGES; j++) {
|
|
if (strlen(reinforcement.no_messages[j]))
|
|
fout(" \"%s\"", reinforcement.no_messages[j]);
|
|
}
|
|
fout(" )");
|
|
|
|
if (optional_string_fred("+Yes Messages:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Yes Messages:");
|
|
fout(" (");
|
|
for (j = 0; j < MAX_REINFORCEMENT_MESSAGES; j++) {
|
|
if (strlen(reinforcement.yes_messages[j]))
|
|
fout(" \"%s\"", reinforcement.yes_messages[j]);
|
|
}
|
|
fout(" )");
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
void Fred_mission_save::save_single_dock_instance(ship* shipp, dock_instance* dock_ptr)
|
|
{
|
|
Assert(shipp && dock_ptr);
|
|
Assert(dock_ptr->docked_objp->type == OBJ_SHIP || dock_ptr->docked_objp->type == OBJ_START);
|
|
|
|
// get ships and objects
|
|
// The original var names were flagged as suspicious going into dock_find_dockpoint_used_by_object()
|
|
// As this code has been unchanged for ages, I'm going to assume it's correct and just change the names
|
|
// to appease clang
|
|
object* objp_a = &Objects[shipp->objnum]; //originally objp
|
|
object* objp_b = dock_ptr->docked_objp; //originally other_objp
|
|
ship* other_shipp = &Ships[objp_b->instance];
|
|
|
|
// write other ship
|
|
if (optional_string_fred("+Docked With:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Docked With:");
|
|
fout(" %s", other_shipp->ship_name);
|
|
|
|
// Goober5000 - hm, Volition seems to have reversed docker and dockee here
|
|
|
|
// write docker (actually dockee) point
|
|
required_string_fred("$Docker Point:", "$Name:");
|
|
parse_comments();
|
|
fout(" %s",
|
|
model_get_dock_name(Ship_info[other_shipp->ship_info_index].model_num,
|
|
dock_find_dockpoint_used_by_object(objp_b, objp_a)));
|
|
|
|
// write dockee (actually docker) point
|
|
required_string_fred("$Dockee Point:", "$Name:");
|
|
parse_comments();
|
|
fout(" %s",
|
|
model_get_dock_name(Ship_info[shipp->ship_info_index].model_num,
|
|
dock_find_dockpoint_used_by_object(objp_a, objp_b)));
|
|
|
|
fso_comment_pop(true);
|
|
}
|
|
|
|
void Fred_mission_save::save_turret_info(ship_subsys* ptr, int ship)
|
|
{
|
|
int i, z;
|
|
ship_weapon* wp = &ptr->weapons;
|
|
|
|
if (wp->ai_class != Ship_info[Ships[ship].ship_info_index].ai_class) {
|
|
if (optional_string_fred("+AI Class:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+AI Class:");
|
|
|
|
fout(" %s", Ai_class_names[wp->ai_class]);
|
|
}
|
|
|
|
z = 0;
|
|
i = wp->num_primary_banks;
|
|
while (i--)
|
|
if (wp->primary_bank_weapons[i] != ptr->system_info->primary_banks[i])
|
|
z = 1;
|
|
|
|
if (z) {
|
|
if (optional_string_fred("+Primary Banks:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Primary Banks:");
|
|
|
|
fout(" ( ");
|
|
for (i = 0; i < wp->num_primary_banks; i++) {
|
|
if (wp->primary_bank_weapons[i] != -1) { // Just in case someone has set a weapon bank to empty
|
|
fout("\"%s\" ", Weapon_info[wp->primary_bank_weapons[i]].name);
|
|
} else {
|
|
fout("\"\" ");
|
|
}
|
|
}
|
|
fout(")");
|
|
}
|
|
|
|
z = 0;
|
|
i = wp->num_secondary_banks;
|
|
while (i--)
|
|
if (wp->secondary_bank_weapons[i] != ptr->system_info->secondary_banks[i])
|
|
z = 1;
|
|
|
|
if (z) {
|
|
if (optional_string_fred("+Secondary Banks:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Secondary Banks:");
|
|
|
|
fout(" ( ");
|
|
for (i = 0; i < wp->num_secondary_banks; i++) {
|
|
if (wp->secondary_bank_weapons[i] != -1) {
|
|
fout("\"%s\" ", Weapon_info[wp->secondary_bank_weapons[i]].name);
|
|
} else {
|
|
fout("\"\" ");
|
|
}
|
|
}
|
|
fout(")");
|
|
}
|
|
|
|
z = 0;
|
|
i = wp->num_secondary_banks;
|
|
while (i--)
|
|
if (wp->secondary_bank_ammo[i] != 100)
|
|
z = 1;
|
|
|
|
if (z) {
|
|
if (optional_string_fred("+Sbank Ammo:", "$Name:", "+Subsystem:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Sbank Ammo:");
|
|
|
|
fout(" ( ");
|
|
for (i = 0; i < wp->num_secondary_banks; i++)
|
|
fout("%d ", wp->secondary_bank_ammo[i]);
|
|
|
|
fout(")");
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
}
|
|
|
|
int Fred_mission_save::save_variables()
|
|
{
|
|
char* type;
|
|
char number[] = "number";
|
|
char string[] = "string";
|
|
char block[] = "block";
|
|
int i;
|
|
int num_block_vars = 0;
|
|
|
|
// sort sexp_variables
|
|
sexp_variable_sort();
|
|
|
|
// get count
|
|
int num_variables = sexp_variable_count();
|
|
|
|
if (save_config.save_format == MissionFormat::RETAIL) {
|
|
generate_special_explosion_block_variables();
|
|
num_block_vars = num_block_variables();
|
|
}
|
|
int total_variables = num_variables + num_block_vars;
|
|
|
|
if (total_variables > 0) {
|
|
|
|
// write 'em out
|
|
required_string_fred("#Sexp_variables");
|
|
parse_comments(2);
|
|
|
|
required_string_fred("$Variables:");
|
|
parse_comments(2);
|
|
|
|
fout("\n(");
|
|
// parse_comments();
|
|
|
|
for (i = 0; i < num_variables; i++) {
|
|
if (Sexp_variables[i].type & SEXP_VARIABLE_NUMBER) {
|
|
type = number;
|
|
} else {
|
|
type = string;
|
|
}
|
|
// index "var name" "default" "type"
|
|
fout("\n\t\t%d\t\t\"%s\"\t\t\"%s\"\t\t\"%s\"",
|
|
i,
|
|
Sexp_variables[i].variable_name,
|
|
Sexp_variables[i].text,
|
|
type);
|
|
|
|
// persistent and network variables
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
// Network variable - Karajorma
|
|
if (Sexp_variables[i].type & SEXP_VARIABLE_NETWORK) {
|
|
fout("\t\t\"%s\"", "network-variable");
|
|
}
|
|
|
|
if (Sexp_variables[i].type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) {
|
|
fout("\t\t\"%s\"", "eternal");
|
|
}
|
|
|
|
// player-persistent - Goober5000
|
|
if (Sexp_variables[i].type & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE) {
|
|
fout("\t\t\"%s\"", "save-on-mission-close");
|
|
// campaign-persistent - Goober5000
|
|
} else if (Sexp_variables[i].type & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS) {
|
|
fout("\t\t\"%s\"", "save-on-mission-progress");
|
|
}
|
|
}
|
|
|
|
// parse_comments();
|
|
}
|
|
|
|
for (i = MAX_SEXP_VARIABLES - num_block_vars; i < MAX_SEXP_VARIABLES; i++) {
|
|
type = block;
|
|
fout("\n\t\t%d\t\t\"%s\"\t\t\"%s\"\t\t\"%s\"",
|
|
i,
|
|
Block_variables[i].variable_name,
|
|
Block_variables[i].text,
|
|
type);
|
|
}
|
|
|
|
fout("\n)");
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_vector(const vec3d& v)
|
|
{
|
|
fout(" %f, %f, %f", v.xyz.x, v.xyz.y, v.xyz.z);
|
|
return 0;
|
|
}
|
|
|
|
// The jump-node section of this function MUST stay in sync with:
|
|
// the "$Jump Node:" parse block in missionparse.cpp
|
|
// clone_jump_node_instance_data() in missioneditor/objectduplication.cpp
|
|
// When you add a new editable jump-node field, touch all three.
|
|
int Fred_mission_save::save_waypoints()
|
|
{
|
|
// object *ptr;
|
|
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Waypoints");
|
|
parse_comments(2);
|
|
fout("\t\t;! %d lists total\n", Waypoint_lists.size());
|
|
|
|
for (auto &jn : Jump_nodes) {
|
|
if (jn.GetSCPObjectNumber() < 0)
|
|
continue;
|
|
|
|
required_string_fred("$Jump Node:", "$Jump Node Name:");
|
|
parse_comments(2);
|
|
save_vector(jn.GetSCPObject()->pos);
|
|
|
|
required_string_fred("$Jump Node Name:", "$Jump Node:");
|
|
parse_comments();
|
|
fout(" %s", jn.GetName());
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
|
|
// (If we are always saving display names, the "only/not written" comments do not apply)
|
|
// The display name is only written if it currently exists, to avoid introducing inconsistencies
|
|
if (save_config.always_save_display_names || jn.HasDisplayName()) {
|
|
char truncated_name[NAME_LENGTH];
|
|
strcpy_s(truncated_name, jn.GetName());
|
|
end_string_at_first_hash_symbol(truncated_name);
|
|
|
|
// Also, the display name is not written if it's just the truncation of the name at the hash
|
|
if (save_config.always_save_display_names || strcmp(jn.GetDisplayName(), truncated_name) != 0) {
|
|
if (optional_string_fred("+Display Name:", "$Jump Node:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Display Name:");
|
|
}
|
|
|
|
fout_ext("", "%s", jn.GetDisplayName());
|
|
}
|
|
}
|
|
|
|
if (jn.IsSpecialModel()) {
|
|
if (optional_string_fred("+Model File:", "$Jump Node:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Model File:");
|
|
}
|
|
|
|
int model = jn.GetModelNumber();
|
|
polymodel* pm = model_get(model);
|
|
fout(" %s", pm->filename);
|
|
}
|
|
|
|
if (jn.IsColored()) {
|
|
if (optional_string_fred("+Alphacolor:", "$Jump Node:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Alphacolor:");
|
|
}
|
|
|
|
const auto& jn_color = jn.GetColor();
|
|
fout(" %u %u %u %u", jn_color.red, jn_color.green, jn_color.blue, jn_color.alpha);
|
|
}
|
|
|
|
int hidden_is_there = optional_string_fred("+Hidden:", "$Jump Node:");
|
|
if (hidden_is_there)
|
|
parse_comments();
|
|
|
|
if (hidden_is_there || jn.IsHidden()) {
|
|
if (!hidden_is_there)
|
|
fout("\n+Hidden:");
|
|
|
|
if (jn.IsHidden())
|
|
fout(" %s", "true");
|
|
else
|
|
fout(" %s", "false");
|
|
}
|
|
|
|
const SCP_string& jn_layer = jn.GetFredLayer();
|
|
if (!jn_layer.empty() && !lcase_equal(jn_layer, "Default")) {
|
|
if (optional_string_fred("+Layer:", "$Jump Node:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Layer:");
|
|
fout(" %s", jn_layer.c_str());
|
|
}
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
// Per-waypoint-path field handling here MUST stay in sync with:
|
|
// parse_waypoint_list() in missionparse.cpp
|
|
// clone_waypoint_path_instance_data() in missioneditor/objectduplication.cpp
|
|
// When you add a new editable waypoint-path field, touch all three.
|
|
bool first_wpt_list = true;
|
|
for (const auto& ii : Waypoint_lists) {
|
|
required_string_either_fred("$Name:", "#Messages");
|
|
required_string_fred("$Name:");
|
|
parse_comments(first_wpt_list ? 1 : 2);
|
|
fout(" %s", ii.get_name());
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
|
|
int nol_in_file = optional_string_fred("+No Draw Lines:", "$List:");
|
|
if (nol_in_file)
|
|
parse_comments();
|
|
if (nol_in_file || ii.get_no_draw_lines()) {
|
|
if (!nol_in_file)
|
|
fout("\n+No Draw Lines:");
|
|
fout(" %s", ii.get_no_draw_lines() ? "true" : "false");
|
|
}
|
|
|
|
if (ii.get_has_custom_color()) {
|
|
if (optional_string_fred("+Color:", "$List:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Color:");
|
|
}
|
|
fout(" %d %d %d", ii.get_color_r(), ii.get_color_g(), ii.get_color_b());
|
|
}
|
|
|
|
const SCP_string& wpt_layer = ii.get_fred_layer();
|
|
if (!wpt_layer.empty() && !lcase_equal(wpt_layer, "Default")) {
|
|
if (optional_string_fred("+Layer:", "$List:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Layer:");
|
|
fout(" %s", wpt_layer.c_str());
|
|
}
|
|
}
|
|
|
|
required_string_fred("$List:");
|
|
parse_comments();
|
|
fout(" (\t\t;! %d points in list\n", ii.get_waypoints().size());
|
|
|
|
save_waypoint_list(&ii);
|
|
fout(")");
|
|
|
|
fso_comment_pop();
|
|
first_wpt_list = false;
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
return err;
|
|
}
|
|
|
|
int Fred_mission_save::save_waypoint_list(const waypoint_list* wp_list)
|
|
{
|
|
Assert(wp_list != NULL);
|
|
|
|
for (const auto& ii : wp_list->get_waypoints()) {
|
|
auto pos = ii.get_pos();
|
|
fout("\t( %f, %f, %f )\n", pos->xyz.x, pos->xyz.y, pos->xyz.z);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int Fred_mission_save::save_wings()
|
|
{
|
|
SCP_string sexp_out;
|
|
int count = 0;
|
|
|
|
fred_parse_flag = 0;
|
|
required_string_fred("#Wings");
|
|
parse_comments(2);
|
|
fout("\t\t;! %d total\n", Num_wings);
|
|
|
|
for (auto& w : Wings) {
|
|
if (!w.wave_count)
|
|
continue;
|
|
|
|
required_string_one_of_fred(3, "$Name:", "#Events", "#Props");
|
|
required_string_fred("$Name:");
|
|
parse_comments(count ? 2 : 1);
|
|
fout(" %s", w.name);
|
|
|
|
count++;
|
|
|
|
// Display name
|
|
// (If we are always saving display names, the "only/not written" comments do not apply)
|
|
// The display name is only written if it currently exists, to avoid introducing inconsistencies
|
|
if (save_config.save_format != MissionFormat::RETAIL && (save_config.always_save_display_names || w.has_display_name())) {
|
|
char truncated_name[NAME_LENGTH];
|
|
strcpy_s(truncated_name, w.name);
|
|
end_string_at_first_hash_symbol(truncated_name);
|
|
|
|
// Also, the display name is not written if it's just the truncation of the name at the hash
|
|
if (save_config.always_save_display_names || strcmp(w.get_display_name(), truncated_name) != 0) {
|
|
if (optional_string_fred("$Display Name:", "$Waves:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n$Display Name:");
|
|
}
|
|
fout_ext(" ", "%s", w.get_display_name());
|
|
}
|
|
}
|
|
|
|
// squad logo - Goober5000
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (strlen(w.wing_squad_filename) > 0) //-V805
|
|
{
|
|
if (optional_string_fred("+Squad Logo:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Squad Logo:");
|
|
|
|
fout(" %s", w.wing_squad_filename);
|
|
}
|
|
}
|
|
|
|
required_string_fred("$Waves:");
|
|
parse_comments();
|
|
fout(" %d", w.num_waves);
|
|
|
|
required_string_fred("$Wave Threshold:");
|
|
parse_comments();
|
|
fout(" %d", w.threshold);
|
|
|
|
required_string_fred("$Special Ship:");
|
|
parse_comments();
|
|
fout(" %d\t\t;! %s", w.special_ship, Ships[w.ship_index[w.special_ship]].ship_name);
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (w.formation >= 0 && w.formation < static_cast<int>(Wing_formations.size())) {
|
|
if (optional_string_fred("+Formation:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Formation:");
|
|
}
|
|
fout(" %s", Wing_formations[w.formation].name);
|
|
}
|
|
if (!fl_equal(w.formation_scale, 1.0f, 0.001f)) {
|
|
if (optional_string_fred("+Formation Scale:", "$Name:")) {
|
|
parse_comments();
|
|
} else {
|
|
fout("\n+Formation Scale:");
|
|
}
|
|
fout(" %f", w.formation_scale);
|
|
}
|
|
}
|
|
|
|
required_string_fred("$Arrival Location:");
|
|
parse_comments();
|
|
fout(" %s", Arrival_location_names[static_cast<int>(w.arrival_location)]);
|
|
|
|
if (w.arrival_location != ArrivalLocation::AT_LOCATION) {
|
|
if (optional_string_fred("+Arrival Distance:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Arrival Distance:");
|
|
|
|
fout(" %d", w.arrival_distance);
|
|
if (optional_string_fred("$Arrival Anchor:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n$Arrival Anchor:");
|
|
|
|
int z = w.arrival_anchor.value();
|
|
if (z < 0) {
|
|
fout(" <error>");
|
|
} else if (z & ANCHOR_SPECIAL_ARRIVAL) {
|
|
// get name
|
|
char tmp[NAME_LENGTH + 15];
|
|
stuff_special_arrival_anchor_name(tmp, z, save_config.save_format == MissionFormat::RETAIL);
|
|
|
|
// save it
|
|
fout(" %s", tmp);
|
|
} else {
|
|
auto anchor_entry = ship_registry_get(z);
|
|
fout(" %s", anchor_entry ? anchor_entry->name : "<error>");
|
|
}
|
|
}
|
|
|
|
// Goober5000
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if ((w.arrival_location == ArrivalLocation::FROM_DOCK_BAY) && (w.arrival_path_mask > 0)) {
|
|
auto anchor_entry = ship_registry_get(w.arrival_anchor);
|
|
Assertion(anchor_entry, "Could not find arrival anchor for wing %s!", w.name);
|
|
auto pm = model_get(anchor_entry->sip()->model_num);
|
|
|
|
fout("\n+Arrival Paths: ( ");
|
|
|
|
for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
|
|
if (w.arrival_path_mask & (1 << n)) {
|
|
fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
}
|
|
|
|
if (w.arrival_delay) {
|
|
if (optional_string_fred("+Arrival delay:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Arrival delay:");
|
|
|
|
fout(" %d", w.arrival_delay);
|
|
}
|
|
|
|
required_string_fred("$Arrival Cue:");
|
|
parse_comments();
|
|
convert_sexp_to_string(sexp_out, w.arrival_cue, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
required_string_fred("$Departure Location:");
|
|
parse_comments();
|
|
fout(" %s", Departure_location_names[static_cast<int>(w.departure_location)]);
|
|
|
|
if (w.departure_location != DepartureLocation::AT_LOCATION) {
|
|
required_string_fred("$Departure Anchor:");
|
|
parse_comments();
|
|
|
|
auto anchor_entry = ship_registry_get(w.departure_anchor);
|
|
fout(" %s", anchor_entry ? anchor_entry->name : "<error>");
|
|
}
|
|
|
|
// Goober5000
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if ((w.departure_location == DepartureLocation::TO_DOCK_BAY) && (w.departure_path_mask > 0)) {
|
|
auto anchor_entry = ship_registry_get(w.departure_anchor);
|
|
Assertion(anchor_entry, "Could not find departure anchor for wing %s!", w.name);
|
|
auto pm = model_get(anchor_entry->sip()->model_num);
|
|
|
|
fout("\n+Departure Paths: ( ");
|
|
|
|
for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
|
|
if (w.departure_path_mask & (1 << n)) {
|
|
fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
|
|
}
|
|
}
|
|
|
|
fout(")");
|
|
}
|
|
}
|
|
|
|
if (w.departure_delay) {
|
|
if (optional_string_fred("+Departure delay:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Departure delay:");
|
|
|
|
fout(" %d", w.departure_delay);
|
|
}
|
|
|
|
required_string_fred("$Departure Cue:");
|
|
parse_comments();
|
|
convert_sexp_to_string(sexp_out, w.departure_cue, SEXP_SAVE_MODE);
|
|
fout(" %s", sexp_out.c_str());
|
|
|
|
required_string_fred("$Ships:");
|
|
parse_comments();
|
|
fout(" (\t\t;! %d total\n", w.wave_count);
|
|
|
|
for (int j = 0; j < w.wave_count; j++) {
|
|
//ship = w.ship_index[j];
|
|
// if (Objects[Ships[ship].objnum].type == OBJ_START)
|
|
// fout("\t\"Player 1\"\n");
|
|
// else
|
|
fout("\t\"%s\"\n", Ships[w.ship_index[j]].ship_name);
|
|
}
|
|
|
|
fout(")");
|
|
|
|
save_ai_goals(w.ai_goals, -1);
|
|
|
|
if (w.hotkey != -1) {
|
|
if (optional_string_fred("+Hotkey:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Hotkey:");
|
|
|
|
fout(" %d", w.hotkey);
|
|
}
|
|
|
|
if (optional_string_fred("+Flags:", "$Name:")) {
|
|
parse_comments();
|
|
fout(" (");
|
|
} else
|
|
fout("\n+Flags: (");
|
|
|
|
auto get_flag_name = [](Ship::Wing_Flags flag) -> const char* {
|
|
for (size_t j = 0; j < Num_parse_wing_flags; ++j) {
|
|
if (Parse_wing_flags[j].def == flag) {
|
|
return Parse_wing_flags[j].name;
|
|
}
|
|
}
|
|
return nullptr;
|
|
};
|
|
|
|
if (w.flags[Ship::Wing_Flags::Ignore_count])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Ignore_count));
|
|
if (w.flags[Ship::Wing_Flags::Reinforcement])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Reinforcement));
|
|
if (w.flags[Ship::Wing_Flags::No_arrival_music])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_music));
|
|
if (w.flags[Ship::Wing_Flags::No_arrival_message])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_message));
|
|
if (w.flags[Ship::Wing_Flags::No_first_wave_message])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_first_wave_message));
|
|
if (w.flags[Ship::Wing_Flags::No_arrival_warp])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_warp));
|
|
if (w.flags[Ship::Wing_Flags::No_departure_warp])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_departure_warp));
|
|
if (w.flags[Ship::Wing_Flags::No_dynamic])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_dynamic));
|
|
if (save_config.save_format != MissionFormat::RETAIL) {
|
|
if (w.flags[Ship::Wing_Flags::Nav_carry])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Nav_carry));
|
|
if (w.flags[Ship::Wing_Flags::Same_arrival_warp_when_docked])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Same_arrival_warp_when_docked));
|
|
if (w.flags[Ship::Wing_Flags::Same_departure_warp_when_docked])
|
|
fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Same_departure_warp_when_docked));
|
|
}
|
|
|
|
fout(" )");
|
|
|
|
if (w.wave_delay_min) {
|
|
if (optional_string_fred("+Wave Delay Min:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Wave Delay Min:");
|
|
|
|
fout(" %d", w.wave_delay_min);
|
|
}
|
|
|
|
if (w.wave_delay_max) {
|
|
if (optional_string_fred("+Wave Delay Max:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Wave Delay Max:");
|
|
|
|
fout(" %d", w.wave_delay_max);
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
|
|
Assert(count == Num_wings);
|
|
return err;
|
|
}
|
|
|
|
// Per-prop field handling here MUST stay in sync with:
|
|
// parse_prop() in missionparse.cpp
|
|
// clone_prop_instance_data() in missioneditor/objectduplication.cpp
|
|
// When you add a new editable prop field, touch all three.
|
|
int Fred_mission_save::save_props()
|
|
{
|
|
auto num_props = count_items_with_value(Props);
|
|
|
|
if ((save_config.save_format != MissionFormat::RETAIL) && (num_props > 0)) {
|
|
fred_parse_flag = 0;
|
|
size_t count = 0;
|
|
|
|
if (optional_string_fred("#Props", "#End")) {
|
|
parse_comments(2);
|
|
} else {
|
|
fout("\n\n#Props");
|
|
}
|
|
fout("\t\t;! " SIZE_T_ARG " total\n", num_props);
|
|
|
|
for (const auto& p : Props) {
|
|
if (p.has_value()) {
|
|
required_string_either_fred("$Name:", "#Events");
|
|
required_string_fred("$Name:");
|
|
parse_comments(count ? 2 : 1);
|
|
fout(" %s", p->prop_name);
|
|
|
|
count++;
|
|
|
|
required_string_fred("\n$Class:");
|
|
parse_comments(0);
|
|
fout(" %s", Prop_info[p->prop_info_index].name.c_str());
|
|
|
|
required_string_fred("$Location:");
|
|
parse_comments();
|
|
save_vector(Objects[p->objnum].pos);
|
|
|
|
required_string_fred("$Orientation:");
|
|
parse_comments();
|
|
save_matrix(Objects[p->objnum].orient);
|
|
|
|
if (optional_string_fred("+Flags:", "$Name:")) {
|
|
parse_comments();
|
|
fout(" (");
|
|
} else
|
|
fout("\n+Flags: (");
|
|
|
|
if (!(Objects[p->objnum].flags[Object::Object_Flags::Collides]))
|
|
fout(" \"no_collide\"");
|
|
fout(" )");
|
|
|
|
if (save_config.save_format != MissionFormat::RETAIL &&
|
|
!p->fred_layer.empty() &&
|
|
!lcase_equal(p->fred_layer, "Default")) {
|
|
if (optional_string_fred("+Layer:", "$Name:"))
|
|
parse_comments();
|
|
else
|
|
fout("\n+Layer:");
|
|
|
|
fout(" %s", p->fred_layer.c_str());
|
|
}
|
|
|
|
fso_comment_pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
fso_comment_pop(true);
|
|
return err;
|
|
}
|