1 /*
2  * Copyright (C) Volition, Inc. 1999.  All rights reserved.
3  *
4  * All source code herein is the property of Volition, Inc. You may not sell
5  * or otherwise commercially exploit the source or things you created based on the
6  * source.
7  *
8  */
9 
10 
11 #include <ai/aigoals.h>
12 #include <asteroid/asteroid.h>
13 #include <cfile/cfile.h>
14 #include <gamesnd/eventmusic.h>
15 #include <globalincs/linklist.h>
16 #include <globalincs/version.h>
17 #include <iff_defs/iff_defs.h>
18 #include <jumpnode/jumpnode.h>
19 #include <localization/fhash.h>
20 #include <localization/localize.h>
21 #include <mission/missionbriefcommon.h>
22 #include <mission/missioncampaign.h>
23 #include <mission/missiongoals.h>
24 #include <mission/missionmessage.h>
25 #include <mission/missionparse.h>
26 #include <missionui/fictionviewer.h>
27 #include <missionui/missioncmdbrief.h>
28 #include <nebula/neb.h>
29 #include <object/objectdock.h>
30 #include <object/objectshield.h>
31 #include <parse/sexp_container.h>
32 #include <sound/ds.h>
33 #include <starfield/nebula.h>
34 #include <starfield/starfield.h>
35 #include <weapon/weapon.h>
36 
37 #include "missionsave.h"
38 #include "util.h"
39 
40 namespace fso {
41 namespace fred {
42 
autosave_mission_file(char * pathname)43 int CFred_mission_save::autosave_mission_file(char* pathname)
44 {
45 	char backup_name[256], name2[256];
46 	int i;
47 
48 	auto len = strlen(pathname);
49 	strcpy_s(backup_name, pathname);
50 	strcpy_s(name2, pathname);
51 	sprintf(backup_name + len, ".%.3d", BACKUP_DEPTH);
52 	cf_delete(backup_name, CF_TYPE_MISSIONS);
53 	for (i = BACKUP_DEPTH; i > 1; i--) {
54 		sprintf(backup_name + len, ".%.3d", i - 1);
55 		sprintf(name2 + len, ".%.3d", i);
56 		cf_rename(backup_name, name2, CF_TYPE_MISSIONS);
57 	}
58 
59 	strcpy(backup_name + len, ".001");
60 
61 	save_mission_internal(backup_name);
62 
63 	return err;
64 }
65 
bypass_comment(const char * comment,const char * end)66 void CFred_mission_save::bypass_comment(const char* comment, const char* end)
67 {
68 	char* ch = strstr(raw_ptr, comment);
69 	if (ch != NULL) {
70 		if (end != NULL) {
71 			char* ep = strstr(raw_ptr, end);
72 			if (ep != NULL && ep < ch) {
73 				return;
74 			}
75 		}
76 		char* writep = ch;
77 		char* readp = strchr(writep, '\n');
78 
79 		// copy all characters past it
80 		while (*readp != '\0') {
81 			*writep = *readp;
82 
83 			writep++;
84 			readp++;
85 		}
86 
87 		*writep = '\0';
88 	}
89 }
90 
convert_special_tags_to_retail(char * text,int max_len)91 void CFred_mission_save::convert_special_tags_to_retail(char* text, int max_len)
92 {
93 	replace_all(text, "$quote", "''", max_len);
94 	replace_all(text, "$semicolon", ",", max_len);
95 }
96 
convert_special_tags_to_retail(SCP_string & text)97 void CFred_mission_save::convert_special_tags_to_retail(SCP_string& text)
98 {
99 	replace_all(text, "$quote", "''");
100 	replace_all(text, "$semicolon", ",");
101 }
102 
convert_special_tags_to_retail()103 void CFred_mission_save::convert_special_tags_to_retail()
104 {
105 	int i, team, stage;
106 
107 	if (save_format != MissionFormat::RETAIL) {
108 		return;
109 	}
110 
111 	for (team = 0; team < Num_teams; team++) {
112 		// command briefing
113 		for (stage = 0; stage < Cmd_briefs[team].num_stages; stage++) {
114 			convert_special_tags_to_retail(Cmd_briefs[team].stage[stage].text);
115 		}
116 
117 		// briefing
118 		for (stage = 0; stage < Briefings[team].num_stages; stage++) {
119 			convert_special_tags_to_retail(Briefings[team].stages[stage].text);
120 		}
121 
122 		// debriefing
123 		for (stage = 0; stage < Debriefings[team].num_stages; stage++) {
124 			convert_special_tags_to_retail(Debriefings[team].stages[stage].text);
125 		}
126 	}
127 
128 	for (i = Num_builtin_messages; i < Num_messages; i++) {
129 		convert_special_tags_to_retail(Messages[i].message, MESSAGE_LENGTH);
130 	}
131 }
132 
fout(const char * format,...)133 int CFred_mission_save::fout(const char* format, ...)
134 {
135 	// don't output anything if we're saving in retail and have version-specific comments active
136 	if (save_format == MissionFormat::RETAIL && !fso_ver_comment.empty()) {
137 		return 0;
138 	}
139 
140 	SCP_string str;
141 	va_list args;
142 
143 	if (err) {
144 		return err;
145 	}
146 
147 	va_start(args, format);
148 	vsprintf(str, format, args);
149 	va_end(args);
150 
151 	cfputs(str.c_str(), fp);
152 	return 0;
153 }
154 
fout_ext(const char * pre_str,const char * format,...)155 int CFred_mission_save::fout_ext(const char* pre_str, const char* format, ...)
156 {
157 	// don't output anything if we're saving in retail and have version-specific comments active
158 	if (save_format == MissionFormat::RETAIL && !fso_ver_comment.empty()) {
159 		return 0;
160 	}
161 
162 	SCP_string str_scp;
163 	SCP_string str_out_scp;
164 	va_list args;
165 	int str_id;
166 
167 	if (err) {
168 		return err;
169 	}
170 
171 	va_start(args, format);
172 	vsprintf(str_scp, format, args);
173 	va_end(args);
174 
175 	if (pre_str) {
176 		str_out_scp = pre_str;
177 	}
178 
179 	// lookup the string in the hash table
180 	str_id = fhash_string_exists(str_scp.c_str());
181 
182 	// doesn't exist, so assign it an ID of -1 and stick it in the table
183 	if (str_id <= -2) {
184 		str_out_scp += " XSTR(\"";
185 		str_out_scp += str_scp;
186 		str_out_scp += "\", -1)";
187 
188 		// add the string to the table
189 		fhash_add_str(str_scp.c_str(), -1);
190 	}
191 		// _does_ exist, so just write it out as it is
192 	else {
193 		char buf[10];
194 		sprintf_safe(buf, "%d", str_id);
195 
196 		str_out_scp += " XSTR(\"";
197 		str_out_scp += str_scp;
198 		str_out_scp += "\", ";
199 		str_out_scp += buf;
200 		str_out_scp += ")";
201 	}
202 
203 	char* str_out_c = vm_strdup(str_out_scp.c_str());
204 
205 	// this could be a multi-line string, so we've got to handle it all properly
206 	if (!fso_ver_comment.empty()) {
207 		bool first_line = true;
208 		char* str_p = str_out_c;
209 
210 		char* ch = strchr(str_out_c, '\n');
211 
212 		// if we have something, and it's not just at the end, then process it specially
213 		if ((ch != NULL) && (*(ch + 1) != '\0')) {
214 			do {
215 				if (*(ch + 1) != '\0') {
216 					*ch = '\0';
217 
218 					if (first_line) {
219 						first_line = false;
220 					} else {
221 						cfputs(fso_ver_comment.back().c_str(), fp);
222 						cfputs(" ", fp);
223 					}
224 
225 					cfputs(str_p, fp);
226 					cfputc('\n', fp);
227 
228 					str_p = ch + 1;
229 				} else {
230 					if (first_line) {
231 						first_line = false;
232 					} else {
233 						cfputs(fso_ver_comment.back().c_str(), fp);
234 						cfputs(" ", fp);
235 					}
236 
237 					cfputs(str_p, fp);
238 
239 					str_p = ch + 1;
240 
241 					break;
242 				}
243 			} while ((ch = strchr(str_p, '\n')) != NULL);
244 
245 			// be sure to account for any ending elements too
246 			if (strlen(str_p)) {
247 				cfputs(fso_ver_comment.back().c_str(), fp);
248 				cfputs(" ", fp);
249 				cfputs(str_p, fp);
250 			}
251 
252 			vm_free(str_out_c);
253 			return 0;
254 		}
255 	}
256 
257 	cfputs(str_out_c, fp);
258 
259 	vm_free(str_out_c);
260 	return 0;
261 }
262 
fout_version(const char * format,...)263 int CFred_mission_save::fout_version(const char* format, ...)
264 {
265 	SCP_string str_scp;
266 	char* ch = NULL;
267 	va_list args;
268 
269 	if (err) {
270 		return err;
271 	}
272 
273 	// don't output anything if we're saving in retail and have version-specific comments active
274 	if (save_format == MissionFormat::RETAIL && !fso_ver_comment.empty()) {
275 		return 0;
276 	}
277 
278 	// output the version first thing, but skip the special case where we use
279 	// fout_version() for multiline value strings (typically indicated by an initial space)
280 	if ((save_format == MissionFormat::COMPATIBILITY_MODE) && (*format != ' ') && !fso_ver_comment.empty()) {
281 		while (*format == '\n') {
282 			str_scp.append(1, *format);
283 			format++;
284 		}
285 
286 		str_scp.append(fso_ver_comment.back().c_str());
287 		str_scp.append(" ");
288 
289 		cfputs(str_scp.c_str(), fp);
290 
291 		str_scp = "";
292 	}
293 
294 	va_start(args, format);
295 	vsprintf(str_scp, format, args);
296 	va_end(args);
297 
298 	char* str_c = vm_strdup(str_scp.c_str());
299 
300 	// this could be a multi-line string, so we've got to handle it all properly
301 	if ((save_format == MissionFormat::COMPATIBILITY_MODE) && !fso_ver_comment.empty()) {
302 		bool first_line = true;
303 		char* str_p = str_c;
304 
305 		ch = strchr(str_c, '\n');
306 
307 		// if we have something, and it's not just at the end, then process it specially
308 		if ((ch != NULL) && (*(ch + 1) != '\0')) {
309 			do {
310 				if (*(ch + 1) != '\0') {
311 					*ch = '\0';
312 
313 					if (first_line) {
314 						first_line = false;
315 					} else {
316 						cfputs(fso_ver_comment.back().c_str(), fp);
317 						cfputs(" ", fp);
318 					}
319 
320 					cfputs(str_p, fp);
321 					cfputc('\n', fp);
322 
323 					str_p = ch + 1;
324 				} else {
325 					if (first_line) {
326 						first_line = false;
327 					} else {
328 						cfputs(fso_ver_comment.back().c_str(), fp);
329 						cfputs(" ", fp);
330 					}
331 
332 					cfputs(str_p, fp);
333 
334 					str_p = ch + 1;
335 
336 					break;
337 				}
338 			} while ((ch = strchr(str_p, '\n')) != NULL);
339 
340 			// be sure to account for any ending elements too
341 			if (strlen(str_p)) {
342 				cfputs(fso_ver_comment.back().c_str(), fp);
343 				cfputs(" ", fp);
344 				cfputs(str_p, fp);
345 			}
346 
347 			vm_free(str_c);
348 			return 0;
349 		}
350 	}
351 
352 	cfputs(str_c, fp);
353 
354 	vm_free(str_c);
355 	return 0;
356 }
357 
parse_comments(int newlines)358 void CFred_mission_save::parse_comments(int newlines)
359 {
360 	char* comment_start = NULL;
361 	int state = 0, same_line = 0, first_comment = 1, tab = 0, flag = 0;
362 	bool version_added = false;
363 
364 	if (newlines < 0) {
365 		newlines = -newlines;
366 		tab = 1;
367 	}
368 
369 	if (newlines) {
370 		same_line = 1;
371 	}
372 
373 	if (fred_parse_flag || !Token_found_flag || !token_found || (token_found && (*Parse_text_raw == '\0'))) {
374 		while (newlines-- > 0) {
375 			fout("\n");
376 		}
377 
378 		if (tab && token_found) {
379 			fout_version("\t%s", token_found);
380 		} else if (token_found) {
381 			fout_version("%s", token_found);
382 		} else if (tab) {
383 			fout("\t");
384 		}
385 
386 		return;
387 	}
388 
389 	while (*raw_ptr != '\0') {
390 		// state values (as far as I could figure out):
391 		// 0 - raw_ptr not inside comment
392 		// 1 - raw_ptr inside /**/ comment
393 		// 2 - raw_ptr inside ; (newline-delimited) comment
394 		// 3,4 - raw_ptr inside ;; (FSO version) comment
395 		if (!state) {
396 			if (token_found && (*raw_ptr == *token_found)) {
397 				if (!strnicmp(raw_ptr, token_found, strlen(token_found))) {
398 					same_line = newlines - 1 + same_line;
399 					while (same_line-- > 0) {
400 						fout("\n");
401 					}
402 
403 					if (tab) {
404 						fout_version("\t");
405 						fout("%s", token_found);
406 					} else {
407 						fout_version("%s", token_found);
408 					}
409 
410 					// If you have a bunch of lines that all start with the same token (like, say, "+Subsystem:"),
411 					// this makes it so it won't just repeatedly match the first one. -MageKing17
412 					raw_ptr++;
413 
414 					if (version_added) {
415 						fso_comment_pop();
416 					}
417 
418 					return;
419 				}
420 			}
421 
422 			if ((*raw_ptr == '/') && (raw_ptr[1] == '*')) {
423 				comment_start = raw_ptr;
424 				state = 1;
425 			}
426 
427 			if ((*raw_ptr == ';') && (raw_ptr[1] != '!')) {
428 				comment_start = raw_ptr;
429 				state = 2;
430 
431 				// check for a FSO version comment, but if we can't understand it then
432 				// just handle it as a regular comment
433 				if ((raw_ptr[1] == ';') && (raw_ptr[2] == 'F') && (raw_ptr[3] == 'S') && (raw_ptr[4] == 'O')) {
434 					int major, minor, build, revis;
435 					int s_num = scan_fso_version_string(raw_ptr, &major, &minor, &build, &revis);
436 
437 					// hack for releases
438 					if (FS_VERSION_REVIS < 1000) {
439 						s_num = 3;
440 					}
441 
442 					if ((s_num == 3) && ((major < FS_VERSION_MAJOR) || ((major == FS_VERSION_MAJOR)
443 						&& ((minor < FS_VERSION_MINOR)
444 							|| ((minor == FS_VERSION_MINOR) && (build <= FS_VERSION_BUILD)))))) {
445 						state = 3;
446 					} else if ((s_num == 4) && ((major < FS_VERSION_MAJOR) || ((major == FS_VERSION_MAJOR)
447 						&& ((minor < FS_VERSION_MINOR) || ((minor == FS_VERSION_MINOR) && ((build < FS_VERSION_BUILD)
448 							|| ((build == FS_VERSION_BUILD) && (revis <= FS_VERSION_REVIS)))))))) {
449 						state = 3;
450 					} else {
451 						state = 4;
452 					}
453 				}
454 			}
455 
456 			if (*raw_ptr == '\n') {
457 				flag = 1;
458 			}
459 
460 			if (flag && state && !(state == 3)) {
461 				fout("\n");
462 			}
463 
464 		} else {
465 			if (*raw_ptr == '\n') {
466 				if (state == 2) {
467 					if (first_comment && !flag) {
468 						fout("\t\t");
469 					}
470 
471 					*raw_ptr = 0;
472 					fout("%s\n", comment_start);
473 					*raw_ptr = '\n';
474 					state = first_comment = same_line = flag = 0;
475 				} else if (state == 4) {
476 					same_line = newlines - 2 + same_line;
477 					while (same_line-- > 0) {
478 						fout("\n");
479 					}
480 
481 					if (*(raw_ptr - 1) == '\r') {
482 						*(raw_ptr - 1) = '\0';
483 					} else {
484 						*raw_ptr = 0;
485 					}
486 
487 					fout("%s\n", comment_start);
488 
489 					if (*(raw_ptr - 1) == '\0') {
490 						*(raw_ptr - 1) = '\r';
491 					} else {
492 						*raw_ptr = '\n';
493 					}
494 
495 					state = first_comment = same_line = flag = 0;
496 				}
497 			}
498 
499 			if ((*raw_ptr == '*') && (raw_ptr[1] == '/') && (state == 1)) {
500 				if (first_comment && !flag) {
501 					fout("\t\t");
502 				}
503 
504 				const char tmp = raw_ptr[2];
505 				raw_ptr[2] = 0;
506 				fout("%s", comment_start);
507 				raw_ptr[2] = tmp;
508 				state = first_comment = flag = 0;
509 			}
510 
511 			if ((*raw_ptr == ';') && (raw_ptr[1] == ';') && (state == 3)) {
512 				const char tmp = raw_ptr[2];
513 				raw_ptr[2] = 0;
514 				if (version_added) {
515 					fso_comment_pop();
516 				} else {
517 					version_added = true;
518 				}
519 				fso_comment_push(comment_start);
520 				raw_ptr[2] = tmp;
521 				state = first_comment = flag = 0;
522 				raw_ptr++;
523 			}
524 		}
525 
526 		raw_ptr++;
527 	}
528 
529 	if (version_added) {
530 		fso_comment_pop();
531 	}
532 
533 	return;
534 }
535 
save_ai_goals(ai_goal * goalp,int ship)536 void CFred_mission_save::save_ai_goals(ai_goal* goalp, int ship)
537 {
538 	const char* str = NULL;
539 	char buf[80];
540 	int i, valid, flag = 1;
541 
542 	for (i = 0; i < MAX_AI_GOALS; i++) {
543 		if (goalp[i].ai_mode == AI_GOAL_NONE) {
544 			continue;
545 		}
546 
547 		if (flag) {
548 			if (optional_string_fred("$AI Goals:", "$Name:")) {
549 				parse_comments();
550 			} else {
551 				fout("\n$AI Goals:");
552 			}
553 
554 			fout(" ( goals ");
555 			flag = 0;
556 		}
557 
558 		if (goalp[i].ai_mode == AI_GOAL_CHASE_ANY) {
559 			fout("( ai-chase-any %d ) ", goalp[i].priority);
560 
561 		} else if (goalp[i].ai_mode == AI_GOAL_UNDOCK) {
562 			fout("( ai-undock %d ) ", goalp[i].priority);
563 
564 		} else if (goalp[i].ai_mode == AI_GOAL_KEEP_SAFE_DISTANCE) {
565 			fout("( ai-keep-safe-distance %d ) ", goalp[i].priority);
566 
567 		} else if (goalp[i].ai_mode == AI_GOAL_PLAY_DEAD) {
568 			fout("( ai-play-dead %d ) ", goalp[i].priority);
569 
570 		} else if (goalp[i].ai_mode == AI_GOAL_PLAY_DEAD_PERSISTENT) {
571 			fout("( ai-play-dead-persistent %d ) ", goalp[i].priority);
572 
573 		} else if (goalp[i].ai_mode == AI_GOAL_WARP) {
574 			fout("( ai-warp-out %d ) ", goalp[i].priority);
575 
576 		} else {
577 			valid = 1;
578 			if (!goalp[i].target_name) {
579 				Warning(LOCATION, "Ai goal has no target where one is required");
580 
581 			} else {
582 				sprintf(buf, "\"%s\"", goalp[i].target_name);
583 				switch (goalp[i].ai_mode) {
584 				case AI_GOAL_WAYPOINTS:
585 					str = "ai-waypoints";
586 					break;
587 
588 				case AI_GOAL_WAYPOINTS_ONCE:
589 					str = "ai-waypoints-once";
590 					break;
591 
592 				case AI_GOAL_DESTROY_SUBSYSTEM:
593 					if (goalp[i].docker.index == -1 || !goalp[i].docker.index) {
594 						valid = 0;
595 						Warning(LOCATION, "AI destroy subsystem goal invalid subsystem name\n");
596 
597 					} else {
598 						sprintf(buf, "\"%s\" \"%s\"", goalp[i].target_name, goalp[i].docker.name);
599 						str = "ai-destroy-subsystem";
600 					}
601 
602 					break;
603 
604 				case AI_GOAL_DOCK:
605 					if (ship < 0) {
606 						valid = 0;
607 						Warning(LOCATION, "Wings aren't allowed to have a docking goal\n");
608 
609 					} else if (goalp[i].docker.index == -1 || !goalp[i].docker.index) {
610 						valid = 0;
611 						Warning(LOCATION, "AI dock goal for \"%s\" has invalid docker point "
612 										  "(docking with \"%s\")\n", Ships[ship].ship_name, goalp[i].target_name);
613 
614 					} else if (goalp[i].dockee.index == -1 || !goalp[i].dockee.index) {
615 						valid = 0;
616 						Warning(LOCATION, "AI dock goal for \"%s\" has invalid dockee point "
617 										  "(docking with \"%s\")\n", Ships[ship].ship_name, goalp[i].target_name);
618 
619 					} else {
620 						sprintf(buf,
621 								"\"%s\" \"%s\" \"%s\"",
622 								goalp[i].target_name,
623 								goalp[i].docker.name,
624 								goalp[i].dockee.name);
625 
626 						str = "ai-dock";
627 					}
628 					break;
629 
630 				case AI_GOAL_CHASE:
631 					str = "ai-chase";
632 					break;
633 
634 				case AI_GOAL_CHASE_WING:
635 					str = "ai-chase-wing";
636 					break;
637 
638 				case AI_GOAL_CHASE_SHIP_CLASS:
639 					str = "ai-chase-ship-class";
640 					break;
641 
642 				case AI_GOAL_GUARD:
643 					str = "ai-guard";
644 					break;
645 
646 				case AI_GOAL_GUARD_WING:
647 					str = "ai-guard-wing";
648 					break;
649 
650 				case AI_GOAL_DISABLE_SHIP:
651 					str = "ai-disable-ship";
652 					break;
653 
654 				case AI_GOAL_DISARM_SHIP:
655 					str = "ai-disarm-ship";
656 					break;
657 
658 				case AI_GOAL_IGNORE:
659 					str = "ai-ignore";
660 					break;
661 
662 				case AI_GOAL_IGNORE_NEW:
663 					str = "ai-ignore-new";
664 					break;
665 
666 				case AI_GOAL_EVADE_SHIP:
667 					str = "ai-evade-ship";
668 					break;
669 
670 				case AI_GOAL_STAY_NEAR_SHIP:
671 					str = "ai-stay-near-ship";
672 					break;
673 
674 				case AI_GOAL_STAY_STILL:
675 					str = "ai-stay-still";
676 					break;
677 
678 				default:
679 					Assert(0);
680 				}
681 
682 				if (valid) {
683 					fout("( %s %s %d ) ", str, buf, goalp[i].priority);
684 				}
685 			}
686 		}
687 
688 		fso_comment_pop();
689 	}
690 
691 	if (!flag) {
692 		fout(")");
693 	}
694 
695 	fso_comment_pop(true);
696 }
697 
save_asteroid_fields()698 int CFred_mission_save::save_asteroid_fields()
699 {
700 	int i;
701 
702 	fred_parse_flag = 0;
703 	required_string_fred("#Asteroid Fields");
704 	parse_comments(2);
705 
706 	for (i = 0; i < 1 /*MAX_ASTEROID_FIELDS*/; i++) {
707 		if (!Asteroid_field.num_initial_asteroids) {
708 			continue;
709 		}
710 
711 		required_string_fred("$Density:");
712 		parse_comments(2);
713 		fout(" %d", Asteroid_field.num_initial_asteroids);
714 
715 		// field type
716 		if (optional_string_fred("+Field Type:")) {
717 			parse_comments();
718 		} else {
719 			fout("\n+Field Type:");
720 		}
721 		fout(" %d", Asteroid_field.field_type);
722 
723 		// debris type
724 		if (optional_string_fred("+Debris Genre:")) {
725 			parse_comments();
726 		} else {
727 			fout("\n+Debris Genre:");
728 		}
729 		fout(" %d", Asteroid_field.debris_genre);
730 
731 		// field_debris_type (only if ship genre)
732 		if (Asteroid_field.debris_genre == DG_SHIP) {
733 			for (int idx = 0; idx < 3; idx++) {
734 				if (Asteroid_field.field_debris_type[idx] != -1) {
735 					if (optional_string_fred("+Field Debris Type:")) {
736 						parse_comments();
737 					} else {
738 						fout("\n+Field Debris Type:");
739 					}
740 					fout(" %d", Asteroid_field.field_debris_type[idx]);
741 				}
742 			}
743 		} else {
744 			// asteroid subtypes stored in field_debris_type as -1 or 1
745 			for (auto idx = 0; idx < 3; idx++) {
746 				if (Asteroid_field.field_debris_type[idx] != -1) {
747 					if (optional_string_fred("+Field Debris Type:")) {
748 						parse_comments();
749 					} else {
750 						fout("\n+Field Debris Type:");
751 					}
752 					fout(" %d", idx);
753 				}
754 			}
755 		}
756 
757 
758 		required_string_fred("$Average Speed:");
759 		parse_comments();
760 		fout(" %f", vm_vec_mag(&Asteroid_field.vel));
761 
762 		required_string_fred("$Minimum:");
763 		parse_comments();
764 		save_vector(Asteroid_field.min_bound);
765 
766 		required_string_fred("$Maximum:");
767 		parse_comments();
768 		save_vector(Asteroid_field.max_bound);
769 
770 		if (Asteroid_field.has_inner_bound == 1) {
771 			if (optional_string_fred("+Inner Bound:")) {
772 				parse_comments();
773 			} else {
774 				fout("\n+Inner Bound:");
775 			}
776 
777 			required_string_fred("$Minimum:");
778 			parse_comments();
779 			save_vector(Asteroid_field.inner_min_bound);
780 
781 			required_string_fred("$Maximum:");
782 			parse_comments();
783 			save_vector(Asteroid_field.inner_max_bound);
784 		}
785 
786 		fso_comment_pop();
787 	}
788 
789 	fso_comment_pop(true);
790 
791 	return err;
792 }
793 
save_bitmaps()794 int CFred_mission_save::save_bitmaps()
795 {
796 	int i;
797 	uint j;
798 
799 	fred_parse_flag = 0;
800 	required_string_fred("#Background bitmaps");
801 	parse_comments(2);
802 	fout("\t\t;! %d total\n", stars_get_num_bitmaps());
803 
804 	required_string_fred("$Num stars:");
805 	parse_comments();
806 	fout(" %d", Num_stars);
807 
808 	required_string_fred("$Ambient light level:");
809 	parse_comments();
810 	fout(" %d", The_mission.ambient_light_level);
811 
812 	// neb2 stuff
813 	if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
814 		required_string_fred("+Neb2:");
815 		parse_comments();
816 		fout(" %s\n", Neb2_texture_name);
817 
818 		required_string_fred("+Neb2Flags:");
819 		parse_comments();
820 		fout(" %d\n", Neb2_poof_flags);
821 	}
822 		// neb 1 stuff
823 	else {
824 		if (Nebula_index >= 0) {
825 			if (optional_string_fred("+Nebula:")) {
826 				parse_comments();
827 			} else {
828 				fout("\n+Nebula:");
829 			}
830 			fout(" %s", Nebula_filenames[Nebula_index]);
831 
832 			required_string_fred("+Color:");
833 			parse_comments();
834 			fout(" %s", Nebula_colors[Mission_palette]);
835 
836 			required_string_fred("+Pitch:");
837 			parse_comments();
838 			fout(" %d", Nebula_pitch);
839 
840 			required_string_fred("+Bank:");
841 			parse_comments();
842 			fout(" %d", Nebula_bank);
843 
844 			required_string_fred("+Heading:");
845 			parse_comments();
846 			fout(" %d", Nebula_heading);
847 		}
848 	}
849 
850 	fso_comment_pop();
851 
852 	// Goober5000 - save all but the lowest priority using the special comment tag
853 	for (i = 0; i < (int)Backgrounds.size(); i++) {
854 		bool tag = (i < (int)Backgrounds.size() - 1);
855 		background_t* background = &Backgrounds[i];
856 
857 		fso_comment_push(";;FSO 3.6.9;;");
858 		if (optional_string_fred("$Bitmap List:")) {
859 			parse_comments(2);
860 		} else {
861 			fout_version("\n\n$Bitmap List:");
862 		}
863 
864 		if (!tag) {
865 			fso_comment_pop(true);
866 		}
867 
868 		// save suns by filename
869 		for (j = 0; j < background->suns.size(); j++) {
870 			starfield_list_entry* sle = &background->suns[j];
871 
872 			// filename
873 			required_string_fred("$Sun:");
874 			parse_comments();
875 			fout(" %s", sle->filename);
876 
877 			// angles
878 			required_string_fred("+Angles:");
879 			parse_comments();
880 			fout(" %f %f %f", sle->ang.p, sle->ang.b, sle->ang.h);
881 
882 			// scale
883 			required_string_fred("+Scale:");
884 			parse_comments();
885 			fout(" %f", sle->scale_x);
886 		}
887 
888 		// save background bitmaps by filename
889 		for (j = 0; j < background->bitmaps.size(); j++) {
890 			starfield_list_entry* sle = &background->bitmaps[j];
891 
892 			// filename
893 			required_string_fred("$Starbitmap:");
894 			parse_comments();
895 			fout(" %s", sle->filename);
896 
897 			// angles
898 			required_string_fred("+Angles:");
899 			parse_comments();
900 			fout(" %f %f %f", sle->ang.p, sle->ang.b, sle->ang.h);
901 
902 			// scale
903 			required_string_fred("+ScaleX:");
904 			parse_comments();
905 			fout(" %f", sle->scale_x);
906 			required_string_fred("+ScaleY:");
907 			parse_comments();
908 			fout(" %f", sle->scale_y);
909 
910 			// div
911 			required_string_fred("+DivX:");
912 			parse_comments();
913 			fout(" %d", sle->div_x);
914 			required_string_fred("+DivY:");
915 			parse_comments();
916 			fout(" %d", sle->div_y);
917 		}
918 
919 		fso_comment_pop();
920 	}
921 
922 	// taylor's environment map thingy
923 	if (strlen(The_mission.envmap_name) > 0) { //-V805
924 		fso_comment_push(";;FSO 3.6.9;;");
925 		if (optional_string_fred("$Environment Map:")) {
926 			parse_comments(2);
927 			fout(" %s", The_mission.envmap_name);
928 		} else {
929 			fout_version("\n\n$Environment Map: %s", The_mission.envmap_name);
930 		}
931 		fso_comment_pop();
932 	} else {
933 		bypass_comment(";;FSO 3.6.9;; $Environment Map:");
934 	}
935 
936 	fso_comment_pop(true);
937 
938 	return err;
939 }
940 
save_briefing()941 int CFred_mission_save::save_briefing()
942 {
943 	int i, j, k, nb;
944 	SCP_string sexp_out;
945 	brief_stage* bs;
946 	brief_icon* bi;
947 
948 	for (nb = 0; nb < Num_teams; nb++) {
949 
950 		required_string_fred("#Briefing");
951 		parse_comments(2);
952 
953 		required_string_fred("$start_briefing");
954 		parse_comments();
955 
956 		save_custom_bitmap("$briefing_background_640:",
957 						   "$briefing_background_1024:",
958 						   Briefings[nb].background[GR_640],
959 						   Briefings[nb].background[GR_1024]);
960 		save_custom_bitmap("$ship_select_background_640:",
961 						   "$ship_select_background_1024:",
962 						   Briefings[nb].ship_select_background[GR_640],
963 						   Briefings[nb].ship_select_background[GR_1024]);
964 		save_custom_bitmap("$weapon_select_background_640:",
965 						   "$weapon_select_background_1024:",
966 						   Briefings[nb].weapon_select_background[GR_640],
967 						   Briefings[nb].weapon_select_background[GR_1024]);
968 
969 		Assert(Briefings[nb].num_stages <= MAX_BRIEF_STAGES);
970 		required_string_fred("$num_stages:");
971 		parse_comments();
972 		fout(" %d", Briefings[nb].num_stages);
973 
974 		for (i = 0; i < Briefings[nb].num_stages; i++) {
975 			bs = &Briefings[nb].stages[i];
976 
977 			required_string_fred("$start_stage");
978 			parse_comments();
979 
980 			required_string_fred("$multi_text");
981 			parse_comments();
982 
983 			// XSTR
984 			fout_ext("\n", "%s", bs->text.c_str());
985 
986 			required_string_fred("$end_multi_text", "$start_stage");
987 			parse_comments();
988 
989 			if (!drop_white_space(bs->voice)[0]) {
990 				strcpy_s(bs->voice, "None");
991 			}
992 
993 			required_string_fred("$voice:");
994 			parse_comments();
995 			fout(" %s", bs->voice);
996 
997 			required_string_fred("$camera_pos:");
998 			parse_comments();
999 			save_vector(bs->camera_pos);
1000 
1001 			required_string_fred("$camera_orient:");
1002 			parse_comments();
1003 			save_matrix(bs->camera_orient);
1004 
1005 			required_string_fred("$camera_time:");
1006 			parse_comments();
1007 			fout(" %d", bs->camera_time);
1008 
1009 			required_string_fred("$num_lines:");
1010 			parse_comments();
1011 			fout(" %d", bs->num_lines);
1012 
1013 			for (k = 0; k < bs->num_lines; k++) {
1014 				required_string_fred("$line_start:");
1015 				parse_comments();
1016 				fout(" %d", bs->lines[k].start_icon);
1017 
1018 				required_string_fred("$line_end:");
1019 				parse_comments();
1020 				fout(" %d", bs->lines[k].end_icon);
1021 
1022 				fso_comment_pop();
1023 			}
1024 
1025 			required_string_fred("$num_icons:");
1026 			parse_comments();
1027 			Assert(bs->num_icons <= MAX_STAGE_ICONS);
1028 			fout(" %d", bs->num_icons);
1029 
1030 			required_string_fred("$Flags:");
1031 			parse_comments();
1032 			fout(" %d", bs->flags);
1033 
1034 			required_string_fred("$Formula:");
1035 			parse_comments();
1036 			convert_sexp_to_string(sexp_out, bs->formula, SEXP_SAVE_MODE);
1037 			fout(" %s", sexp_out.c_str());
1038 
1039 			for (j = 0; j < bs->num_icons; j++) {
1040 				bi = &bs->icons[j];
1041 
1042 				required_string_fred("$start_icon");
1043 				parse_comments();
1044 
1045 				required_string_fred("$type:");
1046 				parse_comments();
1047 				fout(" %d", bi->type);
1048 
1049 				required_string_fred("$team:");
1050 				parse_comments();
1051 				fout(" %s", Iff_info[bi->team].iff_name);
1052 
1053 				required_string_fred("$class:");
1054 				parse_comments();
1055 				if (bi->ship_class < 0) {
1056 					bi->ship_class = 0;
1057 				}
1058 
1059 				fout(" %s", Ship_info[bi->ship_class].name);
1060 
1061 				required_string_fred("$pos:");
1062 				parse_comments();
1063 				save_vector(bi->pos);
1064 
1065 				if (drop_white_space(bi->label)[0]) {
1066 					if (optional_string_fred("$label:")) {
1067 						parse_comments();
1068 					} else {
1069 						fout("\n$label:");
1070 					}
1071 
1072 					fout_ext(" ", "%s", bi->label);
1073 				}
1074 
1075 				if (optional_string_fred("+id:")) {
1076 					parse_comments();
1077 				} else {
1078 					fout("\n+id:");
1079 				}
1080 
1081 				fout(" %d", bi->id);
1082 
1083 				required_string_fred("$hlight:");
1084 				parse_comments();
1085 				fout(" %d", (bi->flags & BI_HIGHLIGHT) ? 1 : 0);
1086 
1087 				if (save_format != MissionFormat::RETAIL) {
1088 					required_string_fred("$mirror:");
1089 					parse_comments();
1090 					fout(" %d", (bi->flags & BI_MIRROR_ICON) ? 1 : 0);
1091 				}
1092 
1093 				if ((save_format != MissionFormat::RETAIL) && (bi->flags & BI_USE_WING_ICON)) {
1094 					required_string_fred("$use wing icon:");
1095 					parse_comments();
1096 					fout(" %d", (bi->flags & BI_USE_WING_ICON) ? 1 : 0);
1097 				}
1098 
1099 				required_string_fred("$multi_text");
1100 				parse_comments();
1101 
1102 				//				sprintf(out,"\n%s", bi->text);
1103 				//				fout(out);
1104 
1105 				required_string_fred("$end_multi_text");
1106 				parse_comments();
1107 
1108 				required_string_fred("$end_icon");
1109 				parse_comments();
1110 
1111 				fso_comment_pop();
1112 			}
1113 
1114 			required_string_fred("$end_stage");
1115 			parse_comments();
1116 
1117 			fso_comment_pop();
1118 		}
1119 		required_string_fred("$end_briefing");
1120 		parse_comments();
1121 
1122 		fso_comment_pop();
1123 	}
1124 
1125 	fso_comment_pop(true);
1126 
1127 	return err;
1128 }
1129 
save_cmd_brief()1130 int CFred_mission_save::save_cmd_brief()
1131 {
1132 	int stage;
1133 
1134 	stage = 0;
1135 	required_string_fred("#Command Briefing");
1136 	parse_comments(2);
1137 
1138 	save_custom_bitmap("$Background 640:",
1139 					   "$Background 1024:",
1140 					   Cur_cmd_brief->background[GR_640],
1141 					   Cur_cmd_brief->background[GR_1024],
1142 					   1);
1143 
1144 	for (stage = 0; stage < Cur_cmd_brief->num_stages; stage++) {
1145 		required_string_fred("$Stage Text:");
1146 		parse_comments(2);
1147 
1148 		// XSTR
1149 		fout_ext("\n", "%s", Cur_cmd_brief->stage[stage].text.c_str());
1150 
1151 		required_string_fred("$end_multi_text", "$Stage Text:");
1152 		parse_comments();
1153 
1154 		required_string_fred("$Ani Filename:");
1155 		parse_comments();
1156 		fout(" %s", Cur_cmd_brief->stage[stage].ani_filename);
1157 
1158 		required_string_fred("+Wave Filename:", "$Ani Filename:");
1159 		parse_comments();
1160 		fout(" %s", Cur_cmd_brief->stage[stage].wave_filename);
1161 
1162 		fso_comment_pop();
1163 	}
1164 
1165 	fso_comment_pop(true);
1166 
1167 	return err;
1168 }
1169 
save_cmd_briefs()1170 int CFred_mission_save::save_cmd_briefs()
1171 {
1172 	int i;
1173 
1174 	for (i = 0; i < Num_teams; i++) {
1175 		Cur_cmd_brief = &Cmd_briefs[i];
1176 		save_cmd_brief();
1177 	}
1178 
1179 	return err;
1180 }
1181 
fso_comment_push(const char * ver)1182 void CFred_mission_save::fso_comment_push(const char* ver)
1183 {
1184 	if (fso_ver_comment.empty()) {
1185 		fso_ver_comment.push_back(SCP_string(ver));
1186 		return;
1187 	}
1188 
1189 	SCP_string before = fso_ver_comment.back();
1190 
1191 	int major, minor, build, revis;
1192 	int in_major, in_minor, in_build, in_revis;
1193 	int elem1, elem2;
1194 
1195 	elem1 = scan_fso_version_string(fso_ver_comment.back().c_str(), &major, &minor, &build, &revis);
1196 	elem2 = scan_fso_version_string(ver, &in_major, &in_minor, &in_build, &in_revis);
1197 
1198 	// check consistency
1199 	if ((elem1 == 3 && elem2 == 4) || (elem1 == 4 && elem2 == 3)) {
1200 		elem1 = elem2 = 3;
1201 	} else if ((elem1 >= 3 && elem2 >= 3) && (revis < 1000 || in_revis < 1000)) {
1202 		elem1 = elem2 = 3;
1203 	}
1204 
1205 	if ((elem1 == 3) && ((major > in_major)
1206 		|| ((major == in_major) && ((minor > in_minor) || ((minor == in_minor) && (build > in_build)))))) {
1207 		// the push'd version is older than our current version, so just push a copy of the previous version
1208 		fso_ver_comment.push_back(before);
1209 	} else if ((elem1 == 4) && ((major > in_major) || ((major == in_major) && ((minor > in_minor)
1210 		|| ((minor == in_minor) && ((build > in_build) || ((build == in_build) || (revis > in_revis)))))))) {
1211 		// the push'd version is older than our current version, so just push a copy of the previous version
1212 		fso_ver_comment.push_back(before);
1213 	} else {
1214 		fso_ver_comment.push_back(SCP_string(ver));
1215 	}
1216 }
1217 
fso_comment_pop(bool pop_all)1218 void CFred_mission_save::fso_comment_pop(bool pop_all)
1219 {
1220 	if (fso_ver_comment.empty()) {
1221 		return;
1222 	}
1223 
1224 	if (pop_all) {
1225 		fso_ver_comment.clear();
1226 		return;
1227 	}
1228 
1229 	fso_ver_comment.pop_back();
1230 }
1231 
save_common_object_data(object * objp,ship * shipp)1232 int CFred_mission_save::save_common_object_data(object* objp, ship* shipp)
1233 {
1234 	int j, z;
1235 	ship_subsys* ptr = NULL;
1236 	ship_info* sip = NULL;
1237 	ship_weapon* wp = NULL;
1238 	float temp_max_hull_strength;
1239 
1240 	sip = &Ship_info[shipp->ship_info_index];
1241 
1242 	if ((int) objp->phys_info.speed) {
1243 		if (optional_string_fred("+Initial Velocity:", "$Name:", "+Subsystem:")) {
1244 			parse_comments();
1245 		} else {
1246 			fout("\n+Initial Velocity:");
1247 		}
1248 
1249 		fout(" %d", (int) objp->phys_info.speed);
1250 	}
1251 
1252 	// Goober5000
1253 	if (save_format != MissionFormat::RETAIL && (shipp->special_hitpoints > 0)) {
1254 		temp_max_hull_strength = (float) shipp->special_hitpoints;
1255 	} else {
1256 		temp_max_hull_strength = sip->max_hull_strength;
1257 	}
1258 
1259 	if ((int) objp->hull_strength != temp_max_hull_strength) {
1260 		if (optional_string_fred("+Initial Hull:", "$Name:", "+Subsystem:")) {
1261 			parse_comments();
1262 		} else {
1263 			fout("\n+Initial Hull:");
1264 		}
1265 
1266 		fout(" %d", (int) objp->hull_strength);
1267 	}
1268 
1269 	if ((int) shield_get_strength(objp) != 100) {
1270 		if (optional_string_fred("+Initial Shields:", "$Name:", "+Subsystem:")) {
1271 			parse_comments();
1272 		} else {
1273 			fout("\n+Initial Shields:");
1274 		}
1275 
1276 		fout(" %d", (int) objp->shield_quadrant[0]);
1277 	}
1278 
1279 	// save normal ship weapons info
1280 	required_string_fred("+Subsystem:", "$Name:");
1281 	parse_comments();
1282 	fout(" Pilot");
1283 
1284 	wp = &shipp->weapons;
1285 	z = 0;
1286 	j = wp->num_primary_banks;
1287 	while (j-- && (j >= 0)) {
1288 		if (wp->primary_bank_weapons[j] != sip->primary_bank_weapons[j]) {
1289 			z = 1;
1290 		}
1291 	}
1292 
1293 	if (z) {
1294 		if (optional_string_fred("+Primary Banks:", "$Name:", "+Subsystem:")) {
1295 			parse_comments();
1296 		} else {
1297 			fout("\n+Primary Banks:");
1298 		}
1299 
1300 		fout(" ( ");
1301 		for (j = 0; j < wp->num_primary_banks; j++) {
1302 			if (wp->primary_bank_weapons[j] != -1) { // Just in case someone has set a weapon bank to empty
1303 				fout("\"%s\" ", Weapon_info[wp->primary_bank_weapons[j]].name);
1304 			} else {
1305 				fout("\"\" ");
1306 			}
1307 		}
1308 
1309 		fout(")");
1310 	}
1311 
1312 	z = 0;
1313 	j = wp->num_secondary_banks;
1314 	while (j-- && (j >= 0)) {
1315 		if (wp->secondary_bank_weapons[j] != sip->secondary_bank_weapons[j]) {
1316 			z = 1;
1317 		}
1318 	}
1319 
1320 	if (z) {
1321 		if (optional_string_fred("+Secondary Banks:", "$Name:", "+Subsystem:")) {
1322 			parse_comments();
1323 		} else {
1324 			fout("\n+Secondary Banks:");
1325 		}
1326 
1327 		fout(" ( ");
1328 		for (j = 0; j < wp->num_secondary_banks; j++) {
1329 			if (wp->secondary_bank_weapons[j] != -1) {
1330 				fout("\"%s\" ", Weapon_info[wp->secondary_bank_weapons[j]].name);
1331 			} else {
1332 				fout("\"\" ");
1333 			}
1334 		}
1335 
1336 		fout(")");
1337 	}
1338 
1339 	z = 0;
1340 	j = wp->num_secondary_banks;
1341 	while (j-- && (j >= 0)) {
1342 		if (wp->secondary_bank_ammo[j] != 100) {
1343 			z = 1;
1344 		}
1345 	}
1346 
1347 	if (z) {
1348 		if (optional_string_fred("+Sbank Ammo:", "$Name:", "+Subsystem:")) {
1349 			parse_comments();
1350 		} else {
1351 			fout("\n+Sbank Ammo:");
1352 		}
1353 
1354 		fout(" ( ");
1355 		for (j = 0; j < wp->num_secondary_banks; j++) {
1356 			fout("%d ", wp->secondary_bank_ammo[j]);
1357 		}
1358 
1359 		fout(")");
1360 	}
1361 
1362 	ptr = GET_FIRST(&shipp->subsys_list);
1363 	Assert(ptr);
1364 
1365 	while (ptr != END_OF_LIST(&shipp->subsys_list) && ptr) {
1366 		// Crashing here!
1367 		if ((ptr->current_hits) || (ptr->system_info && ptr->system_info->type == SUBSYSTEM_TURRET)
1368 			|| (ptr->subsys_cargo_name > 0)) {
1369 			if (optional_string_fred("+Subsystem:", "$Name:")) {
1370 				parse_comments();
1371 			} else {
1372 				fout("\n+Subsystem:");
1373 			}
1374 
1375 			fout(" %s", ptr->system_info->subobj_name);
1376 		}
1377 
1378 		if (ptr->current_hits) {
1379 			if (optional_string_fred("$Damage:", "$Name:", "+Subsystem:")) {
1380 				parse_comments();
1381 			} else {
1382 				fout("\n$Damage:");
1383 			}
1384 
1385 			fout(" %d", (int) ptr->current_hits);
1386 		}
1387 
1388 		if (ptr->subsys_cargo_name > 0) {
1389 			if (optional_string_fred("+Cargo Name:", "$Name:", "+Subsystem:")) {
1390 				parse_comments();
1391 			} else {
1392 				fout("\n+Cargo Name:");
1393 			}
1394 
1395 			fout_ext(NULL, "%s", Cargo_names[ptr->subsys_cargo_name]);
1396 		}
1397 
1398 		if (ptr->system_info->type == SUBSYSTEM_TURRET) {
1399 			save_turret_info(ptr, SHIP_INDEX(shipp));
1400 		}
1401 
1402 		ptr = GET_NEXT(ptr);
1403 
1404 		fso_comment_pop();
1405 	}
1406 
1407 	/*	for (j=0; j<shipp->status_count; j++) {
1408 	required_string_fred("$Status Description:");
1409 	parse_comments(-1);
1410 	fout(" %s", Status_desc_names[shipp->status_type[j]]);
1411 
1412 	required_string_fred("$Status:");
1413 	parse_comments(-1);
1414 	fout(" %s", Status_type_names[shipp->status[j]]);
1415 
1416 	required_string_fred("$Target:");
1417 	parse_comments(-1);
1418 	fout(" %s", Status_target_names[shipp->target[j]]);
1419 	}*/
1420 
1421 	fso_comment_pop(true);
1422 
1423 	return err;
1424 }
1425 
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)1426 void CFred_mission_save::save_custom_bitmap(const char* expected_string_640,
1427 											const char* expected_string_1024,
1428 											const char* string_field_640,
1429 											const char* string_field_1024,
1430 											int blank_lines)
1431 {
1432 	if (save_format != MissionFormat::RETAIL) {
1433 		if ((*string_field_640 != '\0') || (*string_field_1024 != '\0')) {
1434 			while (blank_lines-- > 0) {
1435 				fout("\n");
1436 			}
1437 		}
1438 
1439 		if (*string_field_640 != '\0') {
1440 			fout("\n%s %s", expected_string_640, string_field_640);
1441 		}
1442 
1443 		if (*string_field_1024 != '\0') {
1444 			fout("\n%s %s", expected_string_1024, string_field_1024);
1445 		}
1446 	}
1447 }
1448 
save_cutscenes()1449 int CFred_mission_save::save_cutscenes()
1450 {
1451 	char type[NAME_LENGTH];
1452 	SCP_string sexp_out;
1453 
1454 	// Let's just assume it has them for now -
1455 	if (!(The_mission.cutscenes.empty())) {
1456 		if (save_format != MissionFormat::RETAIL) {
1457 			if (optional_string_fred("#Cutscenes")) {
1458 				parse_comments(2);
1459 			} else {
1460 				fout_version("\n\n#Cutscenes\n\n");
1461 			}
1462 
1463 			for (uint i = 0; i < The_mission.cutscenes.size(); i++) {
1464 				if (strlen(The_mission.cutscenes[i].filename)) {
1465 					// determine the name of this cutscene type
1466 					switch (The_mission.cutscenes[i].type) {
1467 					case MOVIE_PRE_FICTION:
1468 						strcpy_s(type, "$Fiction Viewer Cutscene:");
1469 						break;
1470 					case MOVIE_PRE_CMD_BRIEF:
1471 						strcpy_s(type, "$Command Brief Cutscene:");
1472 						break;
1473 					case MOVIE_PRE_BRIEF:
1474 						strcpy_s(type, "$Briefing Cutscene:");
1475 						break;
1476 					case MOVIE_PRE_GAME:
1477 						strcpy_s(type, "$Pre-game Cutscene:");
1478 						break;
1479 					case MOVIE_PRE_DEBRIEF:
1480 						strcpy_s(type, "$Debriefing Cutscene:");
1481 						break;
1482 					case MOVIE_END_CAMPAIGN:
1483 						strcpy_s(type, "$Campaign End Cutscene:");
1484 						break;
1485 					default:
1486 						Int3();
1487 						continue;
1488 					}
1489 
1490 					if (optional_string_fred(type)) {
1491 						parse_comments();
1492 						fout(" %s", The_mission.cutscenes[i].filename);
1493 					} else {
1494 						fout_version("%s %s\n", type, The_mission.cutscenes[i].filename);
1495 					}
1496 
1497 					required_string_fred("+formula:");
1498 					parse_comments();
1499 					convert_sexp_to_string(sexp_out, The_mission.cutscenes[i].formula, SEXP_SAVE_MODE);
1500 					fout(" %s", sexp_out.c_str());
1501 				}
1502 			}
1503 			required_string_fred("#end");
1504 			parse_comments();
1505 		} else {
1506 			_viewport->dialogProvider->showButtonDialog(DialogType::Warning,
1507 														"Incompatibility with retail mission format",
1508 														"Warning: This mission contains cutscene data, but you are saving in the retail mission format. This information will be lost",
1509 														{ DialogButton::Ok });
1510 		}
1511 	}
1512 
1513 	fso_comment_pop(true);
1514 	return err;
1515 }
1516 
save_debriefing()1517 int CFred_mission_save::save_debriefing()
1518 {
1519 	int j, i;
1520 	SCP_string sexp_out;
1521 
1522 	for (j = 0; j < Num_teams; j++) {
1523 
1524 		Debriefing = &Debriefings[j];
1525 
1526 		required_string_fred("#Debriefing_info");
1527 		parse_comments(2);
1528 
1529 		save_custom_bitmap("$Background 640:",
1530 						   "$Background 1024:",
1531 						   Debriefing->background[GR_640],
1532 						   Debriefing->background[GR_1024],
1533 						   1);
1534 
1535 		required_string_fred("$Num stages:");
1536 		parse_comments(2);
1537 		fout(" %d", Debriefing->num_stages);
1538 
1539 		for (i = 0; i < Debriefing->num_stages; i++) {
1540 			required_string_fred("$Formula:");
1541 			parse_comments(2);
1542 			convert_sexp_to_string(sexp_out, Debriefing->stages[i].formula, SEXP_SAVE_MODE);
1543 			fout(" %s", sexp_out.c_str());
1544 
1545 			// XSTR
1546 			required_string_fred("$Multi text");
1547 			parse_comments();
1548 			fout_ext("\n   ", "%s", Debriefing->stages[i].text.c_str());
1549 
1550 			required_string_fred("$end_multi_text");
1551 			parse_comments();
1552 
1553 			if (!drop_white_space(Debriefing->stages[i].voice)[0]) {
1554 				strcpy_s(Debriefing->stages[i].voice, "None");
1555 			}
1556 
1557 			required_string_fred("$Voice:");
1558 			parse_comments();
1559 			fout(" %s", Debriefing->stages[i].voice);
1560 
1561 			// XSTR
1562 			required_string_fred("$Recommendation text:");
1563 			parse_comments();
1564 			fout_ext("\n   ", "%s", Debriefing->stages[i].recommendation_text.c_str());
1565 
1566 			required_string_fred("$end_multi_text");
1567 			parse_comments();
1568 
1569 			fso_comment_pop();
1570 		}
1571 	}
1572 
1573 	fso_comment_pop(true);
1574 
1575 	return err;
1576 }
1577 
save_events()1578 int CFred_mission_save::save_events()
1579 {
1580 	SCP_string sexp_out;
1581 	int i, j, add_flag;
1582 
1583 	fred_parse_flag = 0;
1584 	required_string_fred("#Events");
1585 	parse_comments(2);
1586 	fout("\t\t;! %d total\n", Num_mission_events);
1587 
1588 	for (i = 0; i < Num_mission_events; i++) {
1589 		required_string_either_fred("$Formula:", "#Goals");
1590 		required_string_fred("$Formula:");
1591 		parse_comments(i ? 2 : 1);
1592 		convert_sexp_to_string(sexp_out, Mission_events[i].formula, SEXP_SAVE_MODE);
1593 		fout(" %s", sexp_out.c_str());
1594 
1595 		if (*Mission_events[i].name) {
1596 			if (optional_string_fred("+Name:", "$Formula:")) {
1597 				parse_comments();
1598 			} else {
1599 				fout("\n+Name:");
1600 			}
1601 
1602 			fout(" %s", Mission_events[i].name);
1603 		}
1604 
1605 		if (optional_string_fred("+Repeat Count:", "$Formula:")) {
1606 			parse_comments();
1607 		} else {
1608 			fout("\n+Repeat Count:");
1609 		}
1610 
1611 		// if we have a trigger count but no repeat count, we want the event to loop until it has triggered enough times
1612 		if (Mission_events[i].repeat_count == 1 && Mission_events[i].trigger_count != 1) {
1613 			fout(" -1");
1614 		} else {
1615 			fout(" %d", Mission_events[i].repeat_count);
1616 		}
1617 
1618 		if (save_format != MissionFormat::RETAIL && Mission_events[i].trigger_count != 1) {
1619 			fso_comment_push(";;FSO 3.6.11;;");
1620 			if (optional_string_fred("+Trigger Count:", "$Formula:")) {
1621 				parse_comments();
1622 			} else {
1623 				fout_version("\n+Trigger Count:");
1624 			}
1625 			fso_comment_pop();
1626 
1627 			fout(" %d", Mission_events[i].trigger_count);
1628 		}
1629 
1630 		if (optional_string_fred("+Interval:", "$Formula:")) {
1631 			parse_comments();
1632 		} else {
1633 			fout("\n+Interval:");
1634 		}
1635 
1636 		fout(" %d", Mission_events[i].interval);
1637 
1638 		if (Mission_events[i].score != 0) {
1639 			if (optional_string_fred("+Score:", "$Formula:")) {
1640 				parse_comments();
1641 			} else {
1642 				fout("\n+Score:");
1643 			}
1644 			fout(" %d", Mission_events[i].score);
1645 		}
1646 
1647 		if (Mission_events[i].chain_delay >= 0) {
1648 			if (optional_string_fred("+Chained:", "$Formula:")) {
1649 				parse_comments();
1650 			} else {
1651 				fout("\n+Chained:");
1652 			}
1653 
1654 			fout(" %d", Mission_events[i].chain_delay);
1655 		}
1656 
1657 		//XSTR
1658 		if (Mission_events[i].objective_text) {
1659 			if (optional_string_fred("+Objective:", "$Formula:")) {
1660 				parse_comments();
1661 			} else {
1662 				fout("\n+Objective:");
1663 			}
1664 
1665 			fout_ext(" ", "%s", Mission_events[i].objective_text);
1666 		}
1667 
1668 		//XSTR
1669 		if (Mission_events[i].objective_key_text) {
1670 			if (optional_string_fred("+Objective key:", "$Formula:")) {
1671 				parse_comments();
1672 			} else {
1673 				fout("\n+Objective key:");
1674 			}
1675 
1676 			fout_ext(" ", "%s", Mission_events[i].objective_key_text);
1677 		}
1678 
1679 		// save team
1680 		if (Mission_events[i].team >= 0) {
1681 			if (optional_string_fred("+Team:")) {
1682 				parse_comments();
1683 			} else {
1684 				fout("\n+Team:");
1685 			}
1686 			fout(" %d", Mission_events[i].team);
1687 		}
1688 
1689 		// save flags, if any
1690 		if (save_format != MissionFormat::RETAIL) {
1691 			// we need to lazily-write the tag because we should only write it if there are also flags to write
1692 			// (some of the flags are transient, internal flags)
1693 			bool wrote_tag = false;
1694 
1695 			for (j = 0; j < Num_mission_event_flags; ++j) {
1696 				if (Mission_events[i].flags & Mission_event_flags[j].def) {
1697 					if (!wrote_tag) {
1698 						wrote_tag = true;
1699 
1700 						fso_comment_push(";;FSO 20.0.0;;");
1701 						if (optional_string_fred("+Event Flags: (", "$Formula:")) {
1702 							parse_comments();
1703 						}
1704 						else {
1705 							fout_version("\n+Event Flags: (");
1706 						}
1707 						fso_comment_pop();
1708 					}
1709 
1710 					fout(" \"%s\"", Mission_event_flags[j].name);
1711 				}
1712 			}
1713 
1714 			if (wrote_tag)
1715 				fout(" )");
1716 		}
1717 
1718 		if (save_format != MissionFormat::RETAIL && Mission_events[i].mission_log_flags != 0) {
1719 			fso_comment_push(";;FSO 3.6.11;;");
1720 			if (optional_string_fred("+Event Log Flags: (", "$Formula:")) {
1721 				parse_comments();
1722 			} else {
1723 				fout_version("\n+Event Log Flags: (");
1724 			}
1725 			fso_comment_pop();
1726 
1727 			for (j = 0; j < MAX_MISSION_EVENT_LOG_FLAGS; j++) {
1728 				add_flag = 1 << j;
1729 				if (Mission_events[i].mission_log_flags & add_flag) {
1730 					fout(" \"%s\"", Mission_event_log_flags[j]);
1731 				}
1732 			}
1733 			fout(" )");
1734 		}
1735 
1736 		// save event annotations
1737 		if (save_format != MissionFormat::RETAIL && !Event_annotations.empty())
1738 		{
1739 			bool at_least_one = false;
1740 			fso_comment_push(";;FSO 21.0.0;;");
1741 			event_annotation default_ea;
1742 
1743 			// see if there is an annotation for this event
1744 			for (const auto &ea : Event_annotations)
1745 			{
1746 				if (ea.path.empty() || ea.path.front() != i)
1747 					continue;
1748 
1749 				if (!at_least_one)
1750 				{
1751 					if (optional_string_fred("$Annotations Start", "$Formula:"))
1752 						parse_comments();
1753 					else
1754 						fout_version("\n$Annotations Start");
1755 					at_least_one = true;
1756 				}
1757 
1758 				if (ea.comment != default_ea.comment)
1759 				{
1760 					if (optional_string_fred("+Comment:", "$Formula:"))
1761 						parse_comments();
1762 					else
1763 						fout_version("\n+Comment:");
1764 
1765 					auto copy = ea.comment;
1766 					lcl_fred_replace_stuff(copy);
1767 					fout(" %s", copy.c_str());
1768 
1769 					if (optional_string_fred("$end_multi_text", "$Formula:"))
1770 						parse_comments();
1771 					else
1772 						fout_version("\n$end_multi_text");
1773 				}
1774 
1775 				if (ea.r != default_ea.r || ea.g != default_ea.g || ea.b != default_ea.b)
1776 				{
1777 					if (optional_string_fred("+Background Color:", "$Formula:"))
1778 						parse_comments();
1779 					else
1780 						fout_version("\n+Background Color:");
1781 
1782 					fout(" %d, %d, %d", ea.r, ea.g, ea.b);
1783 				}
1784 
1785 				if (ea.path.size() > 1)
1786 				{
1787 					if (optional_string_fred("+Path:", "$Formula:"))
1788 						parse_comments();
1789 					else
1790 						fout_version("\n+Path:");
1791 
1792 					bool comma = false;
1793 					auto it = ea.path.begin();
1794 					for (++it; it != ea.path.end(); ++it)
1795 					{
1796 						if (comma)
1797 							fout(",");
1798 						comma = true;
1799 						fout(" %d", *it);
1800 					}
1801 				}
1802 			}
1803 
1804 			if (at_least_one)
1805 			{
1806 				if (optional_string_fred("$Annotations End", "$Formula:"))
1807 					parse_comments();
1808 				else
1809 					fout_version("\n$Annotations End");
1810 			}
1811 
1812 			fso_comment_pop();
1813 		}
1814 	}
1815 
1816 	fso_comment_pop(true);
1817 
1818 	return err;
1819 }
1820 
save_fiction()1821 int CFred_mission_save::save_fiction()
1822 {
1823 	if (mission_has_fiction()) {
1824 		if (save_format != MissionFormat::RETAIL) {
1825 			if (optional_string_fred("#Fiction Viewer")) {
1826 				parse_comments(2);
1827 			} else {
1828 				fout("\n\n#Fiction Viewer");
1829 			}
1830 
1831 			// we have multiple stages now, so save them all
1832 			for (SCP_vector<fiction_viewer_stage>::iterator stage = Fiction_viewer_stages.begin();
1833 				 stage != Fiction_viewer_stages.end(); ++stage) {
1834 				fout("\n");
1835 
1836 				// save file
1837 				required_string_fred("$File:");
1838 				parse_comments();
1839 				fout(" %s", stage->story_filename);
1840 
1841 				// save font
1842 				if (strlen(stage->font_filename) > 0) //-V805
1843 				{
1844 					if (optional_string_fred("$Font:")) {
1845 						parse_comments();
1846 					} else {
1847 						fout("\n$Font:");
1848 					}
1849 					fout(" %s", stage->font_filename);
1850 				} else {
1851 					optional_string_fred("$Font:");
1852 				}
1853 
1854 				// save voice
1855 				if (strlen(stage->voice_filename) > 0) //-V805
1856 				{
1857 					if (optional_string_fred("$Voice:")) {
1858 						parse_comments();
1859 					} else {
1860 						fout("\n$Voice:");
1861 					}
1862 					fout(" %s", stage->voice_filename);
1863 				} else {
1864 					optional_string_fred("$Voice:");
1865 				}
1866 
1867 				// save UI
1868 				if (strlen(stage->ui_name) > 0) {
1869 					if (optional_string_fred("$UI:")) {
1870 						parse_comments();
1871 					} else {
1872 						fout("\n$UI:");
1873 					}
1874 					fout(" %s", stage->ui_name);
1875 				} else {
1876 					optional_string_fred("$UI:");
1877 				}
1878 
1879 				// save background
1880 				save_custom_bitmap("$Background 640:",
1881 								   "$Background 1024:",
1882 								   stage->background[GR_640],
1883 								   stage->background[GR_1024]);
1884 
1885 				// save sexp formula if we have one
1886 				if (stage->formula >= 0 && stage->formula != Locked_sexp_true) {
1887 					SCP_string sexp_out;
1888 					convert_sexp_to_string(sexp_out, stage->formula, SEXP_SAVE_MODE);
1889 
1890 					if (optional_string_fred("$Formula:")) {
1891 						parse_comments();
1892 					} else {
1893 						fout("\n$Formula:");
1894 					}
1895 					fout(" %s", sexp_out.c_str());
1896 				} else {
1897 					optional_string_fred("$Formula:");
1898 				}
1899 			}
1900 		} else {
1901 			_viewport->dialogProvider->showButtonDialog(DialogType::Warning,
1902 														"Incompatibility with retail mission format",
1903 														"Warning: This mission contains fiction viewer data, but you are saving in the retail mission format.",
1904 														{ DialogButton::Ok });
1905 		}
1906 	}
1907 
1908 	fso_comment_pop(true);
1909 
1910 	return err;
1911 }
1912 
save_goals()1913 int CFred_mission_save::save_goals()
1914 {
1915 	SCP_string sexp_out;
1916 	int i;
1917 
1918 	fred_parse_flag = 0;
1919 	required_string_fred("#Goals");
1920 	parse_comments(2);
1921 	fout("\t\t;! %d total\n", Num_goals);
1922 
1923 	for (i = 0; i < Num_goals; i++) {
1924 		int type;
1925 
1926 		required_string_either_fred("$Type:", "#Waypoints");
1927 		required_string_fred("$Type:");
1928 		parse_comments(i ? 2 : 1);
1929 
1930 		type = Mission_goals[i].type & GOAL_TYPE_MASK;
1931 		fout(" %s", Goal_type_names[type]);
1932 
1933 		if (*Mission_goals[i].name) {
1934 			if (optional_string_fred("+Name:", "$Type:")) {
1935 				parse_comments();
1936 			} else {
1937 				fout("\n+Name:");
1938 			}
1939 
1940 			fout(" %s", Mission_goals[i].name);
1941 		}
1942 
1943 		// XSTR
1944 		required_string_fred("$MessageNew:");
1945 		parse_comments();
1946 		fout_ext(" ", "%s", Mission_goals[i].message);
1947 		fout("\n");
1948 		required_string_fred("$end_multi_text");
1949 		parse_comments(0);
1950 
1951 		required_string_fred("$Formula:");
1952 		parse_comments();
1953 		convert_sexp_to_string(sexp_out, Mission_goals[i].formula, SEXP_SAVE_MODE);
1954 		fout(" %s", sexp_out.c_str());
1955 
1956 		if (Mission_goals[i].type & INVALID_GOAL) {
1957 			if (optional_string_fred("+Invalid", "$Type:")) {
1958 				parse_comments();
1959 			} else {
1960 				fout("\n+Invalid");
1961 			}
1962 		}
1963 
1964 		if (Mission_goals[i].flags & MGF_NO_MUSIC) {
1965 			if (optional_string_fred("+No music", "$Type:")) {
1966 				parse_comments();
1967 			} else {
1968 				fout("\n+No music");
1969 			}
1970 		}
1971 
1972 		if (Mission_goals[i].score != 0) {
1973 			if (optional_string_fred("+Score:", "$Type:")) {
1974 				parse_comments();
1975 			} else {
1976 				fout("\n+Score:");
1977 			}
1978 			fout(" %d", Mission_goals[i].score);
1979 		}
1980 
1981 		if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) {
1982 			if (optional_string_fred("+Team:", "$Type:")) {
1983 				parse_comments();
1984 			} else {
1985 				fout("\n+Team:");
1986 			}
1987 			fout(" %d", Mission_goals[i].team);
1988 		}
1989 
1990 		fso_comment_pop();
1991 	}
1992 
1993 	fso_comment_pop(true);
1994 
1995 	return err;
1996 }
1997 
save_matrix(matrix & m)1998 int CFred_mission_save::save_matrix(matrix& m)
1999 {
2000 	fout("\n\t%f, %f, %f,\n", m.vec.rvec.xyz.x, m.vec.rvec.xyz.y, m.vec.rvec.xyz.z);
2001 	fout("\t%f, %f, %f,\n", m.vec.uvec.xyz.x, m.vec.uvec.xyz.y, m.vec.uvec.xyz.z);
2002 	fout("\t%f, %f, %f", m.vec.fvec.xyz.x, m.vec.fvec.xyz.y, m.vec.fvec.xyz.z);
2003 	return 0;
2004 }
2005 
save_messages()2006 int CFred_mission_save::save_messages()
2007 {
2008 	int i;
2009 
2010 	fred_parse_flag = 0;
2011 	required_string_fred("#Messages");
2012 	parse_comments(2);
2013 	fout("\t\t;! %d total\n", Num_messages - Num_builtin_messages);
2014 
2015 	// Goober5000 - special Command info
2016 	if (save_format != MissionFormat::RETAIL) {
2017 		if (stricmp(The_mission.command_sender, DEFAULT_COMMAND) != 0) {
2018 			fout("\n$Command Sender: %s", The_mission.command_sender);
2019 		}
2020 
2021 		if (The_mission.command_persona != Default_command_persona) {
2022 			fout("\n$Command Persona: %s", Personas[The_mission.command_persona].name);
2023 		}
2024 	}
2025 
2026 	for (i = Num_builtin_messages; i < Num_messages; i++) {
2027 		required_string_either_fred("$Name:", "#Reinforcements");
2028 		required_string_fred("$Name:");
2029 		parse_comments(2);
2030 		fout(" %s", Messages[i].name);
2031 
2032 		// team
2033 		required_string_fred("$Team:");
2034 		parse_comments(1);
2035 		if ((Messages[i].multi_team < 0) || (Messages[i].multi_team >= 2)) {
2036 			fout(" %d", -1);
2037 		} else {
2038 			fout(" %d", Messages[i].multi_team);
2039 		}
2040 
2041 		// XSTR
2042 		required_string_fred("$MessageNew:");
2043 		parse_comments();
2044 		fout_ext(" ", "%s", Messages[i].message);
2045 		fout("\n");
2046 		required_string_fred("$end_multi_text");
2047 		parse_comments(0);
2048 
2049 		if (Messages[i].persona_index != -1) {
2050 			if (optional_string_fred("+Persona:", "$Name:")) {
2051 				parse_comments();
2052 			} else {
2053 				fout("\n+Persona:");
2054 			}
2055 
2056 			fout(" %s", Personas[Messages[i].persona_index].name);
2057 		}
2058 
2059 		if (Messages[i].avi_info.name) {
2060 			if (optional_string_fred("+AVI Name:", "$Name:")) {
2061 				parse_comments();
2062 			} else {
2063 				fout("\n+AVI Name:");
2064 			}
2065 
2066 			fout(" %s", Messages[i].avi_info.name);
2067 		}
2068 
2069 		if (Messages[i].wave_info.name) {
2070 			if (optional_string_fred("+Wave Name:", "$Name:")) {
2071 				parse_comments();
2072 			} else {
2073 				fout("\n+Wave Name:");
2074 			}
2075 
2076 			fout(" %s", Messages[i].wave_info.name);
2077 		}
2078 
2079 		fso_comment_pop();
2080 	}
2081 
2082 	fso_comment_pop(true);
2083 
2084 	return err;
2085 }
2086 
save_mission_file(const char * pathname_in)2087 int CFred_mission_save::save_mission_file(const char* pathname_in)
2088 {
2089 	char backup_name[256], savepath[MAX_PATH_LEN], pathname[MAX_PATH_LEN];
2090 
2091 	strcpy_s(pathname, pathname_in);
2092 
2093 	strcpy_s(savepath, "");
2094 	auto p = strrchr(pathname, DIR_SEPARATOR_CHAR);
2095 	if (p) {
2096 		*p = '\0';
2097 		strcpy_s(savepath, pathname);
2098 		*p = DIR_SEPARATOR_CHAR;
2099 		strcat_s(savepath, DIR_SEPARATOR_STR);
2100 	}
2101 	strcat_s(savepath, "saving.xxx");
2102 
2103 	save_mission_internal(savepath);
2104 
2105 	if (!err) {
2106 		strcpy_s(backup_name, pathname);
2107 		if (backup_name[strlen(backup_name) - 4] == '.') {
2108 			backup_name[strlen(backup_name) - 4] = 0;
2109 		}
2110 
2111 		strcat_s(backup_name, ".bak");
2112 		cf_delete(backup_name, CF_TYPE_MISSIONS);
2113 		cf_rename(pathname, backup_name, CF_TYPE_MISSIONS);
2114 		cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
2115 	}
2116 
2117 	return err;
2118 }
2119 
save_mission_info()2120 int CFred_mission_save::save_mission_info()
2121 {
2122 	required_string_fred("#Mission Info");
2123 	parse_comments(0);
2124 
2125 	required_string_fred("$Version:");
2126 	parse_comments(2);
2127 	fout(" %.2f", FRED_MISSION_VERSION);
2128 
2129 	// XSTR
2130 	required_string_fred("$Name:");
2131 	parse_comments();
2132 	fout_ext(" ", "%s", The_mission.name);
2133 
2134 	required_string_fred("$Author:");
2135 	parse_comments();
2136 	fout(" %s", The_mission.author);
2137 
2138 	required_string_fred("$Created:");
2139 	parse_comments();
2140 	fout(" %s", The_mission.created);
2141 
2142 	required_string_fred("$Modified:");
2143 	parse_comments();
2144 	fout(" %s", The_mission.modified);
2145 
2146 	required_string_fred("$Notes:");
2147 	parse_comments();
2148 	fout("\n%s", The_mission.notes);
2149 
2150 	required_string_fred("$End Notes:");
2151 	parse_comments(0);
2152 
2153 	// XSTR
2154 	required_string_fred("$Mission Desc:");
2155 	parse_comments(2);
2156 	fout_ext("\n", "%s", The_mission.mission_desc);
2157 	fout("\n");
2158 
2159 	required_string_fred("$end_multi_text");
2160 	parse_comments(0);
2161 
2162 #if 0
2163     if (optional_string_fred("+Game Type:"))
2164         parse_comments(2);
2165     else
2166         fout("\n\n+Game Type:");
2167     fout("\n%s", Game_types[The_mission.game_type]);
2168 #endif
2169 
2170 	if (optional_string_fred("+Game Type Flags:")) {
2171 		parse_comments(1);
2172 	} else {
2173 		fout("\n+Game Type Flags:");
2174 	}
2175 
2176 	fout(" %d", The_mission.game_type);
2177 
2178 	if (optional_string_fred("+Flags:")) {
2179 		parse_comments(1);
2180 	} else {
2181 		fout("\n+Flags:");
2182 	}
2183 
2184 	fout(" %" PRIu64, The_mission.flags.to_u64());
2185 
2186 	// maybe write out Nebula intensity
2187 	if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
2188 		Assert(Neb2_awacs > 0.0f);
2189 		fout("\n+NebAwacs: %f\n", Neb2_awacs);
2190 
2191 		// storm name
2192 		fout("\n+Storm: %s\n", Mission_parse_storm_name);
2193 	}
2194 
2195 	// Goober5000
2196 	if (save_format != MissionFormat::RETAIL) {
2197 		// write out the nebula clipping multipliers
2198 		fout("\n+Fog Near Mult: %f\n", Neb2_fog_near_mult);
2199 		fout("\n+Fog Far Mult: %f\n", Neb2_fog_far_mult);
2200 
2201 		if (The_mission.contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT) {
2202 			fout("\n$Contrail Speed Threshold: %d\n", The_mission.contrail_threshold);
2203 		}
2204 	}
2205 
2206 	// For multiplayer missions -- write out the number of player starts and number of respawns
2207 	if (The_mission.game_type & MISSION_TYPE_MULTI) {
2208 		if (optional_string_fred("+Num Players:")) {
2209 			parse_comments(2);
2210 		} else {
2211 			fout("\n+Num Players:");
2212 		}
2213 
2214 		fout(" %d", Player_starts);
2215 
2216 		if (optional_string_fred("+Num Respawns:")) {
2217 			parse_comments(2);
2218 		} else {
2219 			fout("\n+Num Respawns:");
2220 		}
2221 
2222 		fout(" %d", The_mission.num_respawns);
2223 
2224 		if (save_format != MissionFormat::RETAIL) {
2225 			fso_comment_push(";;FSO 3.6.11;;");
2226 			if (optional_string_fred("+Max Respawn Time:")) {
2227 				parse_comments(2);
2228 			} else {
2229 				fout_version("\n+Max Respawn Time:");
2230 			}
2231 			fso_comment_pop();
2232 
2233 			fout(" %d", The_mission.max_respawn_delay);
2234 		} else {
2235 			bypass_comment(";;FSO 3.6.11;; +Max Respawn Time:");
2236 		}
2237 	}
2238 
2239 	if (save_format == MissionFormat::RETAIL) {
2240 		if (optional_string_fred("+Red Alert:")) {
2241 			parse_comments(2);
2242 		} else {
2243 			fout("\n+Red Alert:");
2244 		}
2245 
2246 		fout(" %d", (The_mission.flags[Mission::Mission_Flags::Red_alert]) ? 1 : 0);
2247 	}
2248 
2249 	if (save_format == MissionFormat::RETAIL) //-V581
2250 	{
2251 		if (optional_string_fred("+Scramble:")) {
2252 			parse_comments(2);
2253 		} else {
2254 			fout("\n+Scramble:");
2255 		}
2256 
2257 		fout(" %d", (The_mission.flags[Mission::Mission_Flags::Scramble]) ? 1 : 0);
2258 	}
2259 
2260 	if (optional_string_fred("+Disallow Support:")) {
2261 		parse_comments(1);
2262 	} else {
2263 		fout("\n+Disallow Support:");
2264 	}
2265 	// this is compatible with non-SCP variants - Goober5000
2266 	fout(" %d", (The_mission.support_ships.max_support_ships == 0) ? 1 : 0);
2267 
2268 	// here be WMCoolmon's hull and subsys repair stuff
2269 	if (save_format != MissionFormat::RETAIL) {
2270 		if (optional_string_fred("+Hull Repair Ceiling:")) {
2271 			parse_comments(1);
2272 		} else {
2273 			fout("\n+Hull Repair Ceiling:");
2274 		}
2275 		fout(" %f", The_mission.support_ships.max_hull_repair_val);
2276 
2277 		if (optional_string_fred("+Subsystem Repair Ceiling:")) {
2278 			parse_comments(1);
2279 		} else {
2280 			fout("\n+Subsystem Repair Ceiling:");
2281 		}
2282 		fout(" %f", The_mission.support_ships.max_subsys_repair_val);
2283 	}
2284 
2285 	if (Mission_all_attack) {
2286 		if (optional_string_fred("+All Teams Attack")) {
2287 			parse_comments();
2288 		} else {
2289 			fout("\n+All Teams Attack");
2290 		}
2291 	}
2292 
2293 	if (Entry_delay_time) {
2294 		if (optional_string_fred("+Player Entry Delay:")) {
2295 			parse_comments(2);
2296 		} else {
2297 			fout("\n\n+Player Entry Delay:");
2298 		}
2299 
2300 		fout("\n%f", f2fl(Entry_delay_time));
2301 	}
2302 
2303 	if (optional_string_fred("+Viewer pos:")) {
2304 		parse_comments(2);
2305 	} else {
2306 		fout("\n\n+Viewer pos:");
2307 	}
2308 
2309 	save_vector(_viewport->view_pos);
2310 
2311 	if (optional_string_fred("+Viewer orient:")) {
2312 		parse_comments();
2313 	} else {
2314 		fout("\n+Viewer orient:");
2315 	}
2316 
2317 	save_matrix(_viewport->view_orient);
2318 
2319 	// squadron info
2320 	if (!(The_mission.game_type & MISSION_TYPE_MULTI) && (strlen(The_mission.squad_name) > 0)) { //-V805
2321 		// squad name
2322 		fout("\n+SquadReassignName: %s", The_mission.squad_name);
2323 
2324 		// maybe squad logo
2325 		if (strlen(The_mission.squad_filename) > 0) { //-V805
2326 			fout("\n+SquadReassignLogo: %s", The_mission.squad_filename);
2327 		}
2328 	}
2329 
2330 	// Goober5000 - special wing info
2331 	if (save_format != MissionFormat::RETAIL) {
2332 		int i;
2333 		fout("\n");
2334 
2335 		// starting wings
2336 		if (strcmp(Starting_wing_names[0], "Alpha") != 0 || strcmp(Starting_wing_names[1], "Beta") != 0
2337 			|| strcmp(Starting_wing_names[2], "Gamma") != 0) {
2338 			fout("\n$Starting wing names: ( ");
2339 
2340 			for (i = 0; i < MAX_STARTING_WINGS; i++) {
2341 				fout("\"%s\" ", Starting_wing_names[i]);
2342 			}
2343 
2344 			fout(")");
2345 		}
2346 
2347 		// squadron wings
2348 		if (strcmp(Squadron_wing_names[0], "Alpha") != 0 || strcmp(Squadron_wing_names[1], "Beta") != 0
2349 			|| strcmp(Squadron_wing_names[2], "Gamma") != 0 || strcmp(Squadron_wing_names[3], "Delta") != 0
2350 			|| strcmp(Squadron_wing_names[4], "Epsilon") != 0) {
2351 			fout("\n$Squadron wing names: ( ");
2352 
2353 			for (i = 0; i < MAX_SQUADRON_WINGS; i++) {
2354 				fout("\"%s\" ", Squadron_wing_names[i]);
2355 			}
2356 
2357 			fout(")");
2358 		}
2359 
2360 		// tvt wings
2361 		if (strcmp(TVT_wing_names[0], "Alpha") != 0 || strcmp(TVT_wing_names[1], "Zeta") != 0) {
2362 			fout("\n$Team-versus-team wing names: ( ");
2363 
2364 			for (i = 0; i < MAX_TVT_WINGS; i++) {
2365 				fout("\"%s\" ", TVT_wing_names[i]);
2366 			}
2367 
2368 			fout(")");
2369 		}
2370 	}
2371 
2372 	save_custom_bitmap("$Load Screen 640:",
2373 					   "$Load Screen 1024:",
2374 					   The_mission.loading_screen[GR_640],
2375 					   The_mission.loading_screen[GR_1024],
2376 					   1);
2377 
2378 	// Phreak's skybox stuff
2379 	if (strlen(The_mission.skybox_model) > 0) //-V805
2380 	{
2381 		char out_str[NAME_LENGTH];
2382 		char* period;
2383 
2384 		// kill off any extension, we will add one here
2385 		strcpy_s(out_str, The_mission.skybox_model);
2386 		period = strrchr(out_str, '.');
2387 		if (period != NULL) {
2388 			*period = 0;
2389 		}
2390 
2391 		fso_comment_push(";;FSO 3.6.0;;");
2392 		if (optional_string_fred("$Skybox Model:")) {
2393 			parse_comments(2);
2394 			fout(" %s.pof", out_str);
2395 		} else {
2396 			fout_version("\n\n$Skybox Model: %s.pof", out_str);
2397 		}
2398 		fso_comment_pop();
2399 	} else {
2400 		bypass_comment(";;FSO 3.6.0;; $Skybox Model:");
2401 	}
2402 
2403 	// orientation?
2404 	if ((strlen(The_mission.skybox_model) > 0)
2405 		&& !vm_matrix_same(&vmd_identity_matrix, &The_mission.skybox_orientation)) {
2406 		fso_comment_push(";;FSO 3.6.14;;");
2407 		if (optional_string_fred("+Skybox Orientation:")) {
2408 			parse_comments(1);
2409 			save_matrix(The_mission.skybox_orientation);
2410 		} else {
2411 			fout_version("\n+Skybox Orientation:");
2412 			save_matrix(The_mission.skybox_orientation);
2413 		}
2414 		fso_comment_pop();
2415 	} else {
2416 		bypass_comment(";;FSO 3.6.14;; +Skybox Orientation:");
2417 	}
2418 
2419 	// are skybox flags in use?
2420 	if (The_mission.skybox_flags != DEFAULT_NMODEL_FLAGS) {
2421 		//char out_str[4096];
2422 		fso_comment_push(";;FSO 3.6.11;;");
2423 		if (optional_string_fred("+Skybox Flags:")) {
2424 			parse_comments(1);
2425 			fout(" %d", The_mission.skybox_flags);
2426 		} else {
2427 			fout_version("\n+Skybox Flags: %d", The_mission.skybox_flags);
2428 		}
2429 		fso_comment_pop();
2430 	} else {
2431 		bypass_comment(";;FSO 3.6.11;; +Skybox Flags:");
2432 	}
2433 
2434 	// Goober5000's AI profile stuff
2435 	int profile_index = AI_PROFILES_INDEX(The_mission.ai_profile);
2436 	Assert(profile_index >= 0 && profile_index < MAX_AI_PROFILES);
2437 
2438 	fso_comment_push(";;FSO 3.6.9;;");
2439 	if (optional_string_fred("$AI Profile:")) {
2440 		parse_comments(2);
2441 		fout(" %s", The_mission.ai_profile->profile_name);
2442 	} else {
2443 		fout_version("\n\n$AI Profile: %s", The_mission.ai_profile->profile_name);
2444 	}
2445 	fso_comment_pop();
2446 
2447 	// sound environment (EFX/EAX) - taylor
2448 	sound_env* m_env = &The_mission.sound_environment;
2449 	if ((m_env->id >= 0) && (m_env->id < (int) EFX_presets.size())) {
2450 		EFXREVERBPROPERTIES* prop = &EFX_presets[m_env->id];
2451 
2452 		fso_comment_push(";;FSO 3.6.12;;");
2453 
2454 		fout_version("\n\n$Sound Environment: %s", prop->name.c_str());
2455 
2456 		if (m_env->volume != prop->flGain) {
2457 			fout_version("\n+Volume: %f", m_env->volume);
2458 		}
2459 
2460 		if (m_env->damping != prop->flDecayHFRatio) {
2461 			fout_version("\n+Damping: %f", m_env->damping);
2462 		}
2463 
2464 		if (m_env->decay != prop->flDecayTime) {
2465 			fout_version("\n+Decay Time: %f", m_env->decay);
2466 		}
2467 
2468 		fso_comment_pop();
2469 	}
2470 
2471 	return err;
2472 }
2473 
save_mission_internal(const char * pathname)2474 void CFred_mission_save::save_mission_internal(const char* pathname)
2475 {
2476 	time_t rawtime;
2477 
2478 	time(&rawtime);
2479 	auto timeinfo = localtime(&rawtime);
2480 
2481 	strftime(The_mission.modified, sizeof(The_mission.modified), "%x at %X", timeinfo);
2482 
2483 	reset_parse();
2484 	fred_parse_flag = 0;
2485 	fp = cfopen(pathname, "wt", CFILE_NORMAL, CF_TYPE_MISSIONS);
2486 	if (!fp) {
2487 		nprintf(("Error", "Can't open mission file to save.\n"));
2488 		err = -1;
2489 		return;
2490 	}
2491 
2492 	// Goober5000
2493 	convert_special_tags_to_retail();
2494 
2495 	if (save_mission_info()) {
2496 		err = -2;
2497 	} else if (save_plot_info()) {
2498 		err = -3;
2499 	} else if (save_variables()) {
2500 		err = -3;
2501 	} else if (save_containers()) {
2502 		err = -3;
2503 		//	else if (save_briefing_info())
2504 		//		err = -4;
2505 	} else if (save_cutscenes()) {
2506 		err = -4;
2507 	} else if (save_fiction()) {
2508 		err = -3;
2509 	} else if (save_cmd_briefs()) {
2510 		err = -4;
2511 	} else if (save_briefing()) {
2512 		err = -4;
2513 	} else if (save_debriefing()) {
2514 		err = -5;
2515 	} else if (save_players()) {
2516 		err = -6;
2517 	} else if (save_objects()) {
2518 		err = -7;
2519 	} else if (save_wings()) {
2520 		err = -8;
2521 	} else if (save_events()) {
2522 		err = -9;
2523 	} else if (save_goals()) {
2524 		err = -10;
2525 	} else if (save_waypoints()) {
2526 		err = -11;
2527 	} else if (save_messages()) {
2528 		err = -12;
2529 	} else if (save_reinforcements()) {
2530 		err = -13;
2531 	} else if (save_bitmaps()) {
2532 		err = -14;
2533 	} else if (save_asteroid_fields()) {
2534 		err = -15;
2535 	} else if (save_music()) {
2536 		err = -16;
2537 	} else {
2538 		required_string_fred("#End");
2539 		parse_comments(2);
2540 		token_found = NULL;
2541 		parse_comments();
2542 		fout("\n");
2543 	}
2544 
2545 	cfclose(fp);
2546 	if (err)
2547 		mprintf(("Mission saving error code #%d\n", err));
2548 }
2549 
save_music()2550 int CFred_mission_save::save_music()
2551 {
2552 	required_string_fred("#Music");
2553 	parse_comments(2);
2554 
2555 	required_string_fred("$Event Music:");
2556 	parse_comments(2);
2557 	if (Current_soundtrack_num < 0) {
2558 		fout(" None");
2559 	} else {
2560 		fout(" %s", Soundtracks[Current_soundtrack_num].name);
2561 	}
2562 
2563 	// Goober5000 - save using the special comment prefix
2564 	if (stricmp(The_mission.substitute_event_music_name, "None") != 0) {
2565 		fso_comment_push(";;FSO 3.6.9;;");
2566 		if (optional_string_fred("$Substitute Event Music:")) {
2567 			parse_comments(1);
2568 			fout(" %s", The_mission.substitute_event_music_name);
2569 		} else {
2570 			fout_version("\n$Substitute Event Music: %s", The_mission.substitute_event_music_name);
2571 		}
2572 		fso_comment_pop();
2573 	} else {
2574 		bypass_comment(";;FSO 3.6.9;; $Substitute Event Music:");
2575 	}
2576 
2577 	required_string_fred("$Briefing Music:");
2578 	parse_comments();
2579 	if (Mission_music[SCORE_BRIEFING] < 0) {
2580 		fout(" None");
2581 	} else {
2582 		fout(" %s", Spooled_music[Mission_music[SCORE_BRIEFING]].name);
2583 	}
2584 
2585 	// Goober5000 - save using the special comment prefix
2586 	if (stricmp(The_mission.substitute_briefing_music_name, "None") != 0) {
2587 		fso_comment_push(";;FSO 3.6.9;;");
2588 		if (optional_string_fred("$Substitute Briefing Music:")) {
2589 			parse_comments(1);
2590 			fout(" %s", The_mission.substitute_briefing_music_name);
2591 		} else {
2592 			fout_version("\n$Substitute Briefing Music: %s", The_mission.substitute_briefing_music_name);
2593 		}
2594 		fso_comment_pop();
2595 	} else {
2596 		bypass_comment(";;FSO 3.6.9;; $Substitute Briefing Music:");
2597 	}
2598 
2599 	// avoid keeping the old one around
2600 	bypass_comment(";;FSO 3.6.8;; $Substitute Music:");
2601 
2602 	// old stuff
2603 	if (Mission_music[SCORE_DEBRIEF_SUCCESS] != event_music_get_spooled_music_index("Success")) {
2604 		if (optional_string_fred("$Debriefing Success Music:")) {
2605 			parse_comments(1);
2606 		} else {
2607 			fout("\n$Debriefing Success Music:");
2608 		}
2609 		fout(" %s",
2610 			 Mission_music[SCORE_DEBRIEF_SUCCESS] < 0 ? "None"
2611 													  : Spooled_music[Mission_music[SCORE_DEBRIEF_SUCCESS]].name);
2612 	}
2613 	if (save_format != MissionFormat::RETAIL && Mission_music[SCORE_DEBRIEF_AVERAGE] != event_music_get_spooled_music_index("Average")) {
2614 		if (optional_string_fred("$Debriefing Average Music:")) {
2615 			parse_comments(1);
2616 		} else {
2617 			fout("\n$Debriefing Average Music:");
2618 		}
2619 		fout(" %s",
2620 			 Mission_music[SCORE_DEBRIEF_AVERAGE] < 0 ? "None"
2621 													  : Spooled_music[Mission_music[SCORE_DEBRIEF_AVERAGE]].name);
2622 	}
2623 	if (Mission_music[SCORE_DEBRIEF_FAIL] != event_music_get_spooled_music_index("Failure")) {
2624 		if (optional_string_fred("$Debriefing Fail Music:")) {
2625 			parse_comments(1);
2626 		} else {
2627 			fout("\n$Debriefing Fail Music:");
2628 		}
2629 		fout(" %s",
2630 			 Mission_music[SCORE_DEBRIEF_FAIL] < 0 ? "None" : Spooled_music[Mission_music[SCORE_DEBRIEF_FAIL]].name);
2631 	}
2632 
2633 	// Goober5000 - save using the special comment prefix
2634 	if (mission_has_fiction() && Mission_music[SCORE_FICTION_VIEWER] >= 0) {
2635 		fso_comment_push(";;FSO 3.6.11;;");
2636 		if (optional_string_fred("$Fiction Viewer Music:")) {
2637 			parse_comments(1);
2638 			fout(" %s", Spooled_music[Mission_music[SCORE_FICTION_VIEWER]].name);
2639 		} else {
2640 			fout_version("\n$Fiction Viewer Music: %s", Spooled_music[Mission_music[SCORE_FICTION_VIEWER]].name);
2641 		}
2642 		fso_comment_pop();
2643 	} else {
2644 		bypass_comment(";;FSO 3.6.11;; $Fiction Viewer Music:");
2645 	}
2646 
2647 	fso_comment_pop(true);
2648 
2649 	return err;
2650 }
2651 
save_warp_params(WarpDirection direction,ship * shipp)2652 int CFred_mission_save::save_warp_params(WarpDirection direction, ship *shipp)
2653 {
2654 	if (save_format == MissionFormat::RETAIL)
2655 		return err;
2656 
2657 	// for writing to file; c.f. parse_warp_params
2658 	const char *prefix = (direction == WarpDirection::WARP_IN) ? "$Warpin" : "$Warpout";
2659 
2660 	WarpParams *shipp_params, *sip_params;
2661 	if (direction == WarpDirection::WARP_IN)
2662 	{
2663 		// if exactly the same params used, no need to output anything
2664 		if (shipp->warpin_params_index == Ship_info[shipp->ship_info_index].warpin_params_index)
2665 			return err;
2666 
2667 		shipp_params = &Warp_params[shipp->warpin_params_index];
2668 		sip_params = &Warp_params[Ship_info[shipp->ship_info_index].warpin_params_index];
2669 	}
2670 	else
2671 	{
2672 		// if exactly the same params used, no need to output anything
2673 		if (shipp->warpout_params_index == Ship_info[shipp->ship_info_index].warpout_params_index)
2674 			return err;
2675 
2676 		shipp_params = &Warp_params[shipp->warpout_params_index];
2677 		sip_params = &Warp_params[Ship_info[shipp->ship_info_index].warpout_params_index];
2678 	}
2679 
2680 	if (shipp_params->warp_type != sip_params->warp_type)
2681 	{
2682 		// is it a fireball?
2683 		if (shipp_params->warp_type & WT_DEFAULT_WITH_FIREBALL)
2684 		{
2685 			fout("\n%s type: %s", prefix, Fireball_info[shipp_params->warp_type & WT_FLAG_MASK].unique_id);
2686 		}
2687 		// probably a warp type
2688 		else if (shipp_params->warp_type >= 0 && shipp_params->warp_type < Num_warp_types)
2689 		{
2690 			fout("\n%s type: %s", prefix, Warp_types[shipp_params->warp_type]);
2691 		}
2692 	}
2693 
2694 	if (shipp_params->snd_start != sip_params->snd_start)
2695 	{
2696 		if (shipp_params->snd_start.isValid())
2697 			fout("\n%s Start Sound: %s", prefix, gamesnd_get_game_sound(shipp_params->snd_start)->name.c_str());
2698 	}
2699 
2700 	if (shipp_params->snd_end != sip_params->snd_end)
2701 	{
2702 		if (shipp_params->snd_end.isValid())
2703 			fout("\n%s End Sound: %s", prefix, gamesnd_get_game_sound(shipp_params->snd_end)->name.c_str());
2704 	}
2705 
2706 	if (direction == WarpDirection::WARP_OUT && shipp_params->warpout_engage_time != sip_params->warpout_engage_time)
2707 	{
2708 		if (shipp_params->warpout_engage_time > 0)
2709 			fout("\n%s engage time: %.2f", prefix, i2fl(shipp_params->warpout_engage_time) / 1000.0f);
2710 	}
2711 
2712 	if (shipp_params->speed != sip_params->speed)
2713 	{
2714 		if (shipp_params->speed > 0.0f)
2715 			fout("\n%s speed: %.2f", prefix, shipp_params->speed);
2716 	}
2717 
2718 	if (shipp_params->time != sip_params->time)
2719 	{
2720 		if (shipp_params->time > 0)
2721 			fout("\n%s time: %.2f", prefix, i2fl(shipp_params->time) / 1000.0f);
2722 	}
2723 
2724 	if (shipp_params->accel_exp != sip_params->accel_exp)
2725 	{
2726 		if (shipp_params->accel_exp > 0.0f)
2727 			fout("\n%s %s exp: %.2f", prefix, direction == WarpDirection::WARP_IN ? "decel" : "accel", shipp_params->accel_exp);
2728 	}
2729 
2730 	if (shipp_params->radius != sip_params->radius)
2731 	{
2732 		if (shipp_params->radius > 0.0f)
2733 			fout("\n%s radius: %.2f", prefix, shipp_params->radius);
2734 	}
2735 
2736 	if (stricmp(shipp_params->anim, sip_params->anim) != 0)
2737 	{
2738 		if (strlen(shipp_params->anim) > 0)
2739 			fout("\n%s animation: %s", prefix, shipp_params->anim);
2740 	}
2741 
2742 	if (direction == WarpDirection::WARP_OUT && shipp_params->warpout_player_speed != sip_params->warpout_player_speed)
2743 	{
2744 		if (shipp_params->warpout_player_speed > 0.0f)
2745 			fout("\n$Player warpout speed: %.2f", shipp_params->warpout_player_speed);
2746 	}
2747 
2748 	return err;
2749 }
2750 
save_objects()2751 int CFred_mission_save::save_objects()
2752 {
2753 	SCP_string sexp_out;
2754 	int i, z;
2755 	ai_info* aip;
2756 	object* objp;
2757 	ship* shipp;
2758 
2759 	required_string_fred("#Objects");
2760 	parse_comments(2);
2761 	fout("\t\t;! %d total\n", ship_get_num_ships());
2762 
2763 	for (i = z = 0; i < MAX_SHIPS; i++) {
2764 		if (Ships[i].objnum < 0) {
2765 			continue;
2766 		}
2767 
2768 		auto j = Objects[Ships[i].objnum].type;
2769 		if ((j != OBJ_SHIP) && (j != OBJ_START)) {
2770 			continue;
2771 		}
2772 
2773 		shipp = &Ships[i];
2774 		objp = &Objects[shipp->objnum];
2775 		required_string_either_fred("$Name:", "#Wings");
2776 		required_string_fred("$Name:");
2777 		parse_comments(z ? 2 : 1);
2778 		fout(" %s\t\t;! Object #%d", shipp->ship_name, i);
2779 
2780 		// Display name
2781 		// The display name is only written if there was one at the start to avoid introducing inconsistencies
2782 		if (save_format != MissionFormat::RETAIL && shipp->has_display_name()) {
2783 			char truncated_name[NAME_LENGTH];
2784 			strcpy_s(truncated_name, shipp->ship_name);
2785 			end_string_at_first_hash_symbol(truncated_name);
2786 
2787 			// Also, the display name is not written if it's just the truncation of the name at the hash
2788 			if (strcmp(shipp->get_display_name(), truncated_name) != 0) {
2789 				fout("\n$Display name:");
2790 				fout_ext(" ", "%s", shipp->display_name.c_str());
2791 			}
2792 		}
2793 
2794 		required_string_fred("\n$Class:");
2795 		parse_comments(0);
2796 		fout(" %s", Ship_info[shipp->ship_info_index].name);
2797 
2798 		//alt classes stuff
2799 		if (save_format != MissionFormat::RETAIL) {
2800 			for (SCP_vector<alt_class>::iterator ii = shipp->s_alt_classes.begin(); ii != shipp->s_alt_classes.end();
2801 				 ++ii) {
2802 				// is this a variable?
2803 				if (ii->variable_index != -1) {
2804 					fout("\n$Alt Ship Class: @%s", Sexp_variables[ii->variable_index].variable_name);
2805 				} else {
2806 					fout("\n$Alt Ship Class: \"%s\"", Ship_info[ii->ship_class].name);
2807 				}
2808 
2809 				// default class?
2810 				if (ii->default_to_this_class) {
2811 					fout("\n+Default Class:");
2812 				}
2813 			}
2814 		}
2815 
2816 		// optional alternate type name
2817 		if (strlen(Fred_alt_names[i])) {
2818 			fout("\n$Alt: %s\n", Fred_alt_names[i]);
2819 		}
2820 
2821 		// optional callsign
2822 		if (save_format != MissionFormat::RETAIL && strlen(Fred_callsigns[i])) {
2823 			fout("\n$Callsign: %s\n", Fred_callsigns[i]);
2824 		}
2825 
2826 		required_string_fred("$Team:");
2827 		parse_comments();
2828 		fout(" %s", Iff_info[shipp->team].iff_name);
2829 
2830 		if (save_format != MissionFormat::RETAIL && Ship_info[shipp->ship_info_index].uses_team_colors) {
2831 			required_string_fred("$Team Color Setting:");
2832 			parse_comments();
2833 			fout(" %s", shipp->team_name.c_str());
2834 		}
2835 
2836 		required_string_fred("$Location:");
2837 		parse_comments();
2838 		save_vector(Objects[shipp->objnum].pos);
2839 
2840 		required_string_fred("$Orientation:");
2841 		parse_comments();
2842 		save_matrix(Objects[shipp->objnum].orient);
2843 
2844 		if (save_format == MissionFormat::RETAIL) {
2845 			required_string_fred("$IFF:");
2846 			parse_comments();
2847 			fout(" %s", "IFF 1");
2848 		}
2849 
2850 		Assert(shipp->ai_index >= 0);
2851 		aip = &Ai_info[shipp->ai_index];
2852 
2853 		required_string_fred("$AI Behavior:");
2854 		parse_comments();
2855 		fout(" %s", Ai_behavior_names[aip->behavior]);
2856 
2857 		if (shipp->weapons.ai_class != Ship_info[shipp->ship_info_index].ai_class) {
2858 			if (optional_string_fred("+AI Class:", "$Name:")) {
2859 				parse_comments();
2860 			} else {
2861 				fout("\n+AI Class:");
2862 			}
2863 
2864 			fout(" %s", Ai_class_names[shipp->weapons.ai_class]);
2865 		}
2866 
2867 		save_ai_goals(Ai_info[shipp->ai_index].goals, i);
2868 
2869 		// XSTR
2870 		required_string_fred("$Cargo 1:");
2871 		parse_comments();
2872 		fout_ext(" ", "%s", Cargo_names[shipp->cargo1]);
2873 
2874 		save_common_object_data(&Objects[shipp->objnum], &Ships[i]);
2875 
2876 		if (shipp->wingnum >= 0) {
2877 			shipp->arrival_location = ARRIVE_AT_LOCATION;
2878 		}
2879 
2880 		required_string_fred("$Arrival Location:");
2881 		parse_comments();
2882 		fout(" %s", Arrival_location_names[shipp->arrival_location]);
2883 
2884 		if (shipp->arrival_location != ARRIVE_AT_LOCATION) {
2885 			if (optional_string_fred("+Arrival Distance:", "$Name:")) {
2886 				parse_comments();
2887 			} else {
2888 				fout("\n+Arrival Distance:");
2889 			}
2890 
2891 			fout(" %d", shipp->arrival_distance);
2892 			if (optional_string_fred("$Arrival Anchor:", "$Name:")) {
2893 				parse_comments();
2894 			} else {
2895 				fout("\n$Arrival Anchor:");
2896 			}
2897 
2898 			z = shipp->arrival_anchor;
2899 			if (z & SPECIAL_ARRIVAL_ANCHOR_FLAG) {
2900 				// get name
2901 				char tmp[NAME_LENGTH + 15];
2902 				stuff_special_arrival_anchor_name(tmp, z, save_format == MissionFormat::RETAIL);
2903 
2904 				// save it
2905 				fout(" %s", tmp);
2906 			} else if (z >= 0) {
2907 				fout(" %s", Ships[z].ship_name);
2908 			} else {
2909 				fout(" <error>");
2910 			}
2911 		}
2912 
2913 		// Goober5000
2914 		if (save_format != MissionFormat::RETAIL) {
2915 			if ((shipp->arrival_location == ARRIVE_FROM_DOCK_BAY) && (shipp->arrival_path_mask > 0)) {
2916 				int anchor_shipnum;
2917 				polymodel* pm;
2918 
2919 				anchor_shipnum = shipp->arrival_anchor;
2920 				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
2921 
2922 				fout("\n+Arrival Paths: ( ");
2923 
2924 				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
2925 				for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
2926 					if (shipp->arrival_path_mask & (1 << n)) {
2927 						fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
2928 					}
2929 				}
2930 
2931 				fout(")");
2932 			}
2933 		}
2934 
2935 		if (shipp->arrival_delay) {
2936 			if (optional_string_fred("+Arrival Delay:", "$Name:")) {
2937 				parse_comments();
2938 			} else {
2939 				fout("\n+Arrival Delay:");
2940 			}
2941 
2942 			fout(" %d", shipp->arrival_delay);
2943 		}
2944 
2945 		required_string_fred("$Arrival Cue:");
2946 		parse_comments();
2947 		convert_sexp_to_string(sexp_out, shipp->arrival_cue, SEXP_SAVE_MODE);
2948 		fout(" %s", sexp_out.c_str());
2949 
2950 		if (shipp->wingnum >= 0) {
2951 			shipp->departure_location = DEPART_AT_LOCATION;
2952 		}
2953 
2954 		required_string_fred("$Departure Location:");
2955 		parse_comments();
2956 		fout(" %s", Departure_location_names[shipp->departure_location]);
2957 
2958 		if (shipp->departure_location != DEPART_AT_LOCATION) {
2959 			required_string_fred("$Departure Anchor:");
2960 			parse_comments();
2961 
2962 			if (shipp->departure_anchor >= 0) {
2963 				fout(" %s", Ships[shipp->departure_anchor].ship_name);
2964 			} else {
2965 				fout(" <error>");
2966 			}
2967 		}
2968 
2969 		// Goober5000
2970 		if (save_format != MissionFormat::RETAIL) {
2971 			if ((shipp->departure_location == DEPART_AT_DOCK_BAY) && (shipp->departure_path_mask > 0)) {
2972 				int anchor_shipnum;
2973 				polymodel* pm;
2974 
2975 				anchor_shipnum = shipp->departure_anchor;
2976 				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
2977 
2978 				fout("\n+Departure Paths: ( ");
2979 
2980 				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
2981 				for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
2982 					if (shipp->departure_path_mask & (1 << n)) {
2983 						fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
2984 					}
2985 				}
2986 
2987 				fout(")");
2988 			}
2989 		}
2990 
2991 		if (shipp->departure_delay) {
2992 			if (optional_string_fred("+Departure delay:", "$Name:")) {
2993 				parse_comments();
2994 			} else {
2995 				fout("\n+Departure delay:");
2996 			}
2997 
2998 			fout(" %d", shipp->departure_delay);
2999 		}
3000 
3001 		required_string_fred("$Departure Cue:");
3002 		parse_comments();
3003 		convert_sexp_to_string(sexp_out, shipp->departure_cue, SEXP_SAVE_MODE);
3004 		fout(" %s", sexp_out.c_str());
3005 
3006 		save_warp_params(WarpDirection::WARP_IN, shipp);
3007 		save_warp_params(WarpDirection::WARP_OUT, shipp);
3008 
3009 		required_string_fred("$Determination:");
3010 		parse_comments();
3011 		fout(" 10"); // dummy value for backwards compatibility
3012 
3013 		if (optional_string_fred("+Flags:", "$Name:")) {
3014 			parse_comments();
3015 			fout(" (");
3016 		} else {
3017 			fout("\n+Flags: (");
3018 		}
3019 
3020 		if (shipp->flags[Ship::Ship_Flags::Cargo_revealed]) {
3021 			fout(" \"cargo-known\"");
3022 		}
3023 		if (shipp->flags[Ship::Ship_Flags::Ignore_count]) {
3024 			fout(" \"ignore-count\"");
3025 		}
3026 		if (objp->flags[Object::Object_Flags::Protected]) {
3027 			fout(" \"protect-ship\"");
3028 		}
3029 		if (shipp->flags[Ship::Ship_Flags::Reinforcement]) {
3030 			fout(" \"reinforcement\"");
3031 		}
3032 		if (objp->flags[Object::Object_Flags::No_shields]) {
3033 			fout(" \"no-shields\"");
3034 		}
3035 		if (shipp->flags[Ship::Ship_Flags::Escort]) {
3036 			fout(" \"escort\"");
3037 		}
3038 		if (objp->type == OBJ_START) {
3039 			fout(" \"player-start\"");
3040 		}
3041 		if (shipp->flags[Ship::Ship_Flags::No_arrival_music]) {
3042 			fout(" \"no-arrival-music\"");
3043 		}
3044 		if (shipp->flags[Ship::Ship_Flags::No_arrival_warp]) {
3045 			fout(" \"no-arrival-warp\"");
3046 		}
3047 		if (shipp->flags[Ship::Ship_Flags::No_departure_warp]) {
3048 			fout(" \"no-departure-warp\"");
3049 		}
3050 		if (Objects[shipp->objnum].flags[Object::Object_Flags::Invulnerable]) {
3051 			fout(" \"invulnerable\"");
3052 		}
3053 		if (shipp->flags[Ship::Ship_Flags::Hidden_from_sensors]) {
3054 			fout(" \"hidden-from-sensors\"");
3055 		}
3056 		if (shipp->flags[Ship::Ship_Flags::Scannable]) {
3057 			fout(" \"scannable\"");
3058 		}
3059 		if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze]) {
3060 			fout(" \"kamikaze\"");
3061 		}
3062 		if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::No_dynamic]) {
3063 			fout(" \"no-dynamic\"");
3064 		}
3065 		if (shipp->flags[Ship::Ship_Flags::Red_alert_store_status]) {
3066 			fout(" \"red-alert-carry\"");
3067 		}
3068 		if (objp->flags[Object::Object_Flags::Beam_protected]) {
3069 			fout(" \"beam-protect-ship\"");
3070 		}
3071 		if (objp->flags[Object::Object_Flags::Flak_protected]) {
3072 			fout(" \"flak-protect-ship\"");
3073 		}
3074 		if (objp->flags[Object::Object_Flags::Laser_protected]) {
3075 			fout(" \"laser-protect-ship\"");
3076 		}
3077 		if (objp->flags[Object::Object_Flags::Missile_protected]) {
3078 			fout(" \"missile-protect-ship\"");
3079 		}
3080 		if (shipp->ship_guardian_threshold != 0) {
3081 			fout(" \"guardian\"");
3082 		}
3083 		if (objp->flags[Object::Object_Flags::Special_warpin]) {
3084 			fout(" \"special-warp\"");
3085 		}
3086 		if (shipp->flags[Ship::Ship_Flags::Vaporize]) {
3087 			fout(" \"vaporize\"");
3088 		}
3089 		if (shipp->flags[Ship::Ship_Flags::Stealth]) {
3090 			fout(" \"stealth\"");
3091 		}
3092 		if (shipp->flags[Ship::Ship_Flags::Friendly_stealth_invis]) {
3093 			fout(" \"friendly-stealth-invisible\"");
3094 		}
3095 		if (shipp->flags[Ship::Ship_Flags::Dont_collide_invis]) {
3096 			fout(" \"don't-collide-invisible\"");
3097 		}
3098 		//for compatibility reasons ship locked or weapons locked are saved as both locked in retail mode
3099 		if ((save_format == MissionFormat::RETAIL)
3100 			&& ((shipp->flags[Ship::Ship_Flags::Ship_locked]) || (shipp->flags[Ship::Ship_Flags::Weapons_locked]))) {
3101 				fout(" \"locked\"");
3102 		}
3103 		fout(" )");
3104 
3105 		// flags2 added by Goober5000 --------------------------------
3106 		if (save_format != MissionFormat::RETAIL) {
3107 			if (optional_string_fred("+Flags2:", "$Name:")) {
3108 				parse_comments();
3109 				fout(" (");
3110 			} else {
3111 				fout("\n+Flags2: (");
3112 			}
3113 
3114 			if (shipp->flags[Ship::Ship_Flags::Primitive_sensors]) {
3115 				fout(" \"primitive-sensors\"");
3116 			}
3117 			if (shipp->flags[Ship::Ship_Flags::No_subspace_drive]) {
3118 				fout(" \"no-subspace-drive\"");
3119 			}
3120 			if (shipp->flags[Ship::Ship_Flags::Navpoint_carry]) {
3121 				fout(" \"nav-carry-status\"");
3122 			}
3123 			if (shipp->flags[Ship::Ship_Flags::Affected_by_gravity]) {
3124 				fout(" \"affected-by-gravity\"");
3125 			}
3126 			if (shipp->flags[Ship::Ship_Flags::Toggle_subsystem_scanning]) {
3127 				fout(" \"toggle-subsystem-scanning\"");
3128 			}
3129 			if (objp->flags[Object::Object_Flags::Targetable_as_bomb]) {
3130 				fout(" \"targetable-as-bomb\"");
3131 			}
3132 			if (shipp->flags[Ship::Ship_Flags::No_builtin_messages]) {
3133 				fout(" \"no-builtin-messages\"");
3134 			}
3135 			if (shipp->flags[Ship::Ship_Flags::Primaries_locked]) {
3136 				fout(" \"primaries-locked\"");
3137 			}
3138 			if (shipp->flags[Ship::Ship_Flags::Secondaries_locked]) {
3139 				fout(" \"secondaries-locked\"");
3140 			}
3141 			if (shipp->flags[Ship::Ship_Flags::No_death_scream]) {
3142 				fout(" \"no-death-scream\"");
3143 			}
3144 			if (shipp->flags[Ship::Ship_Flags::Always_death_scream]) {
3145 				fout(" \"always-death-scream\"");
3146 			}
3147 			if (shipp->flags[Ship::Ship_Flags::Navpoint_needslink]) {
3148 				fout(" \"nav-needslink\"");
3149 			}
3150 			if (shipp->flags[Ship::Ship_Flags::Hide_ship_name]) {
3151 				fout(" \"hide-ship-name\"");
3152 			}
3153 			if (shipp->flags[Ship::Ship_Flags::Set_class_dynamically]) {
3154 				fout(" \"set-class-dynamically\"");
3155 			}
3156 			if (shipp->flags[Ship::Ship_Flags::Lock_all_turrets_initially]) {
3157 				fout(" \"lock-all-turrets\"");
3158 			}
3159 			if (shipp->flags[Ship::Ship_Flags::Afterburner_locked]) {
3160 				fout(" \"afterburners-locked\"");
3161 			}
3162 			if (shipp->flags[Ship::Ship_Flags::Force_shields_on]) {
3163 				fout(" \"force-shields-on\"");
3164 			}
3165 			if (objp->flags[Object::Object_Flags::Immobile]) {
3166 				fout(" \"immobile\"");
3167 			}
3168 			if (shipp->flags[Ship::Ship_Flags::No_ets]) {
3169 				fout(" \"no-ets\"");
3170 			}
3171 			if (shipp->flags[Ship::Ship_Flags::Cloaked]) {
3172 				fout(" \"cloaked\"");
3173 			}
3174 			if (shipp->flags[Ship::Ship_Flags::Ship_locked]) {
3175 				fout(" \"ship-locked\"");
3176 			}
3177 			if (shipp->flags[Ship::Ship_Flags::Weapons_locked]) {
3178 				fout(" \"weapons-locked\"");
3179 			}
3180 			if (shipp->flags[Ship::Ship_Flags::Scramble_messages]) {
3181 				fout(" \"scramble-messages\"");
3182 			}
3183 			if (!(objp->flags[Object::Object_Flags::Collides])) {
3184 				fout(" \"no_collide\"");
3185 			}
3186 			if (shipp->flags[Ship::Ship_Flags::No_disabled_self_destruct]) {
3187 				fout(" \"no-disabled-self-destruct\"");
3188 			}
3189 			fout(" )");
3190 		}
3191 		// -----------------------------------------------------------
3192 
3193 		fout("\n+Respawn priority: %d", shipp->respawn_priority);    // HA!  Newline added by Goober5000
3194 
3195 		if (shipp->flags[Ship::Ship_Flags::Escort]) {
3196 			if (optional_string_fred("+Escort priority:", "$Name:")) {
3197 				parse_comments();
3198 			} else {
3199 				fout("\n+Escort priority:");
3200 			}
3201 
3202 			fout(" %d", shipp->escort_priority);
3203 		}
3204 
3205 		// special explosions
3206 		if (save_format != MissionFormat::RETAIL) {
3207 			if (shipp->use_special_explosion) {
3208 				fso_comment_push(";;FSO 3.6.13;;");
3209 				if (optional_string_fred("$Special Explosion:", "$Name:")) {
3210 					parse_comments();
3211 
3212 					required_string_fred("+Special Exp Damage:");
3213 					parse_comments();
3214 					fout(" %d", shipp->special_exp_damage);
3215 
3216 					required_string_fred("+Special Exp Blast:");
3217 					parse_comments();
3218 					fout(" %d", shipp->special_exp_blast);
3219 
3220 					required_string_fred("+Special Exp Inner Radius:");
3221 					parse_comments();
3222 					fout(" %d", shipp->special_exp_inner);
3223 
3224 					required_string_fred("+Special Exp Outer Radius:");
3225 					parse_comments();
3226 					fout(" %d", shipp->special_exp_outer);
3227 
3228 					if (shipp->use_shockwave && (shipp->special_exp_shockwave_speed > 0)) {
3229 						optional_string_fred("+Special Exp Shockwave Speed:");
3230 						parse_comments();
3231 						fout(" %d", shipp->special_exp_shockwave_speed);
3232 					} else {
3233 						bypass_comment(";;FSO 3.6.13;; +Special Exp Shockwave Speed:", "$Name:");
3234 					}
3235 
3236 					if (shipp->special_exp_deathroll_time > 0) {
3237 						optional_string_fred("+Special Exp Death Roll Time:");
3238 						parse_comments();
3239 						fout(" %d", shipp->special_exp_deathroll_time);
3240 					} else {
3241 						bypass_comment(";;FSO 3.6.13;; +Special Exp Death Roll Time:", "$Name:");
3242 					}
3243 				} else {
3244 					fout_version("\n$Special Explosion:");
3245 
3246 					fout_version("\n+Special Exp Damage:");
3247 					fout(" %d", shipp->special_exp_damage);
3248 
3249 					fout_version("\n+Special Exp Blast:");
3250 					fout(" %d", shipp->special_exp_blast);
3251 
3252 					fout_version("\n+Special Exp Inner Radius:");
3253 					fout(" %d", shipp->special_exp_inner);
3254 
3255 					fout_version("\n+Special Exp Outer Radius:");
3256 					fout(" %d", shipp->special_exp_outer);
3257 
3258 					if (shipp->use_shockwave && (shipp->special_exp_shockwave_speed > 0)) {
3259 						fout_version("\n+Special Exp Shockwave Speed:");
3260 						fout(" %d", shipp->special_exp_shockwave_speed);
3261 					}
3262 
3263 					if (shipp->special_exp_deathroll_time > 0) {
3264 						fout_version("\n+Special Exp Death Roll Time:");
3265 						fout(" %d", shipp->special_exp_deathroll_time);
3266 					}
3267 
3268 				}
3269 				fso_comment_pop();
3270 			} else {
3271 				bypass_comment(";;FSO 3.6.13;; +Special Exp Shockwave Speed:", "$Name:");
3272 				bypass_comment(";;FSO 3.6.13;; +Special Exp Death Roll Time:", "$Name:");
3273 			}
3274 		}
3275 			// retail format special explosions
3276 		else {
3277 			if (shipp->use_special_explosion) {
3278 				int special_exp_index;
3279 
3280 				if (has_special_explosion_block_index(&Ships[i], &special_exp_index)) {
3281 					fout("\n+Special Exp index:");
3282 					fout(" %d", special_exp_index);
3283 				} else {
3284 					SCP_string text = "You are saving in the retail mission format, but ";
3285 					text += "the mission has too many special explosions defined. \"";
3286 					text += shipp->ship_name;
3287 					text += "\" has therefore lost any special explosion data that was defined for it. ";
3288 					text += "\" Either remove special explosions or SEXP variables if you need it to have one ";
3289 					_viewport->dialogProvider->showButtonDialog(DialogType::Warning,
3290 																"Too many variables!",
3291 																text,
3292 																{ DialogButton::Ok });
3293 				}
3294 			}
3295 		}
3296 
3297 		// Goober5000 ------------------------------------------------
3298 		if (save_format != MissionFormat::RETAIL) {
3299 			if (shipp->special_hitpoints) {
3300 				fso_comment_push(";;FSO 3.6.13;;");
3301 				if (optional_string_fred("+Special Hitpoints:", "$Name:")) {
3302 					parse_comments();
3303 				} else {
3304 					fout_version("\n+Special Hitpoints:");
3305 				}
3306 				fso_comment_pop();
3307 
3308 				fout(" %d", shipp->special_hitpoints);
3309 			} else {
3310 				bypass_comment(";;FSO 3.6.13;; +Special Hitpoints:", "$Name:");
3311 			}
3312 
3313 			if (shipp->special_shield >= 0) {
3314 				fso_comment_push(";;FSO 3.6.13;;");
3315 				if (optional_string_fred("+Special Shield Points:", "$Name:")) {
3316 					parse_comments();
3317 				} else {
3318 					fout_version("\n+Special Shield Points:");
3319 				}
3320 				fso_comment_pop();
3321 
3322 				fout(" %d", shipp->special_shield);
3323 			} else {
3324 				bypass_comment(";;FSO 3.6.13;; +Special Shield Points:", "$Name:");
3325 			}
3326 		}
3327 		// -----------------------------------------------------------
3328 
3329 		if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze]) {
3330 			if (optional_string_fred("+Kamikaze Damage:", "$Name:")) {
3331 				parse_comments();
3332 			} else {
3333 				fout("\n+Kamikaze Damage:");
3334 			}
3335 
3336 			fout(" %d", Ai_info[shipp->ai_index].kamikaze_damage);
3337 		}
3338 
3339 		if (shipp->hotkey != -1) {
3340 			if (optional_string_fred("+Hotkey:", "$Name:")) {
3341 				parse_comments();
3342 			} else {
3343 				fout("\n+Hotkey:");
3344 			}
3345 
3346 			fout(" %d", shipp->hotkey);
3347 		}
3348 
3349 		// mwa -- new code to save off information about initially docked ships.
3350 		// Goober5000 - newer code to save off information about initially docked ships. ;)
3351 		if (object_is_docked(&Objects[shipp->objnum])) {
3352 			// possible incompatibility
3353 			if (save_format == MissionFormat::RETAIL && !dock_check_docked_one_on_one(&Objects[shipp->objnum])) {
3354 				static bool warned = false;
3355 				if (!warned) {
3356 					SCP_string text = "You are saving in the retail mission format, but \"";
3357 					text += shipp->ship_name;
3358 					text += "\" is docked to more than one ship.  If you wish to run this mission in retail, ";
3359 					text += "you should remove the additional ships and save the mission again.";
3360 					_viewport->dialogProvider->showButtonDialog(DialogType::Warning,
3361 																"Incompatibility with retail mission format",
3362 																text,
3363 																{ DialogButton::Ok });
3364 
3365 					warned = true;    // to avoid zillions of boxes
3366 				}
3367 			}
3368 
3369 			// save one-on-one groups as if they were retail
3370 			if (dock_check_docked_one_on_one(&Objects[shipp->objnum])) {
3371 				// retail format only saved information for non-leaders
3372 				if (!(shipp->flags[Ship::Ship_Flags::Dock_leader])) {
3373 					save_single_dock_instance(&Ships[i], Objects[shipp->objnum].dock_list);
3374 				}
3375 			}
3376 				// multiply docked
3377 			else {
3378 				// save all instances for all ships
3379 				for (dock_instance* dock_ptr = Objects[shipp->objnum].dock_list; dock_ptr != NULL;
3380 					 dock_ptr = dock_ptr->next) {
3381 					save_single_dock_instance(&Ships[i], dock_ptr);
3382 				}
3383 			}
3384 		}
3385 
3386 		// check the ship flag about killing off the ship before a mission starts.  Write out the appropriate
3387 		// variable if necessary
3388 		if (shipp->flags[Ship::Ship_Flags::Kill_before_mission]) {
3389 			if (optional_string_fred("+Destroy At:", "$Name:")) {
3390 				parse_comments();
3391 			} else {
3392 				fout("\n+Destroy At: ");
3393 			}
3394 
3395 			fout(" %d", shipp->final_death_time);
3396 		}
3397 
3398 		// possibly write out the orders that this ship will accept.  We'll only do it if the orders
3399 		// are not the default set of orders
3400 		if (shipp->orders_accepted != ship_get_default_orders_accepted(&Ship_info[shipp->ship_info_index])) {
3401 			if (optional_string_fred("+Orders Accepted:", "$Name:")) {
3402 				parse_comments();
3403 			} else {
3404 				fout("\n+Orders Accepted:");
3405 			}
3406 
3407 			fout(" %d\t\t;! note that this is a bitfield!!!", shipp->orders_accepted);
3408 		}
3409 
3410 		if (shipp->group >= 0) {
3411 			if (optional_string_fred("+Group:", "$Name:")) {
3412 				parse_comments();
3413 			} else {
3414 				fout("\n+Group:");
3415 			}
3416 
3417 			fout(" %d", shipp->group);
3418 		}
3419 
3420 		// always write out the score to ensure backwards compatibility. If the score is the same as the value
3421 		// in the table write out a flag to tell the game to simply use whatever is in the table instead
3422 		if (Ship_info[shipp->ship_info_index].score == shipp->score) {
3423 			fso_comment_push(";;FSO 3.6.10;;");
3424 			if (optional_string_fred("+Use Table Score:", "$Name:")) {
3425 				parse_comments();
3426 			} else {
3427 				fout_version("\n+Use Table Score:");
3428 			}
3429 			fso_comment_pop();
3430 		} else {
3431 			bypass_comment(";;FSO 3.6.10;; +Use Table Score:", "$Name:");
3432 		}
3433 
3434 		if (optional_string_fred("+Score:", "$Name:")) {
3435 			parse_comments();
3436 		} else {
3437 			fout("\n+Score:");
3438 		}
3439 
3440 		fout(" %d", shipp->score);
3441 
3442 
3443 		if (save_format != MissionFormat::RETAIL && shipp->assist_score_pct != 0) {
3444 			fso_comment_push(";;FSO 3.6.10;;");
3445 			if (optional_string_fred("+Assist Score Percentage:")) {
3446 				parse_comments();
3447 			} else {
3448 				fout_version("\n+Assist Score Percentage:");
3449 			}
3450 			fso_comment_pop();
3451 
3452 			fout(" %f", shipp->assist_score_pct);
3453 		} else {
3454 			bypass_comment(";;FSO 3.6.10;; +Assist Score Percentage:", "$Name:");
3455 		}
3456 
3457 		// deal with the persona for this ship as well.
3458 		if (shipp->persona_index != -1) {
3459 			if (optional_string_fred("+Persona Index:", "$Name:")) {
3460 				parse_comments();
3461 			} else {
3462 				fout("\n+Persona Index:");
3463 			}
3464 
3465 			fout(" %d", shipp->persona_index);
3466 		}
3467 
3468 		// Goober5000 - deal with texture replacement ----------------
3469 		if (!Fred_texture_replacements.empty()) {
3470 			bool needs_header = true;
3471 			fso_comment_push(";;FSO 3.6.8;;");
3472 
3473 			for (SCP_vector<texture_replace>::iterator ii = Fred_texture_replacements.begin();
3474 				 ii != Fred_texture_replacements.end(); ++ii) {
3475 				// Only look at this entry if it's not from the table. Table entries will just be read by FSO.
3476 				if (!stricmp(shipp->ship_name, ii->ship_name) && !(ii->from_table)) {
3477 					if (needs_header) {
3478 						if (optional_string_fred("$Texture Replace:")) {
3479 							parse_comments(1);
3480 						} else {
3481 							fout_version("\n$Texture Replace:");
3482 						}
3483 
3484 						needs_header = false;
3485 					}
3486 
3487 					// write out this entry
3488 					if (optional_string_fred("+old:")) {
3489 						parse_comments(1);
3490 						fout(" %s", ii->old_texture);
3491 					} else {
3492 						fout_version("\n+old: %s", ii->old_texture);
3493 					}
3494 
3495 					if (optional_string_fred("+new:")) {
3496 						parse_comments(1);
3497 						fout(" %s", ii->new_texture);
3498 					} else {
3499 						fout_version("\n+new: %s", ii->new_texture);
3500 					}
3501 				}
3502 			}
3503 
3504 			fso_comment_pop();
3505 		} else {
3506 			bypass_comment(";;FSO 3.6.8;; $Texture Replace:", "$Name:");
3507 		}
3508 
3509 		// end of texture replacement -------------------------------
3510 
3511 		z++;
3512 
3513 		fso_comment_pop();
3514 	}
3515 
3516 	fso_comment_pop(true);
3517 
3518 	return err;
3519 }
3520 
save_players()3521 int CFred_mission_save::save_players()
3522 {
3523 	bool wrote_fso_data = false;
3524 	int i, j;
3525 	int var_idx;
3526 	int used_pool[MAX_WEAPON_TYPES];
3527 
3528 	if (optional_string_fred("#Alternate Types:")) {    // Make sure the parser doesn't get out of sync
3529 		required_string_fred("#end");
3530 	}
3531 	if (optional_string_fred("#Callsigns:")) {
3532 		required_string_fred("#end");
3533 	}
3534 
3535 	// write out alternate name list
3536 	if (Mission_alt_type_count > 0) {
3537 		fout("\n\n#Alternate Types:\n");
3538 
3539 		// write them all out
3540 		for (i = 0; i < Mission_alt_type_count; i++) {
3541 			fout("$Alt: %s\n", Mission_alt_types[i]);
3542 		}
3543 
3544 		// end
3545 		fout("\n#end\n");
3546 	}
3547 
3548 	// write out callsign list
3549 	if (save_format != MissionFormat::RETAIL && Mission_callsign_count > 0) {
3550 		fout("\n\n#Callsigns:\n");
3551 
3552 		// write them all out
3553 		for (i = 0; i < Mission_callsign_count; i++) {
3554 			fout("$Callsign: %s\n", Mission_callsigns[i]);
3555 		}
3556 
3557 		// end
3558 		fout("\n#end\n");
3559 	}
3560 
3561 	required_string_fred("#Players");
3562 	parse_comments(2);
3563 	fout("\t\t;! %d total\n", Player_starts);
3564 
3565 	for (i = 0; i < Num_teams; i++) {
3566 		required_string_fred("$Starting Shipname:");
3567 		parse_comments();
3568 		Assert(Player_start_shipnum >= 0);
3569 		fout(" %s", Ships[Player_start_shipnum].ship_name);
3570 
3571 		required_string_fred("$Ship Choices:");
3572 		parse_comments();
3573 		fout(" (\n");
3574 
3575 		for (j = 0; j < Team_data[i].num_ship_choices; j++) {
3576 			// Check to see if a variable name should be written for the class rather than a number
3577 			if (strlen(Team_data[i].ship_list_variables[j])) {
3578 				var_idx = get_index_sexp_variable_name(Team_data[i].ship_list_variables[j]);
3579 				Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
3580 				wrote_fso_data = true;
3581 
3582 				fout("\t@%s\t", Sexp_variables[var_idx].variable_name);
3583 			} else {
3584 				fout("\t\"%s\"\t", Ship_info[Team_data[i].ship_list[j]].name);
3585 			}
3586 
3587 			// Now check if we should write a variable or a number for the amount of ships available
3588 			if (strlen(Team_data[i].ship_count_variables[j])) {
3589 				var_idx = get_index_sexp_variable_name(Team_data[i].ship_count_variables[j]);
3590 				Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
3591 				wrote_fso_data = true;
3592 
3593 				fout("@%s\n", Sexp_variables[var_idx].variable_name);
3594 			} else {
3595 				fout("%d\n", Team_data[i].ship_count[j]);
3596 			}
3597 		}
3598 
3599 		fout(")");
3600 
3601 		if (optional_string_fred("+Weaponry Pool:", "$Starting Shipname:")) {
3602 			parse_comments(2);
3603 		} else {
3604 			fout("\n\n+Weaponry Pool:");
3605 		}
3606 
3607 		fout(" (\n");
3608 		generate_weaponry_usage_list_team(i, used_pool);
3609 
3610 		for (j = 0; j < Team_data[i].num_weapon_choices; j++) {
3611 			// first output the weapon name or a variable that sets it
3612 			if (strlen(Team_data[i].weaponry_pool_variable[j])) {
3613 				var_idx = get_index_sexp_variable_name(Team_data[i].weaponry_pool_variable[j]);
3614 				Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
3615 				wrote_fso_data = true;
3616 
3617 				fout("\t@%s\t", Sexp_variables[var_idx].variable_name);
3618 			} else {
3619 				fout("\t\"%s\"\t", Weapon_info[Team_data[i].weaponry_pool[j]].name);
3620 			}
3621 
3622 			// now output the amount of this weapon or a variable that sets it. If this weapon is in the used pool and isn't
3623 			// set by a variable we should add the amount of weapons used by the wings to it and zero the entry so we know
3624 			// that we have dealt with it
3625 			if (strlen(Team_data[i].weaponry_amount_variable[j])) {
3626 				var_idx = get_index_sexp_variable_name(Team_data[i].weaponry_amount_variable[j]);
3627 				Assert(var_idx > -1 && var_idx < MAX_SEXP_VARIABLES);
3628 				wrote_fso_data = true;
3629 
3630 				fout("@%s\n", Sexp_variables[var_idx].variable_name);
3631 			} else {
3632 				if (strlen(Team_data[i].weaponry_pool_variable[j])) {
3633 					fout("%d\n", Team_data[i].weaponry_count[j]);
3634 				} else {
3635 					fout("%d\n", Team_data[i].weaponry_count[j] + used_pool[Team_data[i].weaponry_pool[j]]);
3636 					used_pool[Team_data[i].weaponry_pool[j]] = 0;
3637 				}
3638 			}
3639 		}
3640 
3641 		// now we add anything left in the used pool as a static entry
3642 		for (j = 0; j < static_cast<int>(Weapon_info.size()); j++) {
3643 			if (used_pool[j] > 0) {
3644 				fout("\t\"%s\"\t%d\n", Weapon_info[j].name, used_pool[j]);
3645 			}
3646 		}
3647 
3648 		fout(")");
3649 
3650 		// sanity check
3651 		if (save_format == MissionFormat::RETAIL && wrote_fso_data) {
3652 			// this is such an unlikely (and hard-to-fix) case that a warning should be sufficient
3653 			_viewport->dialogProvider->showButtonDialog(DialogType::Warning,
3654 				"Incompatibility with retail mission format",
3655 				"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.",
3656 				{ DialogButton::Ok });
3657 		}
3658 
3659 		// Goober5000 - mjn.mixael's required weapon feature
3660 		bool uses_required_weapon = false;
3661 		for (j = 0; j < static_cast<int>(Weapon_info.size()); j++) {
3662 			if (Team_data[i].weapon_required[j]) {
3663 				uses_required_weapon = true;
3664 				break;
3665 			}
3666 		}
3667 		if (save_format != MissionFormat::RETAIL && uses_required_weapon) {
3668 			if (optional_string_fred("+Required for mission:", "$Starting Shipname:")) {
3669 				parse_comments(2);
3670 			} else {
3671 				fout("\n+Required for mission:");
3672 			}
3673 
3674 			fout(" (");
3675 			for (j = 0; j < static_cast<int>(Weapon_info.size()); j++) {
3676 				if (Team_data[i].weapon_required[j]) {
3677 					fout(" \"%s\"", Weapon_info[j].name);
3678 				}
3679 			}
3680 			fout(" )");
3681 		}
3682 
3683 		fso_comment_pop();
3684 	}
3685 
3686 	fso_comment_pop(true);
3687 
3688 	return err;
3689 }
3690 
save_plot_info()3691 int CFred_mission_save::save_plot_info()
3692 {
3693 	if (save_format == MissionFormat::RETAIL) {
3694 		if (optional_string_fred("#Plot Info")) {
3695 			parse_comments(2);
3696 
3697 			// XSTR
3698 			required_string_fred("$Tour:");
3699 			parse_comments(2);
3700 			fout_ext(" ", "Blah");
3701 
3702 			required_string_fred("$Pre-Briefing Cutscene:");
3703 			parse_comments();
3704 			fout(" Blah");
3705 
3706 			required_string_fred("$Pre-Mission Cutscene:");
3707 			parse_comments();
3708 			fout(" Blah");
3709 
3710 			required_string_fred("$Next Mission Success:");
3711 			parse_comments();
3712 			fout(" Blah");
3713 
3714 			required_string_fred("$Next Mission Partial:");
3715 			parse_comments();
3716 			fout(" Blah");
3717 
3718 			required_string_fred("$Next Mission Failure:");
3719 			parse_comments();
3720 			fout(" Blah");
3721 		} else {
3722 			fout("\n\n#Plot Info\n\n");
3723 
3724 			fout("$Tour: ");
3725 			fout_ext(NULL, "Blah");
3726 			fout("\n");
3727 			fout("$Pre-Briefing Cutscene: Blah\n");
3728 			fout("$Pre-Mission Cutscene: Blah\n");
3729 			fout("$Next Mission Success: Blah\n");
3730 			fout("$Next Mission Partial: Blah\n");
3731 			fout("$Next Mission Failure: Blah\n");
3732 
3733 			fout("\n");
3734 		}
3735 	}
3736 
3737 	fso_comment_pop(true);
3738 
3739 	return err;
3740 }
3741 
save_reinforcements()3742 int CFred_mission_save::save_reinforcements()
3743 {
3744 	int i, j, type;
3745 
3746 	fred_parse_flag = 0;
3747 	required_string_fred("#Reinforcements");
3748 	parse_comments(2);
3749 	fout("\t\t;! %d total\n", Num_reinforcements);
3750 
3751 	for (i = 0; i < Num_reinforcements; i++) {
3752 		required_string_either_fred("$Name:", "#Background bitmaps");
3753 		required_string_fred("$Name:");
3754 		parse_comments(i ? 2 : 1);
3755 		fout(" %s", Reinforcements[i].name);
3756 
3757 		type = TYPE_ATTACK_PROTECT;
3758 		for (j = 0; j < MAX_SHIPS; j++) {
3759 			if ((Ships[j].objnum != -1) && !stricmp(Ships[j].ship_name, Reinforcements[i].name)) {
3760 				if (Ship_info[Ships[j].ship_info_index].flags[Ship::Info_Flags::Support]) {
3761 					type = TYPE_REPAIR_REARM;
3762 				}
3763 				break;
3764 			}
3765 		}
3766 
3767 		required_string_fred("$Type:");
3768 		parse_comments();
3769 		fout(" %s", Reinforcement_type_names[type]);
3770 
3771 		required_string_fred("$Num times:");
3772 		parse_comments();
3773 		fout(" %d", Reinforcements[i].uses);
3774 
3775 		if (optional_string_fred("+Arrival Delay:", "$Name:")) {
3776 			parse_comments();
3777 		} else {
3778 			fout("\n+Arrival Delay:");
3779 		}
3780 		fout(" %d", Reinforcements[i].arrival_delay);
3781 
3782 		if (optional_string_fred("+No Messages:", "$Name:")) {
3783 			parse_comments();
3784 		} else {
3785 			fout("\n+No Messages:");
3786 		}
3787 		fout(" (");
3788 		for (j = 0; j < MAX_REINFORCEMENT_MESSAGES; j++) {
3789 			if (strlen(Reinforcements[i].no_messages[j])) {
3790 				fout(" \"%s\"", Reinforcements[i].no_messages[j]);
3791 			}
3792 		}
3793 		fout(" )");
3794 
3795 		if (optional_string_fred("+Yes Messages:", "$Name:")) {
3796 			parse_comments();
3797 		} else {
3798 			fout("\n+Yes Messages:");
3799 		}
3800 		fout(" (");
3801 		for (j = 0; j < MAX_REINFORCEMENT_MESSAGES; j++) {
3802 			if (strlen(Reinforcements[i].yes_messages[j])) {
3803 				fout(" \"%s\"", Reinforcements[i].yes_messages[j]);
3804 			}
3805 		}
3806 		fout(" )");
3807 
3808 		fso_comment_pop();
3809 	}
3810 
3811 	fso_comment_pop(true);
3812 
3813 	return err;
3814 }
3815 
save_single_dock_instance(ship * shipp,dock_instance * dock_ptr)3816 void CFred_mission_save::save_single_dock_instance(ship* shipp, dock_instance* dock_ptr)
3817 {
3818 	Assert(shipp && dock_ptr);
3819 	Assert(dock_ptr->docked_objp->type == OBJ_SHIP || dock_ptr->docked_objp->type == OBJ_START);
3820 
3821 	// get ships and objects
3822 	object* objp = &Objects[shipp->objnum];
3823 	object* other_objp = dock_ptr->docked_objp;
3824 	ship* other_shipp = &Ships[other_objp->instance];
3825 
3826 	// write other ship
3827 	if (optional_string_fred("+Docked With:", "$Name:")) {
3828 		parse_comments();
3829 	} else {
3830 		fout("\n+Docked With:");
3831 	}
3832 	fout(" %s", other_shipp->ship_name);
3833 
3834 
3835 	// Goober5000 - hm, Volition seems to have reversed docker and dockee here
3836 
3837 	// write docker (actually dockee) point
3838 	required_string_fred("$Docker Point:", "$Name:");
3839 	parse_comments();
3840 	fout(" %s",
3841 		 model_get_dock_name(Ship_info[other_shipp->ship_info_index].model_num,
3842 							 dock_find_dockpoint_used_by_object(other_objp, objp)));
3843 
3844 	// write dockee (actually docker) point
3845 	required_string_fred("$Dockee Point:", "$Name:");
3846 	parse_comments();
3847 	fout(" %s",
3848 		 model_get_dock_name(Ship_info[shipp->ship_info_index].model_num,
3849 							 dock_find_dockpoint_used_by_object(objp, other_objp)));
3850 
3851 	fso_comment_pop(true);
3852 }
3853 
save_turret_info(ship_subsys * ptr,int ship)3854 void CFred_mission_save::save_turret_info(ship_subsys* ptr, int ship)
3855 {
3856 	int i, z;
3857 	ship_weapon* wp = &ptr->weapons;
3858 
3859 	if (wp->ai_class != Ship_info[Ships[ship].ship_info_index].ai_class) {
3860 		if (optional_string_fred("+AI Class:", "$Name:", "+Subsystem:")) {
3861 			parse_comments();
3862 		} else {
3863 			fout("\n+AI Class:");
3864 		}
3865 
3866 		fout(" %s", Ai_class_names[wp->ai_class]);
3867 	}
3868 
3869 	z = 0;
3870 	i = wp->num_primary_banks;
3871 	while (i--) {
3872 		if (wp->primary_bank_weapons[i] != ptr->system_info->primary_banks[i]) {
3873 			z = 1;
3874 		}
3875 	}
3876 
3877 	if (z) {
3878 		if (optional_string_fred("+Primary Banks:", "$Name:", "+Subsystem:")) {
3879 			parse_comments();
3880 		} else {
3881 			fout("\n+Primary Banks:");
3882 		}
3883 
3884 		fout(" ( ");
3885 		for (i = 0; i < wp->num_primary_banks; i++) {
3886 			if (wp->primary_bank_weapons[i] != -1) { // Just in case someone has set a weapon bank to empty
3887 				fout("\"%s\" ", Weapon_info[wp->primary_bank_weapons[i]].name);
3888 			} else {
3889 				fout("\"\" ");
3890 			}
3891 		}
3892 		fout(")");
3893 	}
3894 
3895 	z = 0;
3896 	i = wp->num_secondary_banks;
3897 	while (i--) {
3898 		if (wp->secondary_bank_weapons[i] != ptr->system_info->secondary_banks[i]) {
3899 			z = 1;
3900 		}
3901 	}
3902 
3903 	if (z) {
3904 		if (optional_string_fred("+Secondary Banks:", "$Name:", "+Subsystem:")) {
3905 			parse_comments();
3906 		} else {
3907 			fout("\n+Secondary Banks:");
3908 		}
3909 
3910 		fout(" ( ");
3911 		for (i = 0; i < wp->num_secondary_banks; i++) {
3912 			if (wp->secondary_bank_weapons[i] != -1) {
3913 				fout("\"%s\" ", Weapon_info[wp->secondary_bank_weapons[i]].name);
3914 			} else {
3915 				fout("\"\" ");
3916 			}
3917 		}
3918 		fout(")");
3919 	}
3920 
3921 	z = 0;
3922 	i = wp->num_secondary_banks;
3923 	while (i--) {
3924 		if (wp->secondary_bank_ammo[i] != 100) {
3925 			z = 1;
3926 		}
3927 	}
3928 
3929 	if (z) {
3930 		if (optional_string_fred("+Sbank Ammo:", "$Name:", "+Subsystem:")) {
3931 			parse_comments();
3932 		} else {
3933 			fout("\n+Sbank Ammo:");
3934 		}
3935 
3936 		fout(" ( ");
3937 		for (i = 0; i < wp->num_secondary_banks; i++) {
3938 			fout("%d ", wp->secondary_bank_ammo[i]);
3939 		}
3940 
3941 		fout(")");
3942 	}
3943 
3944 	fso_comment_pop(true);
3945 }
3946 
save_variables()3947 int CFred_mission_save::save_variables()
3948 {
3949 	char* type;
3950 	char number[] = "number";
3951 	char string[] = "string";
3952 	char block[] = "block";
3953 	int i;
3954 	int num_block_vars = 0;
3955 
3956 	// sort sexp_variables
3957 	sexp_variable_sort();
3958 
3959 	// get count
3960 	int num_variables = sexp_variable_count();
3961 
3962 	if (save_format == MissionFormat::RETAIL) {
3963 		generate_special_explosion_block_variables();
3964 		num_block_vars = num_block_variables();
3965 	}
3966 	int total_variables = num_variables + num_block_vars;
3967 
3968 	if (total_variables > 0) {
3969 
3970 		// write 'em out
3971 		required_string_fred("#Sexp_variables");
3972 		parse_comments(2);
3973 
3974 		required_string_fred("$Variables:");
3975 		parse_comments(2);
3976 
3977 		fout("\n(");
3978 		//		parse_comments();
3979 
3980 		for (i = 0; i < num_variables; i++) {
3981 			if (Sexp_variables[i].type & SEXP_VARIABLE_NUMBER) {
3982 				type = number;
3983 			} else {
3984 				type = string;
3985 			}
3986 			// index "var name" "default" "type"
3987 			fout("\n\t\t%d\t\t\"%s\"\t\t\"%s\"\t\t\"%s\"",
3988 				 i,
3989 				 Sexp_variables[i].variable_name,
3990 				 Sexp_variables[i].text,
3991 				 type);
3992 
3993 			// persistent and network variables
3994 			if (save_format != MissionFormat::RETAIL) {
3995 				// Network variable - Karajorma
3996 				if (Sexp_variables[i].type & SEXP_VARIABLE_NETWORK) {
3997 					fout("\t\t\"%s\"", "network-variable");
3998 				}
3999 
4000 				// player-persistent - Goober5000
4001 				if (Sexp_variables[i].type & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE) {
4002 					fout("\t\t\"%s\"", "save-on-mission-close");
4003 					// campaign-persistent - Goober5000
4004 				} else if (Sexp_variables[i].type & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS) {
4005 					fout("\t\t\"%s\"", "save-on-mission-progress");
4006 				}
4007 			}
4008 
4009 			//			parse_comments();
4010 		}
4011 
4012 		for (i = MAX_SEXP_VARIABLES - num_block_vars; i < MAX_SEXP_VARIABLES; i++) {
4013 			type = block;
4014 			fout("\n\t\t%d\t\t\"%s\"\t\t\"%s\"\t\t\"%s\"",
4015 				 i,
4016 				 Block_variables[i].variable_name,
4017 				 Block_variables[i].text,
4018 				 type);
4019 		}
4020 
4021 		fout("\n)");
4022 
4023 		fso_comment_pop();
4024 	}
4025 
4026 	fso_comment_pop(true);
4027 
4028 	return err;
4029 }
4030 
save_containers()4031 int CFred_mission_save::save_containers()
4032 {
4033 	if (save_format == MissionFormat::RETAIL) {
4034 		return 0;
4035 	}
4036 
4037 	const auto &containers = get_all_sexp_containers();
4038 
4039 	if (containers.empty()) {
4040 		fso_comment_pop(true);
4041 		return 0;
4042 	}
4043 
4044 	required_string_fred("#Sexp_containers");
4045 	parse_comments(2);
4046 
4047 	bool list_found = false;
4048 	bool map_found = false;
4049 
4050 	// What types of container do we have?
4051 	for (const auto &container : containers) {
4052 		if (container.is_list()) {
4053 			list_found = true;
4054 		} else if (container.is_map()) {
4055 			map_found = true;
4056 		}
4057 		if (list_found && map_found) {
4058 			// no point in continuing to check
4059 			break;
4060 		}
4061 	}
4062 
4063 	if (list_found) {
4064 		required_string_fred("$Lists");
4065 		parse_comments(2);
4066 
4067 		for (const auto &container : containers) {
4068 			if (container.is_list()) {
4069 				fout("\n$Name: %s", container.container_name.c_str());
4070 				if (any(container.type & ContainerType::STRING_DATA)) {
4071 					fout("\n$Data Type: String");
4072 				} else if (any(container.type & ContainerType::NUMBER_DATA)) {
4073 					fout("\n$Data Type: Number");
4074 				}
4075 
4076 				if (any(container.type & ContainerType::STRICTLY_TYPED_DATA)) {
4077 					fout("\n+Strictly Typed Data");
4078 				}
4079 
4080 				fout("\n$Data: ( ");
4081 				for (const auto &list_entry : container.list_data) {
4082 					fout("\"%s\" ", list_entry.c_str());
4083 				}
4084 
4085 				fout(")\n");
4086 
4087 				save_container_options(container);
4088 			}
4089 		}
4090 
4091 		required_string_fred("$End Lists");
4092 		parse_comments(1);
4093 	}
4094 
4095 	if (map_found) {
4096 		required_string_fred("$Maps");
4097 		parse_comments(2);
4098 
4099 		for (const auto &container : containers) {
4100 			if (container.is_map()) {
4101 				fout("\n$Name: %s", container.container_name.c_str());
4102 				if (any(container.type & ContainerType::STRING_DATA)) {
4103 					fout("\n$Data Type: String");
4104 				} else if (any(container.type & ContainerType::NUMBER_DATA)) {
4105 					fout("\n$Data Type: Number");
4106 				}
4107 
4108 				if (any(container.type & ContainerType::NUMBER_KEYS)) {
4109 					fout("\n$Key Type: Number");
4110 				} else {
4111 					fout("\n$Key Type: String");
4112 				}
4113 
4114 				if (any(container.type & ContainerType::STRICTLY_TYPED_KEYS)) {
4115 					fout("\n+Strictly Typed Keys");
4116 				}
4117 
4118 				if (any(container.type & ContainerType::STRICTLY_TYPED_DATA)) {
4119 					fout("\n+Strictly Typed Data");
4120 				}
4121 
4122 				fout("\n$Data: ( ");
4123 				for (const auto &map_entry : container.map_data) {
4124 					fout("\"%s\" \"%s\" ", map_entry.first.c_str(), map_entry.second.c_str());
4125 				}
4126 
4127 				fout(")\n");
4128 
4129 				save_container_options(container);
4130 			}
4131 		}
4132 
4133 		required_string_fred("$End Maps");
4134 		parse_comments(1);
4135 	}
4136 
4137 	return err;
4138 }
4139 
save_container_options(const sexp_container & container)4140 void CFred_mission_save::save_container_options(const sexp_container &container)
4141 {
4142 	if (any(container.type & ContainerType::NETWORK)) {
4143 		fout("+Network Container\n");
4144 	}
4145 
4146 	if (container.is_eternal()) {
4147 		fout("+Eternal\n");
4148 	}
4149 
4150 	if (any(container.type & ContainerType::SAVE_ON_MISSION_CLOSE)) {
4151 		fout("+Save On Mission Close\n");
4152 	} else if (any(container.type & ContainerType::SAVE_ON_MISSION_PROGRESS)) {
4153 		fout("+Save On Mission Progress\n");
4154 	}
4155 
4156 	fout("\n");
4157 }
4158 
save_vector(vec3d & v)4159 int CFred_mission_save::save_vector(vec3d& v)
4160 {
4161 	fout(" %f, %f, %f", v.xyz.x, v.xyz.y, v.xyz.z);
4162 	return 0;
4163 }
4164 
save_waypoints()4165 int CFred_mission_save::save_waypoints()
4166 {
4167 	//object *ptr;
4168 
4169 	fred_parse_flag = 0;
4170 	required_string_fred("#Waypoints");
4171 	parse_comments(2);
4172 	fout("\t\t;! %d lists total\n", Waypoint_lists.size());
4173 
4174 	SCP_list<CJumpNode>::iterator jnp;
4175 	for (jnp = Jump_nodes.begin(); jnp != Jump_nodes.end(); ++jnp) {
4176 		required_string_fred("$Jump Node:", "$Jump Node Name:");
4177 		parse_comments(2);
4178 		save_vector(jnp->GetSCPObject()->pos);
4179 
4180 		required_string_fred("$Jump Node Name:", "$Jump Node:");
4181 		parse_comments();
4182 		fout(" %s", jnp->GetName());
4183 
4184 		if (save_format != MissionFormat::RETAIL) {
4185 			if (jnp->IsSpecialModel()) {
4186 				if (optional_string_fred("+Model File:", "$Jump Node:")) {
4187 					parse_comments();
4188 				}
4189 				else {
4190 					fout("\n+Model File:");
4191 				}
4192 
4193 				int model = jnp->GetModelNumber();
4194 				polymodel* pm = model_get(model);
4195 				fout(" %s", pm->filename);
4196 			}
4197 
4198 			if (jnp->IsColored()) {
4199 				if (optional_string_fred("+Alphacolor:", "$Jump Node:")) {
4200 					parse_comments();
4201 				}
4202 				else {
4203 					fout("\n+Alphacolor:");
4204 				}
4205 
4206 				color jn_color = jnp->GetColor();
4207 				fout(" %u %u %u %u", jn_color.red, jn_color.green, jn_color.blue, jn_color.alpha);
4208 			}
4209 
4210 			int hidden_is_there = optional_string_fred("+Hidden:", "$Jump Node:");
4211 			if (hidden_is_there) {
4212 				parse_comments();
4213 			}
4214 
4215 			if (hidden_is_there || jnp->IsHidden()) {
4216 				if (!hidden_is_there) {
4217 					fout("\n+Hidden:");
4218 				}
4219 
4220 				if (jnp->IsHidden()) {
4221 					fout(" %s", "true");
4222 				}
4223 				else {
4224 					fout(" %s", "false");
4225 				}
4226 			}
4227 		}
4228 
4229 		fso_comment_pop();
4230 	}
4231 
4232 	SCP_list<waypoint_list>::iterator ii;
4233 	for (ii = Waypoint_lists.begin(); ii != Waypoint_lists.end(); ++ii) {
4234 		required_string_either_fred("$Name:", "#Messages");
4235 		required_string_fred("$Name:");
4236 		parse_comments((ii == Waypoint_lists.begin()) ? 1 : 2);
4237 		fout(" %s", ii->get_name());
4238 
4239 		required_string_fred("$List:");
4240 		parse_comments();
4241 		fout(" (\t\t;! %d points in list\n", ii->get_waypoints().size());
4242 
4243 		save_waypoint_list(&(*ii));
4244 		fout(")");
4245 
4246 		fso_comment_pop();
4247 	}
4248 
4249 	fso_comment_pop(true);
4250 
4251 	return err;
4252 }
4253 
save_waypoint_list(waypoint_list * wp_list)4254 int CFred_mission_save::save_waypoint_list(waypoint_list* wp_list)
4255 {
4256 	Assert(wp_list != NULL);
4257 	SCP_vector<waypoint>::iterator ii;
4258 
4259 	for (ii = wp_list->get_waypoints().begin(); ii != wp_list->get_waypoints().end(); ++ii) {
4260 		vec3d* pos = ii->get_pos();
4261 		fout("\t( %f, %f, %f )\n", pos->xyz.x, pos->xyz.y, pos->xyz.z);
4262 	}
4263 
4264 	return 0;
4265 }
4266 
save_wings()4267 int CFred_mission_save::save_wings()
4268 {
4269 	SCP_string sexp_out;
4270 	int i, j, z, count = 0;
4271 
4272 	fred_parse_flag = 0;
4273 	required_string_fred("#Wings");
4274 	parse_comments(2);
4275 	fout("\t\t;! %d total", Num_wings);
4276 
4277 	for (i = 0; i < MAX_WINGS; i++) {
4278 		if (!Wings[i].wave_count) {
4279 			continue;
4280 		}
4281 
4282 		count++;
4283 		required_string_either_fred("$Name:", "#Events");
4284 		required_string_fred("$Name:");
4285 		parse_comments(2);
4286 		fout(" %s", Wings[i].name);
4287 
4288 		// squad logo - Goober5000
4289 		if (save_format != MissionFormat::RETAIL) {
4290 			if (strlen(Wings[i].wing_squad_filename) > 0) //-V805
4291 			{
4292 				if (optional_string_fred("+Squad Logo:", "$Name:")) {
4293 					parse_comments();
4294 				} else {
4295 					fout("\n+Squad Logo:");
4296 				}
4297 
4298 				fout(" %s", Wings[i].wing_squad_filename);
4299 			}
4300 		}
4301 
4302 		required_string_fred("$Waves:");
4303 		parse_comments();
4304 		fout(" %d", Wings[i].num_waves);
4305 
4306 		required_string_fred("$Wave Threshold:");
4307 		parse_comments();
4308 		fout(" %d", Wings[i].threshold);
4309 
4310 		required_string_fred("$Special Ship:");
4311 		parse_comments();
4312 		fout(" %d\t\t;! %s", Wings[i].special_ship, Ships[Wings[i].ship_index[Wings[i].special_ship]].ship_name);
4313 
4314 		if (save_format != MissionFormat::RETAIL) {
4315 			if (Wings[i].formation >= 0 && Wings[i].formation < (int)Wing_formations.size())
4316 			{
4317 				if (optional_string_fred("+Formation:", "$Name:")) {
4318 					parse_comments();
4319 				}
4320 				else {
4321 					fout("\n+Formation:");
4322 				}
4323 
4324 				fout(" %s", Wing_formations[Wings[i].formation].name);
4325 			}
4326 		}
4327 
4328 		required_string_fred("$Arrival Location:");
4329 		parse_comments();
4330 		fout(" %s", Arrival_location_names[Wings[i].arrival_location]);
4331 
4332 		if (Wings[i].arrival_location != ARRIVE_AT_LOCATION) {
4333 			if (optional_string_fred("+Arrival Distance:", "$Name:")) {
4334 				parse_comments();
4335 			} else {
4336 				fout("\n+Arrival Distance:");
4337 			}
4338 
4339 			fout(" %d", Wings[i].arrival_distance);
4340 			if (optional_string_fred("$Arrival Anchor:", "$Name:")) {
4341 				parse_comments();
4342 			} else {
4343 				fout("\n$Arrival Anchor:");
4344 			}
4345 
4346 			z = Wings[i].arrival_anchor;
4347 			if (z & SPECIAL_ARRIVAL_ANCHOR_FLAG) {
4348 				// get name
4349 				char tmp[NAME_LENGTH + 15];
4350 				stuff_special_arrival_anchor_name(tmp, z, save_format == MissionFormat::RETAIL);
4351 
4352 				// save it
4353 				fout(" %s", tmp);
4354 			} else if (z >= 0) {
4355 				fout(" %s", Ships[z].ship_name);
4356 			} else {
4357 				fout(" <error>");
4358 			}
4359 		}
4360 
4361 		// Goober5000
4362 		if (save_format != MissionFormat::RETAIL) {
4363 			if ((Wings[i].arrival_location == ARRIVE_FROM_DOCK_BAY) && (Wings[i].arrival_path_mask > 0)) {
4364 				int anchor_shipnum;
4365 				polymodel* pm;
4366 
4367 				anchor_shipnum = Wings[i].arrival_anchor;
4368 				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
4369 
4370 				fout("\n+Arrival Paths: ( ");
4371 
4372 				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
4373 				for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
4374 					if (Wings[i].arrival_path_mask & (1 << n)) {
4375 						fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
4376 					}
4377 				}
4378 
4379 				fout(")");
4380 			}
4381 		}
4382 
4383 		if (Wings[i].arrival_delay) {
4384 			if (optional_string_fred("+Arrival delay:", "$Name:")) {
4385 				parse_comments();
4386 			} else {
4387 				fout("\n+Arrival delay:");
4388 			}
4389 
4390 			fout(" %d", Wings[i].arrival_delay);
4391 		}
4392 
4393 		required_string_fred("$Arrival Cue:");
4394 		parse_comments();
4395 		convert_sexp_to_string(sexp_out, Wings[i].arrival_cue, SEXP_SAVE_MODE);
4396 		fout(" %s", sexp_out.c_str());
4397 
4398 		required_string_fred("$Departure Location:");
4399 		parse_comments();
4400 		fout(" %s", Departure_location_names[Wings[i].departure_location]);
4401 
4402 		if (Wings[i].departure_location != DEPART_AT_LOCATION) {
4403 			required_string_fred("$Departure Anchor:");
4404 			parse_comments();
4405 
4406 			if (Wings[i].departure_anchor >= 0) {
4407 				fout(" %s", Ships[Wings[i].departure_anchor].ship_name);
4408 			} else {
4409 				fout(" <error>");
4410 			}
4411 		}
4412 
4413 		// Goober5000
4414 		if (save_format != MissionFormat::RETAIL) {
4415 			if ((Wings[i].departure_location == DEPART_AT_DOCK_BAY) && (Wings[i].departure_path_mask > 0)) {
4416 				int anchor_shipnum;
4417 				polymodel* pm;
4418 
4419 				anchor_shipnum = Wings[i].departure_anchor;
4420 				Assert(anchor_shipnum >= 0 && anchor_shipnum < MAX_SHIPS);
4421 
4422 				fout("\n+Departure Paths: ( ");
4423 
4424 				pm = model_get(Ship_info[Ships[anchor_shipnum].ship_info_index].model_num);
4425 				for (auto n = 0; n < pm->ship_bay->num_paths; n++) {
4426 					if (Wings[i].departure_path_mask & (1 << n)) {
4427 						fout("\"%s\" ", pm->paths[pm->ship_bay->path_indexes[n]].name);
4428 					}
4429 				}
4430 
4431 				fout(")");
4432 			}
4433 		}
4434 
4435 		if (Wings[i].departure_delay) {
4436 			if (optional_string_fred("+Departure delay:", "$Name:")) {
4437 				parse_comments();
4438 			} else {
4439 				fout("\n+Departure delay:");
4440 			}
4441 
4442 			fout(" %d", Wings[i].departure_delay);
4443 		}
4444 
4445 		required_string_fred("$Departure Cue:");
4446 		parse_comments();
4447 		convert_sexp_to_string(sexp_out, Wings[i].departure_cue, SEXP_SAVE_MODE);
4448 		fout(" %s", sexp_out.c_str());
4449 
4450 		required_string_fred("$Ships:");
4451 		parse_comments();
4452 		fout(" (\t\t;! %d total\n", Wings[i].wave_count);
4453 
4454 		for (j = 0; j < Wings[i].wave_count; j++) {
4455 			//			if (Objects[Ships[ship].objnum].type == OBJ_START)
4456 			//				fout("\t\"Player 1\"\n");
4457 			//			else
4458 			fout("\t\"%s\"\n", Ships[Wings[i].ship_index[j]].ship_name);
4459 		}
4460 
4461 		fout(")");
4462 
4463 		save_ai_goals(Wings[i].ai_goals, -1);
4464 
4465 		if (Wings[i].hotkey != -1) {
4466 			if (optional_string_fred("+Hotkey:", "$Name:")) {
4467 				parse_comments();
4468 			} else {
4469 				fout("\n+Hotkey:");
4470 			}
4471 
4472 			fout(" %d", Wings[i].hotkey);
4473 		}
4474 
4475 		if (optional_string_fred("+Flags:", "$Name:")) {
4476 			parse_comments();
4477 			fout(" (");
4478 		} else {
4479 			fout("\n+Flags: (");
4480 		}
4481 
4482 		if (Wings[i].flags[Ship::Wing_Flags::Ignore_count]) {
4483 			fout(" \"ignore-count\"");
4484 		}
4485 		if (Wings[i].flags[Ship::Wing_Flags::Reinforcement]) {
4486 			fout(" \"reinforcement\"");
4487 		}
4488 		if (Wings[i].flags[Ship::Wing_Flags::No_arrival_music]) {
4489 			fout(" \"no-arrival-music\"");
4490 		}
4491 		if (Wings[i].flags[Ship::Wing_Flags::No_arrival_message]) {
4492 			fout(" \"no-arrival-message\"");
4493 		}
4494 		if (Wings[i].flags[Ship::Wing_Flags::No_arrival_warp]) {
4495 			fout(" \"no-arrival-warp\"");
4496 		}
4497 		if (Wings[i].flags[Ship::Wing_Flags::No_departure_warp]) {
4498 			fout(" \"no-departure-warp\"");
4499 		}
4500 		if (Wings[i].flags[Ship::Wing_Flags::No_dynamic]) {
4501 			fout(" \"no-dynamic\"");
4502 		}
4503 
4504 		fout(" )");
4505 
4506 		if (Wings[i].wave_delay_min) {
4507 			if (optional_string_fred("+Wave Delay Min:", "$Name:")) {
4508 				parse_comments();
4509 			} else {
4510 				fout("\n+Wave Delay Min:");
4511 			}
4512 
4513 			fout(" %d", Wings[i].wave_delay_min);
4514 		}
4515 
4516 		if (Wings[i].wave_delay_max) {
4517 			if (optional_string_fred("+Wave Delay Max:", "$Name:")) {
4518 				parse_comments();
4519 			} else {
4520 				fout("\n+Wave Delay Max:");
4521 			}
4522 
4523 			fout(" %d", Wings[i].wave_delay_max);
4524 		}
4525 
4526 		fso_comment_pop();
4527 	}
4528 
4529 	fso_comment_pop(true);
4530 
4531 	Assert(count == Num_wings);
4532 	return err;
4533 }
4534 
4535 }
4536 }
4537