Files
fs2open.github.com/code/missioneditor/missionsave.cpp
T
Goober5000andClaude Opus 4.7 ba7aa70794 Fix 32-bit truncation in the flagset u64 round-trip
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]>
2026-08-30 16:46:11 -04:00

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(&currentTime);
auto timeinfo = localtime(&currentTime);
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;
}