text: /*
   * 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 "stdafx.h"
  
  #include "CampaignEditorDlg.h"
  #include "CampaignTreeView.h"
  #include "CampaignTreeWnd.h"
  #include "FredRender.h"
  #include "FredView.h"
  #include "MainFrm.h"
  #include "Management.h"
  #include "MissionSave.h"
  
  #include "ai/aigoals.h"
  #include "asteroid/asteroid.h"
  #include "cfile/cfile.h"
  #include "gamesnd/eventmusic.h"
  #include "globalincs/linklist.h"
  #include "globalincs/version.h"
  #include "hud/hudsquadmsg.h"
  #include "iff_defs/iff_defs.h"
  #include "jumpnode/jumpnode.h"
  #include "lighting/lighting.h"
  #include "localization/fhash.h"
  #include "localization/localize.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 "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 "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>
  
  int CFred_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", BACKUP_DEPTH);
  	cf_delete(backup_name, CF_TYPE_MISSIONS);
  	for (i = 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 CFred_mission_save::bypass_comment(const char *comment, const char *end)
  {
  	char *ch = strstr(raw_ptr, comment);
  	if (ch != NULL) {
  		if (end != NULL) {
  			char *ep = strstr(raw_ptr, end);
  			if (ep != NULL && 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 CFred_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 CFred_mission_save::convert_special_tags_to_retail(SCP_string &text)
  {
  	replace_all(text, "$quote", "''");
  	replace_all(text, "$semicolon", ",");
  }
  
  void CFred_mission_save::convert_special_tags_to_retail()
  {
  	int i, team, stage;
  
  	if (Mission_save_format != FSO_FORMAT_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 CFred_mission_save::fout(char *format, ...)
  {
  	// don't output anything if we're saving in retail and have version-specific comments active
  	if (Mission_save_format == FSO_FORMAT_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 CFred_mission_save::fout_ext(char *pre_str, char *format, ...)
  {
  	// don't output anything if we're saving in retail and have version-specific comments active
  	if (Mission_save_format == FSO_FORMAT_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 {
  		char buf[10];
  		sprintf(buf, "%d", str_id);
  
  		str_out_scp += " XSTR(\"";
  		str_out_scp += str_scp;
  		str_out_scp += "\", ";
  		str_out_scp += buf;
  		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 != NULL) && (*(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')) != NULL);
  
  			// 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 CFred_mission_save::fout_version(char *format, ...)
  {
  	SCP_string str_scp;
  	char *ch = NULL;
  	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 (Mission_save_format == FSO_FORMAT_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 ((Mission_save_format == FSO_FORMAT_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 ((Mission_save_format == FSO_FORMAT_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 != NULL) && (*(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')) != NULL);
  
  			// 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 CFred_mission_save::parse_comments(int newlines)
  {
  	char *comment_start = NULL;
  	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");
  
  					*raw_ptr = 0;
  					fout("%s\n", comment_start);
  					*raw_ptr = '\n';
  					state = first_comment = same_line = flag = 0;
  				} else if (state == 4) {
  					same_line = newlines - 2 + same_line;
  					while (same_line-- > 0)
  						fout("\n");
  
  					if (*(raw_ptr - 1) == '\r') {
  						*(raw_ptr - 1) = '\0';
  					} else {
  						*raw_ptr = 0;
  					}
  
  					fout("%s\n", comment_start);
  
  					if (*(raw_ptr - 1) == '\0') {
  						*(raw_ptr - 1) = '\r';
  					} else {
  						*raw_ptr = '\n';
  					}
  
  					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 CFred_mission_save::save_ai_goals(ai_goal *goalp, int ship)
  {
  	char *str = NULL, 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_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_DISARM_SHIP:
  					str = "ai-disarm-ship";
  					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_STAY_STILL:
  					str = "ai-stay-still";
  					break;
  
  				default:
  					Assert(0);
  				}
  
  				if (valid)
  					fout("( %s %s %d ) ", str, buf, goalp[i].priority);
  			}
  		}
  
  		fso_comment_pop();
  	}
  
  	if (!flag)
  		fout(")");
  
  	fso_comment_pop(true);
  }
  
  int CFred_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 ship genre)
  		if (Asteroid_field.debris_genre == DG_SHIP) {
  			for (int idx = 0; idx < 3; idx++) {
  				if (Asteroid_field.field_debris_type[idx] != -1) {
  					if (optional_string_fred("+Field Debris Type:")) {
  						parse_comments();
  					} else {
  						fout("\n+Field Debris Type:");
  					}
  					fout(" %d", Asteroid_field.field_debris_type[idx]);
  				}
  			}
  		} else {
  			// asteroid subtypes stored in field_debris_type as -1 or 1
  			for (auto idx = 0; idx < 3; idx++) {
  				if (Asteroid_field.field_debris_type[idx] != -1) {
  					if (optional_string_fred("+Field Debris Type:")) {
  						parse_comments();
  					} else {
  						fout("\n+Field Debris Type:");
  					}
  					fout(" %d", idx);
  				}
  			}
  		}
  
  
  		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 == 1) {
  			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_target_ships.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_target_ships) {				
  				fout(" \"%s\"", name.c_str());
  			}
  
  			fout(" )");
  			fso_comment_pop();
  		}
  
  		fso_comment_pop();
  	}
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  int CFred_mission_save::save_bitmaps()
  {
  	int i;
  	uint j;
  
  	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 (Mission_save_format != FSO_FORMAT_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(" )");
  		}
  
  		required_string_fred("+Neb2Flags:");
  		parse_comments();
  		fout(" %d", Neb2_poof_flags);
  	}
  	// 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 (i = 0; i < (int)Backgrounds.size(); i++) {
  		bool tag = (i < (int)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 (Mission_save_format == FSO_FORMAT_RETAIL) {
  			MessageBox(nullptr, "Warning: 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.", "Incompatibility with retail mission format", MB_OK);
  		} else if (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 (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 (!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 (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 (!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 CFred_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);
  
  			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 (Mission_save_format != FSO_FORMAT_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 (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 (Mission_save_format != FSO_FORMAT_RETAIL) {
  					required_string_fred("$mirror:");
  					parse_comments();
  					fout(" %d", (bi->flags & BI_MIRROR_ICON) ? 1 : 0);
  				}
  
  				if ((Mission_save_format != FSO_FORMAT_RETAIL) && (bi->flags & BI_USE_WING_ICON)) {
  					required_string_fred("$use wing icon:");
  					parse_comments();
  					fout(" %d", (bi->flags & BI_USE_WING_ICON) ? 1 : 0);
  				}
  
  				if ((Mission_save_format != FSO_FORMAT_RETAIL) && (bi->flags & BI_USE_CARGO_ICON)) {
  					required_string_fred("$use cargo icon:");
  					parse_comments();
  					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 CFred_mission_save::save_campaign_file(char *pathname)
  {
  	int i, j, m, flag;
  
  	Campaign_tree_formp->save_tree();  // flush all changes so they get saved.
  	Campaign_tree_viewp->sort_elements();
  	reset_parse();
  	fred_parse_flag = 0;
  
  	pathname = cf_add_ext(pathname, FS_CAMPAIGN_FILE_EXT);
  	fp = cfopen(pathname, "wt", CFILE_NORMAL, CF_TYPE_MISSIONS);
  	if (!fp) {
  		nprintf(("Error", "Can't open campaign file to save.\n"));
  		return -1;
  	}
  
  	required_string_fred("$Name:");
  	parse_comments(0);
  	fout(" %s", Campaign.name);
  
  	Assert((Campaign.type >= 0) && (Campaign.type < MAX_CAMPAIGN_TYPES));
  	required_string_fred("$Type:");
  	parse_comments();
  	fout(" %s", campaign_types[Campaign.type]);
  
  	// XSTR
  	if (Campaign.desc) {
  		required_string_fred("+Description:");
  		parse_comments();
  		fout_ext("\n", "%s", Campaign.desc);
  		fout("\n$end_multi_text");
  	}
  
  	if (Campaign.type != CAMPAIGN_TYPE_SINGLE) {
  		required_string_fred("+Num Players:");
  		parse_comments();
  		fout(" %d", Campaign.num_players);
  	}
  
  	// campaign flags - Goober5000
  	if (Mission_save_format != FSO_FORMAT_RETAIL) {
  		optional_string_fred("$Flags:");
  		parse_comments();
  		fout(" %d\n", Campaign.flags);
  	}
  
  	// write out the ships and weapons which the player can start the campaign with
  	optional_string_fred("+Starting Ships: (");
  	parse_comments(2);
  	for (i = 0; i < ship_info_size(); i++) {
  		if (Campaign.ships_allowed[i])
  			fout(" \"%s\" ", Ship_info[i].name);
  	}
  	fout(")\n");
  
  	optional_string_fred("+Starting Weapons: (");
  	parse_comments();
  	for (i = 0; i < weapon_info_size(); i++) {
  		if (Campaign.weapons_allowed[i])
  			fout(" \"%s\" ", Weapon_info[i].name);
  	}
  	fout(")\n");
  
  	fred_parse_flag = 0;
  	for (i = 0; i < Campaign.num_missions; i++) {
  		m = Sorted[i];
  		required_string_either_fred("$Mission:", "#End");
  		required_string_fred("$Mission:");
  		parse_comments(2);
  		fout(" %s", Campaign.missions[m].name);
  
  		if (strlen(Campaign.missions[i].briefing_cutscene)) {
  			if (optional_string_fred("+Briefing Cutscene:", "$Mission"))
  				parse_comments();
  			else
  				fout("\n+Briefing Cutscene:");
  
  			fout(" %s", Campaign.missions[i].briefing_cutscene);
  		}
  
  		required_string_fred("+Flags:", "$Mission:");
  		parse_comments();
  
  		// Goober5000
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			// don't save Bastion flag
  			fout(" %d", Campaign.missions[m].flags & ~CMISSION_FLAG_BASTION);
  
  			// new main hall stuff
  			if (optional_string_fred("+Main Hall:", "$Mission:"))
  				parse_comments();
  			else
  				fout("\n+Main Hall:");
  
  			fout(" %s", Campaign.missions[m].main_hall.c_str());
  		} else {
  			// save Bastion flag properly
  			fout(" %d", Campaign.missions[m].flags | ((Campaign.missions[m].main_hall != "") ? CMISSION_FLAG_BASTION : 0));
  		}
  
  		if (Campaign.missions[m].debrief_persona_index > 0) {
  			fso_comment_push(";;FSO 3.6.8;;");
  			if (optional_string_fred("+Debriefing Persona Index:")) {
  				parse_comments(1);
  				fout(" %d", Campaign.missions[m].debrief_persona_index);
  			} else {
  				fout_version("\n+Debriefing Persona Index: %d", Campaign.missions[m].debrief_persona_index);
  			}
  			fso_comment_pop();
  		} else {
  			bypass_comment(";;FSO 3.6.8;; +Debriefing Persona Index:");
  		}
  
  		// save campaign link sexp
  		bool mission_loop = false;
  		bool mission_fork = false;
  		flag = 0;
  		for (j = 0; j < Total_links; j++) {
  			if (Links[j].from == m) {
  				if (!flag) {
  					if (optional_string_fred("+Formula:", "$Mission:"))
  						parse_comments();
  					else
  						fout("\n+Formula:");
  
  					fout(" ( cond\n");
  					flag = 1;
  				}
  
  				//save_campaign_sexp(Links[j].sexp, Campaign.missions[Links[j].to].name);
  				if (Links[j].is_mission_loop) {
  					mission_loop = true;
  				} else if (Links[j].is_mission_fork && (Mission_save_format != FSO_FORMAT_RETAIL)) {
  					mission_fork = true;
  				} else {
  					save_campaign_sexp(Links[j].sexp, Links[j].to);
  				}
  			}
  		}
  
  		if (flag) {
  			fout(")");
  		}
  
  		// now save campaign loop sexp
  		if (mission_loop || mission_fork) {
  			if (mission_loop)
  				required_string_fred("\n+Mission Loop:");
  			else
  				required_string_fred("\n+Mission Fork:");
  			parse_comments();
  
  			int num_mission_special = 0;
  			for (j = 0; j < Total_links; j++) {
  				if ((Links[j].from == m) && (Links[j].is_mission_loop || Links[j].is_mission_fork)) {
  
  					num_mission_special++;
  
  					if ((num_mission_special == 1) && Links[j].mission_branch_txt) {
  						if (mission_loop)
  							required_string_fred("+Mission Loop Text:");
  						else
  							required_string_fred("+Mission Fork Text:");
  						parse_comments();
  						fout_ext("\n", "%s", Links[j].mission_branch_txt);
  						fout("\n$end_multi_text");
  					}
  
  					if ((num_mission_special == 1) && Links[j].mission_branch_brief_anim) {
  						if (mission_loop)
  							required_string_fred("+Mission Loop Brief Anim:");
  						else
  							required_string_fred("+Mission Fork Brief Anim:");
  						parse_comments();
  						fout_ext("\n", "%s", Links[j].mission_branch_brief_anim);
  						fout("\n$end_multi_text");
  					}
  
  					if ((num_mission_special == 1) && Links[j].mission_branch_brief_sound) {
  						if (mission_loop)
  							required_string_fred("+Mission Loop Brief Sound:");
  						else
  							required_string_fred("+Mission Fork Brief Sound:");
  						parse_comments();
  						fout_ext("\n", "%s", Links[j].mission_branch_brief_sound);
  						fout("\n$end_multi_text");
  					}
  
  					if (num_mission_special == 1) {
  						// write out mission loop formula
  						fout("\n+Formula:");
  						fout(" ( cond\n");
  						save_campaign_sexp(Links[j].sexp, Links[j].to);
  						fout(")");
  					}
  					if (mission_fork) {
  						fout("Option: ", Campaign.missions[Links[j].to].name);
  					}
  				}
  			}
  			if (mission_loop && num_mission_special > 1) {
  				char buffer[1024];
  				sprintf(buffer, "Multiple branching loop error from mission %s\nEdit campaign for *at most* 1 loop from each mission.", Campaign.missions[m].name);
  				Message(os::dialogs::MESSAGEBOX_ERROR, buffer);
  			}
  		}
  
  		if (optional_string_fred("+Level:", "$Mission:")) {
  			parse_comments();
  		} else {
  			fout("\n\n+Level:");
  		}
  
  		fout(" %d", Campaign.missions[m].level);
  
  		if (optional_string_fred("+Position:", "$Mission:")) {
  			parse_comments();
  		} else {
  			fout("\n+Position:");
  		}
  
  		fout(" %d", Campaign.missions[m].pos);
  
  		fso_comment_pop();
  	}
  
  	required_string_fred("#End");
  	parse_comments(2);
  	token_found = NULL;
  	parse_comments();
  	fout("\n");
  
  	cfclose(fp);
  	if (err)
  		mprintf(("Campaign saving error code #%d\n", err));
  	else
  		Campaign_wnd->error_checker();
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  void CFred_mission_save::save_campaign_sexp(int node, int link_num)
  {
  	SCP_string sexp_out;
  	Assert(node >= 0);
  
  	// if the link num is -1, then this is a end-of-campaign location
  	if (link_num != -1) {
  		if (build_sexp_string(sexp_out, node, 2, SEXP_SAVE_MODE)) {
  			fout("   (\n      %s\n      ( next-mission \"%s\" )\n   )\n", sexp_out.c_str(), Campaign.missions[link_num].name);
  		} else {
  			fout("   ( %s( next-mission \"%s\" ) )\n", sexp_out.c_str(), Campaign.missions[link_num].name);
  		}
  	} else {
  		if (build_sexp_string(sexp_out, node, 2, SEXP_SAVE_MODE)) {
  			fout("   (\n      %s\n      ( end-of-campaign )\n   )\n", sexp_out.c_str());
  		} else {
  			fout("   ( %s( end-of-campaign ) )\n", sexp_out.c_str());
  		}
  	}
  }
  
  int CFred_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();
  
  		required_string_fred("$Ani Filename:");
  		parse_comments();
  		fout(" %s", Cur_cmd_brief->stage[stage].ani_filename);
  
  		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 CFred_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 CFred_mission_save::fso_comment_push(char *ver)
  {
  	if (fso_ver_comment.empty()) {
  		fso_ver_comment.push_back(SCP_string(ver));
  		return;
  	}
  
  	SCP_string 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(fso_ver_comment.back().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 CFred_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();
  }
  
  int CFred_mission_save::save_common_object_data(object *objp, ship *shipp)
  {
  	int j, z;
  	ship_subsys *ptr = NULL;
  	ship_info *sip = NULL;
  	ship_weapon *wp = NULL;
  	float temp_max_hull_strength;
  
  	sip = &Ship_info[shipp->ship_info_index];
  
  	if ((int) objp->phys_info.speed) {
  		if (optional_string_fred("+Initial Velocity:", "$Name:", "+Subsystem:"))
  			parse_comments();
  		else
  			fout("\n+Initial Velocity:");
  
  		fout(" %d", (int) objp->phys_info.speed);
  	}
  
  	// Goober5000
  	if (Mission_save_format != FSO_FORMAT_RETAIL && (shipp->special_hitpoints > 0)) {
  		temp_max_hull_strength = (float) shipp->special_hitpoints;
  	} else {
  		temp_max_hull_strength = sip->max_hull_strength;
  	}
  
  	if ((int) objp->hull_strength != temp_max_hull_strength) {
  		if (optional_string_fred("+Initial Hull:", "$Name:", "+Subsystem:"))
  			parse_comments();
  		else
  			fout("\n+Initial Hull:");
  
  		fout(" %d", (int) objp->hull_strength);
  	}
  
  	if ((int) shield_get_strength(objp) != 100) {
  		if (optional_string_fred("+Initial Shields:", "$Name:", "+Subsystem:"))
  			parse_comments();
  		else
  			fout("\n+Initial Shields:");
  
  		fout(" %d", (int) 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", (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(NULL, "%s", Cargo_names[ptr->subsys_cargo_name]);
  		}
  
  		if (ptr->system_info->type == SUBSYSTEM_TURRET)
  			save_turret_info(ptr, SHIP_INDEX(shipp));
  
  		ptr = GET_NEXT(ptr);
  
  		fso_comment_pop();
  	}
  
  	/*	for (j=0; j<shipp->status_count; j++) {
  	required_string_fred("$Status Description:");
  	parse_comments(-1);
  	fout(" %s", Status_desc_names[shipp->status_type[j]]);
  
  	required_string_fred("$Status:");
  	parse_comments(-1);
  	fout(" %s", Status_type_names[shipp->status[j]]);
  
  	required_string_fred("$Target:");
  	parse_comments(-1);
  	fout(" %s", Status_target_names[shipp->target[j]]);
  	}*/
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  int CFred_mission_save::save_containers()
  {
  	if (Mission_save_format == FSO_FORMAT_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");
  				}
  
  				fout("\n$Data: ( ");
  				for (const auto &map_entry : container.map_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 CFred_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 CFred_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 (Mission_save_format != FSO_FORMAT_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 CFred_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 (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if (optional_string_fred("#Cutscenes")) {
  				parse_comments(2);
  			} else {
  				fout_version("\n\n#Cutscenes\n\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_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("%s %s\n", 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", sexp_out.c_str());
  				}
  			}
  			required_string_fred("#end");
  			parse_comments();
  		} else {
  			MessageBox(nullptr, "Warning: This mission contains cutscene data, but you are saving in the retail mission format. This information will be lost.", "Incompatibility with retail mission format", MB_OK);
  		}
  	}
  
  	fso_comment_pop(true);
  	return err;
  }
  
  int CFred_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 CFred_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;! %d total\n", Num_mission_events);
  
  	for (i = 0; i < Num_mission_events; 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) {
  			if (optional_string_fred("+Name:", "$Formula:")) {
  				parse_comments();
  			} else {
  				fout("\n+Name:");
  			}
  
  			fout(" %s", Mission_events[i].name);
  		}
  
  		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 (Mission_save_format != FSO_FORMAT_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) {
  			if (optional_string_fred("+Objective:", "$Formula:")) {
  				parse_comments();
  			} else {
  				fout("\n+Objective:");
  			}
  
  			fout_ext(" ", "%s", Mission_events[i].objective_text);
  		}
  
  		//XSTR
  		if (Mission_events[i].objective_key_text) {
  			if (optional_string_fred("+Objective key:", "$Formula:")) {
  				parse_comments();
  			} else {
  				fout("\n+Objective key:");
  			}
  
  			fout_ext(" ", "%s", Mission_events[i].objective_key_text);
  		}
  
  		// 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 (Mission_save_format != FSO_FORMAT_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 (Mission_save_format != FSO_FORMAT_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 (Mission_save_format != FSO_FORMAT_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 CFred_mission_save::save_fiction()
  {
  	if (mission_has_fiction()) {
  		if (Mission_save_format != FSO_FORMAT_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 (SCP_vector<fiction_viewer_stage>::iterator stage = Fiction_viewer_stages.begin(); stage != Fiction_viewer_stages.end(); ++stage) {
  				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);
  				} else
  					optional_string_fred("$Font:");
  
  				// 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);
  				} else
  					optional_string_fred("$Voice:");
  
  				// save UI
  				if (strlen(stage->ui_name) > 0) {
  					if (optional_string_fred("$UI:"))
  						parse_comments();
  					else
  						fout("\n$UI:");
  					fout(" %s", stage->ui_name);
  				} else
  					optional_string_fred("$UI:");
  
  				// 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
  					optional_string_fred("$Formula:");
  			}
  		} else {
  			MessageBox(nullptr, "Warning: This mission contains fiction viewer data, but you are saving in the retail mission format.", "Incompatibility with retail mission format", MB_OK);
  		}
  	}
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  int CFred_mission_save::save_goals()
  {
  	SCP_string sexp_out;
  	int i;
  
  	fred_parse_flag = 0;
  	required_string_fred("#Goals");
  	parse_comments(2);
  	fout("\t\t;! %d total\n", Num_goals);
  
  	for (i = 0; i < Num_goals; 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) {
  			if (optional_string_fred("+Name:", "$Type:"))
  				parse_comments();
  			else
  				fout("\n+Name:");
  
  			fout(" %s", Mission_goals[i].name);
  		}
  
  		// XSTR
  		required_string_fred("$MessageNew:");
  		parse_comments();
  		fout_ext(" ", "%s", Mission_goals[i].message);
  		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 CFred_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 CFred_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 (Mission_save_format != FSO_FORMAT_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);
  		}
  
  		fso_comment_pop();
  	}
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  int CFred_mission_save::save_mission_file(char *pathname)
  {
  	char backup_name[256], savepath[MAX_PATH_LEN], *p;
  
  	strcpy_s(savepath, "");
  	p = strrchr(pathname, '\\');
  	if (p) {
  		*p = '\0';
  		strcpy_s(savepath, pathname);
  		*p = '\\';
  		strcat_s(savepath, "\\");
  	}
  	strcat_s(savepath, "saving.xxx");
  
  	save_mission_internal(savepath);
  
  	if (!err) {
  		strcpy_s(backup_name, pathname);
  		if (backup_name[strlen(backup_name) - 4] == '.')
  			backup_name[strlen(backup_name) - 4] = 0;
  
  		strcat_s(backup_name, ".bak");
  		cf_attrib(pathname, 0, FILE_ATTRIBUTE_READONLY, CF_TYPE_MISSIONS);
  		cf_delete(backup_name, CF_TYPE_MISSIONS);
  		cf_rename(pathname, backup_name, CF_TYPE_MISSIONS);
  		cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
  	}
  
  	return err;
  }
  
  int CFred_mission_save::save_mission_info()
  {
  	required_string_fred("#Mission Info");
  	parse_comments(0);
  
  	required_string_fred("$Version:");
  	parse_comments(2);
  	fout(" %.2f", FRED_MISSION_VERSION);
  
  	// XSTR
  	required_string_fred("$Name:");
  	parse_comments();
  	fout_ext(" ", "%s", The_mission.name);
  
  	required_string_fred("$Author:");
  	parse_comments();
  	fout(" %s", The_mission.author);
  
  	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(" %" PRIu64, The_mission.flags.to_u64());
  
  	// maybe write out Nebula intensity
  	if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
  		Assert(Neb2_awacs > 0.0f);
  		fout("\n+NebAwacs: %f\n", Neb2_awacs);
  
  		// storm name
  		fout("\n+Storm: %s\n", Mission_parse_storm_name);
  	}
  
  	// Goober5000
  	if (Mission_save_format != FSO_FORMAT_RETAIL) {
  		// write out the nebula clipping multipliers
  		fout("\n+Fog Near Mult: %f\n", Neb2_fog_near_mult);
  		fout("\n+Fog Far Mult: %f\n", Neb2_fog_far_mult);
  
  		if (The_mission.contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT) {
  			fout("\n$Contrail Speed Threshold: %d\n", The_mission.contrail_threshold);
  		}
  	}
  
  	// 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 (Mission_save_format != FSO_FORMAT_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 (Mission_save_format == FSO_FORMAT_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 (Mission_save_format == FSO_FORMAT_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(1);
  	} 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);
  
  	// here be WMCoolmon's hull and subsys repair stuff
  	if (Mission_save_format != FSO_FORMAT_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 (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(view_pos);
  
  	if (optional_string_fred("+Viewer orient:")) {
  		parse_comments();
  	} else {
  		fout("\n+Viewer orient:");
  	}
  
  	save_matrix(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 (Mission_save_format != FSO_FORMAT_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();
  	} 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();
  
  	// sound environment (EFX/EAX) - taylor
  	sound_env *m_env = &The_mission.sound_environment;
  	if ((m_env->id >= 0) && (m_env->id < (int) EFX_presets.size())) {
  		EFXREVERBPROPERTIES *prop = &EFX_presets[m_env->id];
  
  		fso_comment_push(";;FSO 3.6.12;;");
  
  		fout_version("\n\n$Sound Environment: %s", prop->name.c_str());
  
  		if (m_env->volume != prop->flGain) {
  			fout_version("\n+Volume: %f", m_env->volume);
  		}
  
  		if (m_env->damping != prop->flDecayHFRatio) {
  			fout_version("\n+Damping: %f", m_env->damping);
  		}
  
  		if (m_env->decay != prop->flDecayTime) {
  			fout_version("\n+Decay Time: %f", m_env->decay);
  		}
  
  		fso_comment_pop();
  	}
  
  	return err;
  }
  
  void CFred_mission_save::save_mission_internal(const char *pathname)
  {
  	CTime t;
  
  	t = CTime::GetCurrentTime();
  	strcpy_s(The_mission.modified, t.Format("%x at %X"));
  
  	reset_parse();
  	fred_parse_flag = 0;
  	fp = cfopen(pathname, "wt", CFILE_NORMAL, CF_TYPE_MISSIONS);
  	if (!fp) {
  		nprintf(("Error", "Can't open mission file to save.\n"));
  		err = -1;
  		return;
  	}
  
  	// 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_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 {
  		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 CFred_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_DEBRIEF_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_DEBRIEF_SUCCESS] < 0 ? "None" : Spooled_music[Mission_music[SCORE_DEBRIEF_SUCCESS]].name);
  	}
  	if (Mission_save_format != FSO_FORMAT_RETAIL && Mission_music[SCORE_DEBRIEF_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_DEBRIEF_AVERAGE] < 0 ? "None" : Spooled_music[Mission_music[SCORE_DEBRIEF_AVERAGE]].name);
  	}
  	if (Mission_music[SCORE_DEBRIEF_FAIL] != 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_DEBRIEF_FAIL] < 0 ? "None" : Spooled_music[Mission_music[SCORE_DEBRIEF_FAIL]].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 CFred_mission_save::save_warp_params(WarpDirection direction, ship *shipp)
  {
  	if (Mission_save_format == FSO_FORMAT_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 (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;
  }
  
  int CFred_mission_save::save_objects()
  {
  	SCP_string sexp_out;
  	int i, z;
  	ai_info *aip;
  	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 = z = 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(z ? 2 : 1);
  		fout(" %s\t\t;! Object #%d", shipp->ship_name, i);
  
  		// Display name
  		// The display name is only written if there was one at the start to avoid introducing inconsistencies
  		if (Mission_save_format != FSO_FORMAT_RETAIL && 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 (strcmp(shipp->get_display_name(), truncated_name) != 0) {
  				fout("\n$Display name:");
  				fout_ext(" ", "%s", shipp->display_name.c_str());
  			}
  		}
  
  		required_string_fred("\n$Class:");
  		parse_comments(0);
  		fout(" %s", Ship_info[shipp->ship_info_index].name);
  
  		//alt classes stuff
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			for (SCP_vector<alt_class>::iterator ii = shipp->s_alt_classes.begin(); ii != shipp->s_alt_classes.end(); ++ii) {
  				// 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:");
  				}
  			}
  		}
  
  		// optional alternate type name
  		if (strlen(Fred_alt_names[i])) {
  			fout("\n$Alt: %s\n", Fred_alt_names[i]);
  		}
  
  		// optional callsign
  		if (Mission_save_format != FSO_FORMAT_RETAIL && strlen(Fred_callsigns[i])) {
  			fout("\n$Callsign: %s\n", Fred_callsigns[i]);
  		}
  
  		required_string_fred("$Team:");
  		parse_comments();
  		fout(" %s", Iff_info[shipp->team].iff_name);
  
  		if (Mission_save_format != FSO_FORMAT_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 (Mission_save_format == FSO_FORMAT_RETAIL) {
  			required_string_fred("$IFF:");
  			parse_comments();
  			fout(" %s", "IFF 1");
  		}
  
  		Assert(shipp->ai_index >= 0);
  		aip = &Ai_info[shipp->ai_index];
  
  		required_string_fred("$AI Behavior:");
  		parse_comments();
  		fout(" %s", Ai_behavior_names[aip->behavior]);
  
  		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]);
  
  		save_common_object_data(&Objects[shipp->objnum], &Ships[i]);
  
  		if (shipp->wingnum >= 0) {
  			shipp->arrival_location = ARRIVE_AT_LOCATION;
  		}
  
  		required_string_fred("$Arrival Location:");
  		parse_comments();
  		fout(" %s", Arrival_location_names[shipp->arrival_location]);
  
  		if (shipp->arrival_location != ARRIVE_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;
  			if (z & SPECIAL_ARRIVAL_ANCHOR_FLAG) {
  				// get name
  				char tmp[NAME_LENGTH + 15];
  				stuff_special_arrival_anchor_name(tmp, z, Mission_save_format == FSO_FORMAT_RETAIL ? 1 : 0);
  
  				// save it
  				fout(" %s", tmp);
  			} else if (z >= 0) {
  				fout(" %s", Ships[z].ship_name);
  			} else {
  				fout(" <error>");
  			}
  		}
  
  		// Goober5000
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if ((shipp->arrival_location == ARRIVE_FROM_DOCK_BAY) && (shipp->arrival_path_mask > 0)) {
  				int anchor_shipnum;
  				polymodel *pm;
  
  				anchor_shipnum = shipp->arrival_anchor;
  				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
  
  				fout("\n+Arrival Paths: ( ");
  
  				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
  				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 = DEPART_AT_LOCATION;
  		}
  
  		required_string_fred("$Departure Location:");
  		parse_comments();
  		fout(" %s", Departure_location_names[shipp->departure_location]);
  
  		if (shipp->departure_location != DEPART_AT_LOCATION) {
  			required_string_fred("$Departure Anchor:");
  			parse_comments();
  
  			if (shipp->departure_anchor >= 0)
  				fout(" %s", Ships[shipp->departure_anchor].ship_name);
  			else
  				fout(" <error>");
  		}
  
  		// Goober5000
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if ((shipp->departure_location == DEPART_AT_DOCK_BAY) && (shipp->departure_path_mask > 0)) {
  				int anchor_shipnum;
  				polymodel *pm;
  
  				anchor_shipnum = shipp->departure_anchor;
  				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
  
  				fout("\n+Departure Paths: ( ");
  
  				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
  				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])
  			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 ((Mission_save_format == FSO_FORMAT_RETAIL) && ((shipp->flags[Ship::Ship_Flags::Ship_locked]) || (shipp->flags[Ship::Ship_Flags::Weapons_locked])))
  			fout(" \"locked\"");
  		fout(" )");
  
  		// flags2 added by Goober5000 --------------------------------
  		if (Mission_save_format != FSO_FORMAT_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::Immobile])
  				fout(" \"immobile\"");
  			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\"");
  			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);
  		}
  
  		// special explosions
  		if (Mission_save_format != FSO_FORMAT_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 {
  					CString text = "You are saving in the retail mission format, but ";
  					text += "the mission has too many special explosions defined. \"";
  					text += shipp->ship_name;
  					text += "\" has therefore lost any special explosion data that was defined for it. ";
  					text += "\" Either remove special explosions or SEXP variables if you need it to have one ";
  					MessageBox(NULL, text, "Too many variables!", MB_OK);
  
  				}
  			}
  		}
  
  		// Goober5000 ------------------------------------------------
  		if (Mission_save_format != FSO_FORMAT_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 (Mission_save_format == FSO_FORMAT_RETAIL && !dock_check_docked_one_on_one(&Objects[shipp->objnum])) {
  				static bool warned = false;
  				if (!warned) {
  					CString text = "You are saving in the retail mission format, but \"";
  					text += shipp->ship_name;
  					text += "\" is docked to more than one ship.  If you wish to run this mission in retail, ";
  					text += "you should remove the additional ships and save the mission again.";
  					MessageBox(NULL, text, "Incompatibility with retail mission format", MB_OK);
  
  					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 != NULL; 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 (Mission_save_format == FSO_FORMAT_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);
  		}
  
  		// 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 (Mission_save_format != FSO_FORMAT_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 (optional_string_fred("+Persona Index:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n+Persona Index:");
  
  			fout(" %d", shipp->persona_index);
  		}
  
  		// Goober5000 - deal with texture replacement ----------------
  		if (!Fred_texture_replacements.empty()) {
  			bool needs_header = true;
  			fso_comment_push(";;FSO 3.6.8;;");
  
  			for (SCP_vector<texture_replace>::iterator ii = Fred_texture_replacements.begin(); ii != Fred_texture_replacements.end(); ++ii) {
  				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 -------------------------------
  
  		z++;
  
  		fso_comment_pop();
  	}
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  int CFred_mission_save::save_players()
  {
  	bool wrote_fso_data = false;
  	int i, j;
  	int var_idx;
  	int used_pool[MAX_WEAPON_TYPES];
  
  	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 (Mission_save_format != FSO_FORMAT_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);
  
  	for (i = 0; i < Num_teams; i++) {
  		required_string_fred("$Starting Shipname:");
  		parse_comments();
  		Assert(Player_start_shipnum >= 0);
  		fout(" %s", Ships[Player_start_shipnum].ship_name);
  
  		required_string_fred("$Ship Choices:");
  		parse_comments();
  		fout(" (\n");
  
  		for (j = 0; j < Team_data[i].num_ship_choices; j++) {
  			// Check to see if a variable name should be written for the class rather than a number
  			if (strlen(Team_data[i].ship_list_variables[j])) {
  				var_idx = get_index_sexp_variable_name(Team_data[i].ship_list_variables[j]);
  				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[Team_data[i].ship_list[j]].name);
  			}
  
  			// Now check if we should write a variable or a number for the amount of ships available
  			if (strlen(Team_data[i].ship_count_variables[j])) {
  				var_idx = get_index_sexp_variable_name(Team_data[i].ship_count_variables[j]);
  				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", Team_data[i].ship_count[j]);
  			}
  		}
  
  		fout(")");
  
  		if (optional_string_fred("+Weaponry Pool:", "$Starting Shipname:")) {
  			parse_comments(2);
  		} else {
  			fout("\n\n+Weaponry Pool:");
  		}
  
  		fout(" (\n");
  		generate_weaponry_usage_list(i, used_pool);
  
  		for (j = 0; j < Team_data[i].num_weapon_choices; j++) {
  			// first output the weapon name or a variable that sets it 
  			if (strlen(Team_data[i].weaponry_pool_variable[j])) {
  				var_idx = get_index_sexp_variable_name(Team_data[i].weaponry_pool_variable[j]);
  				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[Team_data[i].weaponry_pool[j]].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 (strlen(Team_data[i].weaponry_amount_variable[j])) {
  				var_idx = get_index_sexp_variable_name(Team_data[i].weaponry_amount_variable[j]);
  				Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
  				wrote_fso_data = true;
  
  				fout("@%s\n", Sexp_variables[var_idx].variable_name);
  			} else {
  				if (strlen(Team_data[i].weaponry_pool_variable[j])) {
  					fout("%d\n", Team_data[i].weaponry_count[j]);
  				} else {
  					fout("%d\n", Team_data[i].weaponry_count[j] + used_pool[Team_data[i].weaponry_pool[j]]);
  					used_pool[Team_data[i].weaponry_pool[j]] = 0;
  				}
  			}
  		}
  
  		// now we add anything left in the used pool as a static entry
  		for (j = 0; j < weapon_info_size(); j++) {
  			if (used_pool[j] > 0) {
  				fout("\t\"%s\"\t%d\n", Weapon_info[j].name, used_pool[j]);
  			}
  		}
  
  		fout(")");
  
  		// sanity check
  		if (Mission_save_format == FSO_FORMAT_RETAIL && wrote_fso_data) {
  			// this is such an unlikely (and hard-to-fix) case that a warning should be sufficient
  			MessageBox(nullptr, "Warning: 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.", "Incompatibility with retail mission format", MB_OK);
  		}
  
  		// Goober5000 - mjn.mixael's required weapon feature
  		bool uses_required_weapon = false;
  		for (j = 0; j < weapon_info_size(); j++) {
  			if (Team_data[i].weapon_required[j]) {
  				uses_required_weapon = true;
  				break;
  			}
  		}
  		if (Mission_save_format != FSO_FORMAT_RETAIL && uses_required_weapon) {
  			if (optional_string_fred("+Required for mission:", "$Starting Shipname:"))
  				parse_comments(2);
  			else
  				fout("\n+Required for mission:");
  
  			fout(" (");
  			for (j = 0; j < weapon_info_size(); j++) {
  				if (Team_data[i].weapon_required[j])
  					fout(" \"%s\"", Weapon_info[j].name);
  			}
  			fout(" )");
  		}
  
  		fso_comment_pop();
  	}
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  int CFred_mission_save::save_plot_info()
  {
  	if (Mission_save_format == FSO_FORMAT_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 CFred_mission_save::save_reinforcements()
  {
  	int i, j, type;
  
  	fred_parse_flag = 0;
  	required_string_fred("#Reinforcements");
  	parse_comments(2);
  	fout("\t\t;! %d total\n", Num_reinforcements);
  
  	for (i = 0; i < Num_reinforcements; i++) {
  		required_string_either_fred("$Name:", "#Background bitmaps");
  		required_string_fred("$Name:");
  		parse_comments(i ? 2 : 1);
  		fout(" %s", Reinforcements[i].name);
  
  		type = TYPE_ATTACK_PROTECT;
  		for (j = 0; j < MAX_SHIPS; j++)
  			if ((Ships[j].objnum != -1) && !stricmp(Ships[j].ship_name, Reinforcements[i].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", Reinforcements[i].uses);
  
  		if (optional_string_fred("+Arrival Delay:", "$Name:"))
  			parse_comments();
  		else
  			fout("\n+Arrival Delay:");
  		fout(" %d", Reinforcements[i].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(Reinforcements[i].no_messages[j]))
  				fout(" \"%s\"", Reinforcements[i].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(Reinforcements[i].yes_messages[j]))
  				fout(" \"%s\"", Reinforcements[i].yes_messages[j]);
  		}
  		fout(" )");
  
  		fso_comment_pop();
  	}
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  void CFred_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
  	object *objp = &Objects[shipp->objnum];
  	object *other_objp = dock_ptr->docked_objp;
  	ship *other_shipp = &Ships[other_objp->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(other_objp, objp)));
  
  	// 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, other_objp)));
  
  	fso_comment_pop(true);
  }
  
  void CFred_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 CFred_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 (Mission_save_format == FSO_FORMAT_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 (Mission_save_format != FSO_FORMAT_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 CFred_mission_save::save_vector(vec3d &v)
  {
  	fout(" %f, %f, %f", v.xyz.x, v.xyz.y, v.xyz.z);
  	return 0;
  }
  
  int CFred_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());
  
  	SCP_list<CJumpNode>::iterator jnp;
  	for (jnp = Jump_nodes.begin(); jnp != Jump_nodes.end(); ++jnp) {
  		required_string_fred("$Jump Node:", "$Jump Node Name:");
  		parse_comments(2);
  		save_vector(jnp->GetSCPObject()->pos);
  
  		required_string_fred("$Jump Node Name:", "$Jump Node:");
  		parse_comments();
  		fout(" %s", jnp->GetName());
  
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if (jnp->IsSpecialModel()) {
  				if (optional_string_fred("+Model File:", "$Jump Node:"))
  					parse_comments();
  				else
  					fout("\n+Model File:");
  
  				int model = jnp->GetModelNumber();
  				polymodel *pm = model_get(model);
  				fout(" %s", pm->filename);
  			}
  
  			if (jnp->IsColored()) {
  				if (optional_string_fred("+Alphacolor:", "$Jump Node:"))
  					parse_comments();
  				else
  					fout("\n+Alphacolor:");
  
  				color jn_color = jnp->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 || jnp->IsHidden()) {
  				if (!hidden_is_there)
  					fout("\n+Hidden:");
  
  				if (jnp->IsHidden())
  					fout(" %s", "true");
  				else
  					fout(" %s", "false");
  			}
  		}
  
  		fso_comment_pop();
  	}
  
  	SCP_list<waypoint_list>::iterator ii;
  	for (ii = Waypoint_lists.begin(); ii != Waypoint_lists.end(); ++ii) {
  		required_string_either_fred("$Name:", "#Messages");
  		required_string_fred("$Name:");
  		parse_comments((ii == Waypoint_lists.begin()) ? 1 : 2);
  		fout(" %s", ii->get_name());
  
  		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();
  	}
  
  	fso_comment_pop(true);
  
  	return err;
  }
  
  int CFred_mission_save::save_waypoint_list(waypoint_list *wp_list)
  {
  	Assert(wp_list != NULL);
  	SCP_vector<waypoint>::iterator ii;
  
  	for (ii = wp_list->get_waypoints().begin(); ii != wp_list->get_waypoints().end(); ++ii) {
  		vec3d *pos = ii->get_pos();
  		fout("\t( %f, %f, %f )\n", pos->xyz.x, pos->xyz.y, pos->xyz.z);
  	}
  
  	return 0;
  }
  
  int CFred_mission_save::save_wings()
  {
  	SCP_string sexp_out;
  	int i, j, z, ship, count = 0;
  
  	fred_parse_flag = 0;
  	required_string_fred("#Wings");
  	parse_comments(2);
  	fout("\t\t;! %d total", Num_wings);
  
  	for (i = 0; i < MAX_WINGS; i++) {
  		if (!Wings[i].wave_count)
  			continue;
  
  		count++;
  		required_string_either_fred("$Name:", "#Events");
  		required_string_fred("$Name:");
  		parse_comments(2);
  		fout(" %s", Wings[i].name);
  
  		// squad logo - Goober5000
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if (strlen(Wings[i].wing_squad_filename) > 0) //-V805
  			{
  				if (optional_string_fred("+Squad Logo:", "$Name:"))
  					parse_comments();
  				else
  					fout("\n+Squad Logo:");
  
  				fout(" %s", Wings[i].wing_squad_filename);
  			}
  		}
  
  		required_string_fred("$Waves:");
  		parse_comments();
  		fout(" %d", Wings[i].num_waves);
  
  		required_string_fred("$Wave Threshold:");
  		parse_comments();
  		fout(" %d", Wings[i].threshold);
  
  		required_string_fred("$Special Ship:");
  		parse_comments();
  		fout(" %d\t\t;! %s", Wings[i].special_ship,
  			 Ships[Wings[i].ship_index[Wings[i].special_ship]].ship_name);
  
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if (Wings[i].formation >= 0 && Wings[i].formation < (int)Wing_formations.size())
  			{
  				if (optional_string_fred("+Formation:", "$Name:")) {
  					parse_comments();
  				}
  				else {
  					fout("\n+Formation:");
  				}
  
  				fout(" %s", Wing_formations[Wings[i].formation].name);
  			}
  		}
  
  		required_string_fred("$Arrival Location:");
  		parse_comments();
  		fout(" %s", Arrival_location_names[Wings[i].arrival_location]);
  
  		if (Wings[i].arrival_location != ARRIVE_AT_LOCATION) {
  			if (optional_string_fred("+Arrival Distance:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n+Arrival Distance:");
  
  			fout(" %d", Wings[i].arrival_distance);
  			if (optional_string_fred("$Arrival Anchor:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n$Arrival Anchor:");
  
  			z = Wings[i].arrival_anchor;
  			if (z & SPECIAL_ARRIVAL_ANCHOR_FLAG) {
  				// get name
  				char tmp[NAME_LENGTH + 15];
  				stuff_special_arrival_anchor_name(tmp, z, Mission_save_format == FSO_FORMAT_RETAIL ? 1 : 0);
  
  				// save it
  				fout(" %s", tmp);
  			} else if (z >= 0) {
  				fout(" %s", Ships[z].ship_name);
  			} else {
  				fout(" <error>");
  			}
  		}
  
  		// Goober5000
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if ((Wings[i].arrival_location == ARRIVE_FROM_DOCK_BAY) && (Wings[i].arrival_path_mask > 0)) {
  				int anchor_shipnum;
  				polymodel *pm;
  
  				anchor_shipnum = Wings[i].arrival_anchor;
  				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
  
  				fout("\n+Arrival Paths: ( ");
  
  				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
  				for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
  					if (Wings[i].arrival_path_mask & (1 << n)) {
  						fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
  					}
  				}
  
  				fout(")");
  			}
  		}
  
  		if (Wings[i].arrival_delay) {
  			if (optional_string_fred("+Arrival delay:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n+Arrival delay:");
  
  			fout(" %d", Wings[i].arrival_delay);
  		}
  
  		required_string_fred("$Arrival Cue:");
  		parse_comments();
  		convert_sexp_to_string(sexp_out, Wings[i].arrival_cue, SEXP_SAVE_MODE);
  		fout(" %s", sexp_out.c_str());
  
  		required_string_fred("$Departure Location:");
  		parse_comments();
  		fout(" %s", Departure_location_names[Wings[i].departure_location]);
  
  		if (Wings[i].departure_location != DEPART_AT_LOCATION) {
  			required_string_fred("$Departure Anchor:");
  			parse_comments();
  
  			if (Wings[i].departure_anchor >= 0)
  				fout(" %s", Ships[Wings[i].departure_anchor].ship_name);
  			else
  				fout(" <error>");
  		}
  
  		// Goober5000
  		if (Mission_save_format != FSO_FORMAT_RETAIL) {
  			if ((Wings[i].departure_location == DEPART_AT_DOCK_BAY) && (Wings[i].departure_path_mask > 0)) {
  				int anchor_shipnum;
  				polymodel *pm;
  
  				anchor_shipnum = Wings[i].departure_anchor;
  				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
  
  				fout("\n+Departure Paths: ( ");
  
  				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
  				for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
  					if (Wings[i].departure_path_mask & (1 << n)) {
  						fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
  					}
  				}
  
  				fout(")");
  			}
  		}
  
  		if (Wings[i].departure_delay) {
  			if (optional_string_fred("+Departure delay:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n+Departure delay:");
  
  			fout(" %d", Wings[i].departure_delay);
  		}
  
  		required_string_fred("$Departure Cue:");
  		parse_comments();
  		convert_sexp_to_string(sexp_out, Wings[i].departure_cue, SEXP_SAVE_MODE);
  		fout(" %s", sexp_out.c_str());
  
  		required_string_fred("$Ships:");
  		parse_comments();
  		fout(" (\t\t;! %d total\n", Wings[i].wave_count);
  
  		for (j = 0; j < Wings[i].wave_count; j++) {
  			ship = Wings[i].ship_index[j];
  			//			if (Objects[Ships[ship].objnum].type == OBJ_START)
  			//				fout("\t\"Player 1\"\n");
  			//			else
  			fout("\t\"%s\"\n", Ships[Wings[i].ship_index[j]].ship_name);
  		}
  
  		fout(")");
  
  		save_ai_goals(Wings[i].ai_goals, -1);
  
  		if (Wings[i].hotkey != -1) {
  			if (optional_string_fred("+Hotkey:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n+Hotkey:");
  
  			fout(" %d", Wings[i].hotkey);
  		}
  
  		if (optional_string_fred("+Flags:", "$Name:")) {
  			parse_comments();
  			fout(" (");
  		} else
  			fout("\n+Flags: (");
  
  		if (Wings[i].flags[Ship::Wing_Flags::Ignore_count])
  			fout(" \"ignore-count\"");
  		if (Wings[i].flags[Ship::Wing_Flags::Reinforcement])
  			fout(" \"reinforcement\"");
  		if (Wings[i].flags[Ship::Wing_Flags::No_arrival_music])
  			fout(" \"no-arrival-music\"");
  		if (Wings[i].flags[Ship::Wing_Flags::No_arrival_message])
  			fout(" \"no-arrival-message\"");
  		if (Wings[i].flags[Ship::Wing_Flags::No_arrival_warp])
  			fout(" \"no-arrival-warp\"");
  		if (Wings[i].flags[Ship::Wing_Flags::No_departure_warp])
  			fout(" \"no-departure-warp\"");
  		if (Wings[i].flags[Ship::Wing_Flags::No_dynamic])
  			fout(" \"no-dynamic\"");
  		if (Mission_save_format != FSO_FORMAT_RETAIL)
  		{
  			if (Wings[i].flags[Ship::Wing_Flags::Nav_carry])
  				fout(" \"nav-carry-status\"");
  			if (Wings[i].flags[Ship::Wing_Flags::Same_arrival_warp_when_docked])
  				fout(" \"same-arrival-warp-when-docked\"");
  			if (Wings[i].flags[Ship::Wing_Flags::Same_departure_warp_when_docked])
  				fout(" \"same-departure-warp-when-docked\"");
  		}
  
  		fout(" )");
  
  		if (Wings[i].wave_delay_min) {
  			if (optional_string_fred("+Wave Delay Min:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n+Wave Delay Min:");
  
  			fout(" %d", Wings[i].wave_delay_min);
  		}
  
  		if (Wings[i].wave_delay_max) {
  			if (optional_string_fred("+Wave Delay Max:", "$Name:"))
  				parse_comments();
  			else
  				fout("\n+Wave Delay Max:");
  
  			fout(" %d", Wings[i].wave_delay_max);
  		}
  
  		fso_comment_pop();
  	}
  
  	fso_comment_pop(true);
  
  	Assert(count == Num_wings);
  	return err;
  }
  