/*
 * Copyright (C) Volition, Inc. 1999.  All rights reserved.
 *
 * All source code herein is the property of Volition, Inc. You may not sell 
 * or otherwise commercially exploit the source or things you created based on the 
 * source.
 *
*/ 

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <cstdarg>
#include <csetjmp>


#include "ai/aigoals.h"
#include "ai/ailua.h"
#include "asteroid/asteroid.h"
#include "bmpman/bmpman.h"
#include "cfile/cfile.h"
#include "cmdline/cmdline.h"
#include "debris/debris.h"
#include "gamesnd/eventmusic.h"
#include "globalincs/alphacolors.h"
#include "globalincs/linklist.h"
#include "hud/hud.h"
#include "hud/hudescort.h"
#include "hud/hudets.h"
#include "hud/hudsquadmsg.h"
#include "hud/hudwingmanstatus.h"
#include "iff_defs/iff_defs.h"
#include "io/timer.h"
#include "jumpnode/jumpnode.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "localization/localize.h"
#include "math/bitarray.h"
#include "math/fvi.h"
#include "math/staticrand.h"
#include "mission/missionbriefcommon.h"
#include "mission/missioncampaign.h"
#include "mission/missiongoals.h"
#include "mission/missionhotkey.h"
#include "mission/missionlog.h"
#include "mission/missionmessage.h"
#include "mission/missionparse.h"
#include "missionui/fictionviewer.h"
#include "missionui/missioncmdbrief.h"
#include "missionui/redalert.h"
#include "mod_table/mod_table.h"
#include "nebula/neb.h"
#include "nebula/neblightning.h"
#include "network/multi.h"
#include "network/multi_endgame.h"
#include "network/multi_respawn.h"
#include "network/multimsgs.h"
#include "network/multiutil.h"
#include "object/objectdock.h"
#include "object/parseobjectdock.h"
#include "object/objectshield.h"
#include "object/waypoint.h"
#include "parse/generic_log.h"
#include "parse/parselo.h"
#include "parse/sexp_container.h"
#include "prop/prop.h"
#include "scripting/global_hooks.h"
#include "scripting/hook_api.h"
#include "scripting/hook_conditions.h"
#include "scripting/scripting.h"
#include "species_defs/species_defs.h"
#include "playerman/player.h"
#include "popup/popup.h"
#include "popup/popupdead.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "ship/shiphit.h"
#include "sound/ds.h"
#include "starfield/nebula.h"
#include "starfield/starfield.h"
#include "weapon/weapon.h"
#include "tracing/Monitor.h"
#include "missionparse.h"


// MISSION_VERSION should be the earliest version of FSO that can load the current mission format without
// requiring version-specific comments.  It should be updated whenever the format changes, but it should
// not be updated simply because the engine's version changed.
// NOTE: The version can only have two numbers because old FRED builds expect the version to be a float.
const gameversion::version MISSION_VERSION = gameversion::version(23, 1);
const gameversion::version LEGACY_MISSION_VERSION = gameversion::version(0, 10);

LOCAL struct {
	char docker[NAME_LENGTH];
	char dockee[NAME_LENGTH];
	char docker_point[NAME_LENGTH];
	char dockee_point[NAME_LENGTH];
} Initially_docked[MAX_SHIPS];

int Total_initially_docked;

// coverity[GLOBAL_INIT_ORDER] -- safe; default-constructed, no cross-TU dependencies
mission	The_mission;
char Mission_filename[80];

int Mission_palette;  // index into Nebula_palette_filenames[] of palette file to use for mission
int Nebula_index;  // index into Nebula_filenames[] of nebula to use in mission.
int Num_ai_behaviors = MAX_AI_BEHAVIORS;
int Num_cargo = 0;
int Num_arrival_names = MAX_ARRIVAL_NAMES;
int Num_goal_type_names = MAX_GOAL_TYPE_NAMES;
int Num_parse_goals;
int Player_starts = 1;
int Num_teams;
fix Entry_delay_time = 0;

int Num_unknown_ship_classes;
int Num_unknown_prop_classes;
int Num_unknown_weapon_classes;
int Num_unknown_loadout_classes;

ushort Current_file_checksum = 0;
ushort Last_file_checksum = 0;
int    Current_file_length   = 0;

// coverity[GLOBAL_INIT_ORDER] -- safe; default-constructed, no cross-TU dependencies
SCP_vector<mission_default_custom_data> Default_custom_data;

// alternate ship type names
char Mission_alt_types[MAX_ALT_TYPE_NAMES][NAME_LENGTH];
int Mission_alt_type_count = 0;

// callsigns
char Mission_callsigns[MAX_CALLSIGNS][NAME_LENGTH];
int Mission_callsign_count = 0;

#define SHIP_WARP_TIME 5.0f		// how many seconds it takes for ship to warp in

// the ship arrival list will contain a list of ships that are yet to arrive.  This
// list could also include ships that are part of wings!
// coverity[GLOBAL_INIT_ORDER] -- safe; default-constructed, no cross-TU dependencies
p_object Ship_arrival_list;	// for linked list of ships to arrive later

// all the ships that we parse
// coverity[GLOBAL_INIT_ORDER] -- safe; default-constructed, no cross-TU dependencies
SCP_vector<p_object> Parse_objects;

// all the props that we parse
SCP_vector<parsed_prop> Parse_props;


// list for arriving support ship
// coverity[GLOBAL_INIT_ORDER] -- safe; default-constructed, no cross-TU dependencies
p_object	Support_ship_pobj;
p_object *Arriving_support_ship;
char Arriving_repair_targets[MAX_AI_GOALS][NAME_LENGTH];
int Num_arriving_repair_targets;

#define MIN_SUBSYS_STATUS_SIZE		25
subsys_status *Subsys_status = NULL;
int Subsys_index;
int Subsys_status_size;

char Mission_parse_storm_name[NAME_LENGTH] = "none";

team_data Team_data[MAX_TVT_TEAMS];

// variables for player start in single player
char		Player_start_shipname[NAME_LENGTH];
int		Player_start_shipnum;
p_object *Player_start_pobject;

// name of all ships to use while parsing a mission (since a ship might be referenced by
// something before that ship has even been loaded yet)
SCP_vector<SCP_string> Parse_names;

SCP_vector<SCP_string> Mission_parse_warnings;

// Routes a parse-time auto-correction notice to the right surface for the app:
// outside QtFRED, the existing Warning(LOCATION, ...) popup; inside QtFRED, the
// Mission_parse_warnings queue so the ErrorChecker can present it without a popup.
static void parse_warning_or_record(SCP_FORMAT_STRING const char* fmt, ...) SCP_FORMAT_STRING_ARGS(1, 2);
static void parse_warning_or_record(const char* fmt, ...)
{
	SCP_string msg;
	va_list args;
	va_start(args, fmt);
	vsprintf(msg, fmt, args);
	va_end(args);

	if (Qtfred_running) {
		Mission_parse_warnings.push_back(std::move(msg));
	} else {
		Warning(LOCATION, "%s", msg.c_str());
	}
}

SCP_vector<texture_replace> Fred_texture_replacements;

SCP_unordered_set<int> Fred_migrated_immobile_ships;

int Num_path_restrictions;
path_restriction_t Path_restrictions[MAX_PATH_RESTRICTIONS];

constexpr int DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP = 0;

extern int debrief_find_persona_index();

static bool mission_has_layer_name(const mission* pm, const SCP_string& layerName) {
	return std::any_of(pm->fred_layers.begin(), pm->fred_layers.end(), [&layerName](const SCP_string& existingLayer) {
		return stricmp(existingLayer.c_str(), layerName.c_str()) == 0;
	});
}

//XSTR:OFF

const char *Nebula_filenames[NUM_NEBULAS] = {
	"Nebula01",
	"Nebula02",
	"Nebula03"	
};

const char *Neb2_filenames[NUM_NEBULAS] = {
	"Nebfull01",
	"Nebfull02",
	"Nebfull03"
};

// Note: Nebula_colors[] and Nebula_palette_filenames are linked via index numbers
const char *Nebula_colors[NUM_NEBULA_COLORS] = {
	"Red",
	"Blue",
	"Gold",
	"Purple",
	"Maroon",
	"Green",
	"Grey blue",
	"Violet",
	"Grey Green",
};

const char *Ai_behavior_names[MAX_AI_BEHAVIORS] = {
	"Chase",
	"Evade",
	"Get behind",
	"Stay Near",
	"Still",
	"Guard",
	"Avoid",
	"Waypoints",
	"Dock",
	"None",
	"Big Ship",
	"Path",
	"Be Rearmed",
	"Safety",
	"Evade Weapon",
	"Strafe",
	"Play Dead",
	"Bay Emerge",
	"Bay Depart",
	"Sentry Gun",
	"Warp Out",
	"Fly To Ship",
	"Lua AI",
};

char *Cargo_names[MAX_CARGO];
char Cargo_names_buf[MAX_CARGO][NAME_LENGTH];

const char *Ship_class_names[MAX_SHIP_CLASSES];		// to be filled in from Ship_info array

const char *Icon_names[MIN_BRIEF_ICONS] = {
	"Fighter", "Fighter Wing", "Cargo", "Cargo Wing", "Largeship",
	"Largeship Wing", "Capital", "Planet", "Asteroid Field", "Waypoint",
	"Support Ship", "Freighter(no cargo)", "Freighter(has cargo)",
	"Freighter Wing(no cargo)", "Freighter Wing(has cargo)", "Installation",
	"Bomber", "Bomber Wing", "Cruiser", "Cruiser Wing", "Unknown", "Unknown Wing",
	"Player Fighter", "Player Fighter Wing", "Player Bomber", "Player Bomber Wing",
	"Knossos Device", "Transport Wing", "Corvette", "Gas Miner", "Awacs", "Supercap", "Sentry Gun", "Jump Node", "Transport"
};

// definitions for arrival locations for ships/wings
const char *Arrival_location_names[MAX_ARRIVAL_NAMES] = {
	"Hyperspace", "Near Ship", "In front of ship", "In back of ship", "Above ship", "Below ship", "To left of ship", "To right of ship", "Docking Bay",
};

const char *Departure_location_names[MAX_DEPARTURE_NAMES] = {
	"Hyperspace", "Docking Bay",
};

const char *Goal_type_names[MAX_GOAL_TYPE_NAMES] = {
	"Primary", "Secondary", "Bonus",
};

const char *Reinforcement_type_names[] = {
	"Attack/Protect",
	"Repair/Rearm",
};

const char *Old_game_types[OLD_MAX_GAME_TYPES] = {
	"Single Player Only",	
	"Multiplayer Only",
	"Single/Multi Player",
	"Training mission"
};

flag_def_list_new<Ship::Ship_Flags> Parse_ship_flags[] = {
	{"cargo-known", Ship::Ship_Flags::Cargo_revealed, true, false},
	{"ignore-count", Ship::Ship_Flags::Ignore_count, true, false},
	{"reinforcement", Ship::Ship_Flags::Reinforcement, true, false},
	{"escort", Ship::Ship_Flags::Escort, true, false},
	{"no-arrival-music", Ship::Ship_Flags::No_arrival_music, true, false},
	{"no-arrival-warp", Ship::Ship_Flags::No_arrival_warp, true, false},
	{"no-departure-warp", Ship::Ship_Flags::No_departure_warp, true, false},
	{"hidden-from-sensors", Ship::Ship_Flags::Hidden_from_sensors, true, false},
	{"scannable", Ship::Ship_Flags::Scannable, true, false},
	{"red-alert-carry", Ship::Ship_Flags::Red_alert_store_status, true, false},
	{"vaporize", Ship::Ship_Flags::Vaporize, true, false},
	{"stealth", Ship::Ship_Flags::Stealth, true, false},
	{"friendly-stealth-invisible", Ship::Ship_Flags::Friendly_stealth_invis, true, false},
	{"don't-collide-invisible", Ship::Ship_Flags::Dont_collide_invis, true, false},
	{"primitive-sensors", Ship::Ship_Flags::Primitive_sensors, true, false},
	{"no-subspace-drive", Ship::Ship_Flags::No_subspace_drive, true, false},
	{"nav-carry-status", Ship::Ship_Flags::Navpoint_carry, true, false},
	{"affected-by-gravity", Ship::Ship_Flags::Affected_by_gravity, true, false},
	{"toggle-subsystem-scanning", Ship::Ship_Flags::Toggle_subsystem_scanning, true, false},
	{"no-builtin-messages", Ship::Ship_Flags::No_builtin_messages, true, false},
	{"primaries-locked", Ship::Ship_Flags::Primaries_locked, true, false},
	{"secondaries-locked", Ship::Ship_Flags::Secondaries_locked, true, false},
	{"no-death-scream", Ship::Ship_Flags::No_death_scream, true, false},
	{"always-death-scream", Ship::Ship_Flags::Always_death_scream, true, false},
	{"nav-needslink", Ship::Ship_Flags::Navpoint_needslink, true, false},
	{"hide-ship-name", Ship::Ship_Flags::Hide_ship_name, true, false},
	{"set-class-dynamically", Ship::Ship_Flags::Set_class_dynamically, true, false},
	{"lock-all-turrets", Ship::Ship_Flags::Lock_all_turrets_initially, true, false},
	{"afterburners-locked", Ship::Ship_Flags::Afterburner_locked, true, false},
	{"no-ets", Ship::Ship_Flags::No_ets, true, false},
	{"cloaked", Ship::Ship_Flags::Cloaked, true, false},
	{"ship-locked", Ship::Ship_Flags::Ship_locked, true, false},
	{"weapons-locked", Ship::Ship_Flags::Weapons_locked, true, false},
	{"scramble-messages", Ship::Ship_Flags::Scramble_messages, true, false},
	{"no-disabled-self-destruct", Ship::Ship_Flags::No_disabled_self_destruct, true, false},
	{"hide-in-mission-log", Ship::Ship_Flags::Hide_mission_log, true, false},
	{"same-arrival-warp-when-docked", Ship::Ship_Flags::Same_arrival_warp_when_docked, true, false},
	{"same-departure-warp-when-docked", Ship::Ship_Flags::Same_departure_warp_when_docked, true, false},
	{"fail-sound-locked-primary", Ship::Ship_Flags::Fail_sound_locked_primary, true, false},
	{"fail-sound-locked-secondary", Ship::Ship_Flags::Fail_sound_locked_secondary, true, false},
	{"aspect-immune", Ship::Ship_Flags::Aspect_immune, true, false},
	{"cannot-perform-scan-hide-cargo", Ship::Ship_Flags::Cannot_perform_scan_hide_cargo, true, false},
	{"cannot-perform-scan-show-cargo", Ship::Ship_Flags::Cannot_perform_scan_show_cargo, true, false},
	{"no-targeting-limits", Ship::Ship_Flags::No_targeting_limits, true, false},
	{"no-scanned-cargo", Ship::Ship_Flags::No_scanned_cargo, true, false},
	{"force-shields-on", Ship::Ship_Flags::Force_shields_on, true, false},
	{"Destroy before Mission", Ship::Ship_Flags::Kill_before_mission,true, false}, //Not Printed to misson so can use descriptive name
}
;

const size_t Num_Parse_ship_flags = sizeof(Parse_ship_flags) / sizeof(flag_def_list_new<Ship::Ship_Flags>);

flag_def_list_new<AI::AI_Flags> Parse_ship_ai_flags[] = {
	{"kamikaze", AI::AI_Flags::Kamikaze, true, false},
	{"no-dynamic", AI::AI_Flags::No_dynamic, true, false},

};

const size_t Num_Parse_ship_ai_flags = sizeof(Parse_ship_ai_flags) / sizeof(flag_def_list_new<AI::AI_Flags>);

flag_def_list_new<Object::Object_Flags> Parse_ship_object_flags[] = {
	{"protect-ship", Object::Object_Flags::Protected, true, false},
	{"no-shields", Object::Object_Flags::No_shields, true, false},
	{"player-start", Object::Object_Flags::Player_ship, true, false},
	{"invulnerable", Object::Object_Flags::Invulnerable, true, false},
	{"beam-protect-ship", Object::Object_Flags::Beam_protected, true, false},
	{"flak-protect-ship", Object::Object_Flags::Flak_protected, true, false},
	{"laser-protect-ship", Object::Object_Flags::Laser_protected, true, false},
	{"missile-protect-ship", Object::Object_Flags::Missile_protected, true, false},
	{"special-warp", Object::Object_Flags::Special_warpin, true, false},
	{"targetable-as-bomb", Object::Object_Flags::Targetable_as_bomb, true, false},
	{"don't-change-position", Object::Object_Flags::Dont_change_position, true, false},
	{"don't-change-orientation", Object::Object_Flags::Dont_change_orientation, true, false},
	{"no_collide", Object::Object_Flags::Collides, true, false},
	{"ai-attackable-if-no-collide", Object::Object_Flags::Attackable_if_no_collide, true, false},

};
const size_t Num_Parse_ship_object_flags =
	sizeof(Parse_ship_object_flags) / sizeof(flag_def_list_new<Object::Object_Flags>);

// These are a little different than the object flags as they aren't used in traditional flag sexps or parsed flag lists
// Instead, this list is used to popuplate QtFRED's mission specs flag checkboxes. As such the names can be more descriptive than other flag def lists
// NOTE: Inactive flags and special flags are not added to the UI flag list. It is assumed that special flags exist in some other UI form
flag_def_list_new<Mission::Mission_Flags> Parse_mission_flags[] = {
	{"Mission Takes Place In Subspace",           Mission::Mission_Flags::Subspace,                   true, true},
	{"Disallow Promotions/Badges",                Mission::Mission_Flags::No_promotion,               true, false},
	{"Mission Takes Place In Full Nebula",        Mission::Mission_Flags::Fullneb,                    true, true},
	{"Disable Built-in Messages",                 Mission::Mission_Flags::No_builtin_msgs,            true, false},
	{"No Traitor",                                Mission::Mission_Flags::No_traitor,                 true, false},
	{"Toggle Ship Trails",                        Mission::Mission_Flags::Toggle_ship_trails,         true, true},
	{"Support Ship Repairs Hull",                 Mission::Mission_Flags::Support_repairs_hull,       true, true},
	{"All Ships Beam-Freed By Default",           Mission::Mission_Flags::Beam_free_all_by_default,   true, false},
	{"UNUSED 1",                                  Mission::Mission_Flags::Unused_1,                   false, false},
	{"UNUSED 2",                                  Mission::Mission_Flags::Unused_2,                   false, false},
	{"No Briefing",                               Mission::Mission_Flags::No_briefing,                true, false},
	{"Toggle Debriefing (On/Off)",                Mission::Mission_Flags::Toggle_debriefing,          true, false},
	{"UNUSED 3",                                  Mission::Mission_Flags::Unused_3,                   false, false},
	{"UNUSED 4",                                  Mission::Mission_Flags::Unused_4,                   false, false},
	{"2D Mission",                                Mission::Mission_Flags::Mission_2d,                 true, false},
	{"UNUSED 5",                                  Mission::Mission_Flags::Unused_5,                   false, false},
	{"Red Alert Mission",                         Mission::Mission_Flags::Red_alert,                  true, false},
	{"Scramble Mission",                          Mission::Mission_Flags::Scramble,                   true, false},
	{"Disable Built-in Command Messages",         Mission::Mission_Flags::No_builtin_command,         true, false},
	{"Player Starts under AI Control (NO MULTI)", Mission::Mission_Flags::Player_start_ai,            true, false},
	{"All Teams at War",                          Mission::Mission_Flags::All_attack,                 true, false},
	{"Use Autopilot Cinematics",                  Mission::Mission_Flags::Use_ap_cinematics,          true, false},
	{"Deactivate Hardcoded Autopilot",            Mission::Mission_Flags::Deactivate_ap,              true, false},
	{"Toggle Showing Goals In Briefing",          Mission::Mission_Flags::Toggle_showing_goals,       true, false},
	{"Mission End to Mainhall",                   Mission::Mission_Flags::End_to_mainhall,            true, false},
	{"Override #Command with Command Info",       Mission::Mission_Flags::Override_hashcommand,       true, true},
	{"Toggle Starting in Chase View",             Mission::Mission_Flags::Toggle_start_chase_view,    true, false},
	{"Nebula Fog Color Override",                 Mission::Mission_Flags::Neb2_fog_color_override,    true, true},
	{"Full Nebula Background Bitmaps",            Mission::Mission_Flags::Fullneb_background_bitmaps, true, true},
	{"Preload Subspace Tunnel",                   Mission::Mission_Flags::Preload_subspace,           true, false},
	{"Large Ships Do Not Collide By Default",    Mission::Mission_Flags::Large_ships_no_collide_by_default, true, false},
	{"Limit Support Rearm to Mission Pool",       Mission::Mission_Flags::Limited_support_rearm_pool, true, true}
};

parse_object_flag_description<Mission::Mission_Flags> Parse_mission_flag_descriptions[] = {
	{Mission::Mission_Flags::Subspace,                   "Mission takes place in subspace"},
	{Mission::Mission_Flags::No_promotion,               "Cannot get promoted or badges in this mission"},
	{Mission::Mission_Flags::Fullneb,                    "Mission is a full nebula mission"},
	{Mission::Mission_Flags::No_builtin_msgs,            "Disables all builtin messages except Command"},
	{Mission::Mission_Flags::No_traitor,                 "Player cannot become a traitor"},
	{Mission::Mission_Flags::Toggle_ship_trails,         "Toggles ship trails (off in nebula, on outside nebula)"},
	{Mission::Mission_Flags::Support_repairs_hull,       "Toggles support ship repair of ship hulls"},
	{Mission::Mission_Flags::Beam_free_all_by_default,   "All ships are beam-freed by default"},
	{Mission::Mission_Flags::Unused_1,                   "UNUSED 1"}, // Necessary to not break parsing. Is this still true??
	{Mission::Mission_Flags::Unused_2,                   "UNUSED 2"}, // Necessary to not break parsing. Is this still true??
	{Mission::Mission_Flags::No_briefing,                "No briefing, mission starts immediately"},
	{Mission::Mission_Flags::Toggle_debriefing,          "Toggles debriefing on for dogfight. Off for everything else"},
	{Mission::Mission_Flags::Unused_3,                   "UNUSED 3"}, // Necessary to not break parsing. Is this still true??
	{Mission::Mission_Flags::Unused_4,                   "UNUSED 4"}, // Necessary to not break parsing. Is this still true??
	{Mission::Mission_Flags::Mission_2d,                 "Mission is meant to be played top-down style; 2D physics and movement."},
	{Mission::Mission_Flags::Unused_5,                   "UNUSED 5"}, // Necessary to not break parsing. Is this still true??
	{Mission::Mission_Flags::Red_alert,                  "A red-alert mission"},
	{Mission::Mission_Flags::Scramble,                   "A scramble mission"},
	{Mission::Mission_Flags::No_builtin_command,         "Disables builtin Command messages"},
	{Mission::Mission_Flags::Player_start_ai,            "Player starts mission under AI Control"},
	{Mission::Mission_Flags::All_attack,                 "All teams target each other"},
	{Mission::Mission_Flags::Use_ap_cinematics,          "Use autopilot cinematics"},
	{Mission::Mission_Flags::Deactivate_ap,              "Deactivate hardcoded autopilot"},
	{Mission::Mission_Flags::Toggle_showing_goals,       "Show mission goals for training missions, hide otherwise"},
	{Mission::Mission_Flags::End_to_mainhall,            "Return to the mainhall after debrief instead of starting the next mission"},
	{Mission::Mission_Flags::Override_hashcommand,       "Override #Command with the Command info in Mission Specs"},
	{Mission::Mission_Flags::Toggle_start_chase_view,    "Toggles whether the player starts the mission in chase view"},
	{Mission::Mission_Flags::Neb2_fog_color_override,    "Whether to use explicit fog colors instead of checking the palette"},
	{Mission::Mission_Flags::Fullneb_background_bitmaps, "Show background bitmaps despite full nebula"},
	{Mission::Mission_Flags::Preload_subspace,         "Preload the subspace tunnel for both the sexp and specs checkbox"},
	{Mission::Mission_Flags::Large_ships_no_collide_by_default, "Automatically places all large ships in the configured collision group, preventing large ships from colliding with each other"},
	{Mission::Mission_Flags::Limited_support_rearm_pool, "Support ships can only rearm from the mission weapon pool"},
};

const size_t Num_parse_mission_flags = sizeof(Parse_mission_flags) / sizeof(flag_def_list_new<Mission::Mission_Flags>);
const size_t Num_parse_mission_flag_descriptions = sizeof(Parse_mission_flag_descriptions) / sizeof(parse_object_flag_description<Mission::Mission_Flags>);

flag_def_list_new<Mission::Parse_Object_Flags> Parse_object_flags[] = {
    { "cargo-known",					Mission::Parse_Object_Flags::SF_Cargo_known,			true, false },
    { "ignore-count",					Mission::Parse_Object_Flags::SF_Ignore_count,			true, false },
    { "protect-ship",					Mission::Parse_Object_Flags::OF_Protected,				true, false },
    { "reinforcement",					Mission::Parse_Object_Flags::SF_Reinforcement,			true, false },
    { "no-shields",						Mission::Parse_Object_Flags::OF_No_shields,				true, false },
    { "escort",							Mission::Parse_Object_Flags::SF_Escort,					true, false },
    { "player-start",					Mission::Parse_Object_Flags::OF_Player_start,			true, false },
    { "no-arrival-music",				Mission::Parse_Object_Flags::SF_No_arrival_music,		true, false },
    { "no-arrival-warp",				Mission::Parse_Object_Flags::SF_No_arrival_warp,		true, false },
    { "no-departure-warp",				Mission::Parse_Object_Flags::SF_No_departure_warp,		true, false },
    { "locked",							Mission::Parse_Object_Flags::SF_Locked,					true, false },
    { "invulnerable",					Mission::Parse_Object_Flags::OF_Invulnerable,			true, false },
    { "hidden-from-sensors",			Mission::Parse_Object_Flags::SF_Hidden_from_sensors,	true, false },
    { "scannable",						Mission::Parse_Object_Flags::SF_Scannable,				true, false },
    { "kamikaze",						Mission::Parse_Object_Flags::AIF_Kamikaze,				true, false },
    { "no-dynamic",						Mission::Parse_Object_Flags::AIF_No_dynamic,			true, false },
    { "red-alert-carry",				Mission::Parse_Object_Flags::SF_Red_alert_store_status, true, false },
    { "beam-protect-ship",				Mission::Parse_Object_Flags::OF_Beam_protected,			true, false },
    { "flak-protect-ship",				Mission::Parse_Object_Flags::OF_Flak_protected,			true, false },
    { "laser-protect-ship",				Mission::Parse_Object_Flags::OF_Laser_protected,		true, false },
    { "missile-protect-ship",			Mission::Parse_Object_Flags::OF_Missile_protected,		true, false },
    { "guardian",						Mission::Parse_Object_Flags::SF_Guardian,				true, false },
    { "special-warp",					Mission::Parse_Object_Flags::Knossos_warp_in,			true, false },
    { "vaporize",						Mission::Parse_Object_Flags::SF_Vaporize,				true, false },
    { "stealth",						Mission::Parse_Object_Flags::SF_Stealth,				true, false },
    { "friendly-stealth-invisible",		Mission::Parse_Object_Flags::SF_Friendly_stealth_invis, true, false },
    { "don't-collide-invisible",		Mission::Parse_Object_Flags::SF_Dont_collide_invis,		true, false },
    { "primitive-sensors",				Mission::Parse_Object_Flags::SF_Primitive_sensors,		true, false },
    { "no-subspace-drive",				Mission::Parse_Object_Flags::SF_No_subspace_drive,		true, false },
    { "nav-carry-status",				Mission::Parse_Object_Flags::SF_Nav_carry_status,		true, false },
    { "affected-by-gravity",			Mission::Parse_Object_Flags::SF_Affected_by_gravity,	true, false },
    { "toggle-subsystem-scanning",		Mission::Parse_Object_Flags::SF_Toggle_subsystem_scanning, true, false },
    { "targetable-as-bomb",				Mission::Parse_Object_Flags::OF_Targetable_as_bomb,		true, false },
    { "no-builtin-messages",			Mission::Parse_Object_Flags::SF_No_builtin_messages,	true, false },
    { "primaries-locked",				Mission::Parse_Object_Flags::SF_Primaries_locked,		true, false },
    { "secondaries-locked",				Mission::Parse_Object_Flags::SF_Secondaries_locked,		true, false },
    { "no-death-scream",				Mission::Parse_Object_Flags::SF_No_death_scream,		true, false },
    { "always-death-scream",			Mission::Parse_Object_Flags::SF_Always_death_scream,	true, false },
    { "nav-needslink",					Mission::Parse_Object_Flags::SF_Nav_needslink,			true, false },
    { "hide-ship-name",					Mission::Parse_Object_Flags::SF_Hide_ship_name,			true, false },
    { "set-class-dynamically",			Mission::Parse_Object_Flags::SF_Set_class_dynamically,	true, false },
    { "lock-all-turrets",				Mission::Parse_Object_Flags::SF_Lock_all_turrets_initially, true, false },
    { "afterburners-locked",			Mission::Parse_Object_Flags::SF_Afterburner_locked,		true, false },
    { "force-shields-on",				Mission::Parse_Object_Flags::OF_Force_shields_on,		true, false },
    { "immobile",						Mission::Parse_Object_Flags::OF_Immobile,				true, false },
    { "don't-change-position",			Mission::Parse_Object_Flags::OF_Dont_change_position,	true, false },
    { "don't-change-orientation",		Mission::Parse_Object_Flags::OF_Dont_change_orientation,	true, false },
    { "no-ets",							Mission::Parse_Object_Flags::SF_No_ets,					true, false },
    { "cloaked",						Mission::Parse_Object_Flags::SF_Cloaked,				true, false },
    { "ship-locked",					Mission::Parse_Object_Flags::SF_Ship_locked,			true, false },
    { "weapons-locked",					Mission::Parse_Object_Flags::SF_Weapons_locked,			true, false },
    { "scramble-messages",				Mission::Parse_Object_Flags::SF_Scramble_messages,		true, false },
    { "no_collide",						Mission::Parse_Object_Flags::OF_No_collide,				true, false },
    { "no-disabled-self-destruct",		Mission::Parse_Object_Flags::SF_No_disabled_self_destruct, true, false },
    { "hide-in-mission-log",			Mission::Parse_Object_Flags::SF_Hide_mission_log,		true, false },
    { "same-arrival-warp-when-docked",		Mission::Parse_Object_Flags::SF_Same_arrival_warp_when_docked,		true, false },
    { "same-departure-warp-when-docked",	Mission::Parse_Object_Flags::SF_Same_departure_warp_when_docked,	true, false },
    { "ai-attackable-if-no-collide",		Mission::Parse_Object_Flags::OF_Attackable_if_no_collide, true, false },
    { "fail-sound-locked-primary",			Mission::Parse_Object_Flags::SF_Fail_sound_locked_primary, true, false },
    { "fail-sound-locked-secondary",		Mission::Parse_Object_Flags::SF_Fail_sound_locked_secondary, true, false },
    { "aspect-immune",						Mission::Parse_Object_Flags::SF_Aspect_immune, true, false },
	{ "cannot-perform-scan-hide-cargo",		Mission::Parse_Object_Flags::SF_Cannot_perform_scan_hide_cargo, true, false },
	{ "cannot-perform-scan-show-cargo",		Mission::Parse_Object_Flags::SF_Cannot_perform_scan_show_cargo, true, false },
	{ "no-targeting-limits",				Mission::Parse_Object_Flags::SF_No_targeting_limits, true, false},
	{ "no-scanned-cargo",					Mission::Parse_Object_Flags::SF_No_scanned_cargo, true, false },
};

parse_object_flag_description<Mission::Parse_Object_Flags> Parse_object_flag_descriptions[] = {
    { Mission::Parse_Object_Flags::SF_Cargo_known,					"If set, the ship's cargo can be seen without scanning the ship."},
    { Mission::Parse_Object_Flags::SF_Ignore_count,					"Ignore this ship when counting ship types for goals."},
    { Mission::Parse_Object_Flags::OF_Protected,					"Ship and Turret AI will ignore and not attack ship."},
    { Mission::Parse_Object_Flags::SF_Reinforcement,				"This ship is a reinforcement ship."},
    { Mission::Parse_Object_Flags::OF_No_shields,					"Ship will have no shields (ETS will be rebalanced if shields were off and are enabled)."},
    { Mission::Parse_Object_Flags::SF_Escort,						"This ship is an escort ship."},
    { Mission::Parse_Object_Flags::OF_Player_start,					"Ship is a player ship."},
    { Mission::Parse_Object_Flags::SF_No_arrival_music,				"Don't play arrival music when ship arrives."},
    { Mission::Parse_Object_Flags::SF_No_arrival_warp,				"No arrival warp-in effect."},
    { Mission::Parse_Object_Flags::SF_No_departure_warp,			"No departure warp-in effect."},
    { Mission::Parse_Object_Flags::SF_Locked,						"Ship cannot be changed in the Ship Loadout."},
    { Mission::Parse_Object_Flags::OF_Invulnerable,					"Stops ship from taking any damage."},
    { Mission::Parse_Object_Flags::SF_Hidden_from_sensors,			"If set, the ship can't be targeted and appears on radar as a blinking dot."},
    { Mission::Parse_Object_Flags::SF_Scannable,					"Whether or not the ship can be scanned."},
    { Mission::Parse_Object_Flags::AIF_Kamikaze,					"Ship will attack big ships by colliding with them and exploding."},
    { Mission::Parse_Object_Flags::AIF_No_dynamic,					"Will stop allowing the AI to pursue dynamic goals (eg: chasing ships it was not ordered to)."},
    { Mission::Parse_Object_Flags::SF_Red_alert_store_status,		"Ship status should be stored/restored if red alert mission."},
    { Mission::Parse_Object_Flags::OF_Beam_protected,				"Turrets with beam weapons will ignore and not attack ship."},
    { Mission::Parse_Object_Flags::OF_Flak_protected,				"Turrets with flak weapons will ignore and not attack ship."},
    { Mission::Parse_Object_Flags::OF_Laser_protected,				"Turrets with laser weapons will ignore and not attack ship."},
    { Mission::Parse_Object_Flags::OF_Missile_protected,			"Turrets with missile weapons will ignore and not attack ship."},
    { Mission::Parse_Object_Flags::SF_Guardian,						"Ship health cannot be reduced below 1."},
    { Mission::Parse_Object_Flags::Knossos_warp_in,					"Ship uses the special Knossos warp-in animation."},
    { Mission::Parse_Object_Flags::SF_Vaporize,						"Causes a ship to vanish (no deathroll, no debris, no explosion) when destroyed."},
    { Mission::Parse_Object_Flags::SF_Stealth,						"If set, the ship can't be targeted, is invisible on radar, and is ignored by AI unless firing."},
    { Mission::Parse_Object_Flags::SF_Friendly_stealth_invis,		"If set and the ship is also flagged with stealth then the ship can't be targeted even by ships on the same team."},
    { Mission::Parse_Object_Flags::SF_Dont_collide_invis,			"Will cause polygons with an invisible texture to stop colliding with objects."},
    { Mission::Parse_Object_Flags::SF_Primitive_sensors,			"Targets will only be a blip on the radar. Ships cannot targeted and aspect lock cannot be used."},
    { Mission::Parse_Object_Flags::SF_No_subspace_drive,			"Will not allow a ship to jump into subspace."},
    { Mission::Parse_Object_Flags::SF_Nav_carry_status,				"This ship autopilots with the player."},
    { Mission::Parse_Object_Flags::SF_Affected_by_gravity,			"Deprecated. Does nothing."},
    { Mission::Parse_Object_Flags::SF_Toggle_subsystem_scanning,	"Switches between being able to scan a whole ship or individual subsystems."},
    { Mission::Parse_Object_Flags::OF_Targetable_as_bomb,			"Allows ship to be targeted with the bomb targeting key."},
    { Mission::Parse_Object_Flags::SF_No_builtin_messages,			"Ship will not send built-in messages."},
    { Mission::Parse_Object_Flags::SF_Primaries_locked,				"Will stop a ship from firing their primary weapons."},
    { Mission::Parse_Object_Flags::SF_Secondaries_locked,			"Will stop a ship from firing their secondary weapons."},
    { Mission::Parse_Object_Flags::SF_No_death_scream,				"Ship will not send a death message."},
    { Mission::Parse_Object_Flags::SF_Always_death_scream,			"Ship will always send a death message."},
    { Mission::Parse_Object_Flags::SF_Nav_needslink,				"This ship requires \"linking\" for autopilot ."},
    { Mission::Parse_Object_Flags::SF_Hide_ship_name,				"If set, the ship name can't be seen when the ship is targeted."},
    { Mission::Parse_Object_Flags::SF_Set_class_dynamically,		"This ship should have its class assigned rather than simply read from the mission file."},
    { Mission::Parse_Object_Flags::SF_Lock_all_turrets_initially,	"Lock all turrets on this ship at mission start or on arrival."},
    { Mission::Parse_Object_Flags::SF_Afterburner_locked,			"Will stop a ship from firing their afterburner."},
    { Mission::Parse_Object_Flags::OF_Force_shields_on,				"Shields will be activated regardless of other flags."},
    { Mission::Parse_Object_Flags::OF_Immobile,						"Will not let a ship change position or orientation. Upon destruction the ship will still do the death roll and explosion."},
    { Mission::Parse_Object_Flags::OF_Dont_change_position,			"Will not let a ship change position. Upon destruction the ship will still do the death roll and explosion."},
    { Mission::Parse_Object_Flags::OF_Dont_change_orientation,		"Will not let a ship change orientation. Upon destruction the ship will still do the death roll and explosion."},
    { Mission::Parse_Object_Flags::SF_No_ets,						"Will not allow a ship to alter its ETS system."},
    { Mission::Parse_Object_Flags::SF_Cloaked,						"This ship will not be rendered."},
    { Mission::Parse_Object_Flags::SF_Ship_locked,					"Prevents the player from changing the ship class on loadout screen."},
    { Mission::Parse_Object_Flags::SF_Weapons_locked,				"Prevents the player from changing the weapons on the ship on the loadout screen."},
    { Mission::Parse_Object_Flags::SF_Scramble_messages,			"All messages sent from this ship appear scrambled."},
    { Mission::Parse_Object_Flags::OF_No_collide,					"Ship cannot be collided with."},
    { Mission::Parse_Object_Flags::SF_No_disabled_self_destruct,	"Ship will not self-destruct after 90 seconds if engines or weapons destroyed."},
    { Mission::Parse_Object_Flags::SF_Hide_mission_log,				"Mission log events generated for this ship will not be viewable."},
    { Mission::Parse_Object_Flags::SF_Same_arrival_warp_when_docked,"Docked ships use the same warp effect size upon arrival as if they were not docked instead of the enlarged aggregate size."},
    { Mission::Parse_Object_Flags::SF_Same_departure_warp_when_docked,"Docked ship use the same warp effect size upon departure as if they were not docked instead of the enlarged aggregate size."},
    { Mission::Parse_Object_Flags::OF_Attackable_if_no_collide,		"Allows the AI to attack this object, even if no-collide is set."},
    { Mission::Parse_Object_Flags::SF_Fail_sound_locked_primary,	"Play the firing fail sound when the weapon is locked."},
    { Mission::Parse_Object_Flags::SF_Fail_sound_locked_secondary,	"Play the firing fail sound when the weapon is locked."},
    { Mission::Parse_Object_Flags::SF_Aspect_immune,				"Ship cannot be targeted by Aspect Seekers."},
	{ Mission::Parse_Object_Flags::SF_Cannot_perform_scan_hide_cargo, "Ship cannot scan other ships, and the cargo line will not be shown on the HUD."},
	{ Mission::Parse_Object_Flags::SF_Cannot_perform_scan_show_cargo, "Ship cannot scan other ships, but the cargo line will be shown on the HUD."},
	{ Mission::Parse_Object_Flags::SF_No_targeting_limits,			"Ship is always targetable regardless of AWACS or targeting range limits."},
	{ Mission::Parse_Object_Flags::SF_No_scanned_cargo,				"Cargo is never revealed; only shows 'Scanned' or 'Not Scanned'. Needs $Unify Scanning Behavior in game_settings.tbl."},
};

const size_t Num_parse_object_flags = sizeof(Parse_object_flags) / sizeof(flag_def_list_new<Mission::Parse_Object_Flags>);

flag_def_list_new<Ship::Wing_Flags> Parse_wing_flags[] = {
	{"ignore-count", Ship::Wing_Flags::Ignore_count, true, false},
	{"reinforcement", Ship::Wing_Flags::Reinforcement, true, false},
	{"no-arrival-music", Ship::Wing_Flags::No_arrival_music, true, false},
	{"no-arrival-message", Ship::Wing_Flags::No_arrival_message, true, false},
	{"no-first-wave-message", Ship::Wing_Flags::No_first_wave_message, true, false},
	{"no-arrival-warp", Ship::Wing_Flags::No_arrival_warp, true, false},
	{"no-departure-warp", Ship::Wing_Flags::No_departure_warp, true, false},
	{"no-dynamic", Ship::Wing_Flags::No_dynamic, true, false},
	{"nav-carry-status", Ship::Wing_Flags::Nav_carry, true, false},
	{"same-arrival-warp-when-docked", Ship::Wing_Flags::Same_arrival_warp_when_docked, true, false},
	{"same-departure-warp-when-docked", Ship::Wing_Flags::Same_departure_warp_when_docked, true, false}
};

parse_object_flag_description<Ship::Wing_Flags> Parse_wing_flag_descriptions[] = {
	{ Ship::Wing_Flags::Ignore_count,                    "Ignore this wing when counting ship types for goals." },
	{ Ship::Wing_Flags::Reinforcement,                   "This wing is a reinforcement wing." },
	{ Ship::Wing_Flags::No_arrival_music,                "Don't play arrival music when wing arrives." },
	{ Ship::Wing_Flags::No_arrival_message,              "Don't play arrival message when wing arrives." },
	{ Ship::Wing_Flags::No_first_wave_message,           "Don't play the 'first wave' message when this is the first wing to arrive." },
	{ Ship::Wing_Flags::No_arrival_warp,                 "No arrival warp-in effect." },
	{ Ship::Wing_Flags::No_departure_warp,               "No departure warp-in effect." },
	{ Ship::Wing_Flags::No_dynamic,                      "Will stop allowing the AI to pursue dynamic goals (eg: chasing ships it was not ordered to)." },
	{ Ship::Wing_Flags::Nav_carry,                       "Ships in this wing autopilot with the player." },
	{ Ship::Wing_Flags::Same_arrival_warp_when_docked,   "Docked ships use the same warp effect size upon arrival as if they were not docked instead of the enlarged aggregate size." },
	{ Ship::Wing_Flags::Same_departure_warp_when_docked, "Docked ship use the same warp effect size upon departure as if they were not docked instead of the enlarged aggregate size." }};

const size_t Num_parse_wing_flags = sizeof(Parse_wing_flags) / sizeof(flag_def_list_new<Ship::Wing_Flags>);
const size_t Num_parse_wing_flag_descriptions = sizeof(Parse_wing_flag_descriptions) / sizeof(parse_object_flag_description<Ship::Wing_Flags>);

flag_def_list_new<Mission::Parse_Object_Flags> Parse_prop_flags[] = {
    { "no_collide",						Mission::Parse_Object_Flags::OF_No_collide,				true, false },
};

parse_object_flag_description<Mission::Parse_Object_Flags> Parse_prop_flag_descriptions[] = {
    { Mission::Parse_Object_Flags::OF_No_collide,					"Prop cannot be collided with."},
};

const size_t Num_parse_prop_flags = sizeof(Parse_prop_flags) / sizeof(flag_def_list_new<Mission::Parse_Object_Flags>);
const size_t Num_parse_prop_flag_descriptions = sizeof(Parse_prop_flag_descriptions) / sizeof(parse_object_flag_description<Mission::Parse_Object_Flags>);

// These are only the flags that are saved to the mission file.  See the MEF_ #defines.
flag_def_list Mission_event_flags[] = {
	{ "interval & delay use msecs", MEF_USE_MSECS, 0 },
};
int Num_mission_event_flags = sizeof(Mission_event_flags) / sizeof(flag_def_list);

const char *Mission_event_log_flags[MAX_MISSION_EVENT_LOG_FLAGS] = {
	"true",
	"false",
	"always true",			// disabled
	"always false",
	"first repeat",
	"last repeat", 
	"first trigger",
	"last trigger",
	"state change",
};

//XSTR:ON

int Num_reinforcement_type_names = sizeof(Reinforcement_type_names) / sizeof(char *);

vec3d Parse_viewer_pos;
matrix Parse_viewer_orient;

int Loading_screen_bm_index=-1;

fix Mission_end_time;

// calculates a "unique" file signature as a ushort (checksum) and an int (file length)
// the amount of The_mission we're going to checksum
// WARNING : do NOT call this function on the server - it will overwrite goals, etc
#define MISSION_CHECKSUM_SIZE (NAME_LENGTH + NAME_LENGTH + 4 + DATE_TIME_LENGTH + DATE_TIME_LENGTH)

// a timer used to limit arrival music
#define ARRIVAL_MUSIC_MIN_SEPARATION	60000
// a random delay before announcing enemy arrivals
#define ARRIVAL_MESSAGE_DELAY_MIN		2000
#define ARRIVAL_MESSAGE_DELAY_MAX		3000

static int Allow_arrival_music_timestamp;
static int Allow_arrival_message_timestamp;
static int Arrival_message_delay_timestamp;
static int Arrival_message_subject;

static int Allow_backup_message_timestamp;

// multi TvT
static int Allow_arrival_music_timestamp_m[2];
static int Allow_arrival_message_timestamp_m[2];
static int Arrival_message_delay_timestamp_m[2];

// local prototypes
void parse_player_info2(mission *pm);
bool post_process_mission(mission *pm);
int allocate_subsys_status();
void parse_common_object_data(p_object	*objp);
void parse_asteroid_fields(mission *pm);
int mission_set_arrival_location(anchor_t anchor, ArrivalLocation location, int distance, int objnum, int path_mask, vec3d *new_pos, matrix *new_orient);
anchor_t get_anchor(const char *name);
void mission_parse_set_up_initial_docks();
void mission_parse_set_arrival_locations();
void mission_set_wing_arrival_location( wing *wingp, int num_to_set );
void parse_object_set_handled_flag_helper(p_object *pobjp, p_dock_function_info *infop);
void parse_object_clear_all_handled_flags();
int parse_object_on_arrival_list(p_object *pobjp);
int add_path_restriction();

static bool Warned_about_team_out_of_range;

// Goober5000
void mission_parse_mark_non_arrival(p_object *p_objp);
void mission_parse_mark_non_arrival(wing *wingp);
void mission_parse_mark_non_arrivals();

// Goober5000 - FRED import
void convertFSMtoFS2();


MONITOR(NumShipArrivals)
MONITOR(NumShipDepartures)

const std::shared_ptr<scripting::Hook<scripting::hooks::ShipDepartConditions>> OnDepartureStartedHook = scripting::Hook<scripting::hooks::ShipDepartConditions>::Factory(
	"On Departure Started", "Called when a ship initiates a departure (acquires a hangar bay path or begins to warp) but is not actually exiting the mission yet.",
	{
		{"Self", "ship", "An alias for Ship."},
		{"Ship", "ship", "The ship that has begun the departure process."},
	});

 const std::shared_ptr<scripting::Hook<>> OnLoadoutAboutToParseHook = scripting::Hook<>::Factory("On Loadout About To Parse",
	"Called during mission load just before parsing the team loadout.",{});

custom_string* get_custom_string_by_name(SCP_string name)
 {
	for (size_t i = 0; i < The_mission.custom_strings.size(); i++) {
		if (The_mission.custom_strings[i].name == name) {
			return &The_mission.custom_strings[i];
		}
	}

	return nullptr;
}

// Goober5000
void parse_custom_bitmap(const char *expected_string_640, const char *expected_string_1024, char *string_field_640, char *string_field_1024)
{
	bool found640 = false, found1024 = false;

	// custom mission loading background, or whatever
	if (optional_string(expected_string_640))
	{
		found640 = true;
		stuff_string(string_field_640, F_NAME, MAX_FILENAME_LEN);
	}
	if (optional_string(expected_string_1024))
	{
		found1024 = true;
		stuff_string(string_field_1024, F_NAME, MAX_FILENAME_LEN);
	}

	// error testing
	if (Fred_running && (found640) && !(found1024))
	{
		mprintf(("Mission: found an entry for %s but not a corresponding entry for %s!\n", expected_string_640, expected_string_1024));
	}
	if (Fred_running && !(found640) && (found1024))
	{
		mprintf(("Mission: found an entry for %s but not a corresponding entry for %s!\n", expected_string_1024, expected_string_640));
	}
}

void parse_mission_info(mission *pm, bool basic = false)
{
	char game_string[NAME_LENGTH];

	// Goober5000
	skip_to_start_of_string("#Mission Info");

	required_string("#Mission Info");
	
	required_string("$Version:");
	pm->required_fso_version = gameversion::parse_version_inline();

	if (!gameversion::check_at_least(pm->required_fso_version))
		throw parse::VersionException("Mission requires version " + gameversion::format_version(pm->required_fso_version), pm->required_fso_version);

	required_string("$Name:");
	stuff_string(pm->name, F_NAME);

	required_string("$Author:");
	stuff_string(pm->author, F_NAME);

	required_string("$Created:");
	stuff_string(pm->created, F_DATE, DATE_TIME_LENGTH);

	required_string("$Modified:");
	stuff_string(pm->modified, F_DATE, DATE_TIME_LENGTH);

	required_string("$Notes:");
	stuff_string(pm->notes, F_NOTES, NOTES_LENGTH);

	if (optional_string("$Mission Desc:"))
		stuff_string(pm->mission_desc, F_MULTITEXT, MISSION_DESC_LENGTH);		

	if ( optional_string("+Game Type:")) {
		// HACK HACK HACK -- stuff_string was changed to *not* ignore carriage returns.  Since the
		// old style missions may have carriage returns, deal with it here.
		ignore_white_space();
		stuff_string(game_string, F_NAME, NAME_LENGTH);
		for ( int i = 0; i < OLD_MAX_GAME_TYPES; i++ ) {
			if ( !stricmp(game_string, Old_game_types[i]) ) {

				// this block of code is now old mission compatibility code.  We specify game
				// type in a different manner than before.
				if ( i == OLD_GAME_TYPE_SINGLE_ONLY )
					pm->game_type = MISSION_TYPE_SINGLE;
				else if ( i == OLD_GAME_TYPE_MULTI_ONLY )
					pm->game_type = MISSION_TYPE_MULTI;
				else if ( i == OLD_GAME_TYPE_SINGLE_MULTI )
					pm->game_type = (MISSION_TYPE_SINGLE | MISSION_TYPE_MULTI );
				else if ( i == OLD_GAME_TYPE_TRAINING )
					pm->game_type = MISSION_TYPE_TRAINING;
				else
					Int3();

				if ( pm->game_type & MISSION_TYPE_MULTI )
					pm->game_type |= MISSION_TYPE_MULTI_COOP;

				break;
			}
		}
	}

	if ( optional_string("+Game Type Flags:") ) {
		stuff_int(&pm->game_type);
	}

	// multiplayer team v. team games have two teams.  If we have three teams, we need to use
	// a new mission mode!
	if ( (pm->game_type & MISSION_TYPE_MULTI) && (pm->game_type & MISSION_TYPE_MULTI_TEAMS) ){
		Num_teams = 2;
	}

	if (optional_string("+Flags:")){
        stuff_flagset(&pm->flags);
	}

	// nebula mission stuff
	if(optional_string("+NebAwacs:")){
		stuff_float(&Neb2_awacs);
	}
	if(optional_string("+Storm:")){
		stuff_string(Mission_parse_storm_name, F_NAME, NAME_LENGTH);

		if (!basic)
			nebl_set_storm(Mission_parse_storm_name);
	}

	bool found_neb2_modern = false;

	if (optional_string("+Fog Near Mult:")) {
		stuff_float(&Neb2_fog_legacy_near_mult);
	}
	if (optional_string("+Fog Far Mult:")) {
		stuff_float(&Neb2_fog_legacy_far_mult);
	}

	// Look for the modern values
	if (optional_string("+Fog 1000m Visibility:")) {
		stuff_float(&Neb2_fog_1000m_visibility);
		found_neb2_modern = true;
	}
	if (optional_string("+Fog Near Distance:")) {
		stuff_float(&Neb2_fog_near_distance);
		found_neb2_modern = true;
	}
	if (optional_string("+Fog Skybox Clip Distance:")) {
		stuff_float(&Neb2_fog_skybox_clip_distance);
		found_neb2_modern = true;
	}
	if (optional_string("+Fog Clip Distance:")) {
		stuff_float(&Neb2_fog_clip_distance);
		found_neb2_modern = true;
	}

	// write legacy values on save if we are compatible with older formats
	Neb2_fog_save_legacy_values = !found_neb2_modern;

	// Convert legacy values if this mission was made before modern values were introduced
	if (!found_neb2_modern) {
		//This stems from the weird unchangeable constants of legacy fog
		float denom = std::max(75.f * Neb2_fog_legacy_far_mult - Neb2_fog_legacy_near_mult, 1.0f);
		Neb2_fog_1000m_visibility = powf(10.f, -100.f / denom);
		Neb2_fog_near_distance = 10.f * Neb2_fog_legacy_near_mult;
		Neb2_fog_skybox_clip_distance = 0.f; // Apparently, skybox fog was just outright broken...
		Neb2_fog_clip_distance = Default_max_draw_distance;
	}

	// Goober5000 - ship contrail speed threshold
	if (optional_string("$Contrail Speed Threshold:")){
		stuff_int(&pm->contrail_threshold);
	}

	if (optional_string("+Large Ship Collision Group:")) {
		stuff_int(&pm->large_ship_no_collide_collision_group);

		if (pm->large_ship_no_collide_collision_group < 0 || pm->large_ship_no_collide_collision_group > 31) {
			WarningEx(LOCATION,
				"Invalid large ship collision group id %d specified. Valid IDs range from 0 to 31. Using group %d instead.\n",
				pm->large_ship_no_collide_collision_group,
				DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP);
			pm->large_ship_no_collide_collision_group = DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP;
		}
	}

	if (optional_string("+Volumetric Nebula:")) {
		pm->volumetrics.emplace().parse_volumetric_nebula();
	}

	// get the number of players if in a multiplayer mission
	if ( pm->game_type & MISSION_TYPE_MULTI ) {
		if ( optional_string("+Num Players:") ) {
			stuff_int( &(pm->num_players) );
		}
	}

	// get the number of respawns
	if ( pm->game_type & MISSION_TYPE_MULTI ) {
		if ( optional_string("+Num Respawns:") ){
			stuff_int( (int*)&(pm->num_respawns) );
		}
	}

	if ( pm->game_type & MISSION_TYPE_MULTI ) {
		if ( optional_string("+Max Respawn Time:") ){
			stuff_int( &The_mission.max_respawn_delay );
		}
	}

	if ( optional_string("+Red Alert:")) {
		int temp;
		stuff_int(&temp);

        pm->flags.set(Mission::Mission_Flags::Red_alert, temp != 0);    
    } 

	if ( optional_string("+Scramble:")) {
		int temp;
		stuff_int(&temp);

        pm->flags.set(Mission::Mission_Flags::Scramble, temp != 0);
	}

	// if we are just requesting basic info then skip everything else.  the reason
	// for this is to make sure that we don't modify things outside of the mission struct
	// that might not get reset afterwards (like what can happen in the techroom) - taylor
	//
	// NOTE: this can be dangerous so be sure that any get_mission_info() call (defaults to basic info) will
	//       only reference data parsed before this point!! (like current FRED2 and game code does)
	if (basic)
		return;

	if ( optional_string("+Disallow Support:"))
	{
		int temp;
		stuff_int(&temp);

		pm->support_ships.max_support_ships = (temp > 0) ? 0 : -1;
	}

	if (optional_string("+Disallow Support Rearm:")) {
		int temp;
		stuff_int(&temp);
		pm->support_ships.disallow_rearm = (temp != 0);
	}

	if (optional_string("+Allow Support Rearm Weapon Precedence:")) {
		int temp;
		stuff_int(&temp);
		pm->support_ships.allow_rearm_weapon_precedence = (temp != 0);
	}

	if ( optional_string("+Hull Repair Ceiling:"))
	{
		float temp;
		stuff_float(&temp);

		//ONLY set max_hull_repair_val if the value given is valid -C
		if (temp <= 100.0f && temp >= 0.0f) {
			pm->support_ships.max_hull_repair_val = temp;
		}
	}

	if ( optional_string("+Subsystem Repair Ceiling:"))
	{
		float temp;
		stuff_float(&temp);

		//ONLY set max_subsys_repair_val if the value given is valid -C
		if (temp <= 100.0f && temp >= 0.0f) {
			pm->support_ships.max_subsys_repair_val = temp;
		}
	}

	if (optional_string("+Support Rearm Pool From Loadout:")) {
		int temp;
		stuff_int(&temp);
		pm->support_ships.rearm_pool_from_loadout = (temp != 0);
	}

	if (optional_string("+All Teams Attack")){
		Mission_all_attack = 1;
	}

	//	Maybe delay the player's entry.
	if (optional_string("+Player Entry Delay:")) {
		float	temp;
		
		stuff_float(&temp);
		Assert(temp >= 0.0f);
		Entry_delay_time = fl2f(temp);
	}

	if (optional_string("+Viewer pos:")){
		stuff_vec3d(&Parse_viewer_pos);
	}

	if (optional_string("+Viewer orient:")){
		stuff_matrix(&Parse_viewer_orient);
	}

	// possible squadron reassignment (only in single player)
	if(optional_string("+SquadReassignName:")){
		char buf[std::max(NAME_LENGTH, MAX_FILENAME_LEN)];

		stuff_string(buf, F_NAME, NAME_LENGTH);
		if (!(Game_mode & GM_MULTIPLAYER))
			strcpy_s(pm->squad_name, buf);

		if(optional_string("+SquadReassignLogo:")){
			stuff_string(buf, F_NAME, MAX_FILENAME_LEN);
			if (!(Game_mode & GM_MULTIPLAYER))
				strcpy_s(pm->squad_filename, buf);
		}
	}

	// wing stuff by Goober5000 ------------------------------------------
	// the wing name arrays are initialized in ship_level_init
	if (optional_string("$Starting wing names:"))
	{
		stuff_string_list(Starting_wing_names, MAX_STARTING_WINGS);
	}

	if (optional_string("$Squadron wing names:"))
	{
		stuff_string_list(Squadron_wing_names, MAX_SQUADRON_WINGS);
	}

	if (optional_string("$Team-versus-team wing names:"))
	{
		stuff_string_list(TVT_wing_names, MAX_TVT_WINGS);
	}
	// end of wing stuff -------------------------------------------------


	// Goober5000 - made this into a function since we use much the same technique for the briefing background
	parse_custom_bitmap("$Load Screen 640:", "$Load Screen 1024:", pm->loading_screen[GR_640], pm->loading_screen[GR_1024]);

	if (optional_string("$Skybox Model:"))
	{
		stuff_string(pm->skybox_model, F_NAME, MAX_FILENAME_LEN);

		if (optional_string("$Skybox Model Animations:")) {
			animation::ModelAnimationParseHelper::parseAnimsetInfo(pm->skybox_model_animations, 'b', pm->name);
		}
		if (optional_string("$Skybox Model Moveables:")) {
			animation::ModelAnimationParseHelper::parseMoveablesetInfo(pm->skybox_model_animations);
		}
	}

	if (optional_string("+Skybox Orientation:"))
	{
		stuff_matrix(&pm->skybox_orientation);
	}

	if (optional_string("+Skybox Flags:")){
		pm->skybox_flags = 0;
		stuff_int(&pm->skybox_flags); 
	}

	// Goober5000 - AI on a per-mission basis
	if (optional_string("$AI Profile:"))
	{
		int index;
		char temp[NAME_LENGTH];

		stuff_string(temp, F_NAME, NAME_LENGTH);
		index = ai_profile_lookup(temp);

		if (index >= 0)
			The_mission.ai_profile = &Ai_profiles[index];
		else
			WarningEx(LOCATION, "Mission: %s\nUnknown AI profile %s!", pm->name.c_str(), temp );
	}

	if (optional_string("$Lighting Profile:"))
	{
		stuff_string(The_mission.lighting_profile_name, F_NAME);
		if (The_mission.lighting_profile_name.empty())
			The_mission.lighting_profile_name = lighting_profiles::default_name();
	}
	else
		The_mission.lighting_profile_name = lighting_profiles::default_name();
	lighting_profiles::switch_to(The_mission.lighting_profile_name);

	if (optional_string("$Sound Environment:")) {
		char preset[65] = { '\0' };
		stuff_string(preset, F_NAME, sizeof(preset)-1);

		int preset_id = ds_eax_get_preset_id(preset);

		if (preset_id >= 0) {
			sound_env_get(&pm->sound_environment, preset_id);
		}

		// NOTE: values will be clamped properly when the effect is actually set

		if (optional_string("+Volume:")) {
			stuff_float(&pm->sound_environment.volume);
		}

		if (optional_string("+Damping:")) {
			stuff_float(&pm->sound_environment.damping);
		}

		if (optional_string("+Decay Time:")) {
			stuff_float(&pm->sound_environment.decay);
		}
	}

	float gravity_accel = 0.0f;
	if (optional_string("$Gravity Acceleration:")) {
		stuff_float(&gravity_accel);
		pm->gravity = vm_vec_new(0.0f, -gravity_accel, 0.0f);
	}
}

void parse_player_info(mission *pm)
{
	char temp[NAME_LENGTH];
	Assert(pm != NULL);

	// alternate type names begin here
	if(optional_string("#Alternate Types:")){		
		// read them all in
		while(!optional_string("#end")){
			required_string("$Alt:");
			stuff_string(temp, F_NAME, NAME_LENGTH);

			// maybe store it
			mission_parse_add_alt(temp);			
		}
	}
	
	// callsigns begin here
	if(optional_string("#Callsigns:")){		
		// read them all in
		while(!optional_string("#end")){
			required_string("$Callsign:");
			stuff_string(temp, F_NAME, NAME_LENGTH);

			// maybe store it
			mission_parse_add_callsign(temp);			
		}
	}
	
	required_string("#Players");

	// starting general orders go here
	ai_lua_reset_general_orders();

	if (optional_string("+General Orders Enabled:")) {
		SCP_vector<SCP_string> accepted_flags;
		stuff_string_list(accepted_flags);

		for (const SCP_string& accepted : accepted_flags) {
			int lua_order_id = ai_lua_find_general_order_id(accepted);

			if (lua_order_id >= 0) {
				ai_lua_enable_general_order(lua_order_id, true);
			}
		}
	}
	if (optional_string("+General Orders Valid:")) {
		SCP_vector<SCP_string> accepted_flags;
		stuff_string_list(accepted_flags);

		for (const SCP_string& accepted : accepted_flags) {
			int lua_order_id = ai_lua_find_general_order_id(accepted);

			if (lua_order_id >= 0) {
				ai_lua_validate_general_order(lua_order_id, true);
			}
		}
	}

	while (required_string_either("#Objects", "$")){
		parse_player_info2(pm);
	}
}

void parse_player_info2(mission *pm)
{
	int nt, i;
	SCP_vector<loadout_row> list, list2, support_rearm_list;
	team_data *ptr;

	if (OnLoadoutAboutToParseHook->isActive()) {
		OnLoadoutAboutToParseHook->run();
	}

	// The support rearm pool starts at -1 (unlimited) per-weapon from support_ship_info::reset().
	// Entries are overridden below for explicit "+Support Rearm Pool:" data, or seeded from the
	// loadout when "+Support Rearm Pool From Loadout:" is set.

	// read in a ship/weapon pool for each team.
	for ( nt = 0; nt < Num_teams; nt++ ) {
		int num_choices;

		ptr = &Team_data[nt];
		// get the shipname for single player missions
		// MWA -- make this required later!!!!
		if ( optional_string("$Starting Shipname:") )
			stuff_string( Player_start_shipname, F_NAME, NAME_LENGTH );

		if (optional_string("+Do Not Validate Loadout")) {
			ptr->do_not_validate = true;
		} else {
			ptr->do_not_validate = false;
		}

		required_string("$Ship Choices:");
		stuff_loadout_list(list, ParseLookupType::MISSION_LOADOUT_SHIP_LIST);

		num_choices = 0;

		// check ship class loadout entries
		for (auto &sc : list) {
			// in a campaign, see if the player is allowed the ships or not.  Remove them from the
			// pool if they are not allowed
			if (Game_mode & GM_CAMPAIGN_MODE || (MULTIPLAYER_CLIENT)) {
				if ( !Campaign.ships_allowed[sc.index] )
					continue;
			}
			if (sc.index < 0 || sc.index >= ship_info_size())
				continue;

			ptr->ship_list[num_choices] = sc.index;

			// if the list isn't set by a variable leave the variable name empty
			if (sc.index_sexp_var == NOT_SET_BY_SEXP_VARIABLE) {
				strcpy_s(ptr->ship_list_variables[num_choices], "") ;
			}
			else {
				strcpy_s(ptr->ship_list_variables[num_choices], Sexp_variables[sc.index_sexp_var].variable_name);
			}

			ptr->ship_count[num_choices] = sc.count;
			ptr->loadout_total += sc.count;

			// if the list isn't set by a variable leave the variable name empty
			if (sc.count_sexp_var == NOT_SET_BY_SEXP_VARIABLE) {
				strcpy_s(ptr->ship_count_variables[num_choices], "");
			}
			else {
				strcpy_s(ptr->ship_count_variables[num_choices], Sexp_variables[sc.count_sexp_var].variable_name);
			}

			num_choices++;
		}
		ptr->num_ship_choices = num_choices;

		ptr->default_ship = -1;
		if (optional_string("+Default_ship:")) {
			char str[NAME_LENGTH];
			stuff_string(str, F_NAME, NAME_LENGTH);
			ptr->default_ship = ship_info_lookup(str);
			if (-1 == ptr->default_ship) {
				WarningEx(LOCATION, "Mission: %s\nUnknown default ship %s!  Defaulting to %s.", pm->name.c_str(), str, Ship_info[ptr->ship_list[0]].name );
				ptr->default_ship = ptr->ship_list[0]; // default to 1st in list
			}
			// see if the player's default ship is an allowable ship (campaign only). If not, then what
			// do we do?  choose the first allowable one?
			if (Game_mode & GM_CAMPAIGN_MODE || (MULTIPLAYER_CLIENT)) {
				if ( !(Campaign.ships_allowed[ptr->default_ship]) ) {
					for (i = 0; i < ship_info_size(); i++ ) {
						if ( Campaign.ships_allowed[i] ) {
							ptr->default_ship = i;
							break;
						}
					}
					Assertion( i < ship_info_size(), "Mission: %s: Could not find a valid default ship.\n", pm->name.c_str() );
				}
			}
		}

		if (ptr->default_ship == -1)  // invalid or not specified, make first in list
			ptr->default_ship = ptr->ship_list[0];

		required_string("+Weaponry Pool:");
		stuff_loadout_list(list2, ParseLookupType::MISSION_LOADOUT_WEAPON_LIST);

		num_choices = 0;

		// When seeding the rearm pool from the loadout, reset this team's row to 0 so the per-weapon
		// loadout counts below accumulate from a clean baseline (rather than the -1 "unlimited" default).
		if (pm->support_ships.rearm_pool_from_loadout) {
			for (int wep = 0; wep < MAX_WEAPON_TYPES; ++wep) {
				pm->support_ships.rearm_weapon_pool[nt][wep] = 0;
			}
		}

		// check weapon class loadout entries
		for (auto &wc : list2) {
			// in a campaign, see if the player is allowed the weapons or not.  Remove them from the
			// pool if they are not allowed
			if (Game_mode & GM_CAMPAIGN_MODE || (MULTIPLAYER_CLIENT)) {
				if ( !Campaign.weapons_allowed[wc.index] ) {
					continue;
				}
			}
			if (wc.index < 0 || wc.index >= weapon_info_size())
				continue;

			// always allow the pool to be added in FRED, it is a verbal warning
			// to let the mission dev know about the problem
			if ( !(Weapon_info[wc.index].wi_flags[Weapon::Info_Flags::Player_allowed]) && !Fred_running ) {
				error_display(0, "Weapon '%s' in weapon pool isn't allowed on player loadout! Ignoring it ...\n", Weapon_info[wc.index].name);
				continue;
			}

			ptr->weaponry_pool[num_choices] = wc.index; 
			ptr->weaponry_count[num_choices] = wc.count;
			if (pm->support_ships.rearm_pool_from_loadout) {
				if (Weapon_info[wc.index].disallow_rearm) {
					pm->support_ships.rearm_weapon_pool[nt][wc.index] = 0;
				} else if (wc.count > 0 && pm->support_ships.rearm_weapon_pool[nt][wc.index] >= 0) {
					pm->support_ships.rearm_weapon_pool[nt][wc.index] += wc.count;
				}
			}

			// if the list isn't set by a variable leave the variable name empty
			if (wc.index_sexp_var == NOT_SET_BY_SEXP_VARIABLE) {
				strcpy_s(ptr->weaponry_pool_variable[num_choices], "");
			}
			else {
				strcpy_s(ptr->weaponry_pool_variable[num_choices], Sexp_variables[wc.index_sexp_var].variable_name);
			}

			// if the list isn't set by a variable leave the variable name empty
			if (wc.count_sexp_var == NOT_SET_BY_SEXP_VARIABLE) {
				strcpy_s(ptr->weaponry_amount_variable[num_choices], "");
			}
			else {
				strcpy_s(ptr->weaponry_amount_variable[num_choices], Sexp_variables[wc.count_sexp_var].variable_name);
			}

			num_choices++; 
		}
		ptr->num_weapon_choices = num_choices;

		if (optional_string("+Support Rearm Pool:")) {
			support_rearm_list.clear();
			stuff_loadout_list(support_rearm_list, ParseLookupType::MISSION_LOADOUT_WEAPON_LIST);

			if (pm->support_ships.rearm_pool_from_loadout) {
				error_display(0, "+Support Rearm Pool is set but +Support Rearm Pool From Loadout is also enabled! The explicit pool will be ignored.\n");
			} else {
				for (const auto& wc : support_rearm_list) {
					if (wc.index < 0 || wc.index >= weapon_info_size()) {
						continue;
					}

					if (!(Weapon_info[wc.index].wi_flags[Weapon::Info_Flags::Player_allowed]) && !Fred_running) {
						error_display(0,
							"Weapon '%s' in support rearm pool isn't allowed on player loadout! Ignoring it ...\n",
							Weapon_info[wc.index].name);
						continue;
					}

					auto& slot = pm->support_ships.rearm_weapon_pool[nt][wc.index];
					if (Weapon_info[wc.index].disallow_rearm) {
						slot = 0;
					} else if (wc.count < 0) {
						slot = -1;
					} else if (wc.count == 0) {
						slot = 0;
					} else if (wc.count > 0) {
						// First explicit entry replaces the -1 (unlimited) default; later duplicate entries accumulate.
						slot = (slot < 0) ? wc.count : slot + wc.count;
					}
				}
			}
		}

		memset(ptr->weapon_required, 0, MAX_WEAPON_TYPES * sizeof(bool));
		if (optional_string("+Required for mission:"))
		{
			int num_weapons;
			int weapon_list_buf[MAX_WEAPON_TYPES];
			num_weapons = sz2i(stuff_int_list(weapon_list_buf, MAX_WEAPON_TYPES, ParseLookupType::WEAPON_LIST_TYPE));

			for (i = 0; i < num_weapons; i++)
				ptr->weapon_required[weapon_list_buf[i]] = true;
		}
	}

	if ( nt != Num_teams )
		Error(LOCATION, "Not enough ship/weapon pools for mission.  There are %d teams and only %d pools.", Num_teams, nt);
}

void parse_cutscenes(mission *pm) 
{
	if (optional_string("#Cutscenes"))
	{
		mission_cutscene scene;

		while (!optional_string("#end"))
		{
			// this list should correspond to the MOVIE_* enums
			scene.type = optional_string_one_of(7,
				"$Fiction Viewer Cutscene:",
				"$Command Brief Cutscene:",
				"$Briefing Cutscene:",
				"$Pre-game Cutscene:",
				"$Debriefing Cutscene:",
				"$Post-debriefing Cutscene:",
				"$Campaign End Cutscene:");

			// Didn't find one of the valid cutscene types
			if (scene.type < 0) {

				// $Post-briefing cutscene was added as an alias of $Pre-game cutscene when
				// this section was only editable by hand. Now that there's a dialog that explains
				// what each option is, we can drop the alias and quietly convert it if it's found
				// for backwards compatibility. Log print just in case.
				if (optional_string("$Post-briefing Cutscene:")) {
					scene.type = MOVIE_PRE_GAME;
					mprintf(("Found cutscene defined as '$Post-briefing Cutscene' and converted it to '$Pre-game cutscene'\n"));
				} else {
					// no more cutscenes specified?
					break;
				}
			}

			// get the cutscene file
			stuff_string(scene.filename, F_NAME, NAME_LENGTH);

			// get the sexp if we have one
			if (optional_string("+formula:"))
				scene.formula = get_sexp_main();
			else
				scene.formula = Locked_sexp_true;

			// add it
			pm->cutscenes.push_back(scene);
		}
	}
}

void parse_plot_info(mission * /*pm*/)
{
	if (optional_string("#Plot Info"))
	{
		char dummy_filespec[FILESPEC_LENGTH];
		char dummy_name[NAME_LENGTH];

		required_string("$Tour:");
		stuff_string(dummy_name, F_NAME, NAME_LENGTH);

		required_string("$Pre-Briefing Cutscene:");
		stuff_string(dummy_filespec, F_FILESPEC, FILESPEC_LENGTH);

		required_string("$Pre-Mission Cutscene:");
		stuff_string(dummy_filespec, F_FILESPEC, FILESPEC_LENGTH);

		required_string("$Next Mission Success:");
		stuff_string(dummy_name, F_NAME, NAME_LENGTH);

		required_string("$Next Mission Partial:");
		stuff_string(dummy_name, F_NAME, NAME_LENGTH);

		required_string("$Next Mission Failure:");
		stuff_string(dummy_name, F_NAME, NAME_LENGTH);
	}
}

void parse_briefing_info(mission * /*pm*/)
{
	char junk[4096];

	if ( !optional_string("#Briefing Info") )
		return;

	required_string("$Briefing Voice 1:");
	stuff_string(junk, F_FILESPEC, sizeof(junk));

	required_string("$Briefing Text 1:");
	stuff_string(junk, F_MULTITEXTOLD, sizeof(junk));

	required_string("$Briefing Voice 2:");
	stuff_string(junk, F_FILESPEC, sizeof(junk));

	required_string("$Briefing Text 2:");
	stuff_string(junk, F_MULTITEXTOLD, sizeof(junk));

	required_string("$Briefing Voice 3:");
	stuff_string(junk, F_FILESPEC, sizeof(junk));

	required_string("$Briefing Text 3:");
	stuff_string(junk, F_MULTITEXTOLD, sizeof(junk));

	required_string("$Debriefing Voice 1:");
	stuff_string(junk, F_FILESPEC, sizeof(junk));

	required_string("$Debriefing Text 1:");
	stuff_string(junk, F_MULTITEXTOLD, sizeof(junk));

	required_string("$Debriefing Voice 2:");
	stuff_string(junk, F_FILESPEC, sizeof(junk));

	required_string("$Debriefing Text 2:");
	stuff_string(junk, F_MULTITEXTOLD, sizeof(junk));

	required_string("$Debriefing Voice 3:");
	stuff_string(junk, F_FILESPEC, sizeof(junk));

	required_string("$Debriefing Text 3:");
	stuff_string(junk, F_MULTITEXTOLD, sizeof(junk));
}

/**
 * Parse the event music and briefing music for the mission
 */
void parse_music(mission *pm, int flags)
{
	int index;
	char *ch;
	char temp[NAME_LENGTH];

	if ( !optional_string("#Music") ) {
		return;
	}

	required_string("$Event Music:");
	stuff_string(pm->event_music_name, F_NAME, NAME_LENGTH);

	// Goober5000
	if (optional_string("$Substitute Event Music:"))
		stuff_string(pm->substitute_event_music_name, F_NAME, NAME_LENGTH);

	required_string("$Briefing Music:");
	stuff_string(pm->briefing_music_name, F_NAME, NAME_LENGTH);

	// Goober5000
	if (optional_string("$Substitute Briefing Music:"))
		stuff_string(pm->substitute_briefing_music_name, F_NAME, NAME_LENGTH);

	// old stuff, apparently
	if (optional_string("$Debriefing Success Music:"))
	{
		stuff_string(temp, F_NAME, NAME_LENGTH);
		index = event_music_get_spooled_music_index(temp);
		if ((index >= 0) && ((Spooled_music[index].flags & SMF_VALID) || Fred_running)) {
			event_music_set_score(SCORE_DEBRIEFING_SUCCESS, temp);
		}
	}

	// not old, just added since it makes sense
	if (optional_string("$Debriefing Average Music:"))
	{
		stuff_string(temp, F_NAME, NAME_LENGTH);
		index = event_music_get_spooled_music_index(temp);
		if ((index >= 0) && ((Spooled_music[index].flags & SMF_VALID) || Fred_running)) {
			event_music_set_score(SCORE_DEBRIEFING_AVERAGE, temp);
		}
	}

	// old stuff
	if (optional_string("$Debriefing Fail Music:") || optional_string("$Debriefing Failure Music:"))
	{
		stuff_string(temp, F_NAME, NAME_LENGTH);
		index = event_music_get_spooled_music_index(temp);
		if ((index >= 0) && ((Spooled_music[index].flags & SMF_VALID) || Fred_running)) {
			event_music_set_score(SCORE_DEBRIEFING_FAILURE, temp);
		}
	}

	// new stuff
	if (optional_string("$Fiction Viewer Music:"))
	{
		stuff_string(temp, F_NAME, NAME_LENGTH);
		event_music_set_score(SCORE_FICTION_VIEWER, temp);
	}

	// Goober5000 - if briefing not specified in import, default to BRIEF1
	if (!stricmp(pm->briefing_music_name, "none") && (flags & MPF_IMPORT_FSM))
		strcpy_s(pm->briefing_music_name, "BRIEF1");

	// Goober5000 - old way of grabbing substitute music, but here for reverse compatibility
	if (optional_string("$Substitute Music:"))
	{
		stuff_string(pm->substitute_event_music_name, F_NAME, NAME_LENGTH, "," );
		Mp++;
		stuff_string(pm->substitute_briefing_music_name, F_NAME, NAME_LENGTH);
	}

	// Goober5000 - if the mission is being imported, the substitutes are the specified tracks
	// (with FS1 prefixes) and we generate new specified tracks
	if (flags & MPF_IMPORT_FSM)
	{
		// no specified music?
		if (!stricmp(pm->event_music_name, "none"))
			goto done_event_music;

		// set the FS1 equivalent as the substitute
		strcpy_s(pm->substitute_event_music_name, "FS1-");
		strcat_s(pm->substitute_event_music_name, pm->event_music_name);

		// if we have Marauder, it's in FS2 as Deuteronomy, so we're done
		if (!stricmp(pm->event_music_name, "7: Marauder") && event_music_get_soundtrack_index("5: Deuteronomy") >= 0)
		{
			strcpy_s(pm->event_music_name, "5: Deuteronomy");
			goto done_event_music;
		}

		// search for something with the same track number
		strcpy_s(temp, pm->event_music_name);
		ch = strchr(temp, ':');
		if (ch != NULL)
		{
			*(ch + 1) = '\0';
				
			for (auto &st: Soundtracks)
			{
				if (!strncmp(temp, st.name, strlen(temp)))
				{
					strcpy_s(pm->event_music_name, st.name);
					goto done_event_music;
				}
			}
		}

		if (!Soundtracks.empty())
		{
			// last resort: pick a random track out of the 7 FS2 soundtracks
			int num = std::max((int)Soundtracks.size(), 7);
			strcpy_s(pm->event_music_name, Soundtracks[Random::next(num)].name);
		}


done_event_music:


		// no specified music?
		if (!stricmp(pm->briefing_music_name, "none"))
			goto done_briefing_music;

		// set the FS1 equivalent as the substitute
		strcpy_s(pm->substitute_briefing_music_name, "FS1-");
		strcat_s(pm->substitute_briefing_music_name, pm->briefing_music_name);

		// Choco Mousse is the FS1 title soundtrack, so use Aquitaine in FS2
		if (!stricmp(pm->briefing_music_name, "Choco Mousse") && event_music_get_spooled_music_index("Aquitaine") >= 0)
		{
			strcpy_s(pm->briefing_music_name, "Aquitaine");
			goto done_briefing_music;
		}

		// we might have a match with the same track number
		if (event_music_get_spooled_music_index(pm->briefing_music_name) >= 0)
			goto done_briefing_music;

		if (!Spooled_music.empty())
		{
			// last resort: pick a random track out of the first 7 FS2 briefings (the regular ones)...
			int num = std::max((int)Spooled_music.size(), 7);
			strcpy_s(pm->briefing_music_name, Spooled_music[Random::next(num)].name);
		}


done_briefing_music:

		/* NO-OP */ ;
	}


	// set the soundtrack, preferring the substitute in FS2 (not FRED!)
	index = event_music_get_soundtrack_index(pm->substitute_event_music_name);
	if ((index >= 0) && (Soundtracks[index].flags & EMF_VALID) && !Fred_running)
	{
		event_music_set_soundtrack(pm->substitute_event_music_name);
	}
	else
	{
		event_music_set_soundtrack(pm->event_music_name);
	}

	// set the briefing, preferring the substitute in FS2 (not FRED!)
	index = event_music_get_spooled_music_index(pm->substitute_briefing_music_name);
	if ((index >= 0) && (Spooled_music[index].flags & SMF_VALID) && !Fred_running)
	{
		event_music_set_score(SCORE_BRIEFING, pm->substitute_briefing_music_name);
	}
	else
	{
		event_music_set_score(SCORE_BRIEFING, pm->briefing_music_name);
	}
}

/**
 * Parse fiction viewer
 */
void parse_fiction(mission * /*pm*/)
{
	if (optional_string("#Fiction Viewer"))
	{
		bool fiction_viewer_loaded = false;

		while (check_for_string("$File:"))
		{
			fiction_viewer_stage stage;
			memset(&stage, 0, sizeof(fiction_viewer_stage));
			stage.formula = Locked_sexp_true;

			required_string("$File:");
			stuff_string(stage.story_filename, F_FILESPEC, MAX_FILENAME_LEN);

			if (optional_string("$Font:"))
				stuff_string(stage.font_filename, F_FILESPEC, MAX_FILENAME_LEN);

			if (optional_string("$Voice:"))
				stuff_string(stage.voice_filename, F_FILESPEC, MAX_FILENAME_LEN);

			if (optional_string("$UI:"))
				stuff_string(stage.ui_name, F_NAME, NAME_LENGTH);

			parse_custom_bitmap("$Background 640:", "$Background 1024:", stage.background[GR_640], stage.background[GR_1024]);

			// get the sexp if we have one
			if (optional_string("$Formula:"))
				stage.formula = get_sexp_main();

			if (strlen(stage.story_filename) > 0)
			{
				// now, store this stage
				Fiction_viewer_stages.push_back(stage);

				// see if this is the stage we want to display, then display it
				if (!Fred_running && !fiction_viewer_loaded && is_sexp_true(stage.formula))
				{
					fiction_viewer_load((int)(Fiction_viewer_stages.size() - 1));
					fiction_viewer_loaded = true;
				}
			}
			else
			{
				error_display(0, "Fiction viewer story filename may not be empty!");
			}
		}
	}
}

/**
 * Parse command briefing
 */
void parse_cmd_brief(mission * /*pm*/)
{
	int stage;

	Assert(!Cur_cmd_brief->num_stages);
	stage = 0;

	required_string("#Command Briefing");

	// Yarn - use the same code as for mission loading screens
	parse_custom_bitmap("$Background 640:", "$Background 1024:", Cur_cmd_brief->background[GR_640], Cur_cmd_brief->background[GR_1024]);

	while (optional_string("$Stage Text:")) {
		Assert(stage < CMD_BRIEF_STAGES_MAX);
		stuff_string(Cur_cmd_brief->stage[stage].text, F_MULTITEXT, NULL);

		required_string("$Ani Filename:");
		stuff_string(Cur_cmd_brief->stage[stage].ani_filename, F_FILESPEC, MAX_FILENAME_LEN);
		if (optional_string("+Wave Filename:"))
			stuff_string(Cur_cmd_brief->stage[stage].wave_filename, F_FILESPEC, MAX_FILENAME_LEN);
		else
			Cur_cmd_brief->stage[stage].wave_filename[0] = 0;

		stage++;
	}

	Cur_cmd_brief->num_stages = stage;
}

void parse_cmd_briefs(mission *pm)
{
	int i;

	// a hack follows because old missions don't have a command briefing
	if (required_string_either("#Command Briefing", "#Briefing"))
		return;

	for (i=0; i<Num_teams; i++) {
		Cur_cmd_brief = &Cmd_briefs[i];
		parse_cmd_brief(pm);
	}
}

/**
 * Parse the data required for the mission briefing
 *
 * NOTE: This updates the global Briefing struct with all the data necessary to drive the briefing
 */
void parse_briefing(mission * /*pm*/, int flags)
{
	int nt, i, j, stage_num = 0, icon_num = 0;
	brief_stage *bs;
	brief_icon *bi;
	briefing *bp;

	char not_used_text[MAX_ICON_TEXT_LEN];
	
	// MWA -- 2/3/98.  we can now have multiple briefing and debriefings in a mission
	for ( nt = 0; nt < Num_teams; nt++ ) {
		if ( !optional_string("#Briefing") )
			break;

		bp = &Briefings[nt];
		required_string("$start_briefing");

		// Goober5000 - use the same code as for mission loading screens
		parse_custom_bitmap("$briefing_background_640:", "$briefing_background_1024:", bp->background[GR_640], bp->background[GR_1024]);
		parse_custom_bitmap("$ship_select_background_640:", "$ship_select_background_1024:", bp->ship_select_background[GR_640], bp->ship_select_background[GR_1024]);
		parse_custom_bitmap("$weapon_select_background_640:", "$weapon_select_background_1024:", bp->weapon_select_background[GR_640], bp->weapon_select_background[GR_1024]);

		required_string("$num_stages:");
		stuff_int(&bp->num_stages);
		Assert(bp->num_stages <= MAX_BRIEF_STAGES);

		stage_num = 0;
		while (required_string_either("$end_briefing", "$start_stage")) {
			required_string("$start_stage");
			Assert(stage_num < MAX_BRIEF_STAGES);

			if (stage_num >= bp->num_stages) {
				error_display(1,
							  "$num_stages did not match the number of specified stages! %d stages were specified but there is at least one more.",
							  bp->num_stages);
			}

			bs = &bp->stages[stage_num++];
			required_string("$multi_text");
			stuff_string(bs->text, F_MULTITEXT, NULL);
			required_string("$voice:");
			stuff_string(bs->voice, F_FILESPEC, MAX_FILENAME_LEN);
			required_string("$camera_pos:");
			stuff_vec3d(&bs->camera_pos);
			required_string("$camera_orient:");
			stuff_matrix(&bs->camera_orient);
			required_string("$camera_time:");
			stuff_int(&bs->camera_time);

			if (optional_string("$no_grid"))
				bs->draw_grid = false;

			if (optional_string("$grid_color:")) {
				int rgba[4] = {0, 0, 0, 0};
				stuff_int_list(rgba, 4, ParseLookupType::RAW_INTEGER_TYPE);
				gr_init_alphacolor(&bs->grid_color, rgba[0], rgba[1], rgba[2], rgba[3]);
			} else {
				bs->grid_color = Color_briefing_grid;
			}

			if ( optional_string("$num_lines:") ) {
				stuff_int(&bs->num_lines);

				if ( Fred_running )	{
					Assert(bs->lines!=NULL);
				} else {
					if ( bs->num_lines > 0 )	{
						bs->lines = (brief_line *)vm_malloc(sizeof(brief_line)*bs->num_lines);
						Assert(bs->lines!=NULL);
					}
				}

				for (i=0; i<bs->num_lines; i++) {
					required_string("$line_start:");
					stuff_int(&bs->lines[i].start_icon);
					required_string("$line_end:");
					stuff_int(&bs->lines[i].end_icon);
				}
			}
			else {
				bs->num_lines = 0;
			}

			required_string("$num_icons:");
			stuff_int(&bs->num_icons);

			if ( Fred_running )	{
				Assert(bs->icons!=NULL);
			} else {
				if ( bs->num_icons > 0 )	{
					bs->icons = (brief_icon *)vm_malloc(sizeof(brief_icon)*bs->num_icons);
					Assert(bs->icons!=NULL);
				}
			}

			if ( optional_string("$flags:") )
				stuff_int(&bs->flags);
			else
				bs->flags = 0;

			if ( optional_string("$formula:") )
				bs->formula = get_sexp_main();
			else
				bs->formula = Locked_sexp_true;

			Assert(bs->num_icons <= MAX_STAGE_ICONS );

			// static alias stuff - stupid, but it seems to be necessary
			auto temp_team_names = std::unique_ptr<const char* []>(new const char*[Iff_info.size()]);
			for (i = 0; i < (int)Iff_info.size(); i++)
				temp_team_names[i] = Iff_info[i].iff_name;

			while (required_string_either("$end_stage", "$start_icon"))
			{
				required_string("$start_icon");
				Assert(icon_num < MAX_STAGE_ICONS);
				// Make sure we don't cause a buffer overflow if $num_icons is wrong
				if (icon_num >= bs->num_icons) {
					error_display(1,
								  "$num_icons did not match the number of specified icons! %d icons were specified but there is at least one more.",
								  bs->num_icons);
				}
				bi = &bs->icons[icon_num++];

				required_string("$type:");
				stuff_int(&bi->type);

				// Goober5000 - import
				if (flags & MPF_IMPORT_FSM)
				{
					// someone changed the jump node icon to a Knossos, so change it back
					if (bi->type == ICON_KNOSSOS_DEVICE)
						bi->type = ICON_JUMP_NODE;

					// change largeship to transport
					else if (bi->type == ICON_LARGESHIP)
						bi->type = ICON_TRANSPORT;

					// ditto
					else if (bi->type == ICON_LARGESHIP_WING)
						bi->type = ICON_TRANSPORT_WING;
				}

				find_and_stuff("$team:", &bi->team, F_NAME, temp_team_names.get(), Iff_info.size(), "team name");

				find_and_stuff("$class:", &bi->ship_class, F_NAME, Ship_class_names, Ship_info.size(), "ship class");
				bi->modelnum = -1;
				bi->model_instance_num = -1;

				// Goober5000 - import
				if (flags & MPF_IMPORT_FSM)
				{
					// the Faustus is a largeship
					if (!strnicmp(Ship_info[bi->ship_class].name, "GTSC Faustus", 12))
					{
						if (bi->type == ICON_CRUISER)
							bi->type = ICON_LARGESHIP;

						else if (bi->type == ICON_CRUISER_WING)
							bi->type = ICON_LARGESHIP_WING;
					}
					// the Hades is a supercap
					else if (!strnicmp(Ship_info[bi->ship_class].name, "GTD Hades", 9))
					{
						bi->type = ICON_SUPERCAP;
					}
				}

				required_string("$pos:");
				stuff_vec3d(&bi->pos);

				bi->label[0] = 0;
				if (optional_string("$label:"))
					stuff_string(bi->label, F_MESSAGE, MAX_LABEL_LEN);
				bi->closeup_label[0] = 0;
				if (optional_string("$closeup label:")) {
					stuff_string(bi->closeup_label, F_MESSAGE, MAX_LABEL_LEN);
				}

				bi->scale_factor = 1.0f;
				if (optional_string("$icon scale:")) {
					int scale;
					stuff_int(&scale);
					bi->scale_factor = scale / 100.0f;
				}

				if (optional_string("+id:")) {
					stuff_int(&bi->id);
					if (bi->id >= Cur_brief_id)
						Cur_brief_id = bi->id + 1;

				} else {
					bi->id = -1;
					for (i=0; i<stage_num-1; i++)
						for (j=0; j < bp->stages[i].num_icons; j++)
						{
							if (!stricmp(bp->stages[i].icons[j].label, bi->label))
								bi->id = bp->stages[i].icons[j].id;
						}

					if (bi->id < 0)
						bi->id = Cur_brief_id++;
				}

				bi->flags=0;
				int val;
				required_string("$hlight:");	
				stuff_int(&val);
				if ( val>0 ) {
					bi->flags |= BI_HIGHLIGHT;
				}

				if (optional_string("$mirror:"))
				{
					stuff_int(&val);
					if ( val>0 ) {
						bi->flags |= BI_MIRROR_ICON;
					}	
				}

				if (optional_string("$use wing icon:"))
				{
					stuff_int(&val);
					if ( val>0 ) {
						bi->flags |= BI_USE_WING_ICON;
					}
				}

				if (optional_string("$use cargo icon:")) {
					stuff_int(&val);
					if (val > 0) {
						bi->flags |= BI_USE_CARGO_ICON;
					}
				}

				required_string("$multi_text");
				stuff_string(not_used_text, F_MULTITEXT, MAX_ICON_TEXT_LEN);
				required_string("$end_icon");
			} // end while

			if (icon_num != bs->num_icons) {
				error_display(1,
							  "$num_icons did not match the number of specified icons! %d icons were specified but only %d were parsed.",
							  bs->num_icons,
							  icon_num);
			}
			icon_num = 0;
			required_string("$end_stage");
		}	// end while
		if (stage_num != bp->num_stages) {
			error_display(1,
						  "$num_stages did not match the number of specified icons! %d stages were specified but only %d were parsed.",
						  bp->num_stages,
						  stage_num);
		}
		required_string("$end_briefing");
	}

	if ( nt != Num_teams )
		Error(LOCATION, "Not enough briefings in mission file.  There are %d teams and only %d briefings.", Num_teams, nt );
}

/**
 * Parse the data required for the mission debriefings
 */
void parse_debriefing_new(mission * /*pm*/)
{
	int				stage_num, nt;
	debriefing		*db;
	debrief_stage	*dbs;
	
	// 2/3/98 -- MWA.  We can now have multiple briefings and debriefings on a team
	for ( nt = 0; nt < Num_teams; nt++ ) {

		if ( !optional_string("#Debriefing_info") )
			break;

		stage_num = 0;

		db = &Debriefings[nt];

		// Yarn - use the same code as for mission loading screens
		parse_custom_bitmap("$Background 640:", "$Background 1024:", db->background[GR_640], db->background[GR_1024]);

		required_string("$Num stages:");
		stuff_int(&db->num_stages);
		Assert(db->num_stages <= MAX_DEBRIEF_STAGES);

		while (required_string_either("#", "$Formula")) {
			Assert(stage_num < MAX_DEBRIEF_STAGES);
			dbs = &db->stages[stage_num++];
			required_string("$Formula:");
			dbs->formula = get_sexp_main();
			required_string("$multi text");
			stuff_string(dbs->text, F_MULTITEXT, NULL);
			required_string("$Voice:");
			stuff_string(dbs->voice, F_FILESPEC, MAX_FILENAME_LEN);
			required_string("$Recommendation text:");
			stuff_string(dbs->recommendation_text, F_MULTITEXT, NULL);
		} // end while

		Assert(db->num_stages == stage_num);
	}

	if ( nt != Num_teams )
		Error(LOCATION, "Not enough debriefings for mission.  There are %d teams and only %d debriefings;\n", Num_teams, nt );
}

void position_ship_for_knossos_warpin(object *objp)
{
	ship *shipp = &Ships[objp->instance];
	ship_info *sip = &Ship_info[shipp->ship_info_index];
	object *knossos_objp = nullptr;
	float half_length, min_dist = -1.0f;
	vec3d center_pos, actual_local_center;
	vec3d new_point, new_center_pos, offset;

	// Assume no valid knossos device
	shipp->special_warpin_objnum = -1;

	// find closest knossos device (allow multiple knossoses)
	for (ship_obj *so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so))
	{
		object *ship_objp = &Objects[so->objnum];
		if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
			continue;

		if (Ship_info[Ships[ship_objp->instance].ship_info_index].flags[Ship::Info_Flags::Knossos_device])
		{
			// is this the closest?  (can use dist_squared since we're only comparing)
			float dist = vm_vec_dist_squared(&ship_objp->pos, &objp->pos);
			if (min_dist < 0.0f || dist < min_dist)
			{
				knossos_objp = ship_objp;
				min_dist = dist;
			}
		}
	}

	if (knossos_objp == nullptr)
		return;

	// set ship special_warpin_objnum
	shipp->special_warpin_objnum = OBJ_INDEX(knossos_objp);

	// determine the correct center of the model (which may not be the model's origin)
	if (object_is_docked(objp))
		dock_calc_docked_actual_center(&actual_local_center, objp);
	else
		ship_class_get_actual_center(sip, &actual_local_center);

	// find world position of the center of the ship assembly
	vm_vec_unrotate(&center_pos, &actual_local_center, &objp->orient);
	vm_vec_add2(&center_pos, &objp->pos);

	// determine the half-length
	if (object_is_docked(objp))
	{
		// we need to get the longitudinal radius of our ship, so find the semilatus rectum along the Z-axis
		half_length = dock_calc_max_semilatus_rectum_parallel_to_axis(objp, Z_AXIS);
	}
	else
		half_length = 0.5f * ship_class_get_length(sip);

	// position self for warp on plane of device
	float dist = fvi_ray_plane(&new_point, &knossos_objp->pos, &knossos_objp->orient.vec.fvec, &center_pos, &objp->orient.vec.fvec, 0.0f);
	vm_vec_scale_add(&new_center_pos, &center_pos, &objp->orient.vec.fvec, (dist - half_length));

	// now move the actual ship based on how we moved the center
	vm_vec_sub(&offset, &new_center_pos, &center_pos);
	vm_vec_add2(&objp->pos, &offset);
	
	// if ship is HUGE, make it go through the center of the knossos
	if (sip->is_huge_ship())
	{
		vm_vec_sub(&offset, &knossos_objp->pos, &new_point);
		vm_vec_add2(&objp->pos, &offset);
	}
}

/**
 * This is conceptually almost the same as ::obj_move_one_docked_object and is used in the same way.
 */
void parse_dock_one_docked_object(p_object *pobjp, p_object *parent_pobjp)
{
	int dockpoint, parent_dockpoint;
	char *dockpoint_name, *parent_dockpoint_name;
	object *objp, *parent_objp;

	// get the actual in-game objects that will be docked
	objp = pobjp->created_object;
	parent_objp = parent_pobjp->created_object;

	// check valid
	if (!objp || !parent_objp)
	{
		Int3();
		return;
	}

	// get dockpoint names
	dockpoint_name = dock_find_dockpoint_used_by_object(pobjp, parent_pobjp);
	parent_dockpoint_name = dock_find_dockpoint_used_by_object(parent_pobjp, pobjp);

	// check valid
	if (!dockpoint_name || !parent_dockpoint_name)
	{
		Int3();
		return;
	}

	// resolve names to dockpoints
	dockpoint = model_find_dock_name_index(Ship_info[Ships[objp->instance].ship_info_index].model_num, dockpoint_name);
	parent_dockpoint = model_find_dock_name_index(Ship_info[Ships[parent_objp->instance].ship_info_index].model_num, parent_dockpoint_name);

	// check valid
	if ((dockpoint < 0) || (parent_dockpoint < 0))
	{
		if (dockpoint < 0)
			ReleaseWarning(LOCATION, "Dockpoint %s could not be found on model %s", dockpoint_name, model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num)->filename);
		if (parent_dockpoint < 0)
			ReleaseWarning(LOCATION, "Dockpoint %s could not be found on model %s", parent_dockpoint_name, model_get(Ship_info[Ships[parent_objp->instance].ship_info_index].model_num)->filename);

		return;
	}

	// dock them
	nprintf(("AI", "Initially docked: %s to parent %s\n", Ships[objp->instance].ship_name, Ships[parent_objp->instance].ship_name));
	ai_dock_with_object(objp, dockpoint, parent_objp, parent_dockpoint, AIDO_DOCK_NOW);
}

int parse_create_object_sub(p_object *objp, bool standalone_ship = false);

// Goober5000
void parse_create_docked_object_helper(p_object *pobjp, p_dock_function_info *infop)
{
	// create the object
	parse_create_object_sub(pobjp);

	// remove the object from the arrival list
	// (don't remove the leader, because he'll be removed the usual way)
	if (pobjp != infop->parameter_variables.objp_value)
	{
		// If an object is present at the beginning of the mission, it may not have been put on the arrival
		// list.  This is totally dependent on the order the objects are parsed in the mission file because
		// that is the order in which they are created.  All the objects in a docked group are created
		// simultaneously when and only when the leader is created.

		// if it's on the list, remove it
		if (parse_object_on_arrival_list(pobjp))
			list_remove(&Ship_arrival_list, pobjp);
	}
}

/**
 * This is a bit tricky because of the way initial docking is now handled.
 * Docking groups require special treatment.
 */
int parse_create_object(p_object *pobjp, bool standalone_ship)
{
	object *objp;

	// if this guy is part of a dock group, create the entire group, starting with the leader
	if (object_is_docked(pobjp))
	{
		p_dock_function_info dfi;

		// we should only be calling this for dock leaders, because the dock leader
		// governs the creation of his entire group
		Assert((pobjp->flags[Mission::Parse_Object_Flags::SF_Dock_leader]));

		// if the leader will be destroyed before the mission starts, then *only* create the leader;
		// don't create the rest of the group (this is what retail did)
		if (pobjp->destroy_before_mission_time >= 0)
			return parse_create_object_sub(pobjp);

		// store the leader as a parameter
		dfi.parameter_variables.objp_value = pobjp;

		// Just as we couldn't create the parse dock trees until we had parsed them all,
		// we can't dock the in-game objects until we have created them all. :p

		// create all the objects
		dock_evaluate_all_docked_objects(pobjp, &dfi, parse_create_docked_object_helper);

		// dock all the objects
		dock_dock_docked_objects(pobjp);

		// now clear the handled flags
		parse_object_clear_all_handled_flags();
	}
	// create normally
	else
	{
		parse_create_object_sub(pobjp, standalone_ship);
	}

	// get the main object
	objp = pobjp->created_object;

	// if arriving through knossos, adjust objp->pos to plane of knossos and set object reference
	// special warp is single player only
	if ((pobjp->flags[Mission::Parse_Object_Flags::Knossos_warp_in]) && !(Game_mode & GM_MULTIPLAYER))
	{
		if (!Fred_running)
			position_ship_for_knossos_warpin(objp);
	}

	// warp it in (moved from parse_create_object_sub)
	if ((Game_mode & GM_IN_MISSION) && (!Fred_running) && (!Game_restoring))
	{
		if ((Ships[objp->instance].wingnum < 0) && (pobjp->arrival_location != ArrivalLocation::FROM_DOCK_BAY))
		{
			shipfx_warpin_start(objp);
		}
	}

	// return the main object's objnum
	return OBJ_INDEX(objp);
}

void parse_bring_in_docked_wing(p_object *p_objp, int wingnum, int shipnum);
void parse_copy_wing_ai_to_ship(wing *wingp, ai_info *aip);

/**
 * Given a stuffed p_object struct, create an object and fill in the necessary fields.
 * @return object number.
 *
 * Per-ship field handling here MUST stay in sync with:
 *   Fred_mission_save::save_objects() / save_common_object_data() in missionsave.cpp
 *   clone_ship_instance_data() in missioneditor/objectduplication.cpp
 * When you add a new editable ship field, touch all three.
 */
int parse_create_object_sub(p_object *p_objp, bool standalone_ship)
{
	int	i, j, k, objnum, shipnum;
	int anchor_objnum = -1;
	bool brought_in_docked_wing = false;
	ai_info *aip;
	ship_subsys *ptr;
	ship *shipp;
	ship_info *sip;
	subsys_status *sssp;
	ship_weapon *wp;

	MONITOR_INC(NumShipArrivals, 1);

	// base level creation - need ship name in case of duplicate textures
	objnum = ship_create(&p_objp->orient, &p_objp->pos, p_objp->ship_class, p_objp->name, standalone_ship);
	Assert(objnum != -1);
	shipnum = Objects[objnum].instance;

	shipp = &Ships[shipnum];
	sip = &Ship_info[shipp->ship_info_index];

	// Goober5000 - make the parse object aware of what it was created as
	p_objp->created_object = &Objects[objnum];

	// Goober5000 - set the collision group if one was provided
	Objects[objnum].collision_group_id = p_objp->collision_group_id;

	// Mission-level performance helper: large ships may be grouped so they skip mutual collision checks.
	if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default] && sip->is_big_or_huge()) {
		Objects[objnum].collision_group_id |= (1 << The_mission.large_ship_no_collide_collision_group);
	}

	// Goober5000 - set some fields that the mission log might need (if logged via parse_bring_in_docked_wing just below)
	shipp->display_name = p_objp->display_name;
	shipp->alt_type_index = p_objp->alt_type_index;
	shipp->callsign_index = p_objp->callsign_index;
	shipp->team = p_objp->team;
	shipp->ship_iff_color = p_objp->alt_iff_color;

	// if this is a multiplayer dogfight game, and its from a player wing, make it team traitor
	if (MULTI_DOGFIGHT && (p_objp->wingnum >= 0) && p_objp->flags[Mission::Parse_Object_Flags::SF_From_player_wing])
		shipp->team = Iff_traitor;

	if (Ship_info[shipp->ship_info_index].uses_team_colors && !p_objp->team_color_setting.empty())
		shipp->team_name = p_objp->team_color_setting;

	// Goober5000 - if this object is being created because he's docked to something,
	// and he's in a wing, then mark the wing as having arrived
	if (object_is_docked(p_objp) && !(p_objp->flags[Mission::Parse_Object_Flags::SF_Dock_leader]) && (p_objp->wingnum >= 0))
	{
		if (!Fred_running)
		{
			parse_bring_in_docked_wing(p_objp, p_objp->wingnum, shipnum);
			brought_in_docked_wing = true;
		}
	}

	shipp->group = p_objp->group;
	shipp->fred_layer = p_objp->fred_layer;
	shipp->escort_priority = p_objp->escort_priority;
	shipp->ship_guardian_threshold = p_objp->ship_guardian_threshold;
	shipp->use_special_explosion = p_objp->use_special_explosion;
	shipp->special_exp_damage = p_objp->special_exp_damage;
	shipp->special_exp_blast = p_objp->special_exp_blast;
	shipp->special_exp_inner = p_objp->special_exp_inner;
	shipp->special_exp_outer = p_objp->special_exp_outer;
	shipp->use_shockwave = p_objp->use_shockwave;
	shipp->special_exp_shockwave_speed = p_objp->special_exp_shockwave_speed;
	shipp->special_exp_deathroll_time = p_objp->special_exp_deathroll_time;

	shipp->special_hitpoints = p_objp->special_hitpoints;
	shipp->special_shield = p_objp->special_shield;

	shipp->ship_max_shield_strength = p_objp->ship_max_shield_strength;
	shipp->ship_max_hull_strength =  p_objp->ship_max_hull_strength;
	shipp->max_shield_recharge = p_objp->max_shield_recharge;

	// Goober5000 - ugh, this is really stupid having to do this here; if the
	// ship creation code was better organized this wouldn't be necessary
	if (shipp->special_hitpoints > 0)
	{
		float hull_factor = shipp->ship_max_hull_strength / sip->max_hull_strength;
		ship_subsys *ss;

		for (ss = GET_FIRST(&shipp->subsys_list); ss != END_OF_LIST(&shipp->subsys_list) && ss != NULL; ss = GET_NEXT(ss))
		{
			ss->max_hits *= hull_factor;

			if (Fred_running)
				ss->current_hits = 0.0f;
			else
				ss->current_hits = ss->max_hits;
		}

		ship_recalc_subsys_strength(shipp);
	}

	// Goober5000 - this is also stupid; this is in ship_set but it needs to be done here because of the special hitpoints mod
	Objects[objnum].hull_strength = shipp->ship_max_hull_strength;

	shipp->respawn_priority = p_objp->respawn_priority;

	// AI stuff.  Note a lot of the AI was already initialized in ship_create.
	aip = &(Ai_info[shipp->ai_index]);

	aip->ai_class = p_objp->ai_class;
	shipp->weapons.ai_class = p_objp->ai_class;  // Fred uses this instead of above.
	//Fixes a bug where the AI class attributes were not copied if the AI class was set in the mission.
	if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_class_bug])
		ship_set_new_ai_class(shipp, p_objp->ai_class);

	aip->mode = AIM_NONE;

	// make sure aim_safety has its submode defined
	if (aip->mode == AIM_SAFETY) {
		aip->submode = AISS_1;
	}

	shipp->cargo1 = p_objp->cargo1;
	strcpy_s(shipp->cargo_title, p_objp->cargo_title);

	shipp->arrival_location = p_objp->arrival_location;
	shipp->arrival_distance = p_objp->arrival_distance;
	shipp->arrival_anchor = p_objp->arrival_anchor;
	shipp->arrival_path_mask = p_objp->arrival_path_mask;
	shipp->arrival_cue = p_objp->arrival_cue;
	shipp->arrival_delay = p_objp->arrival_delay;
	shipp->departure_location = p_objp->departure_location;
	shipp->departure_anchor = p_objp->departure_anchor;
	shipp->departure_path_mask = p_objp->departure_path_mask;
	shipp->departure_cue = p_objp->departure_cue;
	shipp->departure_delay = p_objp->departure_delay;
	shipp->wingnum = p_objp->wingnum;
	shipp->hotkey = p_objp->hotkey;
	shipp->score = p_objp->score;
	shipp->assist_score_pct = p_objp->assist_score_pct;
	shipp->persona_index = p_objp->persona_index;

	if (p_objp->warpin_params_index >= 0)
		shipp->warpin_params_index = p_objp->warpin_params_index;
	if (p_objp->warpout_params_index >= 0)
		shipp->warpout_params_index = p_objp->warpout_params_index;

	// now that we have our correct warpout params, set the warp effects
	if (!Fred_running) {
		ship_set_warp_effects(&Objects[objnum]);
	}

	// reset texture animations
	shipp->base_texture_anim_timestamp = _timestamp();

	// handle the replacement textures
	shipp->apply_replacement_textures(p_objp->replacement_textures);

	// Copy across the alt classes (if any) for FRED
	if (Fred_running) {
		shipp->s_alt_classes = p_objp->alt_classes; 
	}

	// check the parse object's flags for possible things to set on this newly created ship
	resolve_parse_flags(&Objects[objnum], p_objp->flags);


	// other flag checks
////////////////////////

	// forcing the shields on or off depending on flags -- but only if shield strength supports it

	// no strength means we can't have shields, period
    if (p_objp->ship_max_shield_strength == 0.0f)
        Objects[objnum].flags.set(Object::Object_Flags::No_shields);
    // force shields on means we have them regardless of other flags; per r5332 this ranks above the next check
    else if (p_objp->flags[Mission::Parse_Object_Flags::OF_Force_shields_on])
        Objects[objnum].flags.remove(Object::Object_Flags::No_shields);
    // intrinsic no-shields means we have them off in-game
    else if (!Fred_running && (sip->flags[Ship::Info_Flags::Intrinsic_no_shields]))
        Objects[objnum].flags.set(Object::Object_Flags::No_shields);

	// don't set the flag if the mission is ongoing in a multiplayer situation. This will be set by the players in the
	// game only before the game or during respawning.
	// MWA -- changed the next line to remove the !(Game_mode & GM_MULTIPLAYER).  We shouldn't be setting
	// this flag in single player mode -- it gets set in post process mission.
    if ((p_objp->flags[Mission::Parse_Object_Flags::OF_Player_start]) && (Fred_running || ((Game_mode & GM_MULTIPLAYER) && !(Game_mode & GM_IN_MISSION)))) {
		Objects[objnum].flags.set(Object::Object_Flags::Player_ship);
	}

	// a couple of ai_info flags.  Also, do a reasonable default for the kamikaze damage regardless of
	// whether this flag is set or not
	if (p_objp->flags[Mission::Parse_Object_Flags::AIF_Kamikaze])
	{
		Ai_info[shipp->ai_index].ai_flags.set(AI::AI_Flags::Kamikaze);
		Ai_info[shipp->ai_index].kamikaze_damage = p_objp->kamikaze_damage;
	}

	if (p_objp->flags[Mission::Parse_Object_Flags::AIF_No_dynamic])
		Ai_info[shipp->ai_index].ai_flags.set(AI::AI_Flags::No_dynamic);

	if (p_objp->flags[Mission::Parse_Object_Flags::SF_Red_alert_store_status])
	{
		if (!(Game_mode & GM_MULTIPLAYER)) {
			shipp->flags.set(Ship::Ship_Flags::Red_alert_store_status);
		}
	}

	if (p_objp->flags[Mission::Parse_Object_Flags::Knossos_warp_in])
	{
        Objects[objnum].flags.set(Object::Object_Flags::Special_warpin);
		Knossos_warp_ani_used = true;
	}

	// set the orders that this ship will accept.  It will have already been set to default from the
	// ship create code, so only set them if the parse object flags say they are unique
	if (p_objp->flags[Mission::Parse_Object_Flags::SF_Use_unique_orders])
	{
		shipp->orders_accepted = p_objp->orders_accepted;

		// MWA  5/15/98 -- Added the following debug code because some orders that ships
		// will accept were apparently written out incorrectly with Fred.  This Int3() should
		// trap these instances.
#ifndef NDEBUG
		if (Fred_running && !Qtfred_running)
		{
			std::set<size_t> default_orders, remaining_orders;

			default_orders = ship_get_default_orders_accepted(&Ship_info[shipp->ship_info_index]);
			std::set_difference(p_objp->orders_accepted.begin(), p_objp->orders_accepted.end(), default_orders.begin(), default_orders.end(),
								  std::inserter(remaining_orders, remaining_orders.begin()));
			if (!remaining_orders.empty())
			{
				Warning(LOCATION, "Ship %s has orders which it will accept that are\nnot part of default orders accepted.\n\nPlease reedit this ship and change the orders again\n", shipp->ship_name);
			}
		}
#endif
	}

////////////////////////


	// if ship is in a wing, set some equivalent flags on the ship
	if (shipp->wingnum >= 0)
	{
		auto wingp = &Wings[shipp->wingnum];

		if (wingp->flags[Ship::Wing_Flags::No_arrival_warp])
			shipp->flags.set(Ship::Ship_Flags::No_arrival_warp);

		if (wingp->flags[Ship::Wing_Flags::No_departure_warp])
			shipp->flags.set(Ship::Ship_Flags::No_departure_warp);

		if (wingp->flags[Ship::Wing_Flags::Same_arrival_warp_when_docked])
			shipp->flags.set(Ship::Ship_Flags::Same_arrival_warp_when_docked);

		if (wingp->flags[Ship::Wing_Flags::Same_departure_warp_when_docked])
			shipp->flags.set(Ship::Ship_Flags::Same_departure_warp_when_docked);

		if (wingp->flags[Ship::Wing_Flags::Nav_carry])
			shipp->flags.set(Ship::Ship_Flags::Navpoint_carry);

		// if it's wing leader, take the opportunity to set the wing leader's info index in the wing struct
		if (!Fred_running && p_objp->pos_in_wing == 0 && wingp->special_ship_ship_info_index != -1) {
			wingp->special_ship_ship_info_index = p_objp->ship_class;
		}
	}

	// if the wing index and wing pos are set for this parse object, set them for the ship.  This
	// is useful in multiplayer when ships respawn
	shipp->wing_status_wing_index = p_objp->wing_status_wing_index;
	shipp->wing_status_wing_pos = p_objp->wing_status_wing_pos;


	// set up the ai_goals for this object -- all ships created here are AI controlled.
	if (p_objp->ai_goals != -1)
	{
		int sexp;

		// set up the ai goals for this object.
		for (sexp = CDR(p_objp->ai_goals); sexp != -1; sexp = CDR(sexp))
			ai_add_ship_goal_sexp(sexp, ai_goal_type::EVENT_SHIP, aip);

		// free the sexpression nodes only for non-wing ships.  wing code will handle its own case
		if (p_objp->wingnum < 0)
		{
			free_sexp2(p_objp->ai_goals);	// free up sexp nodes for reuse, since they aren't needed anymore.
			p_objp->ai_goals = -1;
		}
	}

	if (brought_in_docked_wing)
		parse_copy_wing_ai_to_ship(&Wings[p_objp->wingnum], aip);

	Assert(sip->model_num != -1);

	// initialize subsystem statii here.  The subsystems are given a percentage damaged.  So a percent value
	// of 20% means that the subsystem is 20% damaged (*not* 20% of max hits).  This is opposite the way
	// that the initial velocity/hull strength/shields work
	i = p_objp->subsys_count;
	while (i--)
	{
		sssp = &Subsys_status[p_objp->subsys_index + i];
		if (!stricmp(sssp->name, NOX("Pilot")))
		{
			ptr = nullptr;
			wp = &shipp->weapons;
		}
		else
		{
			ptr = ship_get_subsys(shipp, sssp->name);	// find the subsystem in the ship list that corresponds to the parsed subsystem
			if (!ptr)
			{
				Warning(LOCATION, "Unable to find '%s' in the ship %s (class %s) subsys_list!", sssp->name, shipp->ship_name, sip->name);
				continue;
			}
			wp = &ptr->weapons;
		}

		if (sssp->primary_banks[0] != SUBSYS_STATUS_NO_CHANGE)
		{
			for (j = k = 0; j < MAX_SHIP_PRIMARY_BANKS; ++j)
			{
				// skip over any empty primary banks unless we are in FRED
				if ((sssp->primary_banks[j] >= 0) || Fred_running)
				{
					wp->primary_bank_weapons[k] = sssp->primary_banks[j];
					++k;
				}
			}

			// double check the Pilot subsystem...
			if (!ptr && !Fred_running)
			{
				// if we somehow ended up with more banks than this ship actually has, clear the excess
				// (this can happen if the mission was edited by hand)
				while (k > sip->num_primary_banks)
					wp->primary_bank_weapons[--k] = -1;
			}

			if (Fred_running)
			{
				// only do this for the Pilot subsystem
				if (!ptr)
					wp->num_primary_banks = sip->num_primary_banks;
			}
			else
				wp->num_primary_banks = k;
		}

		for (j = 0; j < wp->num_primary_banks; ++j)
		{
			if (Fred_running)
			{
				wp->primary_bank_ammo[j] = sssp->primary_ammo[j];
			}
			else if (wp->primary_bank_weapons[j] >= 0 && Weapon_info[wp->primary_bank_weapons[j]].wi_flags[Weapon::Info_Flags::Ballistic])
			{
				Assertion(Weapon_info[wp->primary_bank_weapons[j]].cargo_size > 0.0f,
					"Primary weapon cargo size <= 0. Ship (%s) Subsystem (%s) Bank (%i) Weapon (%s)",
					shipp->ship_name, sssp->name,j,Weapon_info[wp->primary_bank_weapons[j]].name);
				// Pilot subsystem ammo depends on ship ammo capacity
				// in contrast non pilot subsystems depend on the bank capacity of the subsystem
				int ammo_cap;
				if (!ptr) {
					ammo_cap = sip->primary_bank_ammo_capacity[j];
				} else {
					ammo_cap = wp->primary_bank_capacity[j];
				}
				int capacity = (int)std::lround(sssp->primary_ammo[j] / 100.0f * ammo_cap);
				wp->primary_bank_ammo[j] = (int)std::lround(capacity / Weapon_info[wp->primary_bank_weapons[j]].cargo_size);
			}
		}

		if (sssp->secondary_banks[0] != SUBSYS_STATUS_NO_CHANGE)
		{
			for (j = k = 0; j < MAX_SHIP_SECONDARY_BANKS; ++j)
			{
				// skip over any empty secondary banks unless we are in FRED
				if ((sssp->secondary_banks[j] >= 0) || Fred_running)
				{
					wp->secondary_bank_weapons[k] = sssp->secondary_banks[j];
					++k;
				}
			}

			// double check the Pilot subsystem...
			if (!ptr && !Fred_running)
			{
				// if we somehow ended up with more banks than this ship actually has, clear the excess
				// (this can happen if the mission was edited by hand)
				while (k > sip->num_secondary_banks)
					wp->secondary_bank_weapons[--k] = -1;
			}

			if (Fred_running)
			{
				// only do this for the Pilot subsystem
				if (!ptr)
					wp->num_secondary_banks = sip->num_secondary_banks;
			}
			else
				wp->num_secondary_banks = k;
		}

		for (j = 0; j < wp->num_secondary_banks; ++j)
		{
			if (Fred_running)
			{
				wp->secondary_bank_ammo[j] = sssp->secondary_ammo[j];
			}
			else if (wp->secondary_bank_weapons[j] >= 0)
			{
				if (Weapon_info[wp->secondary_bank_weapons[j]].wi_flags[Weapon::Info_Flags::SecondaryNoAmmo]) {
					wp->secondary_bank_ammo[j] = 0;
					continue;
				}

				Assertion(Weapon_info[wp->secondary_bank_weapons[j]].cargo_size > 0.0f,
					"Secondary weapon cargo size <= 0. Ship (%s) Subsystem (%s) Bank (%i) Weapon (%s)",
					shipp->ship_name, sssp->name, j, Weapon_info[wp->secondary_bank_weapons[j]].name);
				// Pilot subsystem ammo depends on ship ammo capacity
				// in contrast non pilot subsystems depend on the bank capacity of the subsystem
				int ammo_cap;
				if (!ptr) {
					ammo_cap = sip->secondary_bank_ammo_capacity[j];
				} else {
					ammo_cap = wp->secondary_bank_capacity[j];
				}
				int capacity = (int)std::lround(sssp->secondary_ammo[j] / 100.0f * ammo_cap);
				wp->secondary_bank_ammo[j] = (int)std::lround(capacity / Weapon_info[wp->secondary_bank_weapons[j]].cargo_size);
			}
		}

		// if we are parsing a Pilot subsystem, skip the rest
		if (!ptr)
			continue;

		if (shipp->flags[Ship::Ship_Flags::Lock_all_turrets_initially])
		{
			// mark all turrets as locked
			if(ptr->system_info->type == SUBSYSTEM_TURRET)
			{
				ptr->weapons.flags.set(Ship::Weapon_Flags::Turret_Lock);
			}
		}

		if (Fred_running)
		{
			ptr->current_hits = sssp->percent;
			ptr->max_hits = 100.0f;
		}
		else
		{
			ptr->max_hits = ptr->system_info->max_subsys_strength * (shipp->ship_max_hull_strength / sip->max_hull_strength);

			float new_hits = ptr->max_hits * ((100.0f - sssp->percent) / 100.f);
			if (!(ptr->flags[Ship::Subsystem_Flags::No_aggregate])) {
				shipp->subsys_info[ptr->system_info->type].aggregate_current_hits -= (ptr->max_hits - new_hits);
			}

			if ((100.0f - sssp->percent) < 0.5)
			{
				ptr->current_hits = 0.0f;
				if (ptr->submodel_instance_1 != nullptr)
					ptr->submodel_instance_1->blown_off = true;
			}
			else
			{
				ptr->current_hits = new_hits;
			}
		}

		ptr->subsys_cargo_name = sssp->subsys_cargo_name;
		strcpy_s(ptr->subsys_cargo_title, sssp->subsys_cargo_title);

		if (sssp->ai_class != SUBSYS_STATUS_NO_CHANGE)
			ptr->weapons.ai_class = sssp->ai_class;

		ptr->turret_animation_position = MA_POS_NOT_SET;	// model animation position is not set
		ptr->turret_animation_done_time = 0;
	}
	
	// initial hull strength, shields, and velocity are all expressed as a percentage of the max value/
	// so a initial_hull value of 90% means 90% of max.  This way is opposite of how subsystems are dealt
	// with
	if (Fred_running)
	{
		Objects[objnum].phys_info.speed = i2fl(p_objp->initial_velocity);
		Objects[objnum].hull_strength = i2fl(p_objp->initial_hull);
		Objects[objnum].shield_quadrant[0] = i2fl(p_objp->initial_shields);

	}
	else
	{
		int max_allowed_sparks, num_sparks, iLoop;

		Objects[objnum].hull_strength = p_objp->initial_hull * shipp->ship_max_hull_strength / 100.0f;
		float quad_strength = shipp->max_shield_recharge * p_objp->initial_shields * shield_get_max_quad(&Objects[objnum]) / 100.0f;
		for (auto &quadrant : Objects[objnum].shield_quadrant)
			quadrant = quad_strength;

		// initial velocities now do not apply to ships which warp in after mission starts
		// WMC - Make it apply for ships with IN_PLACE_ANIM type
		// zookeeper - Also make it apply for hyperspace warps
		if (!(Game_mode & GM_IN_MISSION) || (Warp_params[shipp->warpin_params_index].warp_type == WT_IN_PLACE_ANIM || Warp_params[shipp->warpin_params_index].warp_type == WT_HYPERSPACE))
		{
			Objects[objnum].phys_info.speed = (float) p_objp->initial_velocity * sip->max_speed / 100.0f;
			// prev_ramp_vel needs to be in local coordinates
			// set z of prev_ramp_vel to initial velocity
			vm_vec_zero(&Objects[objnum].phys_info.prev_ramp_vel);
			Objects[objnum].phys_info.prev_ramp_vel.xyz.z = Objects[objnum].phys_info.speed;
			// convert to global coordinates and set to ship velocity and desired velocity
			vm_vec_unrotate(&Objects[objnum].phys_info.vel, &Objects[objnum].phys_info.prev_ramp_vel, &Objects[objnum].orient);
			Objects[objnum].phys_info.desired_vel = Objects[objnum].phys_info.vel;
		}

		// recalculate damage of subsystems
		ship_recalc_subsys_strength(&Ships[shipnum]);

		// create sparks on a ship whose hull is damaged.  We will create two sparks for every 20%
		// of hull damage done.  100 means no sparks.  between 80 and 100 do two sparks.  60 and 80 is
		// four, etc.
		auto pm = model_get(sip->model_num);
		max_allowed_sparks = get_max_sparks(&Objects[objnum]);
		num_sparks = (int)((100.0f - p_objp->initial_hull) / 5.0f);
		if (num_sparks > max_allowed_sparks)
			num_sparks = max_allowed_sparks;

		for (iLoop = 0; iLoop < num_sparks; iLoop++)
		{
			// DA 10/20/98 - sparks must be chosen on the hull and not any submodel
			vec3d v1 = submodel_get_random_point(sip->model_num, pm->detail[0]);
			ship_hit_sparks_no_rotate(&Objects[objnum], &v1);
		}
	}

	// in mission, we add a log entry -- set ship positions for ships not in wings, and then do
	// warpin effect
	if ((Game_mode & GM_IN_MISSION) && (!Fred_running))
	{
		mission_log_add_entry(LOG_SHIP_ARRIVED, shipp->ship_name, NULL);

		if (!Game_restoring)
		{
			// if this ship isn't in a wing, determine its arrival location
			if (shipp->wingnum == -1)
			{
				// multiplayer clients set the arrival location of ships to be at location since their
				// position has already been determined.  Don't actually set the variable since we
				// don't want the warp effect to show if coming from a dock bay.
				auto location = p_objp->arrival_location;

				if (MULTIPLAYER_CLIENT)
					location = ArrivalLocation::AT_LOCATION;

				anchor_objnum = mission_set_arrival_location(p_objp->arrival_anchor, location, p_objp->arrival_distance, objnum, p_objp->arrival_path_mask, NULL, NULL);

				// Goober5000 - warpin start moved to parse_create_object
			}
		}
		
		// possibly add this ship to a hotkey set
		// Ships can now have both a ship-hotkey and a wing-hotkey -- FSF
		if (shipp->hotkey != -1)
			mission_hotkey_mf_add(shipp->hotkey, shipp->objnum, HOTKEY_MISSION_FILE_ADDED);
		if ((shipp->wingnum != -1) && (Wings[shipp->wingnum].hotkey != -1))
			mission_hotkey_mf_add(Wings[shipp->wingnum].hotkey, shipp->objnum, HOTKEY_MISSION_FILE_ADDED);

		// possibly add this ship to the hud escort list
		if (shipp->flags[Ship::Ship_Flags::Escort])
			hud_add_remove_ship_escort(objnum, 1);
	}

	// for multiplayer games, make a call to the network code to assign the object signature
	// of the newly created object.  The network host of the netgame will always assign a signature
	// to a newly created object.  The network signature will get to the clients of the game in
	// different manners depending on whether or not an individual ship or a wing was created.
	if (Game_mode & GM_MULTIPLAYER)
	{
		Objects[objnum].net_signature = p_objp->net_signature;

		// Goober5000 - for an initially docked group, only send the packet for the dock leader... this is necessary so that the
		// docked hierarchy of objects can be created in the right order on the client side
		if (!object_is_docked(p_objp) || (p_objp->flags[Mission::Parse_Object_Flags::SF_Dock_leader]))
		{
			if ((Game_mode & GM_IN_MISSION) && MULTIPLAYER_MASTER && (p_objp->wingnum == -1))
				send_ship_create_packet(&Objects[objnum], (p_objp == Arriving_support_ship));
		}
		// also add this ship to the multi ship tracking and interpolation struct
		multi_rollback_ship_record_add_ship(objnum);
	}

	// If the ship is in a wing, this will be done in mission_set_wing_arrival_location() instead
	// If the ship is in a wing, but the wing is docked then addition of bool brought_in_docked_wing accounts for that status --wookieejedi
	if (Game_mode & GM_IN_MISSION && ((shipp->wingnum == -1) || (brought_in_docked_wing))) {
		object *anchor_objp = (anchor_objnum >= 0) ? &Objects[anchor_objnum] : nullptr;

		if (scripting::hooks::OnShipArrive->isActive()) {
			scripting::hooks::OnShipArrive->run(scripting::hooks::ShipArriveConditions{ shipp, p_objp->arrival_location, anchor_objp },
				scripting::hook_param_list(
					scripting::hook_param("Ship", 'o', &Objects[objnum]),
					scripting::hook_param("Parent", 'o', anchor_objp, anchor_objp != nullptr)
				));
		}
	}

	if (!Fred_running) {
		// if this is an asteroid target, add it to the list
		for (SCP_string& name : Asteroid_field.target_names) {
			if (stricmp(name.c_str(), shipp->ship_name) == 0) {
				asteroid_add_target(&Objects[objnum]);
				break;
			}
		}
	}

	// assign/update parse object in ship registry entry if needed
	// (this is unrelated to ship registry state management and is only here because apparently in-game joining needs it;
	// in the normal course of ship creation, the pointers and status are updated elsewhere)
	auto ship_it = Ship_registry_map.find(shipp->ship_name);
	if (ship_it != Ship_registry_map.end()) {
		auto entry = &Ship_registry[ship_it->second];

		if (entry->status == ShipStatus::INVALID) {
			Warning(LOCATION, "Potential bug: ship registry status for %s is INVALID", shipp->ship_name);
		}

		// arriving support ships have unique housekeeping and are not in Parse_objects
		if (p_objp == Arriving_support_ship) {
			if (entry->pobj_num < -1 || entry->pobj_num >= 0) {
				Warning(LOCATION, "Potential bug: an arriving support ship %s has a bogus pobj_num index %d", shipp->ship_name, entry->pobj_num);
				entry->pobj_num = -1;
			}
		}
		else {
			int pobj_num = POBJ_INDEX(p_objp);
			if (!SCP_vector_inbounds(Parse_objects, pobj_num)) {
				Warning(LOCATION, "Potential bug: ship registry parse object for %s is not listed in Parse_objects", shipp->ship_name);
				entry->pobj_num = -1;
			}
			else if (entry->pobj_num == -1) {
				Warning(LOCATION, "Potential bug: ship registry parse object for %s is not assigned", shipp->ship_name);
				entry->pobj_num = pobj_num;
			}
			else if (!SCP_vector_inbounds(Parse_objects, entry->pobj_num)) {
				Warning(LOCATION, "Potential bug: ship registry parse object for %s is out of bounds", shipp->ship_name);
				entry->pobj_num = pobj_num;
			}
			else if (entry->pobj_num != pobj_num) {
				Warning(LOCATION, "Potential bug: ship registry parse object index for %s (%d) is different from its expected value (%d)", shipp->ship_name, entry->pobj_num, pobj_num);
				entry->pobj_num = pobj_num;
			}
		}
	}

	// now that everything has been set up, do some sanity checks
#ifndef NDEBUG
	{
		auto pm = model_get(sip->model_num);
		Assertion(pm->n_guns == sip->num_primary_banks, "For %s (%s), the number of primary banks in the ship class does not match the number of gun banks in the model!", sip->name, pm->filename);
		Assertion(shipp->weapons.num_primary_banks <= pm->n_guns, "For %s (%s), the number of primary banks in the ship is greater than the number of gun banks in the model!", shipp->ship_name, pm->filename);
		Assertion(pm->n_missiles == sip->num_secondary_banks, "For %s (%s), the number of secondary banks in the ship class does not match the number of missile banks in the model!", sip->name, pm->filename);
		Assertion(shipp->weapons.num_secondary_banks <= pm->n_missiles, "For %s (%s), the number of secondary banks in the ship is greater than the number of missile banks in the model!", shipp->ship_name, pm->filename);
	}
#endif

	return objnum;
}

/**
 * There are a bunch of assumptions in the code that, in FS2, the wing will be created first, and
 * then it will create its component ships.  If a wing arrives because all its ships were docked
 * to something else, these assumptions are turned inside out.  So we have to sort of bootstrap
 * the creation of the wing by running a subset of the code from parse_wing_create_ships().
 * 
 * Note that parse_copy_wing_ai_to_ship() needs to be done too but will be called in a different spot.
 */
void parse_bring_in_docked_wing(p_object *p_objp, int wingnum, int shipnum)
{
	Assert(!Fred_running);
	Assert(p_objp != NULL);
	Assert(wingnum >= 0);
	Assert(shipnum >= 0);
	wing *wingp = &Wings[wingnum];

	// link ship and wing together
	// (do this first because mission log relies on ship_index)
	wingp->ship_index[p_objp->pos_in_wing] = shipnum;
	Ships[shipnum].wingnum = wingnum;

	// has this wing arrived at all yet?
	if (wingp->current_wave == 0)
	{
		wingp->current_wave++;
		mission_log_add_entry( LOG_WING_ARRIVED, wingp->name, NULL, wingp->current_wave );
		wingp->wave_delay_timestamp = TIMESTAMP::invalid();
	}
	// how did we get more than one wave here?
	else if (wingp->current_wave > 1)
		Error(LOCATION, "Wing %s was created from docked ships but somehow has more than one wave!", wingp->name);

	// increment tallies
	wingp->total_arrived_count++;
	wingp->current_count++;
	// make sure we haven't created too many ships
	Assert(wingp->current_count <= MAX_SHIPS_PER_WING);

	// at this point the wing has arrived, so handle the stuff for this particular ship

	// set up wingman status index
	hud_wingman_status_set_index(wingp, &Ships[shipnum], p_objp);

	// copy to parse object
	p_objp->wing_status_wing_index = Ships[shipnum].wing_status_wing_index;
	p_objp->wing_status_wing_pos = Ships[shipnum].wing_status_wing_pos;
}

void parse_copy_wing_ai_to_ship(wing *wingp, ai_info *aip)
{
	// copy any goals from the wing to the newly created ship
	for (int index = 0; index < MAX_AI_GOALS; index++)
	{
		if (wingp->ai_goals[index].ai_mode != AI_GOAL_NONE)
			ai_copy_mission_wing_goal(&wingp->ai_goals[index], aip);
	}

	if (wingp->flags[Ship::Wing_Flags::No_dynamic])
		aip->ai_flags.set(AI::AI_Flags::No_dynamic);
}

// Goober5000
void resolve_parse_flags(object *objp, flagset<Mission::Parse_Object_Flags> &parse_flags)
{
    Assert(objp != NULL);
    ship *shipp = &Ships[objp->instance];

    if (parse_flags[Mission::Parse_Object_Flags::SF_Cargo_known])
        shipp->flags.set(Ship::Ship_Flags::Cargo_revealed);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Ignore_count])
        shipp->flags.set(Ship::Ship_Flags::Ignore_count);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Protected])
        objp->flags.set(Object::Object_Flags::Protected);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Reinforcement])
    {
        //Individual ships in wings can't be reinforcements - FUBAR
        if (shipp->wingnum >= 0)
        {
            Warning(LOCATION, "Ship %s is a reinforcement unit but is a member of a wing. Ignoring reinforcement flag.", shipp->ship_name);
        }
        else
        {
            shipp->flags.set(Ship::Ship_Flags::Reinforcement);
        }
    }

    if ((parse_flags[Mission::Parse_Object_Flags::OF_No_shields]) && (parse_flags[Mission::Parse_Object_Flags::OF_Force_shields_on]))
    {
        parse_warning_or_record("Ship %s has both the \"force-shields-on\" and \"no-shields\" flags; this is inconsistent.", shipp->ship_name);
    }
    if (parse_flags[Mission::Parse_Object_Flags::OF_No_shields])
        objp->flags.set(Object::Object_Flags::No_shields);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Escort])
        shipp->flags.set(Ship::Ship_Flags::Escort);

    // P_OF_PLAYER_START is handled in parse_create_object_sub

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_arrival_music])
        shipp->flags.set(Ship::Ship_Flags::No_arrival_music);

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_arrival_warp])
        shipp->flags.set(Ship::Ship_Flags::No_arrival_warp);

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_departure_warp])
        shipp->flags.set(Ship::Ship_Flags::No_departure_warp);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Ship_locked])
        shipp->flags.set(Ship::Ship_Flags::Ship_locked);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Weapons_locked])
        shipp->flags.set(Ship::Ship_Flags::Weapons_locked);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Locked]) {
        shipp->flags.set(Ship::Ship_Flags::Ship_locked);
        shipp->flags.set(Ship::Ship_Flags::Weapons_locked);
    }

    if (parse_flags[Mission::Parse_Object_Flags::OF_Invulnerable])
        objp->flags.set(Object::Object_Flags::Invulnerable);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Hidden_from_sensors])
        shipp->flags.set(Ship::Ship_Flags::Hidden_from_sensors);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Scannable])
        shipp->flags.set(Ship::Ship_Flags::Scannable);

    // P_AIF_KAMIKAZE, P_AIF_NO_DYNAMIC, and P_SF_RED_ALERT_CARRY are handled in parse_create_object_sub

    if (parse_flags[Mission::Parse_Object_Flags::OF_Beam_protected])
        objp->flags.set(Object::Object_Flags::Beam_protected);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Flak_protected])
        objp->flags.set(Object::Object_Flags::Flak_protected);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Laser_protected])
        objp->flags.set(Object::Object_Flags::Laser_protected);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Missile_protected])
        objp->flags.set(Object::Object_Flags::Missile_protected);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Vaporize])
        shipp->flags.set(Ship::Ship_Flags::Vaporize);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Stealth])
        shipp->flags.set(Ship::Ship_Flags::Stealth);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Friendly_stealth_invis])
        shipp->flags.set(Ship::Ship_Flags::Friendly_stealth_invis);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Dont_collide_invis])
        shipp->flags.set(Ship::Ship_Flags::Dont_collide_invis);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Primitive_sensors])
        shipp->flags.set(Ship::Ship_Flags::Primitive_sensors);

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_subspace_drive])
        shipp->flags.set(Ship::Ship_Flags::No_subspace_drive);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Nav_carry_status])
        shipp->flags.set(Ship::Ship_Flags::Navpoint_carry);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Affected_by_gravity])
        shipp->flags.set(Ship::Ship_Flags::Affected_by_gravity);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Toggle_subsystem_scanning])
        shipp->flags.set(Ship::Ship_Flags::Toggle_subsystem_scanning);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Targetable_as_bomb])
        objp->flags.set(Object::Object_Flags::Targetable_as_bomb);

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_builtin_messages])
        shipp->flags.set(Ship::Ship_Flags::No_builtin_messages);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Primaries_locked])
        shipp->flags.set(Ship::Ship_Flags::Primaries_locked);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Secondaries_locked])
        shipp->flags.set(Ship::Ship_Flags::Secondaries_locked);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Set_class_dynamically])
        shipp->flags.set(Ship::Ship_Flags::Set_class_dynamically);

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_death_scream])
        shipp->flags.set(Ship::Ship_Flags::No_death_scream);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Always_death_scream])
        shipp->flags.set(Ship::Ship_Flags::Always_death_scream);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Nav_needslink])
        shipp->flags.set(Ship::Ship_Flags::Navpoint_needslink);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Hide_ship_name])
        shipp->flags.set(Ship::Ship_Flags::Hide_ship_name);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Lock_all_turrets_initially])
        shipp->flags.set(Ship::Ship_Flags::Lock_all_turrets_initially);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Afterburner_locked])
        shipp->flags.set(Ship::Ship_Flags::Afterburner_locked);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Force_shields_on])
        shipp->flags.set(Ship::Ship_Flags::Force_shields_on);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Dont_change_position])
        objp->flags.set(Object::Object_Flags::Dont_change_position);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Dont_change_orientation])
        objp->flags.set(Object::Object_Flags::Dont_change_orientation);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Immobile])
    {
        // handle "soft deprecation" of Immobile by setting the two half-flags, but only in FRED
        // (FRED has dialog support for the two half-flags but not the legacy Immobile flag)
        if (Fred_running)
        {
            objp->flags.set(Object::Object_Flags::Dont_change_position);
            objp->flags.set(Object::Object_Flags::Dont_change_orientation);

            // keep track of migrated ships
            Fred_migrated_immobile_ships.insert(objp->instance);
        }
        else
            objp->flags.set(Object::Object_Flags::Immobile);
    }

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_ets])
        shipp->flags.set(Ship::Ship_Flags::No_ets);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Cloaked])
        shipp->flags.set(Ship::Ship_Flags::Cloaked);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Scramble_messages])
        shipp->flags.set(Ship::Ship_Flags::Scramble_messages);

    if (parse_flags[Mission::Parse_Object_Flags::OF_No_collide])
        objp->flags.remove(Object::Object_Flags::Collides);

    if (parse_flags[Mission::Parse_Object_Flags::SF_No_disabled_self_destruct])
        shipp->flags.set(Ship::Ship_Flags::No_disabled_self_destruct);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Dock_leader])
        shipp->flags.set(Ship::Ship_Flags::Dock_leader);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Warp_broken])
        shipp->flags.set(Ship::Ship_Flags::Warp_broken);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Warp_never])
        shipp->flags.set(Ship::Ship_Flags::Warp_never);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Has_display_name])
        shipp->flags.set(Ship::Ship_Flags::Has_display_name);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Hide_mission_log])
        shipp->flags.set(Ship::Ship_Flags::Hide_mission_log);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Same_arrival_warp_when_docked])
        shipp->flags.set(Ship::Ship_Flags::Same_arrival_warp_when_docked);

    if (parse_flags[Mission::Parse_Object_Flags::SF_Same_departure_warp_when_docked])
		shipp->flags.set(Ship::Ship_Flags::Same_departure_warp_when_docked);

    if (parse_flags[Mission::Parse_Object_Flags::OF_Attackable_if_no_collide])
		objp->flags.set(Object::Object_Flags::Attackable_if_no_collide);

	if (parse_flags[Mission::Parse_Object_Flags::SF_Fail_sound_locked_primary])
		shipp->flags.set(Ship::Ship_Flags::Fail_sound_locked_primary);

	if (parse_flags[Mission::Parse_Object_Flags::SF_Fail_sound_locked_secondary])
		shipp->flags.set(Ship::Ship_Flags::Fail_sound_locked_secondary);

	if (parse_flags[Mission::Parse_Object_Flags::SF_Aspect_immune])
		shipp->flags.set(Ship::Ship_Flags::Aspect_immune);

	if (parse_flags[Mission::Parse_Object_Flags::SF_Cannot_perform_scan_hide_cargo])
		shipp->flags.set(Ship::Ship_Flags::Cannot_perform_scan_hide_cargo);
	if (parse_flags[Mission::Parse_Object_Flags::SF_Cannot_perform_scan_show_cargo])
		shipp->flags.set(Ship::Ship_Flags::Cannot_perform_scan_show_cargo);
	if (parse_flags[Mission::Parse_Object_Flags::SF_No_scanned_cargo])
		shipp->flags.set(Ship::Ship_Flags::No_scanned_cargo);

	if (parse_flags[Mission::Parse_Object_Flags::SF_No_targeting_limits])
		shipp->flags.set(Ship::Ship_Flags::No_targeting_limits);

	if (parse_flags[Mission::Parse_Object_Flags::SF_From_player_wing])
		shipp->flags.set(Ship::Ship_Flags::From_player_wing);
}

void fix_old_special_explosions(p_object *p_objp, int variable_index) 
{
	int i;

	Assertion(!(p_objp->use_special_explosion), "Mission appears to be using both the new and old method of special explosions for %s. Old method values used", p_objp->name); 
	
	// check all the variables are valid
	for ( i = variable_index; i < (variable_index + BLOCK_EXP_SIZE); i++ ) {
		if (!( Block_variables[i].type & SEXP_VARIABLE_BLOCK )) {
			Warning (LOCATION, "%s is using the old special explosions method but does not appear to have variables for all the values", p_objp->name);
			return;
		}
	}

	p_objp->use_special_explosion = true;

	p_objp->special_exp_damage = atoi(Block_variables[variable_index+DAMAGE].text);
	p_objp->special_exp_blast = atoi(Block_variables[variable_index+BLAST].text);
	p_objp->special_exp_inner = atoi(Block_variables[variable_index+INNER_RAD].text);
	p_objp->special_exp_outer = atoi(Block_variables[variable_index+OUTER_RAD].text);
	p_objp->use_shockwave = (atoi(Block_variables[variable_index+PROPAGATE].text) ? 1:0);
	p_objp->special_exp_shockwave_speed = atoi(Block_variables[variable_index+SHOCK_SPEED].text);
	p_objp->special_exp_deathroll_time = 0;
}

void fix_old_special_hits(p_object *p_objp, int variable_index)
{
	int i; 

	Assertion( ((p_objp->special_hitpoints == 0) && (p_objp->special_shield == -1)),"Mission appears to be using both the new and old method of special hitpoints for %s", p_objp->name);  
	
	// check all the variables are valid
	for ( i = variable_index; i < (variable_index + BLOCK_HIT_SIZE); i++ ) {
		if (!( Block_variables[i].type & SEXP_VARIABLE_BLOCK )) {
			Warning (LOCATION, "%s is using the old special hitpoints method but does not appear to have variables for all the values", p_objp->name);
			return;
		}
	}

	p_objp->special_hitpoints = atoi(Block_variables[variable_index+HULL_STRENGTH].text);
	p_objp->special_shield = atoi(Block_variables[variable_index+SHIELD_STRENGTH].text);
}

// this will be called when Parse_objects is cleared between missions and upon shutdown
p_object::~p_object()
{
	dock_free_dock_list(this);
}

const char* p_object::get_display_name() {
	if (has_display_name()) {
		return display_name.c_str();
	} else {
		return name;
	}
}
bool p_object::has_display_name() {
	return flags[Mission::Parse_Object_Flags::SF_Has_display_name];
}

extern int parse_warp_params(const WarpParams *inherit_from, WarpDirection direction, const char *info_type_name, const char *sip_name, bool set_supercap_warp_physics = false);

/**
 * Mp points at the text of an object, which begins with the "$Name:" field.
 * Snags all object information.  Creating the ship now only happens after everything has been parsed.
 *
 * @param pm Mission
 * @param flag is parameter that is used to tell what kind information we are retrieving from the mission.
 * if we are just getting player starts, then don't create the objects
 * @param p_objp Object
 */
int parse_object(mission *pm, int  /*flag*/, p_object *p_objp)
{
	int	i;
    char name[NAME_LENGTH];
	ship_info *sip;

	Assert(pm != NULL);

	required_string("$Name:");
	stuff_string(p_objp->name, F_NAME, NAME_LENGTH);
	if (mission_parse_find_parse_object(p_objp->name))
		error_display(0, NOX("Redundant ship name: %s\n"), p_objp->name);

	// if this name has a hash, create a default display name
	if (get_pointer_to_first_hash_symbol(p_objp->name)) {
		p_objp->display_name = p_objp->name;
		end_string_at_first_hash_symbol(p_objp->display_name);
		p_objp->flags.set(Mission::Parse_Object_Flags::SF_Has_display_name);
	}

	if (optional_string("$Display Name:")) {
		stuff_string(p_objp->display_name, F_NAME);
		p_objp->flags.set(Mission::Parse_Object_Flags::SF_Has_display_name);
	}

	find_and_stuff("$Class:", &p_objp->ship_class, F_NAME, Ship_class_names, Ship_info.size(), "ship class");
	if (p_objp->ship_class < 0)
	{
		if (Fred_running) {
			Warning(LOCATION, "Ship \"%s\" has an invalid ship type (ships.tbl probably changed).  Making it type 0\n", p_objp->name);
		} 
		else {
			mprintf(("MISSIONS: Ship \"%s\" has an invalid ship type (ships.tbl probably changed).  Making it type 0\n", p_objp->name));
		}

		p_objp->ship_class = 0;
		Num_unknown_ship_classes++;
	}
	sip = &Ship_info[p_objp->ship_class];

	// initialize class-specific fields
	p_objp->ai_class = sip->ai_class;
	p_objp->warpin_params_index = sip->warpin_params_index;
	p_objp->warpout_params_index = sip->warpout_params_index;
	p_objp->ship_max_shield_strength = sip->max_shield_strength;
	p_objp->ship_max_hull_strength = sip->max_hull_strength;
	Assert(p_objp->ship_max_hull_strength > 0.0f);	// Goober5000: div-0 check (not shield because we might not have one)
	p_objp->max_shield_recharge = sip->max_shield_recharge;
	p_objp->replacement_textures = sip->replacement_textures;	// initialize our set with the ship class set, which may be empty

	// Karajorma - See if there are any alternate classes specified for this ship. 
	// The alt class can either be a variable or a ship class name
	char alt_ship_class[TOKEN_LENGTH > NAME_LENGTH ? TOKEN_LENGTH : NAME_LENGTH];
	int is_variable; 

	while (optional_string("$Alt Ship Class:")) {	
		alt_class new_alt_class; 

		is_variable = get_string_or_variable(alt_ship_class); 

		if (is_variable) {
			new_alt_class.variable_index = get_index_sexp_variable_name(alt_ship_class);
			if(new_alt_class.variable_index >= 0) {
				new_alt_class.ship_class = ship_info_lookup(Sexp_variables[new_alt_class.variable_index].text);
			}
			else {
				new_alt_class.ship_class = -1;
			}
		}
		else {
			new_alt_class.variable_index = -1;
			new_alt_class.ship_class = ship_info_lookup(alt_ship_class); 
		}

		if (new_alt_class.ship_class < 0 ) {
			if (!Fred_running) {
				Warning(LOCATION, "Ship \"%s\" has an invalid Alternate Ship Class type (ships.tbl probably changed). Skipping this entry", p_objp->name); 
				continue; 
			}
			else {
				// incorrect initial values for a variable can be fixed in FRED
				if (new_alt_class.variable_index != -1) {
					Warning(LOCATION, "Ship \"%s\" has an invalid Alternate Ship Class type.", p_objp->name); 
				}
				// but there is little we can do if someone spelled a ship class incorrectly
				else {
					Warning(LOCATION, "Ship \"%s\" has an invalid Alternate Ship Class type. Skipping this entry", p_objp->name); 
					continue; 			
				}
			}
		}

		if (optional_string("+Default Class:")) {
			new_alt_class.default_to_this_class = true; 
		}
		else {
			new_alt_class.default_to_this_class = false;
		}

		p_objp->alt_classes.push_back(new_alt_class); 
	}

	// if this is a multiplayer dogfight mission, skip support ships
	if(MULTI_DOGFIGHT && (sip->flags[Ship::Info_Flags::Support]))
		return 0;

	// optional alternate name type
	if(optional_string("$Alt:"))
	{
		// alternate name
		stuff_string(name, F_NAME, NAME_LENGTH);

		// try and find the alternate name
		p_objp->alt_type_index = mission_parse_lookup_alt(name);
		if(p_objp->alt_type_index < 0)
			WarningEx(LOCATION, "Mission %s\nError looking up alternate ship type name %s!\n", pm->name.c_str(), name);
		else
			mprintf(("Using alternate ship type name: %s\n", name));
	}

	// optional callsign
	if(optional_string("$Callsign:"))
	{
		// alternate callsign
		stuff_string(name, F_NAME, NAME_LENGTH);

		// try and find the callsign
		p_objp->callsign_index = mission_parse_lookup_callsign(name);
		if(p_objp->callsign_index < 0)
			WarningEx(LOCATION, "Mission %s\nError looking up callsign %s!\n", pm->name.c_str(), name);
		else
			mprintf(("Using callsign: %s\n", name));
	}

	auto temp_team_names = std::unique_ptr<const char*[]>(new const char*[Iff_info.size()]);
	for (i = 0; i < (int)Iff_info.size(); i++)
		temp_team_names[i] = Iff_info[i].iff_name;

	find_and_stuff("$Team:", &p_objp->team, F_NAME, temp_team_names.get(), Iff_info.size(), "team name");

	// save current team for loadout purposes, so that in multi we always respawn
	// from the original loadout slot even if the team changes
	p_objp->loadout_team = p_objp->team;

	if (optional_string("$Team Color Setting:")) {
		char temp[NAME_LENGTH];
		stuff_string(temp, F_NAME, NAME_LENGTH);
		p_objp->team_color_setting = temp;

		//If team color is not default then verify
		if (stricmp(p_objp->team_color_setting.c_str(), sip->default_team_name.c_str()) != 0) {
			if (Team_Colors.find(p_objp->team_color_setting) == Team_Colors.end()) {
				mprintf(("Invalid team color specified in mission file for ship %s, resetting to default\n", p_objp->name));
				p_objp->team_color_setting = sip->default_team_name;
			}
		}
	}

	required_string("$Location:");
	stuff_vec3d(&p_objp->pos);

	required_string("$Orientation:");
	stuff_matrix(&p_objp->orient);

	// legacy code, not even used in FS1
	if (optional_string("$IFF:"))
	{
		stuff_string(name, F_NAME, NAME_LENGTH);
	}
	if (optional_string("$AI Behavior:"))
	{
		stuff_string(name, F_NAME, NAME_LENGTH);
	}

	if (optional_string("+AI Class:")) 
	{
		p_objp->ai_class = match_and_stuff(F_NAME, Ai_class_names, Num_ai_classes, "AI class");

		if (p_objp->ai_class < 0) 
		{
			Warning(LOCATION, "AI Class for ship %s does not exist in ai.tbl. Setting to first available class.\n", p_objp->name);
			p_objp->ai_class = 0;
		}		
	}

	if (optional_string("$AI Goals:"))
		p_objp->ai_goals = get_sexp_main();

	if (!required_string_either("$AI Goals:", "$Cargo 1:"))
	{
		required_string("$AI Goals:");
		p_objp->ai_goals = get_sexp_main();
	}

	int temp;
	find_and_stuff_or_add("$Cargo 1:", &temp, F_NAME, Cargo_names, &Num_cargo, MAX_CARGO, "cargo");
	p_objp->cargo1 = char(temp);
	if (optional_string("$Cargo 2:"))
	{
		stuff_string(name, F_NAME, NAME_LENGTH);
	}

	if (optional_string("$Cargo Title:"))
	{
		stuff_string(p_objp->cargo_title, F_NAME, NAME_LENGTH);
	}

	parse_common_object_data(p_objp);  // get initial conditions and subsys status

	find_and_stuff("$Arrival Location:", &temp, F_NAME, Arrival_location_names, Num_arrival_names, "Arrival Location");
	if (temp >= 0)
		p_objp->arrival_location = static_cast<ArrivalLocation>(temp);

	if (optional_string("+Arrival Distance:"))
	{
		stuff_int(&p_objp->arrival_distance);

		// Goober5000
		if ((p_objp->arrival_distance <= 0) && (
			   (p_objp->arrival_location == ArrivalLocation::NEAR_SHIP)
			|| (p_objp->arrival_location == ArrivalLocation::IN_FRONT_OF_SHIP) || (p_objp->arrival_location == ArrivalLocation::IN_BACK_OF_SHIP)
			|| (p_objp->arrival_location == ArrivalLocation::ABOVE_SHIP) || (p_objp->arrival_location == ArrivalLocation::BELOW_SHIP)
			|| (p_objp->arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP) || (p_objp->arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP) ))
		{
			parse_warning_or_record("Arrival distance for ship %s cannot be %d — corrected to 1.", p_objp->name, p_objp->arrival_distance);
			p_objp->arrival_distance = 1;
		}
	}

	if (p_objp->arrival_location != ArrivalLocation::AT_LOCATION)
	{
		required_string("$Arrival Anchor:");
		stuff_string(name, F_NAME, NAME_LENGTH);
		p_objp->arrival_anchor = get_anchor(name);
	}

	p_objp->arrival_path_mask = -1;		// -1 only until resolved
	if (optional_string("+Arrival Paths:"))
	{
		// temporarily use mask to point to the restriction index
		p_objp->arrival_path_mask = add_path_restriction();
	}

	if (optional_string("+Arrival Delay:"))
	{
		int delay;
		stuff_int(&delay);
		if (delay < 0)
		{
			parse_warning_or_record("Arrival delay on ship %s cannot be negative — corrected to 0.", p_objp->name);
			delay = 0;
		}

		if (!Fred_running)
			p_objp->arrival_delay = -delay;			// use negative numbers to mean we haven't set up a timer yet
		else
			p_objp->arrival_delay = delay;
	}

	required_string("$Arrival Cue:");
	p_objp->arrival_cue = get_sexp_main();

	find_and_stuff("$Departure Location:", &temp, F_NAME, Departure_location_names, Num_arrival_names, "Departure Location");
	if (temp >= 0)
		p_objp->departure_location = static_cast<DepartureLocation>(temp);

	if (p_objp->departure_location != DepartureLocation::AT_LOCATION)
	{
		required_string("$Departure Anchor:");
		stuff_string(name, F_NAME, NAME_LENGTH);
		p_objp->departure_anchor = get_anchor(name);
	}

	p_objp->departure_path_mask = -1;		// -1 only until resolved
	if (optional_string("+Departure Paths:"))
	{
		// temporarily use mask to point to the restriction index
		p_objp->departure_path_mask = add_path_restriction();
	}

	if (optional_string("+Departure Delay:"))
	{
		int delay;
		stuff_int(&delay);
		if (delay < 0)
		{
			parse_warning_or_record("Departure delay on ship %s cannot be negative — corrected to 0.", p_objp->name);
			delay = 0;
		}

		if (!Fred_running)
			p_objp->departure_delay = -delay;		// use negative numbers to mean that delay timer not yet set
		else
			p_objp->departure_delay = delay;
	}

	required_string("$Departure Cue:");
	p_objp->departure_cue = get_sexp_main();

	// look for warp parameters
	p_objp->warpin_params_index = parse_warp_params(&Warp_params[p_objp->warpin_params_index], WarpDirection::WARP_IN, "Ship", p_objp->name);
	p_objp->warpout_params_index = parse_warp_params(&Warp_params[p_objp->warpout_params_index], WarpDirection::WARP_OUT, "Ship", p_objp->name);

	// dummy string; not used
	if (optional_string("$Misc Properties:"))
		stuff_string(name, F_NAME, NAME_LENGTH);

	required_string("$Determination:");
	int dummy; 
	stuff_int(&dummy);

    // set flags
    if (optional_string("+Flags:"))
    {
        SCP_vector<SCP_string> unparsed_vec;
        parse_string_flag_list(p_objp->flags, Parse_object_flags, Num_parse_object_flags, &unparsed_vec);
        if (!unparsed_vec.empty()) {
			for (const auto& unparsed: unparsed_vec) {
				// catch typos or deprecations
				if (!stricmp(unparsed.c_str(), "no-collide") || !stricmp(unparsed.c_str(), "no_collide")) {
					p_objp->flags.set(Mission::Parse_Object_Flags::OF_No_collide);
				} else if (!stricmp(unparsed.c_str(), "cannot-perform-scan")) {
					p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_perform_scan_hide_cargo);
				} else {
					WarningEx(LOCATION, "Unknown flag in parse object flags: %s", unparsed.c_str());
				}
            }
        }
    }

    // second set - Goober5000
    if (optional_string("+Flags2:"))
    {
        SCP_vector<SCP_string> unparsed_vec;
        parse_string_flag_list(p_objp->flags, Parse_object_flags, Num_parse_object_flags, &unparsed_vec);
        if (!unparsed_vec.empty()) {
            for (const auto& unparsed: unparsed_vec) {
				// catch typos or deprecations
				if (!stricmp(unparsed.c_str(), "no-collide") || !stricmp(unparsed.c_str(), "no_collide")) {
					p_objp->flags.set(Mission::Parse_Object_Flags::OF_No_collide);
				} else if (!stricmp(unparsed.c_str(), "cannot-perform-scan")) {
					p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_perform_scan_hide_cargo);
				} else {
					WarningEx(LOCATION, "Unknown flag in parse object flags: %s", unparsed.c_str());
				}
            }
        }
    }


	// always store respawn priority, just for ease of implementation
	if(optional_string("+Respawn Priority:"))
		stuff_int(&p_objp->respawn_priority);	

    if (optional_string("+Escort Priority:"))
    {
        stuff_int(&p_objp->escort_priority);
    }

	if (optional_string("+Guardian Threshold:")) 
	{
		stuff_int(&p_objp->ship_guardian_threshold);
	} else {
		if (p_objp->flags[Mission::Parse_Object_Flags::SF_Guardian]) {
			p_objp->ship_guardian_threshold = SHIP_GUARDIAN_THRESHOLD_DEFAULT;
		}
	}

    if (p_objp->flags[Mission::Parse_Object_Flags::OF_Player_start])
    {
        p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cargo_known);	// make cargo known for players
        Player_starts++;
    }

	if (optional_string("$Special Explosion:")) {
		p_objp->use_special_explosion = true;
		bool period_detected = false;

		if (required_string("+Special Exp Damage:")) {
			stuff_int(&p_objp->special_exp_damage);

			if (*Mp == '.') {
				period_detected = true;
				advance_to_eoln(NULL);
			}
		}

		if (required_string("+Special Exp Blast:")) {
			stuff_int(&p_objp->special_exp_blast);

			if (*Mp == '.') {
				period_detected = true;
				advance_to_eoln(NULL);
			}
		}

		if (required_string("+Special Exp Inner Radius:")) {
			stuff_int(&p_objp->special_exp_inner);

			if (*Mp == '.') {
				period_detected = true;
				advance_to_eoln(NULL);
			}
		}

		if (required_string("+Special Exp Outer Radius:")) {
			stuff_int(&p_objp->special_exp_outer);

			if (*Mp == '.') {
				period_detected = true;
				advance_to_eoln(NULL);
			}
		}

		if (optional_string("+Special Exp Shockwave Speed:")) {
			stuff_int(&p_objp->special_exp_shockwave_speed);
			p_objp->use_shockwave = true;

			if (*Mp == '.') {
				period_detected = true;
				advance_to_eoln(NULL);
			}
		}

		if (optional_string("+Special Exp Death Roll Time:")) {
			stuff_int(&p_objp->special_exp_deathroll_time);
		}

		if (period_detected) {
			nprintf(("Warning", "Special explosion attributes have been returned to integer format\n"));
		}
	}

	if (optional_string("+Special Hitpoints:")) {
		stuff_int(&p_objp->special_hitpoints);
	}

	if (optional_string("+Special Shield Points:")) {
		stuff_int(&p_objp->special_shield);
	}

	if (optional_string("+Special Exp index:")) {
		int variable_index; 
		stuff_int(&variable_index);
		fix_old_special_explosions(p_objp, variable_index);
	}

	if (optional_string("+Special Hitpoint index:")) {
		int variable_index; 
		stuff_int(&variable_index);
		fix_old_special_hits(p_objp, variable_index);
	}

	// set custom shield value
	if (p_objp->special_shield != -1) {
		p_objp->ship_max_shield_strength = (float) p_objp->special_shield; 
	}
	
	// set custom hitpoint value
	if (p_objp->special_hitpoints > 0) {
		p_objp->ship_max_hull_strength = (float) p_objp->special_hitpoints; 
	}

	// if the kamikaze flag is set, we should have the next flag
	if (optional_string("+Kamikaze Damage:"))
	{
		int damage;

		stuff_int(&damage);
		p_objp->kamikaze_damage = damage;
	}

	if (optional_string("+Hotkey:"))
	{
		stuff_int(&p_objp->hotkey);
		Assert((p_objp->hotkey >= 0) && (p_objp->hotkey < 10));
	}

	// Goober5000
	while (optional_string("+Docked With:"))
	{
		char docked_with[NAME_LENGTH];
		char docker_point[NAME_LENGTH];
		char dockee_point[NAME_LENGTH];

		// grab docking information
		// (whoever designed the original docking system
		// reversed the dockpoints in the mission file)
		stuff_string(docked_with, F_NAME, NAME_LENGTH);
		required_string("$Docker Point:");
		stuff_string(dockee_point, F_NAME, NAME_LENGTH);
		required_string("$Dockee Point:");
		stuff_string(docker_point, F_NAME, NAME_LENGTH);	

		// make sure we don't overflow the limit
		if (Total_initially_docked >= MAX_SHIPS)
		{
			mprintf(("Too many initially docked instances; skipping...\n"));
			continue;
		}

		// put this information into the Initially_docked array
		strcpy_s(Initially_docked[Total_initially_docked].docker, p_objp->name);
		strcpy_s(Initially_docked[Total_initially_docked].dockee, docked_with);
		strcpy_s(Initially_docked[Total_initially_docked].docker_point, docker_point);
		strcpy_s(Initially_docked[Total_initially_docked].dockee_point, dockee_point);
		Total_initially_docked++;
	}

	// check the optional parameter for destroying the ship before the mission starts.  If this parameter is
	// here, then we need to destroy the ship N seconds before the mission starts (for debris purposes).
	// store the time value here.  We want to create this object for sure.  Set the arrival cue and arrival
	// delay to bogus values
	if (optional_string("+Destroy At:"))
	{
		stuff_int(&p_objp->destroy_before_mission_time);
		if (p_objp->destroy_before_mission_time < 0)
		{
			parse_warning_or_record("'Destroy before mission' value on ship %s cannot be negative — corrected to 0.", p_objp->name);
			p_objp->destroy_before_mission_time = 0;
		}

		p_objp->arrival_cue = Locked_sexp_true;
		p_objp->arrival_delay = timestamp(0);
	}

	// check for the optional "orders accepted" string which contains the orders from the default
    // set that this ship will actually listen to
	// Obsolete and only for backwards compatibility
    if (optional_string("+Orders Accepted:"))
    {
		int tmp_orders;
        stuff_int(&tmp_orders);
		
        if (tmp_orders != -1) {
			p_objp->flags.set(Mission::Parse_Object_Flags::SF_Use_unique_orders);

			//Iterate over all player orders for the bitfield, but restrict to those that the integer could actually provide
			for(size_t j = 0; j < Player_orders.size() && j < std::numeric_limits<decltype(tmp_orders)>::digits; j++) {
				if((1 << j) & tmp_orders)
					p_objp->orders_accepted.insert(j + 1); //The first "true" order starts at idx 1, since 0 can be "no order"
			}
		}
    }

	if (optional_string("+Orders Accepted List:"))
	{
		SCP_vector<SCP_string> accepted_flags;
		stuff_string_list(accepted_flags);

		for (const SCP_string& accepted : accepted_flags) {
			for (size_t j = 0; j < Player_orders.size(); j++) {
				if (Player_orders[j].parse_name == accepted)
					p_objp->orders_accepted.insert(j);
			}
		}

		p_objp->flags.set(Mission::Parse_Object_Flags::SF_Use_unique_orders);
	}

	if (optional_string("+Group:"))
		stuff_int(&p_objp->group);

	if (optional_string("+Layer:")) {
		stuff_string(p_objp->fred_layer, F_NAME);
		if (!mission_has_layer_name(&The_mission, p_objp->fred_layer)) {
			if (p_objp->fred_layer.empty()) {
				p_objp->fred_layer = "Default";
			} else {
				The_mission.fred_layers.push_back(p_objp->fred_layer);
			}
		}
	}

	bool table_score = false; 
	if (optional_string("+Use Table Score:")) {
		table_score = true; 
	}

	if (optional_string("+Score:")) {
		if (!table_score) {
			stuff_int(&p_objp->score);
		}
		// throw away the value the mission file has and use the table value.
		else {
			int temp_score; 
			stuff_int(&temp_score); 
		}
	}
	else {
		table_score = true;
	}
	
	if (table_score) {
		p_objp->score = sip->score;
	}

	if (optional_string("+Assist Score Percentage:")) {
		stuff_float(&p_objp->assist_score_pct);
		// value must be a percentage
		if (p_objp->assist_score_pct < 0) {
			p_objp->assist_score_pct = 0;
		} 
		else if (p_objp->assist_score_pct > 1) {
			p_objp->assist_score_pct = 1;
		}
	}

	// parse the persona index if present
	// For backwards compatbility only
	if (optional_string("+Persona Index:")) {
		stuff_int(&p_objp->persona_index);
		if (p_objp->persona_index < -1 || p_objp->persona_index >= (int)Personas.size()) {
			parse_warning_or_record("Persona index %d for ship %s is out of range — corrected to -1.", p_objp->persona_index, p_objp->name);
			p_objp->persona_index = -1;
		}
	}

	if (optional_string("+Persona Name:")) {
		SCP_string persona;
		stuff_string(persona, F_NAME);
		p_objp->persona_index = message_persona_name_lookup(persona.c_str());
	}

	// texture replacement - Goober5000
	if (optional_string("$Texture Replace:") || optional_string("$Duplicate Model Texture Replace:"))
	{
		texture_replace tr;
		char *p;

		tr.from_table = false;

		while (optional_string("+old:"))
		{
			strcpy_s(tr.ship_name, p_objp->name);
			tr.new_texture_id = -1;

			stuff_string(tr.old_texture, F_NAME, MAX_FILENAME_LEN);
			required_string("+new:");
			stuff_string(tr.new_texture, F_NAME, MAX_FILENAME_LEN);

			// get rid of extensions
			p = strchr(tr.old_texture, '.');
			if (p)
			{
				mprintf(("Extraneous extension found on replacement texture %s!\n", tr.old_texture));
				*p = 0;
			}
			p = strchr(tr.new_texture, '.');
			if (p)
			{
				mprintf(("Extraneous extension found on replacement texture %s!\n", tr.new_texture));
				*p = 0;
			}

			// add it if we aren't over the limit
			if (p_objp->replacement_textures.size() < MAX_MODEL_TEXTURES)
				p_objp->replacement_textures.push_back(tr);
			else
				mprintf(("Too many replacement textures specified for ship '%s'!\n", p_objp->name));
		}
	}

	// for multiplayer, assign a network signature to this parse object.  Doing this here will
	// allow servers to use the signature with clients when creating new ships, instead of having
	// to pass ship names all the time
	if (Game_mode & GM_MULTIPLAYER)
		p_objp->net_signature = multi_assign_network_signature(MULTI_SIG_SHIP);

	// this is a valid/legal ship to create
	return 1;
}

void mission_parse_handle_late_arrivals(p_object *p_objp)
{
	ship_info *sip = NULL;

	// only for objects which show up after the start of a mission
	if (p_objp->created_object != NULL)
		return;

	Assert( p_objp->ship_class >= 0 );

	sip = &Ship_info[p_objp->ship_class];

	// we need the model to process the texture set, so go ahead and load it now
	sip->model_num = model_load(sip->pof_file, sip);
}

// Goober5000 - I split this because 1) it's clearer; and 2) initially multiple docked ships would have been
// insanely difficult otherwise
//
// Maybe create the object.
// Don't create the new ship blindly.  First, check the sexp for the arrival cue
// to determine when this ship should arrive.  If not right away, stick this ship
// onto the ship arrival list to be looked at later.  Also check to see if it should use the
// wings arrival cue.  The ship may get created later depending on whether or not the wing
// is created.
// Always create ships when FRED is running.
void mission_parse_maybe_create_parse_object(p_object *pobjp)
{
	// Bail if it was already created.  This should only happen when we previously
	// created all the objects in a docked group simultaneously.
	if (pobjp->created_object != NULL)
	{
		Assert(object_is_docked(pobjp));
		return;
	}

	// Goober5000
	// shunt this guy to the arrival list if he meets one of the following conditions:
	// 1) he's docked but not the dock leader
	// 2) this is FS2 (i.e. not FRED2) AND he meets one of the following conditions:
	//    a) he's not cued to arrive yet
	//    b) his arrival delay hasn't elapsed
	//    c) he's reinforcement
    if ((object_is_docked(pobjp) && !(pobjp->flags[Mission::Parse_Object_Flags::SF_Dock_leader])) || 
        (!Fred_running && (!eval_sexp(pobjp->arrival_cue) || 
            !timestamp_elapsed(pobjp->arrival_delay) || 
            (pobjp->flags[Mission::Parse_Object_Flags::SF_Reinforcement]))))
	{
		// we can't add ships getting destroyed to the arrival list!!!
		Assert (pobjp->destroy_before_mission_time < 0);

		// add to arrival list
		list_append(&Ship_arrival_list, pobjp);

		// we need to deal with replacement textures now, so that texture page-in will work properly
		mission_parse_handle_late_arrivals(pobjp);
	}
	// ingame joiners bail here.
	else if((Game_mode & GM_MULTIPLAYER) && (Net_player->flags & NETINFO_FLAG_INGAME_JOIN))
	{
		return;
	}
	// the ship is present at the beginning of the mission, so we create it
	else
	{
		int	real_objnum = parse_create_object(pobjp);	// this object may later get destroyed depending on wing status!!!!

		// if the ship is supposed to be destroyed before the mission, then blow up the ship and mark the pieces
		// as last forever.  Only call this stuff when you are blowing up the ship
		if (pobjp->destroy_before_mission_time >= 0)
		{
			object *objp = &Objects[real_objnum];

			// FreeSpace
			if (!Fred_running)
			{
				shipfx_blow_up_model(objp, 0, 0, &objp->pos);
				objp->flags.set(Object::Object_Flags::Should_be_dead);

				// Make sure that the ship is marked as destroyed so the AI doesn't freak out later
				ship_add_exited_ship(&Ships[objp->instance], Ship::Exit_Flags::Destroyed);

				// Same with the ship registry so that SEXPs don't refer to phantom ships
				auto entry = &Ship_registry[Ship_registry_map[pobjp->name]];
				entry->status = ShipStatus::EXITED;
				entry->objnum = -1;
				entry->shipnum = -1;
				entry->cleanup_mode = SHIP_DESTROYED;

				// once the ship is exploded, find the debris pieces belonging to this object, mark them
				// as not to expire, and move them forward in time N seconds
				for (auto &db: Debris)
				{
					if (!(db.flags[Debris_Flags::Used]))		// not used, move onto the next one.
						continue;
					if (db.source_objnum != real_objnum)		// not from this ship, move to next one
						continue;
					
					debris_remove_from_hull_list(&db);
					db.flags.set(Debris_Flags::DoNotExpire);   // mark as don't expire
					db.lifeleft = -1.0f;						// be sure that lifeleft == -1.0 so that it really doesn't expire!

					// now move the debris along its path for N seconds
					objp = &Objects[db.objnum];
					physics_sim(&objp->pos, &objp->orient, &objp->phys_info, &The_mission.gravity, (float) pobjp->destroy_before_mission_time);
				}
			}
			// FRED
			else
			{
				// be sure to set the variable in the ships structure for the final death time!!!
				Ships[objp->instance].final_death_time = pobjp->destroy_before_mission_time;
				Ships[objp->instance].flags.set(Ship::Ship_Flags::Kill_before_mission);
			}
		}
	}
}

void parse_common_object_data(p_object *p_objp)
{
	int i;

	// Genghis: used later for subsystem checking
	auto sip = &Ship_info[p_objp->ship_class];

	// now change defaults if present
	if (optional_string("+Initial Velocity:"))
		stuff_int(&p_objp->initial_velocity);
	if (optional_string("+Initial Hull:"))
		stuff_int(&p_objp->initial_hull);
	if (optional_string("+Initial Shields:"))
		stuff_int(&p_objp->initial_shields);

	p_objp->subsys_index = Subsys_index;
	while (optional_string("+Subsystem:")) {
		i = allocate_subsys_status();

		p_objp->subsys_count++;
		stuff_string(Subsys_status[i].name, F_NAME, NAME_LENGTH);
		
		// Genghis: check that the subsystem name makes sense for this ship type
		if (subsystem_stricmp(Subsys_status[i].name, NOX("pilot")))
		{
			int j;
			for (j=0; j < sip->n_subsystems; ++j)
				if (!subsystem_stricmp(sip->subsystems[j].subobj_name, Subsys_status[i].name))
					break;
			//if (j == sip->n_subsystems)
				//Warning(LOCATION, "Ship \"%s\", class \"%s\"\nUnknown subsystem \"%s\" found in mission!", objp->name, sip->name, Subsys_status[i].name);
		}

		if (optional_string("$Damage:"))
			stuff_float(&Subsys_status[i].percent);

		if (optional_string("+Cargo Name:")) {
			char cargo_name[NAME_LENGTH];
			stuff_string(cargo_name, F_NAME, NAME_LENGTH);
			int index = string_lookup(cargo_name, Cargo_names, Num_cargo, "cargo", false);
			if (index == -1) {
				if (Num_cargo < MAX_CARGO) {
					index = Num_cargo;
					strcpy(Cargo_names[Num_cargo++], cargo_name);
				}
				else {
					WarningEx(LOCATION, "Maximum number of cargo names (%d) exceeded, defaulting to Nothing!", MAX_CARGO);
					index = 0;
				}
			}
			Subsys_status[i].subsys_cargo_name = index;
		}

		if (optional_string("+Cargo Title:")) {
			stuff_string(Subsys_status[i].subsys_cargo_title, F_NAME, NAME_LENGTH);
		}

		if (optional_string("+AI Class:"))
		{
			Subsys_status[i].ai_class = match_and_stuff(F_NAME, Ai_class_names, Num_ai_classes, "AI class");

			if (Subsys_status[i].ai_class < 0)
			{
				Warning(LOCATION, "AI Class for ship %s and subsystem %s does not exist in ai.tbl. Setting to first available class.\n", p_objp->name, Subsys_status[i].name);
				Subsys_status[i].ai_class = 0;
			}
		}

		if (optional_string("+Primary Banks:"))
			stuff_int_list(Subsys_status[i].primary_banks, MAX_SHIP_PRIMARY_BANKS, ParseLookupType::WEAPON_LIST_TYPE);

		// Goober5000
		if (optional_string("+Pbank Ammo:"))
			stuff_int_list(Subsys_status[i].primary_ammo, MAX_SHIP_PRIMARY_BANKS, ParseLookupType::RAW_INTEGER_TYPE);

		if (optional_string("+Secondary Banks:"))
			stuff_int_list(Subsys_status[i].secondary_banks, MAX_SHIP_SECONDARY_BANKS, ParseLookupType::WEAPON_LIST_TYPE);

		if (optional_string("+Sbank Ammo:"))
			stuff_int_list(Subsys_status[i].secondary_ammo, MAX_SHIP_SECONDARY_BANKS, ParseLookupType::RAW_INTEGER_TYPE);
	}
}


/**
 * Checks if any ships of a certain ship class are still available in the team loadout
 * @return The index of the ship in team_data->ship_list if found or -1 if it isn't
 */
int get_reassigned_index(team_data *current_team, int ship_class) 
{
	// Search through the available ships to see if there is a matching ship class in the loadout
	for (int i=0; i < current_team->num_ship_choices; i++)
	{
		if (ship_class == current_team->ship_list[i])
		{
			if (current_team->ship_count[i] > 0) {
				return i;
			}
			else {
				return -1;
			}
		}
	}

	return -1;
}

/**
 * Updates the loadout quanities for a ship class.
 */
void update_loadout_totals(team_data *current_team, int loadout_index)
{
	// Fix the loadout variables to show that the class has less available if there are still ships available
	if (current_team->ship_count[loadout_index] > 0)
	{
		Assert (current_team->loadout_total > 0); 

		current_team->ship_count[loadout_index]--;
		current_team->loadout_total--;
	}
}

/**
 * Attempts to set the class of this ship based which ship classes still remain unassigned in the ship loadout
 * The ship class specified by the mission file itself is tested first. Followed by the list of alt classes. 
 * If an alt class flagged as default_to_this_class is reached the ship will be assigned to that class.
 * If the class can't be assigned because no ships of that class remain the function returns false.  
 */
bool is_ship_assignable(p_object *p_objp)
{
	int loadout_index = -1;

	team_data *data_for_team = &Team_data[p_objp->team];

	// First lets check if the ship specified in the mission file is of an assignable class
	loadout_index = get_reassigned_index(data_for_team, p_objp->ship_class);
	if (loadout_index != -1 )
	{
		Assert (data_for_team->loadout_total > 0);

		update_loadout_totals(data_for_team, loadout_index);
			
		// Since the ship in the mission file matched one available in the loadout we need go no further
		return true;
	}

	// Now we check the alt_classes (if there are any)
	for (SCP_vector<alt_class>::iterator pac = p_objp->alt_classes.begin(); pac != p_objp->alt_classes.end(); ++pac) {
		// we don't check availability unless we are asked to
		if (pac->default_to_this_class == false) {
			loadout_index = pac->ship_class;
			break;
		}
		else {
			loadout_index = get_reassigned_index(data_for_team, pac->ship_class);
			if (loadout_index != -1 ) {
				update_loadout_totals(data_for_team, loadout_index);
				break;
			}
		}
	}

	// If we managed to assign a class we'd may need to actually swap to it
	if (loadout_index != -1 ) {
		if (p_objp->ship_class != data_for_team->ship_list[loadout_index])
		{
			swap_parse_object(p_objp, data_for_team->ship_list[loadout_index]);
		}
		return true;
	}

	return false;
}

/**
 * Checks the list of Parse_objects to see if any of them should be reassigned based on the 
 * number of ships of that class that were present in the loadout.
 */
void process_loadout_objects() 
{	
	SCP_vector<size_t> reassignments;
	
	// Loop through all the Parse_objects looking for ships that should be affected by the loadout code.
	for (size_t i=0; i < Parse_objects.size(); i++)
	{
		p_object *p_objp = &Parse_objects[i];
        if (p_objp->flags[Mission::Parse_Object_Flags::SF_Set_class_dynamically])
		{
			if (!(is_ship_assignable(p_objp)))
			{
				// store the ship so we can come back to it later.
				reassignments.push_back(i);
			}
		}
	}
		
	// Now we go though the ships we were unable to assign earlier and reassign them on a first come first 
	// served basis.
	for (size_t m=0; m < reassignments.size(); m++)
	{
		p_object *p_objp = &Parse_objects[reassignments[m]];
		team_data *current_team = &Team_data[p_objp->team];
		bool loadout_assigned = false;
        Assert(p_objp->flags[Mission::Parse_Object_Flags::SF_Set_class_dynamically]);

		// First thing to check is whether we actually have any ships left to assign
		if (current_team->loadout_total == 0)
		{
			// If there is nothing left to assign we should use the ship in the mission file
			loadout_assigned = true;
		}
		// We do have ships left in the team loadout that we can assign
		else
		{
			// Go through the loadout until we find an unassigned ship
			for (int j=0; j < current_team->num_ship_choices; j++)
			{
				if (current_team->ship_count[j] > 0)
				{
					update_loadout_totals(current_team, j);
					// We will need to assign a new class too (if a p_object the same class was available
					// it should have been assigned by attempt_loadout_assignation_from_defaults()
					Assert (p_objp->ship_class != current_team->ship_list[j]);
					swap_parse_object(p_objp, current_team->ship_list[j]);

					loadout_assigned = true;
					break ;
				}
			}
		}
			
		// We should never reach here with an unassigned loadout
		Assert (loadout_assigned);
	}
}

extern UI_TIMESTAMP Multi_ping_timestamp;
void parse_objects(mission *pm, int flag)
{	
	Assert(pm != NULL);

	required_string("#Objects");	

	// parse in objects
	while (required_string_either("#Wings", "$Name:"))
	{
		p_object pobj;

		// parse a single object
		int valid = parse_object(pm, flag, &pobj);

		// not all objects are always valid or legal
		if (!valid)
			continue;

		// add it
		Parse_objects.push_back(pobj);

		// send out a ping if we are multi so that psnet2 doesn't kill us off for a long load
		// NOTE that we can't use the timestamp*() functions here since they won't increment
		//      during this loading process
		if (Game_mode & GM_MULTIPLAYER)
		{
			if (!Multi_ping_timestamp.isValid() || ui_timestamp_elapsed(Multi_ping_timestamp))
			{
				multi_ping_send_all();
				Multi_ping_timestamp = ui_timestamp(10000); // timeout is 10 seconds between pings
			}
		}
	}

	// Goober5000 - I moved the docking stuff to post_process_ships_wings because of interdependencies
	// between ships and wings.  Neither docking stuff nor ship stuff (for ships present at mission start)
	// will be valid until after post_process_ships_wings is run.

	// Karajorma - Now that we've parsed all the objects we can set the class of those which were flagged 
	// to be set based on the number of ships available in the loadout. 
	if (!Fred_running)
	{
		process_loadout_objects();
	}	
}

/**
 * Replaces a p_object with a new one based on a Ship_info index.
 */
void swap_parse_object(p_object *p_obj, int new_ship_class)
{
	ship_info *new_ship_info = &Ship_info[new_ship_class];
	ship_info *old_ship_info = &Ship_info[p_obj->ship_class];
	int subsys_ind = p_obj->subsys_index;
	subsys_status *ship_subsystems = &Subsys_status[subsys_ind];

	// Class
	// First things first. Change the class of the p_object
	p_obj->ship_class = new_ship_class;

	if (p_obj->wingnum > -1 && p_obj->pos_in_wing == 0) {
		Wings[p_obj->wingnum].special_ship_ship_info_index = new_ship_class;
	}

	// Hitpoints
	// We need to take into account that the ship might have been assigned special hitpoints so we can't 
	// simply swap old for new. 
	Assert (p_obj->ship_max_hull_strength > 0);
	Assert (old_ship_info->max_hull_strength > 0);
	
	float hp_multiplier = p_obj->ship_max_hull_strength / old_ship_info->max_hull_strength;
	p_obj->ship_max_hull_strength = new_ship_info->max_hull_strength * hp_multiplier;

	// Shields
	// Again we have to watch out for special hitpoints but this time we can't assume that there will be a 
	// shield. So first lets see if there is one. 
	if ((p_obj->ship_max_shield_strength != old_ship_info->max_shield_strength) && 
		(p_obj->ship_max_shield_strength > 0) &&
		(new_ship_info->max_shield_strength > 0))
	{
		// This ship is using special hitpoints to alter the shield strength
		float shield_multiplier = p_obj->ship_max_shield_strength / i2fl(old_ship_info->max_shield_strength);
		p_obj->ship_max_shield_strength = new_ship_info->max_shield_strength * shield_multiplier;
	}
	// Not using special hitpoints or a class which has a shield strength of zero
	else
	{
		p_obj->ship_max_shield_strength = new_ship_info->max_shield_strength;
	}
	
	// Primary weapons
	// First find out what is the correct number for a ship of this class
	int num_pbanks = new_ship_info->num_primary_banks;
	// Now cycle through the primary banks looking for banks that were added or removed
	for (int i=0; i < MAX_SHIP_PRIMARY_BANKS; i++)
	{
		// If we're dealing with a primary bank that actually should exist on this ship
		if ( i < num_pbanks )
		{
			// We only care if a weapon hasn't been parsed in for this bank
			if (ship_subsystems->primary_banks[i] == -1)
			{
				// Give the ship the default weapon for this bank. 
				ship_subsystems->primary_banks[i] = new_ship_info->primary_bank_weapons[i];
			}
		}		
		// Any primary banks the ship doesn't have should be set to -1
		else
		{
			ship_subsystems->primary_banks[i] = -1;
		}
	}

	// Secondary weapons 
	// Again we first have to find out how many we should have
	int num_sbanks = new_ship_info->num_secondary_banks;
	// Now cycle through the secondary banks looking for banks that were added or removed
	for (int j=0; j < MAX_SHIP_SECONDARY_BANKS; j++)
	{
		// If we're dealing with a primary bank that actually should exist on this ship
		if ( j < num_sbanks )
		{
			// We only care if a weapon hasn't been parsed in for this bank
			if (ship_subsystems->secondary_banks[j] == -1){
				// Give the ship the default weapon for this bank. 
				ship_subsystems->secondary_banks[j] = new_ship_info->secondary_bank_weapons[j];
			}
		}		
		// Any secondary banks the ship doesn't have should be set to -1
		else
		{
			ship_subsystems->secondary_banks[j] = -1;
		}
	}
}

// Goober5000 - this needs to search the vector instead of using the registry, because it can be used during parsing before the registry is populated
p_object *mission_parse_find_parse_object(const char *name)
{
	SCP_vector<p_object>::iterator ii;

	// look for original ships
	for (ii = Parse_objects.begin(); ii != Parse_objects.end(); ++ii)
		if (!stricmp(ii->name, name))
			return &(*ii);

	// boo
	return NULL;
}

int find_wing_name(char *name)
{
	int	i;

	for (i = 0; i < Num_wings; i++)
	{
		if (!stricmp(name, Wings[i].name))
			return i;
	}

	return -1;
}

/**
* @brief						Tries to create a wing of ships
* @param[inout]	wingp			Pointer to the wing structure of the wing to be created
* @param[in] num_to_create		Number of ships to create
* @param[in] force_create		If true, the wing will be created regardless of whether or not the arrival conditions
*								have been met yet.
* @param[in] force_arrival		If true, the wing will assume its arrival cue is true
* @param[in] specific_instance	Set this to create a specific ship from this wing
* @returns						Number of ships created
*/
int parse_wing_create_ships( wing *wingp, int num_to_create, bool force_create, bool force_arrival, int specific_instance )
{
	int wingnum, objnum, num_create_save;
	int time_to_arrive;
	int pre_create_count;
	int i, j;

	// we need to send this in multiplayer
	pre_create_count = wingp->total_arrived_count;

	// force_create is used to force creation of the wing -- used for multiplayer
	if ( !force_create ) {
		// we only want to evaluate the arrival cue of the wing if:
		// 1) single player
		// 2) multiplayer and I am the host of the game
		// can't create any ships if the arrival cue is false or the timestamp has not elapsed.

		if ( !force_arrival && !eval_sexp(wingp->arrival_cue) )
			return 0;

		// once the sexpressions becomes true, then check the arrival delay on the wing.  The first time, the
		// arrival delay will be <= 0 meaning that no timer object has been set yet.  Set up the timestamp
		// which should always give a number >= 0;
		if ( wingp->arrival_delay <= 0 ) {
			wingp->arrival_delay = timestamp( -wingp->arrival_delay * 1000 );
			Assert ( wingp->arrival_delay >= 0 );
		}

		if ( !timestamp_elapsed( wingp->arrival_delay ) )
			return 0;

		// if wing is coming from docking bay, then be sure that ship we are arriving from actually exists
		// (or will exist).
		if ( wingp->arrival_location == ArrivalLocation::FROM_DOCK_BAY ) {
			Assert( wingp->arrival_anchor.isValid() );
			auto anchor_ship_entry = ship_registry_get(wingp->arrival_anchor);

			// see if ship is yet to arrive.  If so, then return 0 so we can evaluate again later.
			if (!anchor_ship_entry || anchor_ship_entry->status == ShipStatus::NOT_YET_PRESENT)
				return 0;

			// see if ship is in mission.
			if (!anchor_ship_entry->has_shipp()) {
				int num_remaining;

				// since this wing cannot arrive from this place, we need to mark the wing as destroyed and
				// set the wing variables appropriately.  Good for directives.

				// set the gone flag
                wingp->flags.set(Ship::Wing_Flags::Gone);

				// mark the number of waves and number of ships destroyed equal to the last wave and the number
				// of ships yet to arrive
				num_remaining = ( (wingp->num_waves - wingp->current_wave) * wingp->wave_count);
				wingp->total_arrived_count += num_remaining;
				wingp->current_wave = wingp->num_waves;

				if ( mission_log_get_time(LOG_SHIP_DESTROYED, anchor_ship_entry->name, nullptr, nullptr) || mission_log_get_time(LOG_SELF_DESTRUCTED, anchor_ship_entry->name, nullptr, nullptr) ) {
					wingp->total_destroyed += num_remaining;
				} else if ( mission_log_get_time(LOG_SHIP_DEPARTED, anchor_ship_entry->name, nullptr, nullptr) ) {
					wingp->total_departed += num_remaining;
				} else {
					wingp->total_vanished += num_remaining;
				}

				mission_parse_mark_non_arrival(wingp);	// Goober5000
				WarningEx(LOCATION, "Warning: Wing %s cannot arrive from docking bay of destroyed or departed %s.\n", wingp->name, anchor_ship_entry->name);
				return 0;
			}

			// Goober5000 - check status of fighterbays - if they're destroyed, we can't launch - but we want to reeval later
			if (ship_fighterbays_all_destroyed(anchor_ship_entry->shipp())) {
				WarningEx(LOCATION, "Warning: Wing %s cannot arrive from destroyed docking bay of %s.\n", wingp->name, anchor_ship_entry->name);
				return 0;
			}
		}

		if ( num_to_create == 0 )
			return 0;

		// check the wave_delay_timestamp field.  If it is not valid, make it valid (based on wave delay min
		// and max values).  If it is valid, and not elapsed, then return.  If it is valid and elasped, then
		// continue on.
		if ( !wingp->wave_delay_timestamp.isValid() ) {

			// if at least one of these is valid, then reset the timestamp.  If they are both zero, we will create the
			// wave
			if ( (wingp->wave_delay_min > 0) || (wingp->wave_delay_max > 0) ) {
				Assert ( wingp->wave_delay_min <= wingp->wave_delay_max );
				time_to_arrive = wingp->wave_delay_min + (int)(frand() * (wingp->wave_delay_max - wingp->wave_delay_min));

				// MWA -- 5/18/98
				// HACK HACK -- in the presense of Mike Comet and Mitri, I have introduced one of the most
				// serious breaches of coding standards.  I'm to lazy to fix this the correct way.  Insert
				// a delay before the next wave of the wing can arrive to that clients in the game have ample
				// time to kill off any ships in the wing before the next wave arrives.
				if ( Game_mode & GM_MULTIPLAYER ){
					time_to_arrive += 7;
				}
				wingp->wave_delay_timestamp = _timestamp(time_to_arrive * MILLISECONDS_PER_SECOND);
				return 0;
			}

			// if we get here, both min and max values are 0;  See comments above for a most serious hack
			time_to_arrive = 0;
			if ( Game_mode & GM_MULTIPLAYER )
				time_to_arrive += 7;
			wingp->wave_delay_timestamp = _timestamp(time_to_arrive * MILLISECONDS_PER_SECOND);
		}

		// now check to see if the wave_delay_timestamp is elapsed or not
		if ( !timestamp_elapsed(wingp->wave_delay_timestamp) )
			return 0;
	}

	// finally we can create the wing.

	num_create_save = num_to_create;

	wingnum = WING_INDEX(wingp);					// get the wing number

	// if there are no ships to create, then all ships must be player start ships -- do nothing in this case.
	if ( num_to_create == 0 ){
		return 0;
	}

	wingp->current_wave++;						// we are creating new ships
	// we need to create num_to_create ships.  Since the arrival cues for ships in a wing
	// are ignored, then *all* ships must be in the Ship_arrival_list.

	objnum = -1;

	// Goober5000 - we have to do this via the array because we have no guarantee we'll be able to iterate along the list
	// (since created objects plus anything they're docked to will be removed from it)
	for (SCP_vector<p_object>::iterator ii = Parse_objects.begin(); ii != Parse_objects.end(); ++ii)
	{
		ai_info *aip;
		p_object *p_objp = &(*ii);

		// compare the wingnums.  When they are equal, we can create the ship.  In the case of
		// wings that have multiple waves, this code implies that we essentially creating clones
		// of the ships that were created in Fred for the wing when more ships for a new wave
		// arrive.  The threshold value of a wing can also make one of the ships in a wing be "cloned"
		// more often than other ships in the wing.  I don't think this matters much.
		if (p_objp->wingnum != wingnum)
			continue;

		Assert((p_objp->pos_in_wing >= 0) && (p_objp->pos_in_wing < MAX_SHIPS_PER_WING));

		// skip ships destroyed before mission in a similar way that red-alert-deleted ships are skipped
		if (p_objp->destroy_before_mission_time >= 0)
		{
			num_to_create--;
			num_create_save--;
			ship_add_ship_type_count(p_objp->ship_class, -1);
			wingp->red_alert_skipped_ships++;	// even though it isn't red-alert, this is needed for keeping indexes correct

			// skip over this parse object
			if (num_to_create == 0)
				break;
			else
				continue;
		}

		// ensure on arrival list
		if (!parse_object_on_arrival_list(p_objp))
			continue;
	
		// when ingame joining, we need to create a specific ship out of the list of ships for a
		// wing.  specific_instance is a 0 based integer which specified which ship in the wing
		// to create.  So, only create the ship we actually need to.
		if ((Game_mode & GM_MULTIPLAYER) && (specific_instance > 0))
		{
			specific_instance--;
			continue;
		}
		// when not creating a specific ship, we should skip over any ships that weren't carried along in the red-alert
        else if (p_objp->flags[Mission::Parse_Object_Flags::Red_alert_deleted])
		{
			num_to_create--;
			num_create_save--;
			ship_add_ship_type_count(p_objp->ship_class, -1);
			wingp->red_alert_skipped_ships++;

			// clear the flag so that this parse object can be used for the next wave
            p_objp->flags.remove(Mission::Parse_Object_Flags::Red_alert_deleted);

			// skip over this parse object
			if (num_to_create == 0)
				break;
			else
				continue;
		}

        Assert(!(p_objp->flags[Mission::Parse_Object_Flags::SF_Cannot_arrive]));		// get allender

		// if we have the maximum number of ships in the wing, we must bail as well
		if (wingp->current_count >= MAX_SHIPS_PER_WING)
		{
			Int3();					// this is bogus -- we should always allow all ships to be created
			num_to_create = 0;
			break;
		}

		// bash the ship name to be the name of the wing + some number if there is > 1 wave in this wing
		wingp->total_arrived_count++;
		if (wingp->num_waves > 1)
		{
			wing_bash_ship_name(p_objp, wingp, wingp->total_arrived_count + wingp->red_alert_skipped_ships);

			// subsequent waves of ships will not be in the ship registry, so add them
			if (!ship_registry_exists(p_objp->name))
			{
				ship_registry_entry entry(p_objp->name);
				entry.status = ShipStatus::NOT_YET_PRESENT;
				entry.pobj_num = POBJ_INDEX(p_objp);

				Ship_registry.push_back(entry);
				Ship_registry_map[p_objp->name] = static_cast<int>(Ship_registry.size() - 1);
			}
		}

		// also, if multiplayer, set the parse object's net signature to be wing's net signature
		// base + total_arrived_count (before adding 1)
		// Cyborg -- The original ships in the wave have their net_signature set at mission parse
		// so only do this if this is a subsequent wave.
		if (Game_mode & GM_MULTIPLAYER && wingp->current_wave > 1)
		{
			// Cyborg -- Also, then we need to subtract the original wave's number of fighters 
			// and also subtract 1 to use the wing's starting signature
			p_objp->net_signature = (ushort) (wingp->net_signature + wingp->total_arrived_count - (wingp->wave_count + 1));
		}


		objnum = parse_create_object(p_objp);
		aip = &Ai_info[Ships[Objects[objnum].instance].ai_index];

		parse_copy_wing_ai_to_ship(wingp, aip);

		// update housekeeping variables
		// NOTE:  for the initial wing setup we use actual position to get around
		//        object order isses, but ships in all following waves just get
		//        tacked onto the end of the list
		if (wingp->current_wave == 1) {
			wingp->ship_index[p_objp->pos_in_wing] = Objects[objnum].instance;
		} else {
			wingp->ship_index[wingp->current_count] = Objects[objnum].instance;
		}

		// set up wingman status index
		hud_wingman_status_set_index(wingp, &Ships[Objects[objnum].instance], p_objp);

		p_objp->wing_status_wing_index = Ships[Objects[objnum].instance].wing_status_wing_index;
		p_objp->wing_status_wing_pos = Ships[Objects[objnum].instance].wing_status_wing_pos;

		wingp->current_count++;

		// keep any player ship on the parse object list -- used for respawns
		// 5/8/98 -- MWA -- don't remove ships from the list when you are ingame joining
		if (!(p_objp->flags[Mission::Parse_Object_Flags::OF_Player_start]))
		{
			if ((Game_mode & GM_NORMAL) || !(Net_player->flags & NETINFO_FLAG_INGAME_JOIN))
			{
				// only remove ship if one wave in wing
				if (wingp->num_waves == wingp->current_wave)
				{
					// remove p_objp from the list
					list_remove(&Ship_arrival_list, p_objp);
					
					// free up sexp nodes for reuse
					if (p_objp->ai_goals != -1)
					{
						free_sexp2(p_objp->ai_goals);
						p_objp->ai_goals = -1;
					}
				}
			}
		}

		// keep track of how many ships to create.  Stop when we have done all that we are supposed to do.
		num_to_create--;
		if (num_to_create == 0)
			break;
	}

	// we should always have enough ships in the list!!!
	Assert (num_to_create == 0);

	// wing current_count needs to match the end of the ship_index[] list, but there
	// is a very off chance it could have holes in it (especially if it's a red-alert
	// wing that arrives late), so make sure to compact the list
	int length = MAX_SHIPS_PER_WING;
	for (i = 0; i < length; i++)
	{
		if (wingp->ship_index[i] == -1)
		{
			// shift actual values downward
			for (j = i; j < length - 1; j++)
			{
				wingp->ship_index[j] = wingp->ship_index[j+1];

				// update "special" ship too
				if (wingp->special_ship == j+1)
					wingp->special_ship--;
			}

			// last value becomes -1
			wingp->ship_index[j] = -1;
			length--;

			// stay on the current index in case we still have a -1
			i--;
		}
	}
	
	// possibly play some event driven music here.  Send a network packet indicating the wing was
	// created.  Only do this stuff if actually in the mission.
	if ( (objnum != -1) && (Game_mode & GM_IN_MISSION) ) {		// if true, we have created at least one new ship.
		int it, ship_num;

		// see if this wing is a player starting wing, and if so, call the maybe_add_form_goal
		// function to possibly make the wing form on the player
		for (it = 0; it < MAX_STARTING_WINGS; it++ ) {
			if ( Starting_wings[it] == wingnum ){
				break;
			}
		}
		if ( it < MAX_STARTING_WINGS ){
			ai_maybe_add_form_goal( wingp );
		}

		mission_log_add_entry( LOG_WING_ARRIVED, wingp->name, NULL, wingp->current_wave );
		ship_num = wingp->ship_index[0];

		if ( !(Ships[ship_num].flags[Ship::Ship_Flags::No_arrival_music]) && !(wingp->flags[Ship::Wing_Flags::No_arrival_music]) ) {
			if ( timestamp_elapsed(Allow_arrival_music_timestamp) ) {
				Allow_arrival_music_timestamp = timestamp(ARRIVAL_MUSIC_MIN_SEPARATION);
				event_music_arrival(Ships[ship_num].team);	
			}
		}

		// possibly change the location where these ships arrive based on the wings arrival location
		mission_set_wing_arrival_location( wingp, num_create_save );

		// if in multiplayer (and I am the host) and in the mission, send a wing create command to all
		// other players
		if ( MULTIPLAYER_MASTER ){
			send_wing_create_packet( wingp, num_create_save, pre_create_count );
		}

#ifndef NDEBUG
		// test code to check to be sure that all ships in the wing are ignoring the same types
		// of orders from the leader
		if ( Fred_running ) {
			Assert( wingp->ship_index[wingp->special_ship] != -1 );
			const std::set<size_t>& orders = Ships[wingp->ship_index[0]].orders_accepted;
			for (it = 0; it < wingp->current_count; it++ ) {
				if (it == wingp->special_ship)
					continue;

				if ( orders != Ships[wingp->ship_index[it]].orders_accepted ) {
					Warning(LOCATION, "ships in wing %s are ignoring different player orders.  Please find Mark A\nto talk to him about this.", wingp->name );
					break;
				}
			}
		}
#endif

	}

	wingp->wave_delay_timestamp = TIMESTAMP::invalid();		// we will need to set this up properly for the next wave
	return num_create_save;
}

void parse_wing(mission *pm)
{
	int wingnum, i, wing_goals;
	char name[NAME_LENGTH], ship_names[MAX_SHIPS_PER_WING][NAME_LENGTH];
	char wing_flag_strings[Num_parse_wing_flags][NAME_LENGTH];
	wing *wingp;

	Assert(pm != NULL);
	wingp = &Wings[Num_wings];

	required_string("$Name:");
	stuff_string(wingp->name, F_NAME, NAME_LENGTH);

	wingnum = find_wing_name(wingp->name);
	if (wingnum != -1)
		error_display(0, NOX("Redundant wing name: %s\n"), wingp->name);
	wingnum = Num_wings;

	// if this name has a hash, create a default display name
	if (get_pointer_to_first_hash_symbol(wingp->name))
	{
		wingp->display_name = wingp->name;
		end_string_at_first_hash_symbol(wingp->display_name);
		wingp->flags.set(Ship::Wing_Flags::Has_display_name);
	}

	if (optional_string("$Display Name:"))
	{
		stuff_string(wingp->display_name, F_NAME);
		wingp->flags.set(Ship::Wing_Flags::Has_display_name);
	}

	// squad logo - Goober5000
	if (optional_string("+Squad Logo:"))
	{
		int flag = -1;

		stuff_string(wingp->wing_squad_filename, F_NAME, MAX_FILENAME_LEN);

		// load it only if FRED isn't running
		if (!Fred_running)
		{
			// check all previous wings to see if we already loaded it (we want to save memory)
			for (i = 0; i < Num_wings; i++)
			{
				// do we have a previous texture?
				if (Wings[i].wing_insignia_texture != -1)
				{
					// if we have a match
					if (!stricmp(Wings[i].wing_squad_filename, wingp->wing_squad_filename))
					{
						flag = i;
						break;
					}
				}
			}

			// if we have loaded it already, just use the old bitmap index
			if (flag != -1)
			{
				wingp->wing_insignia_texture = Wings[flag].wing_insignia_texture;
			}
			else
			{
				wing_load_squad_bitmap(wingp);
			}
		}
	}

	required_string("$Waves:");
	stuff_int(&wingp->num_waves);
	Assert ( wingp->num_waves >= 1 );		// there must be at least 1 wave

	required_string("$Wave Threshold:");
	stuff_int(&wingp->threshold);

	required_string("$Special Ship:");
	stuff_int(&wingp->special_ship);

	// Use a custom formation if specified
	if (optional_string("+Formation:")) {
		char f[NAME_LENGTH];
		stuff_string(f, F_NAME, NAME_LENGTH);

		wingp->formation = wing_formation_lookup(f);
		if (wingp->formation < 0) {
			Warning(LOCATION, "Invalid Formation %s.", f);
		}
	}
	if (optional_string("+Formation Scale:")) {
		stuff_float(&wingp->formation_scale);
	}

	int temp;
	find_and_stuff("$Arrival Location:", &temp, F_NAME, Arrival_location_names, Num_arrival_names, "Arrival Location");
	if (temp >= 0)
		wingp->arrival_location = static_cast<ArrivalLocation>(temp);

	if ( optional_string("+Arrival Distance:") )
	{
		stuff_int( &wingp->arrival_distance );

		// Goober5000
		if ((wingp->arrival_distance <= 0) && (
			   (wingp->arrival_location == ArrivalLocation::NEAR_SHIP)
			|| (wingp->arrival_location == ArrivalLocation::IN_FRONT_OF_SHIP) || (wingp->arrival_location == ArrivalLocation::IN_BACK_OF_SHIP)
			|| (wingp->arrival_location == ArrivalLocation::ABOVE_SHIP) || (wingp->arrival_location == ArrivalLocation::BELOW_SHIP)
			|| (wingp->arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP) || (wingp->arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP) ))
		{
			parse_warning_or_record("Arrival distance for wing %s cannot be %d — corrected to 1.", wingp->name, wingp->arrival_distance);
			wingp->arrival_distance = 1;
		}
	}

	if ( wingp->arrival_location != ArrivalLocation::AT_LOCATION )
	{
		required_string("$Arrival Anchor:");
		stuff_string(name, F_NAME, NAME_LENGTH);
		wingp->arrival_anchor = get_anchor(name);
	}

	wingp->arrival_path_mask = -1;		// -1 only until resolved
	if (optional_string("+Arrival Paths:"))
	{
		// temporarily use mask to point to the restriction index
		wingp->arrival_path_mask = add_path_restriction();
	}

	if (optional_string("+Arrival delay:"))
	{
		int delay;
		stuff_int(&delay);
		if (delay < 0)
		{
			parse_warning_or_record("Arrival delay on wing %s cannot be negative — corrected to 0.", wingp->name);
			delay = 0;
		}

		if (!Fred_running)
			wingp->arrival_delay = -delay;			// use negative numbers to mean we haven't set up a timer yet
		else
			wingp->arrival_delay = delay;
	}

	required_string("$Arrival Cue:");
	wingp->arrival_cue = get_sexp_main();
	
	find_and_stuff("$Departure Location:", &temp, F_NAME, Departure_location_names, Num_arrival_names, "Departure Location");
	if (temp >= 0)
		wingp->departure_location = static_cast<DepartureLocation>(temp);

	if ( wingp->departure_location != DepartureLocation::AT_LOCATION )
	{
		required_string("$Departure Anchor:");
		stuff_string( name, F_NAME, NAME_LENGTH );
		wingp->departure_anchor = get_anchor(name);
	}

	wingp->departure_path_mask = -1;		// -1 only until resolved
	if (optional_string("+Departure Paths:"))
	{
		// temporarily use mask to point to the restriction index
		wingp->departure_path_mask = add_path_restriction();
	}

	if (optional_string("+Departure delay:"))
	{
		int delay;
		stuff_int(&delay);
		if (delay < 0)
		{
			parse_warning_or_record("Departure delay on wing %s cannot be negative — corrected to 0.", wingp->name);
			delay = 0;
		}

		if (!Fred_running)
			wingp->departure_delay = -delay;		// use negative numbers to mean that delay timer not yet set
		else
			wingp->departure_delay = delay;
	}

	required_string("$Departure Cue:");
	wingp->departure_cue = get_sexp_main();

	// stores a list of all names of ships in the wing
	required_string("$Ships:");
	wingp->wave_count = (int)stuff_string_list( ship_names, MAX_SHIPS_PER_WING );

	// get the wings goals, if any
	wing_goals = -1;
	if ( optional_string("$AI Goals:") )
		wing_goals = get_sexp_main();

	if (optional_string("+Hotkey:")) {
		stuff_int(&wingp->hotkey);
		Assert((wingp->hotkey >= 0) && (wingp->hotkey < 10));
	}

	if (optional_string("+Flags:")) {
		auto count = stuff_string_list(wing_flag_strings, Num_parse_wing_flags);

		for (size_t j = 0; j < count; j++) {
			auto tok = wing_flag_strings[j];
			bool matched = false;
			for (auto& Parse_wing_flag : Parse_wing_flags) {
				if (!stricmp(tok, Parse_wing_flag.name)) {
					wingp->flags.set(Parse_wing_flag.def);
					matched = true;
					break;
				}
			}

			if (!matched) {
				Warning(LOCATION, "Unknown wing flag '%s', skipping!", tok);
			}
		}
	}

	// get the wave arrival delay bounds (if present).  Used as lower and upper bounds (in seconds)
	// which determine when new waves of a wing should arrive.
	if ( optional_string("+Wave Delay Min:") )
		stuff_int( &(wingp->wave_delay_min) );
	if ( optional_string("+Wave Delay Max:") )
		stuff_int( &(wingp->wave_delay_max) );

	// 7/13/98 -- MWA
	// error checking against the player ship wings (i.e. starting & tvt) to be sure that wave count doesn't exceed one for
	// these wings.
	if ( MULTI_NOT_TEAM ) {
		for (i = 0; i < MAX_STARTING_WINGS; i++ ) {
			if ( !stricmp(Starting_wing_names[i], wingp->name) ) {
				if ( wingp->num_waves > 1 ) {
					// only end the game if we're the server - clients will eventually find out :)
					if(Net_player->flags & NETINFO_FLAG_AM_MASTER){
						multi_quit_game(PROMPT_NONE, MULTI_END_NOTIFY_NONE, MULTI_END_ERROR_WAVE_COUNT);																
					}
				}
			}
		}
	}
	else if (MULTI_TEAM) {
		for (i = 0; i < MAX_TVT_WINGS; i++ ) {
			if ( !stricmp(TVT_wing_names[i], wingp->name) ) {
				if ( wingp->num_waves > 1 ) {
					// only end the game if we're the server - clients will eventually find out :)
					if(Net_player->flags & NETINFO_FLAG_AM_MASTER){
						multi_quit_game(PROMPT_NONE, MULTI_END_NOTIFY_NONE, MULTI_END_ERROR_WAVE_COUNT);																
					}
				}
			}
		}
	}

	// Get the next starting signature for this in this wing.  We want to reserve wave_count * num_waves
	// of signature.  These can be used to construct wings for ingame joiners.
	if ( Game_mode & GM_MULTIPLAYER ) {
		int next_signature;

		wingp->net_signature = multi_assign_network_signature( MULTI_SIG_SHIP );
		// Cyborg -- Subtract one because the original wave already has its signatures set.
		next_signature = wingp->net_signature + (wingp->wave_count * (wingp->num_waves - 1));
		if ( next_signature > SHIP_SIG_MAX )
			Error(LOCATION, "Too many total ships in mission (%d) for network signature assignment", SHIP_SIG_MAX);
		multi_set_network_signature( (ushort)next_signature, MULTI_SIG_SHIP );
	}

	// set up the ai_goals for this wing -- all ships created from this wing will inherit these goals
	// goals for the wing are stored slightly differently than for ships.  We simply store the index
	// into the sexpression array of each goal (max 10).  When a ship in this wing is created, each
	// goal in the wings goal array is given to the ship.
	if ( wing_goals != -1 ) {
		int sexp;

		// this will assign the goals to the wings as well as to any ships in the wing that have been
		// already created.
		for ( sexp = CDR(wing_goals); sexp != -1; sexp = CDR(sexp) )
			ai_add_wing_goal_sexp(sexp, ai_goal_type::EVENT_WING, wingp);  // used by Fred

		free_sexp2(wing_goals);  // free up sexp nodes for reuse, since they aren't needed anymore.
	}

	// make a temporary map since we want to keep this separate from the ship registry
	SCP_unordered_map<SCP_string, int> parse_object_indexes;
	i = 0;
	for (const auto& pobj : Parse_objects)
		parse_object_indexes.emplace(pobj.name, i++);

	// Goober5000 - to avoid confusing mismatches in the ship registry and hotkey list, and possibly other places,
	// make sure the order of parse objects matches their order in the wing
	auto prev_iter = parse_object_indexes.end();
	for (i = 0; i < wingp->wave_count; i++) {
		auto this_iter = parse_object_indexes.find(ship_names[i]);
		if (this_iter == parse_object_indexes.end()) {
			Error(LOCATION, "Cannot load mission -- for wing %s, ship %s is not present in #Objects section.\n", wingp->name, ship_names[i]);
			break;
		}

		if (i > 0) {
			// compare the parse object indexes of the previous ship and this ship
			if (this_iter->second < prev_iter->second) {
				// swap the parse objects
				std::swap(Parse_objects[this_iter->second], Parse_objects[prev_iter->second]);

				// swap the swapped net signatures so they are in their original order
				std::swap(Parse_objects[this_iter->second].net_signature, Parse_objects[prev_iter->second].net_signature);

				// swap the indexes in our temporary map
				std::swap(this_iter->second, prev_iter->second);

				// start over from the beginning of the wing
				i = -1;
				this_iter = parse_object_indexes.end();
			}
		}

		prev_iter = this_iter;
	}

	// set the wing number for all ships in the wing
	for (i = 0; i < wingp->wave_count; i++ ) {
		char *ship_name = ship_names[i];

		// Goober5000 - since the ship/wing creation stuff is reordered to accommodate multiple docking,
		// everything is still only in the parse array at this point (in both FRED and FS2)

		// find the parse object and assign it the wing number
		auto iter = parse_object_indexes.find(ship_name);
		if (iter != parse_object_indexes.end()) {
			auto p_objp = &Parse_objects[iter->second];

			// get Allender -- ship appears to be in multiple wings
			// (or appears multiple times in the same wing)
			if (p_objp->wingnum >= 0)
				Error(LOCATION, "Cannot load mission -- tried to assign ship %s to wing %s but it was already assigned to wing %s.\n", ship_name, wingp->name, p_objp->wingnum < Num_wings ? Wings[p_objp->wingnum].name : "<out of range wingnum>");

			// assign wingnum
			p_objp->wingnum = wingnum;
			p_objp->pos_in_wing = i;

			// we have found our "special ship" (our wing leader)
			if (!Fred_running && i == 0){
				wingp->special_ship_ship_info_index = p_objp->ship_class;
			}

			// Goober5000 - if this is a player start object, there shouldn't be a wing arrival delay (Mantis #2678)
			if ((p_objp->flags[Mission::Parse_Object_Flags::OF_Player_start]) && (wingp->arrival_delay != 0)) {
				parse_warning_or_record("Wing %s specifies an arrival delay of %ds, but it also contains a player — corrected to 0.", wingp->name, abs(wingp->arrival_delay));
				if (!Fred_running && wingp->arrival_delay > 0) {
					// timestamp has been set, so set it again
					wingp->arrival_delay = timestamp(0);
				} else {
					// no timestamp, or timestamp invalid
					wingp->arrival_delay = 0;
				}
			}
		}
	}

	// Goober5000 - wing creation stuff moved to post_process_ships_wings
}

// Per-prop field handling here MUST stay in sync with:
//   Fred_mission_save::save_props() in missionsave.cpp
//   clone_prop_instance_data() in missioneditor/objectduplication.cpp
// When you add a new editable prop field, touch all three.
void parse_prop(mission* /*pm*/)
{
	parsed_prop p;
	required_string("$Name:");
	stuff_string(p.name, F_NAME, NAME_LENGTH);

	// Maybe do this by name instead?
	required_string("$Class:");
	SCP_string class_name;
	stuff_string(class_name, F_NAME);
	int idx = prop_info_lookup(class_name.c_str());
	if (idx < 0) {
		SCP_string text;
		sprintf(text, "Prop \"%s\" has an invalid prop type (props.tbl probably changed).", p.name);

		if (Prop_info.empty()) {
			text += "  No props.tbl is loaded. Prop will not be added to the mission!";
		} else {
			text += "  Prop will be added to the mission with type 0.";
			idx = 0;
		}

		if (Fred_running) {
			Warning(LOCATION, "%s", text.c_str());
		} else {
			mprintf(("MISSIONS: %s", text.c_str()));
		}

		Num_unknown_prop_classes++;
	}
	p.prop_info_index = idx;

	required_string("$Location:");
	stuff_vec3d(&p.position);

	required_string("$Orientation:");
	stuff_matrix(&p.orientation);

	// set flags
	if (optional_string("+Flags:")) {
		SCP_vector<SCP_string> unparsed;
		parse_string_flag_list(p.flags, Parse_prop_flags, Num_parse_prop_flags, &unparsed);
		if (!unparsed.empty()) {
			for (const auto& f : unparsed) {
				WarningEx(LOCATION, "Unknown flag in parse prop flags: %s", f.c_str());
			}
		}
	}

	if (optional_string("+Layer:")) {
		stuff_string(p.fred_layer, F_NAME);
		if (!mission_has_layer_name(&The_mission, p.fred_layer)) {
			if (p.fred_layer.empty()) {
				p.fred_layer = "Default";
			} else {
				The_mission.fred_layers.push_back(p.fred_layer);
			}
		}
	}

	// if idx is still -1 then we have an empty props.tbl so we parse
	// everything here and just discard it. A warning has already been generated above.
	if (idx < 0) {
		return;
	}

	Parse_props.emplace_back(std::move(p));
}

void parse_wings(mission* pm)
{
	required_string("#Wings");
	while (true) {
		int which = required_string_one_of(3, "#Events", "#Props", "$Name:");

		if (which == -1 || which == 0 || which == 1) // #Events or #Props
			break;

		Assert(Num_wings < MAX_WINGS);
		parse_wing(pm);
		Num_wings++;
	}
}

void parse_props(mission* pm)
{
	if (optional_string("#Props")) {
		while (required_string_either("#Events", "$Name:")) {
			parse_prop(pm);
		}
	}
}

// Goober5000
void resolve_and_check_anchor(bool check_for_hangar, SCP_set<anchor_t> &anchors_checked, anchor_t &anchor, const char *other_name, bool other_is_ship, bool is_arrival)
{
	if (!anchor.isValid())
		return;
	int anchor_val = anchor.value();

	// if it's a parse names index, convert it to a ship registry index
	if (anchor_val & ANCHOR_IS_PARSE_NAMES_INDEX)
	{
		anchor_val &= ~ANCHOR_IS_PARSE_NAMES_INDEX;
		Assertion(Parse_names.in_bounds(anchor_val), "Anchor %d is out of bounds.  Get a coder!", anchor_val);
		anchor_val = ship_registry_get_index(Parse_names[anchor_val]);
		anchor = anchor_t(anchor_val);
	}

	if (check_for_hangar)
	{
		SCP_string message;
		check_anchor_for_hangar_bay(message, anchors_checked, anchor, other_name, other_is_ship, is_arrival);
		if (!message.empty())
			Warning(LOCATION, "%s", message.c_str());
	}
}

/**
 * Resolve parse names, particularly for arrival/departure anchors
 * NB: between parsing and the time this function is run, the anchors store the index into Parse_names;
 * at all other times, they store the index into the ship registry
 */
void post_process_parse_names()
{
	SCP_set<anchor_t> anchors_checked;

	// check the parse names
	for (const auto &parse_name : Parse_names)
	{
		auto ship_entry = ship_registry_get(parse_name);
		if (!ship_entry)
			Warning(LOCATION, "Ship name \"%s\" was referenced, but this ship doesn't exist!", parse_name.c_str());
	}

	// resolve anchors for parse objects (ships)
	for (auto &pobj: Parse_objects)
	{
		resolve_and_check_anchor(pobj.arrival_location == ArrivalLocation::FROM_DOCK_BAY, anchors_checked, pobj.arrival_anchor, pobj.name, true, true);
		resolve_and_check_anchor(pobj.departure_location == DepartureLocation::TO_DOCK_BAY, anchors_checked, pobj.departure_anchor, pobj.name, true, false);
	}

	// resolve anchors for wings
	for (int i = 0; i < Num_wings; ++i)
	{
		auto wingp = &Wings[i];

		resolve_and_check_anchor(wingp->arrival_location == ArrivalLocation::FROM_DOCK_BAY, anchors_checked, wingp->arrival_anchor, wingp->name, false, true);
		resolve_and_check_anchor(wingp->departure_location == DepartureLocation::TO_DOCK_BAY, anchors_checked, wingp->departure_anchor, wingp->name, false, false);
	}
}

// Goober5000
void resolve_path_masks(bool path_user_is_ship, const char *path_user, anchor_t anchor, int *path_mask)
{
	path_restriction_t *prp;

	// if we have no restrictions, do a quick out
	if (*path_mask < 0)
	{
		*path_mask = 0;
		return;
	}

	// get path restriction info
	prp = &Path_restrictions[*path_mask];

	// uninitialized; compute the mask from scratch
	if (prp->cached_mask & (1 << MAX_SHIP_BAY_PATHS))
	{
		int j, bay_path;

		// get anchor ship
		Assertion(anchor.isValid() && !(anchor.value() & ANCHOR_SPECIAL_ARRIVAL), "%s %s anchor %d is invalid or is a special arrival.  Get a coder!", path_user_is_ship ? "Ship" : "Wing", path_user, anchor.value());
		auto anchor_ship_entry = ship_registry_get(anchor);
		Assertion(anchor_ship_entry, "%s %s anchor %d could not be resolved to a ship.  Get a coder!", path_user_is_ship ? "Ship" : "Wing", path_user, anchor.value());

		// Load the anchor ship model with subsystems and all; it'll need to be done for this mission anyway
		auto anchor_sip = anchor_ship_entry->sip();
		anchor_sip->model_num = model_load(anchor_sip->pof_file, anchor_sip);

		// resolve names to indexes
		*path_mask = 0;
		for	(j = 0; j < prp->num_paths; j++)
		{
			bay_path = model_find_bay_path(anchor_sip->model_num, prp->path_names[j]);
			if (bay_path < 0)
				continue;

			*path_mask |= (1 << bay_path);
		}

		// cache the result
		prp->cached_mask = *path_mask;
	}
	// already computed; so reuse it
	else
	{
		*path_mask = prp->cached_mask;
	}
}

/**
 * Resolve arrival/departure path masks
 * NB: between parsing and the time this function is run, the path_mask variables store the index of the path info;
 * at all other times, they store the masks of the bay paths as expected
 */
void post_process_path_stuff()
{
	int i;
	wing *wingp;

	// take care of parse objects (ships)
	for (SCP_vector<p_object>::iterator pobjp = Parse_objects.begin(); pobjp != Parse_objects.end(); ++pobjp)
	{
		resolve_path_masks(true, pobjp->name, pobjp->arrival_anchor, &pobjp->arrival_path_mask);
		resolve_path_masks(true, pobjp->name, pobjp->departure_anchor, &pobjp->departure_path_mask);
	}

	// take care of wings
	for (i = 0; i < Num_wings; i++)
	{
		wingp = &Wings[i];

		resolve_path_masks(false, wingp->name, wingp->arrival_anchor, &wingp->arrival_path_mask);
		resolve_path_masks(false, wingp->name, wingp->departure_anchor, &wingp->departure_path_mask);
	}
}

// MjnMixael
void post_process_mission_props()
{
	for (const auto& propp : Parse_props) {
		int objnum = prop_create(&propp.orientation, &propp.position, propp.prop_info_index, propp.name);

		if (objnum >= 0) {
			auto& obj = Objects[objnum];

			if (propp.flags[Mission::Parse_Object_Flags::OF_No_collide]) {
				obj.flags.remove(Object::Object_Flags::Collides);
			}

			auto createdProp = prop_id_lookup(obj.instance);
			if (createdProp != nullptr) {
				createdProp->fred_layer = propp.fred_layer;
			}
		}
	}
}

// Goober5000
void post_process_ships_wings()
{
	// error checking for custom wings
	if (strcmp(Starting_wing_names[0], TVT_wing_names[0]) != 0)
	{
		// In QtFRED this is surfaced via ErrorChecker::checkPlayerWings so the editor can load the mission.
		if (!Qtfred_running)
			Error(LOCATION, "The first starting wing and the first team-versus-team wing must have the same wing name.\n");
	}

	// set up wing indexes
	for (int i = 0; i < MAX_STARTING_WINGS; i++)
		Starting_wings[i] = wing_name_lookup(Starting_wing_names[i], 1);
	for (int i = 0; i < MAX_SQUADRON_WINGS; i++)
		Squadron_wings[i] = wing_name_lookup(Squadron_wing_names[i], 1);
	for (int i = 0; i < MAX_TVT_WINGS; i++)
		TVT_wings[i] = wing_name_lookup(TVT_wing_names[i], 1);

	// when TVT, hack starting wings to be team wings
	if (MULTI_TEAM)
	{
		Assert(MAX_TVT_WINGS <= MAX_STARTING_WINGS);
		for (int i = 0; i < MAX_STARTING_WINGS; i++)
		{
			if (i < MAX_TVT_WINGS)
				Starting_wings[i] = TVT_wings[i];
			else
				Starting_wings[i] = -1;
		}
	}

	// Goober5000 - add all the parse objects to the ship registry.  This must be done before any other parse object processing.
	for (auto &p_obj : Parse_objects)
	{
		ship_registry_entry entry(p_obj.name);
		entry.status = ShipStatus::NOT_YET_PRESENT;
		entry.pobj_num = POBJ_INDEX(&p_obj);

		Ship_registry.push_back(entry);
		Ship_registry_map[p_obj.name] = static_cast<int>(Ship_registry.size() - 1);
	}

	// Goober5000 - resolve the parse names.  Needs to be done once the ship registry is valid but before the path masks are resolved.
	post_process_parse_names();

	// Goober5000 - resolve the path masks.  Needs to be done early because
	// mission_parse_maybe_create_parse_object relies on it.
	post_process_path_stuff();

	// Goober5000 - now that we've parsed all the objects, resolve the initially docked references.
	// This must be done before anything that relies on the dock references but can't be done until
	// both ships and wings have been parsed.
	mission_parse_set_up_initial_docks();

	// Goober5000 - Now do any post-processing for parse objects.
	for (auto &p_obj : Parse_objects)
	{
		// set a flag if this parse object is in a starting wing
		if (p_obj.wingnum >= 0)
		{
			for (int i = 0; i < MAX_STARTING_WINGS; i++)
				if (p_obj.wingnum == Starting_wings[i])
					p_obj.flags.set(Mission::Parse_Object_Flags::SF_From_player_wing);
		}

		// also load any replacement textures (do this outside the parse loop because we may have ship class replacements too)
		for (SCP_vector<texture_replace>::iterator tr = p_obj.replacement_textures.begin(); tr != p_obj.replacement_textures.end(); ++tr)
		{
			// load the texture
			if (!stricmp(tr->new_texture, "invisible"))
			{
				// invisible is a special case
				tr->new_texture_id = REPLACE_WITH_INVISIBLE;
			}
			else
			{
				// try to load texture or anim as normal
				tr->new_texture_id = bm_load_either(tr->new_texture);
			}

			// not found?
			if (tr->new_texture_id < 0)
				mprintf(("Could not load replacement texture %s for ship %s\n", tr->new_texture, p_obj.name));

			// account for FRED
			if (Fred_running)
			{
				Fred_texture_replacements.push_back(*tr);
				Fred_texture_replacements.back().new_texture_id = -1;
			}
		}

		// Goober5000 - preload stuff for certain object flags
		// (done after parsing object, but before creating it)
		if (p_obj.flags[Mission::Parse_Object_Flags::Knossos_warp_in])
			Knossos_warp_ani_used = true;

		// check the warp parameters too
		if (p_obj.warpin_params_index >= 0 && (Warp_params[p_obj.warpin_params_index].warp_type == WT_KNOSSOS || Warp_params[p_obj.warpin_params_index].warp_type == WT_DEFAULT_THEN_KNOSSOS))
			Knossos_warp_ani_used = true;
		if (p_obj.warpout_params_index >= 0 && (Warp_params[p_obj.warpout_params_index].warp_type == WT_KNOSSOS || Warp_params[p_obj.warpout_params_index].warp_type == WT_DEFAULT_THEN_KNOSSOS))
			Knossos_warp_ani_used = true;
	}

	// Goober5000 - now create all objects that we can.  This must be done before any ship stuff
	// but can't be done until the dock references are resolved.  This was originally done
	// in parse_object().
	for (auto &p_obj : Parse_objects)
	{
		// Evaluate the arrival cue and maybe set up the arrival delay.  This can't be done until the ship registry is populated
		// (because SEXPs now require a complete ship registry) but must be done before the arrival list check inside
		// mission_parse_maybe_create_parse_object.  That check is, in fact, the only reason this is needed.  We don't need to
		// pre-emptively set up the delay for wings because there is no equivalent wing arrival list check.  In any case, the
		// arrival_delay is always validated in mission_did_ship_arrive (for ships) and parse_wing_create_ships (for wings).
		// Addendum: Don't mess with any arrival delays which are strictly positive, meaning they have already been set.
		// (This is the case for ships destroyed before mission.  In the retail codebase, the destroy-before-mission chunk was
		// parsed after the arrival cue and delay were parsed and checked, so it overwrote them.)
		if (!Fred_running && (p_obj.arrival_cue >= 0) && (p_obj.arrival_delay <= 0))
		{
			// eval the arrival cue.  if the cue is true, set up the timestamp for the arrival delay
			if (eval_sexp(p_obj.arrival_cue))
				p_obj.arrival_delay = timestamp(-p_obj.arrival_delay * 1000);
		}

		// create as usual
		mission_parse_maybe_create_parse_object(&p_obj);
	}

	// ----------------- at this point the ships have been created -----------------
	// Now set up the wings.  This must be done after both dock stuff and ship stuff.

	// Goober5000 - for FRED, the ships are initialized after the wings, so we must now tell the wings
	// where their ships are
	if (Fred_running)
	{
		// even though the ships are already created, only the parse objects know the wing info
		for (SCP_vector<p_object>::iterator p_objp = Parse_objects.begin(); p_objp != Parse_objects.end(); ++p_objp)
		{
			// link the ship into the wing's array of ship indices (previously done in parse_wing
			// in a rather less backwards way)
			if (p_objp->wingnum >= 0)
			{
				int shipnum = ship_name_lookup(p_objp->name);

				Assert(shipnum >= 0 && shipnum < MAX_SHIPS);
				Assert(p_objp->pos_in_wing >= 0 && p_objp->pos_in_wing < MAX_SHIPS_PER_WING);

				Wings[p_objp->wingnum].ship_index[p_objp->pos_in_wing] = shipnum;
			}
		}
	}
	// Goober5000 - for FS2, the wings are initialized first, so all we have to do is create their ships
	else
	{
		for (int i = 0; i < Num_wings; i++)
		{
			wing *wingp = &Wings[i];

			// create the wing if is isn't a reinforcement.
			if (!(wingp->flags[Ship::Wing_Flags::Reinforcement]))
				parse_wing_create_ships(wingp, wingp->wave_count);
		}
	}

	
	// ----------------- at this point wings and ships should both be valid -----------------
	// Now do some error checking for multi

	static_assert(MAX_TVT_WINGS_PER_TEAM == 1, "Unless you also update the section of code below or redo the loadout code, for TvT, there should be just one player wing, otherwise, wings may start disappearing in game.");

	// now see if we found the missing wing.  We're looking for a wing that is there after a wing that is not.
	// for now TvT mission do not have enough player wings to be affected by this bug.
	if (The_mission.game_type & (MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_COOP)) {
		bool Squadron_wing_names_found[MAX_SQUADRON_WINGS];

		// quickly look through the squadron wing names to see if we have to warn the modder about this mission
		// to avoid them avoid the multi missing wing bug.
		for (int i = 0; i < MAX_SQUADRON_WINGS; i++) {
			Squadron_wing_names_found[i] = false;

			for (int j = 0; j < Num_wings; j++) {
				if (!strcmp(Wings[j].name, Squadron_wing_names[i])) {
					Squadron_wing_names_found[i] = true;
				}
			}
		}

		bool found = false;
		do {
			found = false;
			// we only search up to the MAX_STARTING_WINGS because non-starting wings should not be in starting wing indices (0-2)
			for (int i = 1; i < MAX_STARTING_WINGS; i++) {
				// If there was a wing for this squadron entry, check the last one. If it's empty, we found a mistake, so move the wing names over.
				if (Squadron_wing_names_found[i] && !Squadron_wing_names_found[i - 1]) {
					parse_warning_or_record("Squadron wings are not in the correct order and may cause wings to disappear in multi. Either wing %s should exist or the %s entry needs to come before it in the list — wing names have been swapped.", Squadron_wing_names[i - 1], Squadron_wing_names[i]);
					char temp_chars[NAME_LENGTH];
					strcpy_s(temp_chars, Squadron_wing_names[i - 1]);
					strcpy_s(Squadron_wing_names[i - 1], Squadron_wing_names[i]);
					strcpy_s(Squadron_wing_names[i], temp_chars);
					Squadron_wing_names_found[i] = !Squadron_wing_names_found[i];
					Squadron_wing_names_found[i - 1] = !Squadron_wing_names_found[i - 1];
					found = true;
				}
			}
		} while (found);
	}
}

// mission events are sexpressions which cause things to happen based on the outcome
// of other events in a mission.  Essentially scripting the different things that can happen
// in a mission

void parse_event(mission *pm)
{
	SCP_UNUSED(pm);

	Mission_events.emplace_back();
	auto event = &Mission_events.back();

	required_string( "$Formula:" );
	event->formula = get_sexp_main();

	if (optional_string("+Name:")){
		stuff_string(event->name, F_NAME);
	}

	if ( optional_string("+Repeat Count:")){
		stuff_int( &(event->repeat_count) );

		// sanity check on the repeat count variable
		// _argv[-1] - negative repeat count is now legal; means repeat indefinitely.
		if ( event->repeat_count == 0 ){
			parse_warning_or_record("Repeat count for mission event %s is 0 — must be >= 1 or negative; corrected to 1.", event->name.c_str());
			event->repeat_count = 1;
		}
	}

	if ( optional_string("+Trigger Count:")){
		stuff_int( &(event->trigger_count) );

		// sanity check on the trigger count variable; do this first so the != 1 check below is correct
		// negative trigger count is also legal
		if ( event->trigger_count == 0 ){
			parse_warning_or_record("Trigger count for mission event %s is 0 — must be >= 1 or negative; corrected to 1.", event->name.c_str());
			event->trigger_count = 1;
		}

		// a trigger count of 1 is the default, so the field only has runtime effect when it's something else
		if (event->trigger_count != 1) {
			event->flags |= MEF_USING_TRIGGER_COUNT;

			// if we have a trigger count but no repeat count, we want the event to loop until it has triggered enough times
			if (event->repeat_count == 1) {
				event->repeat_count = -1;
			}
		}
	}

	if ( optional_string("+Interval:")){
		stuff_int( &(event->interval) );
	}

	if ( optional_string("+Score:") ){
		stuff_int(&event->score);
	}

	if ( optional_string("+Chained:") ){
		stuff_int(&event->chain_delay);
	}

	if ( optional_string("+Objective:") ) {
		stuff_string(event->objective_text, F_NAME);
	}

	if ( optional_string("+Objective key:") ) {
		stuff_string(event->objective_key_text, F_NAME);
	}

	if( optional_string("+Team:") ) {
		stuff_int(&event->team);

		// sanity check
		if (event->team < -1 || event->team >= MAX_TVT_TEAMS) {
			if (Fred_running && !Warned_about_team_out_of_range) {
				Warning(LOCATION, "+Team: value was out of range in the mission file!  This was probably caused by a bug in an older version of FRED.  Using -1 for now.");
				Warned_about_team_out_of_range = true;
			}
			event->team = -1;
		}
	}

	if (optional_string("+Event Flags:")) {
		parse_string_flag_list(event->flags, Mission_event_flags, Num_mission_event_flags);
	}

	if( optional_string("+Event Log Flags:") ) {
		SCP_vector<SCP_string> buffer;
		
		stuff_string_list(buffer); 
		for (int i = 0; i < (int)buffer.size(); i++) {

			for (int j = 0; j < MAX_MISSION_EVENT_LOG_FLAGS; j++) {
				if (!stricmp(buffer[i].c_str(), Mission_event_log_flags[j])) {
					// add the flag to the variable, bitshifted by the index used in Mission_event_log_flags[]
					event->mission_log_flags |= 1 << j; 
					break;
				}
			}
		}
	}

	if (optional_string("$Annotations Start")) {
		// annotations are only used in FRED
		if (Fred_running) {
			while (check_for_string("+Comment:") || check_for_string("+Background Color:") || check_for_string("+Path:")) {
				event_annotation ea;
				ea.path.push_back((int)(event - &Mission_events[0]));

				if (optional_string("+Comment:")) {
					stuff_string(ea.comment, F_MULTITEXT);
					lcl_replace_stuff(ea.comment, true);
				}

				if (optional_string("+Background Color:")) {
					stuff_ubyte(&ea.r);
					check_first_non_grayspace_char(Mp, ',', &Mp);
					stuff_ubyte(&ea.g);
					check_first_non_grayspace_char(Mp, ',', &Mp);
					stuff_ubyte(&ea.b);
				}

				if (optional_string("+Path:")) {
					int num;
					while (true) {
						ignore_gray_space();
						if (stuff_int_optional(&num) != 2) {
							break;
						}
						ea.path.push_back(num);
					}
				}

				Event_annotations.push_back(std::move(ea));
			}
			required_string("$Annotations End");
		} else {
			skip_to_string("$Annotations End");
		}
	}
}

void parse_events(mission *pm)
{
	required_string("#Events");

	while (required_string_either( "#Goals", "$Formula:")) {
		parse_event(pm);
	}
}

void parse_goal(mission *pm)
{
	SCP_UNUSED(pm);
	int dummy;

	Mission_goals.emplace_back();
	auto goalp = &Mission_goals.back();

	find_and_stuff("$Type:", &goalp->type, F_NAME, Goal_type_names, Num_goal_type_names, "goal type");

	required_string("+Name:");
	stuff_string(goalp->name, F_NAME);

	// backwards compatibility for old Fred missions - all new missions should use $MessageNew
	if(optional_string("$Message:")){
		stuff_string(goalp->message, F_NAME);
	} else {
		required_string("$MessageNew:");
		stuff_string(goalp->message, F_MULTITEXT);
	}

	if (optional_string("$Rating:")){
		stuff_int(&dummy);  // not used
	}

	required_string("$Formula:");
	goalp->formula = get_sexp_main();

	if ( optional_string("+Invalid:") )
		goalp->type |= INVALID_GOAL;
	if ( optional_string("+Invalid") )
		goalp->type |= INVALID_GOAL;
	if ( optional_string("+No music") )
		goalp->flags |= MGF_NO_MUSIC;

	if ( optional_string("+Score:") ){
		stuff_int(&goalp->score);
	}

	if ( optional_string("+Team:") ){
		stuff_int( &goalp->team );

		// sanity check
		if (goalp->team < -1 || goalp->team >= (int)Iff_info.size()) {
			if (Fred_running && !Warned_about_team_out_of_range) {
				Warning(LOCATION, "+Team: value was out of range in the mission file!  This was probably caused by a bug in an older version of FRED.  Using -1 for now.");
				Warned_about_team_out_of_range = true;
			}
			goalp->team = -1;
		}
	}
}

void parse_goals(mission *pm)
{
	required_string("#Goals");

	while (required_string_either("#Waypoints", "$Type:")){
		parse_goal(pm);
	}

	if ((pm->game_type & MISSION_TYPE_MULTI) && Mission_goals.size() > UINT8_MAX)
		throw parse::ParseException("Number of goals is too high and breaks multi!");
}

// Per-waypoint-path field handling here MUST stay in sync with:
//   Fred_mission_save::save_waypoints() (waypoint-list section) in missionsave.cpp
//   clone_waypoint_path_instance_data() in missioneditor/objectduplication.cpp
// When you add a new editable waypoint-path field, touch all three.
void parse_waypoint_list(mission *pm)
{
	Assert(pm != NULL);

	char name_buf[NAME_LENGTH];
	required_string("$Name:");
	stuff_string(name_buf, F_NAME, NAME_LENGTH);

	bool no_draw_lines = false;
	if (optional_string("+No Draw Lines:"))
		stuff_boolean(&no_draw_lines);

	bool has_custom_color = false;
	int cr = 255, cg = 255, cb = 255;
	if (optional_string("+Color:")) {
		has_custom_color = true;
		stuff_int(&cr);
		stuff_int(&cg);
		stuff_int(&cb);
	}

	SCP_string wpt_fred_layer = "Default";
	if (optional_string("+Layer:")) {
		stuff_string(wpt_fred_layer, F_NAME);
		if (!mission_has_layer_name(&The_mission, wpt_fred_layer)) {
			if (wpt_fred_layer.empty()) {
				wpt_fred_layer = "Default";
			} else {
				The_mission.fred_layers.push_back(wpt_fred_layer);
			}
		}
	}

	SCP_vector<vec3d> vec_list;
	required_string("$List:");
	stuff_vec3d_list(vec_list);

	waypoint_add_list(name_buf, vec_list);

	// Apply display properties to the list just added
	waypoint_list* wl = find_matching_waypoint_list(name_buf);
	if (wl) {
		if (no_draw_lines || has_custom_color) {
			wl->set_no_draw_lines(no_draw_lines);
			if (has_custom_color)
				wl->set_color(cr, cg, cb);
		}
		wl->set_fred_layer(wpt_fred_layer);
	}
}

// The jump-node section of this function MUST stay in sync with:
//   Fred_mission_save::save_waypoints() (jump-node section) in missionsave.cpp
//   clone_jump_node_instance_data() in missioneditor/objectduplication.cpp
// When you add a new editable jump-node field, touch all three.
void parse_waypoints_and_jumpnodes(mission *pm)
{
	vec3d pos;

	required_string("#Waypoints");

	char file_name[MAX_FILENAME_LEN] = { 0 };
	char jump_name[NAME_LENGTH] = { 0 };
	char jump_display_name[NAME_LENGTH] = {0};

	while (optional_string("$Jump Node:")) {
		stuff_vec3d(&pos);
		CJumpNode jnp(&pos);

		if (optional_string("$Jump Node Name:") || optional_string("+Jump Node Name:")) {
			stuff_string(jump_name, F_NAME, NAME_LENGTH);
			jnp.SetName(jump_name);
		}

		if (optional_string("+Display Name:")) {
			stuff_string(jump_display_name, F_NAME, NAME_LENGTH);
			jnp.SetDisplayName(jump_display_name);
		}

		if(optional_string("+Model File:")){
			stuff_string(file_name, F_NAME, MAX_FILENAME_LEN);
			jnp.SetModel(file_name);
		}

		if(optional_string("+Alphacolor:")) {
			ubyte r,g,b,a;
			stuff_ubyte(&r);
			stuff_ubyte(&g);
			stuff_ubyte(&b);
			stuff_ubyte(&a);
			jnp.SetAlphaColor(r, g, b, a);
		}

		if(optional_string("+Hidden:")) {
			int hide;
			stuff_boolean(&hide);
			jnp.SetVisibility(!hide);
		}

		if (optional_string("+Layer:")) {
			SCP_string layer_name;
			stuff_string(layer_name, F_NAME);
			if (!mission_has_layer_name(&The_mission, layer_name)) {
				if (layer_name.empty()) {
					layer_name = "Default";
				} else {
					The_mission.fred_layers.push_back(layer_name);
				}
			}
			jnp.SetFredLayer(layer_name);
		}

		Jump_nodes.push_back(std::move(jnp));
	}

	while (required_string_either("#Messages", "$Name:"))
		parse_waypoint_list(pm);
}

void parse_messages(mission *pm, int flags)
{
	required_string("#Messages");

	// command stuff by Goober5000 ---------------------------------------
	if (optional_string("$Command Sender:"))
	{
		char temp[NAME_LENGTH];
		stuff_string(temp, F_NAME, NAME_LENGTH);

		if (*temp == '#')
			strcpy_s(pm->command_sender, &temp[1]);
		else
			strcpy_s(pm->command_sender, temp);
	}

	if (optional_string("$Command Persona:"))
	{
		int idx;
		char temp[NAME_LENGTH];
		stuff_string(temp, F_NAME, NAME_LENGTH);

		idx = message_persona_name_lookup(temp);
		if (idx >= 0)
			pm->command_persona = idx;
		else
			Warning(LOCATION, "Supplied Command Persona is invalid!  Defaulting to %s.", Personas[Default_command_persona].name);
	}
	// end of command stuff ----------------------------------------------


	mprintf(("Starting mission message count : %d\n", (int)Message_waves.size()));

	while (required_string_either("#Reinforcements", "$Name")) {
		MessageFormat format = (flags & MPF_IMPORT_FSM) ? MessageFormat::FS1_MISSION : MessageFormat::FS2_MISSION;
		message_parse(format);
	}

	mprintf(("Ending mission message count : %d\n", (int)Message_waves.size()));
}

void parse_reinforcement(mission *pm)
{
	reinforcements reinforcement;
	p_object *rforce_obj = NULL;
	int instance = -1;

	Assert(pm != NULL);

	required_string("$Name:");
	stuff_string(reinforcement.name, F_NAME, NAME_LENGTH);

	find_and_stuff("$Type:", &reinforcement.type, F_NAME, Reinforcement_type_names, Num_reinforcement_type_names, "reinforcement type");

	required_string("$Num times:");
	stuff_int(&reinforcement.uses);

	if ( optional_string("+Arrival delay:") )
	{
		int delay;
		stuff_int(&delay);
		if (delay < 0)
		{
			parse_warning_or_record("Arrival delay on reinforcement %s cannot be negative — corrected to 0.", reinforcement.name);
			delay = 0;
		}

		reinforcement.arrival_delay = delay;
	}

	if ( optional_string("+No Messages:") ){
		stuff_string_list( reinforcement.no_messages, MAX_REINFORCEMENT_MESSAGES );
	}

	if ( optional_string("+Yes Messages:") ){
		stuff_string_list( reinforcement.yes_messages, MAX_REINFORCEMENT_MESSAGES );
	}	

	// sanity check on the names of reinforcements
	rforce_obj = mission_parse_find_parse_object(reinforcement.name);

	if (rforce_obj == NULL) {
		if ((instance = wing_name_lookup(reinforcement.name, 1)) == -1) {
			parse_warning_or_record("Reinforcement %s not found as ship or wing — declaration ignored.", reinforcement.name);
			return;
		}
	} else {
		// Individual ships in wings can't be reinforcements - FUBAR
		if (rforce_obj->wingnum >= 0)
		{
			parse_warning_or_record("Reinforcement %s is part of a wing — reinforcement declaration ignored.", reinforcement.name);
			return;
		}
		else
		{
			instance = rforce_obj->wingnum;
		}
	}

	// now, if the reinforcement is a wing, then set the number of waves of the wing == number of
	// uses of the reinforcement
	if (instance >= 0) {
		Wings[instance].num_waves = reinforcement.uses;
	}

	Reinforcements.push_back(std::move(reinforcement));
}

void parse_reinforcements(mission *pm)
{
	required_string("#Reinforcements");

	while (required_string_either("#Background bitmaps", "$Name:"))
		parse_reinforcement(pm);
}

void parse_one_background(background_t *background)
{
	// clear here too because this function can be called from more than one place
	background->flags.reset();
	background->suns.clear();
	background->bitmaps.clear();

	// we might have some flags
	if (optional_string("+Flags:"))
	{
		// we'll assume the list will contain no more than 4 distinct tokens
		char flag_strings[4][NAME_LENGTH];
		int num_strings = (int)stuff_string_list(flag_strings, 4);

		for (auto i = 0; i < num_strings; ++i)
		{
			// if this flag is found, this background was saved with correctly calculated angles, so it should be loaded as such
			if (!stricmp(flag_strings[i], "corrected angles"))
				background->flags.set(Starfield::Background_Flags::Corrected_angles_in_mission_file);
		}
	}

	// parse suns
	while (optional_string("$Sun:"))
	{
		starfield_list_entry sle;

		// filename
		stuff_string(sle.filename, F_NAME, MAX_FILENAME_LEN);

		// angles
		required_string("+Angles:");
		stuff_float(&sle.ang.p);
		stuff_float(&sle.ang.b);
		stuff_float(&sle.ang.h);

		// correct legacy bitmap angles which used incorrect math in older versions
		if (!background->flags[Starfield::Background_Flags::Corrected_angles_in_mission_file])
		{
			stars_correct_background_sun_angles(&sle.ang);
			sle.ang.b = 0.0f;	// stars do not bank
		}

		// scale
		required_string("+Scale:");
		stuff_float(&sle.scale_x);
		sle.scale_y = sle.scale_x;
		sle.div_x = 1;
		sle.div_y = 1;

		// add it
		background->suns.push_back(sle);
	}

	// parse starfields
	while (optional_string("$Starbitmap:"))
	{
		starfield_list_entry sle;

		// filename
		stuff_string(sle.filename, F_NAME, MAX_FILENAME_LEN);

		// angles
		required_string("+Angles:");
		stuff_float(&sle.ang.p);
		stuff_float(&sle.ang.b);
		stuff_float(&sle.ang.h);

		// correct legacy bitmap angles which used incorrect math in older versions
		if (!background->flags[Starfield::Background_Flags::Corrected_angles_in_mission_file])
			stars_correct_background_bitmap_angles(&sle.ang);

		// scale
		if (optional_string("+Scale:"))
		{
			stuff_float(&sle.scale_x);
			sle.scale_y = sle.scale_x;
			sle.div_x = 1;
			sle.div_y = 1;
		}
		else
		{
			required_string("+ScaleX:");
			stuff_float(&sle.scale_x);

			required_string("+ScaleY:");
			stuff_float(&sle.scale_y);

			required_string("+DivX:");
			stuff_int(&sle.div_x);

			required_string("+DivY:");
			stuff_int(&sle.div_y);
		}

		// add it
		background->bitmaps.push_back(sle);
	}

	if (Fred_running)
	{
		// the angles are now stored correctly, so by default we also want to save them correctly
		background->flags.set(Starfield::Background_Flags::Corrected_angles_in_mission_file);
	}
}

void parse_bitmaps(mission *pm)
{
	char str[MAX_FILENAME_LEN];
	int z;

	required_string("#Background bitmaps");

	required_string("$Num stars:");
	stuff_int(&Num_stars);
	if (Num_stars >= MAX_STARS)
		Num_stars = MAX_STARS;

	required_string("$Ambient light level:");
	stuff_int(&pm->ambient_light_level);

	if (pm->ambient_light_level == 0)
	{
		pm->ambient_light_level = DEFAULT_AMBIENT_LIGHT_LEVEL;
	}

	// This should call light_set_ambient() to
	// set the ambient light

	// neb2 info

	// all poofs on by default
	for (size_t i = 0; i < Poof_info.size(); i++)
		set_bit(Neb2_poof_flags.get(), i);
	bool nebula = false;
	if (optional_string("+Neb2:")) {
		nebula = true;
		stuff_string(Neb2_texture_name, F_NAME, MAX_FILENAME_LEN);
	}
	if (optional_string("+Neb2Color:")) {
		nebula = true;
		int neb_colors[3];
		stuff_int_list(neb_colors, 3, ParseLookupType::RAW_INTEGER_TYPE);
		Neb2_fog_color[0] = (ubyte)neb_colors[0];
		Neb2_fog_color[1] = (ubyte)neb_colors[1];
		Neb2_fog_color[2] = (ubyte)neb_colors[2];
		pm->flags |= Mission::Mission_Flags::Neb2_fog_color_override;
	}
	if (nebula) {
		// Obsolete and only for backwards compatibility
		if (optional_string("+Neb2Flags:")) {
			int temp;
			stuff_int(&temp);
			bit_array_set_from_int(Neb2_poof_flags.get(), Poof_info.size(), temp);
		}

		// Get poofs by name
		if (optional_string("+Neb2 Poofs List:")) {
			SCP_vector<SCP_string> poofs_list;
			stuff_string_list(poofs_list);
			neb2_set_poof_bits(poofs_list);
		}

		// initialize neb effect. its gross to do this here, but Fred is dumb so I have no choice ... :(
		if(Fred_running && (pm->flags[Mission::Mission_Flags::Fullneb])){
			neb2_post_level_init(pm->flags[Mission::Mission_Flags::Neb2_fog_color_override]);
		}
	}

	if(pm->flags[Mission::Mission_Flags::Fullneb]){
		// no regular nebula stuff
		nebula_close();
	} else {
		if (optional_string("+Nebula:")) {
			stuff_string(str, F_NAME, MAX_FILENAME_LEN);
			
			// parse the proper nebula type (full or not)	
			for (z=0; z<NUM_NEBULAS; z++){
				if(pm->flags[Mission::Mission_Flags::Fullneb]){
					if (!stricmp(str, Neb2_filenames[z])) {
						Nebula_index = z;
						break;
					}
				} else {
					if (!stricmp(str, Nebula_filenames[z])) {
						Nebula_index = z;
						break;
					}
				}
			}

			if (z == NUM_NEBULAS)
				WarningEx(LOCATION, "Mission %s\nUnknown nebula %s!", pm->name.c_str(), str);

			if (optional_string("+Color:")) {
				stuff_string(str, F_NAME, MAX_FILENAME_LEN);
				for (z=0; z<NUM_NEBULA_COLORS; z++){
					if (!stricmp(str, Nebula_colors[z])) {
						Mission_palette = z;
						break;
					}
				}
			}

			if (z == NUM_NEBULA_COLORS)
				WarningEx(LOCATION, "Mission %s\nUnknown nebula color %s!", pm->name.c_str(), str);

			if (optional_string("+Pitch:")){
				stuff_int(&Nebula_pitch);
			} else {
				Nebula_pitch = 0;
			}

			if (optional_string("+Bank:")){
				stuff_int(&Nebula_bank);
			} else {
				Nebula_bank = 0;
			}

			if (optional_string("+Heading:")){
				stuff_int(&Nebula_heading);
			} else {
				Nebula_heading = 0;
			}						
		}

		nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading);
	}	

	// Goober5000
	while (optional_string("$Bitmap List:") || check_for_string("$Sun:") || check_for_string("$Starbitmap:"))
	{
		stars_add_blank_background(false);
		parse_one_background(&Backgrounds.back());
	}

	if (optional_string("$Environment Map:")) {
		stuff_string(pm->envmap_name, F_NAME, MAX_FILENAME_LEN);
	}

	// bypass spurious stuff from e.g. FS1 missions
	skip_to_start_of_string("#");
}

void parse_asteroid_fields(mission *pm)
{
	int i;

	Assert(pm != NULL);

	Asteroid_field.num_initial_asteroids = 0;

	i = 0;

	if (!optional_string("#Asteroid Fields"))
		return;

	while (required_string_either("#", "$density:")) {
		float speed, density;
		int type;

		Assert(i < 1);
		required_string("$Density:");
		stuff_float(&density);

		Asteroid_field.num_initial_asteroids = (int) density;

		Asteroid_field.field_type = FT_ACTIVE;
		if (optional_string("+Field Type:")) {
			stuff_int( &type );
			Asteroid_field.field_type = (field_type_t)type;
		}

		Asteroid_field.debris_genre = DG_ASTEROID;
		if (optional_string("+Debris Genre:")) {
			stuff_int( &type );
			Asteroid_field.debris_genre = (debris_genre_t)type;
		}

		Asteroid_field.field_debris_type.clear();
		Asteroid_field.field_asteroid_type.clear();

		// Debris types
		if (Asteroid_field.debris_genre == DG_DEBRIS) {

			// Obsolete and only for backwards compatibility
			for (int j = 0; j < MAX_RETAIL_DEBRIS_TYPES; j++) {
				if (optional_string("+Field Debris Type:")) {
					int subtype;
					stuff_int(&subtype);
					Asteroid_field.field_debris_type.push_back(subtype);
				}
			}

			// Get asteroids by name
			while (optional_string("+Field Debris Type Name:")) {
				SCP_string ast_name;
				stuff_string(ast_name, F_NAME);
				int subtype;
				subtype = get_asteroid_index(ast_name.c_str());
				if (subtype >= 0) {
					Asteroid_field.field_debris_type.push_back(subtype);
				} else {
					WarningEx(LOCATION, "Mission %s\n Invalid asteroid debris %s!", pm->name.c_str(), ast_name.c_str());
				}
			}

		// Asteroid types
		} else {

			// Retail asteroid subtypes
			SCP_string colors[NUM_ASTEROID_SIZES] = {"Brown", "Blue", "Orange"};

			// Obsolete and only for backwards compatibility
			for (int j = 0; j < NUM_ASTEROID_SIZES; j++) {
				if (optional_string("+Field Debris Type:")) {
					int subtype;
					stuff_int(&subtype);
					if (subtype >= 0 && subtype < NUM_ASTEROID_SIZES) {
						Asteroid_field.field_asteroid_type.push_back(colors[subtype]);
					} else {
						WarningEx(LOCATION, "Invalid +Field Debris Type value %d in asteroid field (must be 0-%d); ignoring.", subtype, NUM_ASTEROID_SIZES - 1);
					}
				}
			}

			// Get asteroids by name
			while (optional_string("+Field Debris Type Name:")) {
				SCP_string ast_name;
				stuff_string(ast_name, F_NAME);

				// Old saving for asteroids was bugged and saved the asteroid size rather than the subtype color
				// so we'll compensate for that here
				for (size_t k = 0; k < NUM_ASTEROID_SIZES; k++) {
					// if we get the name for small/medium/large asteroid then convert it to retail colors
					if (stricmp(ast_name.c_str(), Asteroid_info[k].name) == 0) {
						ast_name = colors[k];
						break;
					}
				}

				auto list = get_list_valid_asteroid_subtypes();

				//validate the asteroid subtype name
				bool valid = false;
				for (const auto& entry : list) {
					if (ast_name == entry) {
						valid = true;
						break;
					}
				}

				if (valid){
					Asteroid_field.field_asteroid_type.push_back(std::move(ast_name));
				} else {
					WarningEx(LOCATION, "Mission %s\n Invalid asteroid %s!", pm->name.c_str(), ast_name.c_str());
				}
			}
		}

		bool invalid_asteroids = false;
		for (int& ast_type : Asteroid_field.field_debris_type) {
			if (ast_type >= (int)Asteroid_info.size()) {
				invalid_asteroids = true;
				ast_type = -1;
			}
		}

		if (invalid_asteroids)
			Warning(LOCATION, "The Asteroid field contains invalid entries!");

		// backward compatibility
		// Is this a good idea? This doesn't seem like a good idea. What is this compatibility actually for??
		// If you've defined an asteroid field but didn't define any 'roids, then tough luck. Fix your mission.
		// If this is for a retail mission then this needs to be hardcoded for that specific mission file probably. - Mjn
		if ((Asteroid_field.debris_genre == DG_ASTEROID) && (Asteroid_field.field_asteroid_type.empty())) {
			Asteroid_field.field_asteroid_type.push_back("Brown");
		}

		required_string("$Average Speed:");
		stuff_float(&speed);

		vm_vec_rand_vec_quick(&Asteroid_field.vel);
		vm_vec_scale(&Asteroid_field.vel, speed);

		Asteroid_field.speed = speed;

		required_string("$Minimum:");
		stuff_vec3d(&Asteroid_field.min_bound);

		required_string("$Maximum:");
		stuff_vec3d(&Asteroid_field.max_bound);

		vec3d a_rad;
		vm_vec_sub(&a_rad, &Asteroid_field.max_bound, &Asteroid_field.min_bound);
		vm_vec_scale(&a_rad, 0.5f);
		float b_rad = vm_vec_mag(&a_rad);

		Asteroid_field.bound_rad = MAX(3000.0f, b_rad);

		if (optional_string("+Inner Bound:")) {
			Asteroid_field.has_inner_bound = true;

			required_string("$Minimum:");
			stuff_vec3d(&Asteroid_field.inner_min_bound);

			required_string("$Maximum:");
			stuff_vec3d(&Asteroid_field.inner_max_bound);
		} else {
			Asteroid_field.has_inner_bound = false;
		}

		if (optional_string("+Use Enhanced Checks")) {
			Asteroid_field.enhanced_visibility_checks = true;
		}

		if (optional_string("$Asteroid Targets:")) {
			stuff_string_list(Asteroid_field.target_names);
		}
		i++;
	}
}

void parse_variables()
{
	int j, num_variables = 0;

	if (! optional_string("#Sexp_variables") ) {
		return;
	} else {
		num_variables = stuff_sexp_variable_list();
	}

	// yeesh - none of this should be done in FRED :)
	// It shouldn't be done for missions in the tecroom either. They should default to whatever FRED set them to
	if ( Fred_running || !(Game_mode & GM_CAMPAIGN_MODE) ) {
		return;
	}

	// Goober5000 - now set the default value, if it's a persistent variable

	// loop through the current mission's variables
	for (j = 0; j < num_variables; j++) {
		// check against existing campaign variables first
		for (const auto &campaign_var : Campaign.persistent_variables) {
			// if the active mission has a variable with the same name as a variable saved to the campaign file, override its initial value with the persistent value
			if ( !stricmp(Sexp_variables[j].variable_name, campaign_var.variable_name) ) {
				// if the mission variable is eternal and shares the same name as a non-eternal persistent variable, warn but do nothing
				if (Sexp_variables[j].type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) {
					error_display(0, "Variable %s is marked eternal in the mission file but has the same name as another non-eternal persistent variable in the campaign.  One of these should be renamed to avoid confusion.", Sexp_variables[j].text);
				}
				else if (Sexp_variables[j].type & SEXP_VARIABLE_IS_PERSISTENT) {
					Sexp_variables[j].type = campaign_var.type;
					strcpy_s(Sexp_variables[j].text, campaign_var.text);
				} else {
					error_display(0, "Variable %s is not marked persistent in the mission file but has the same name as another persistent variable in the campaign.  One of these should be renamed to avoid confusion, or the mission variable should be marked persistent.", Sexp_variables[j].text);
				}
				break;
			}
		}

		// now check against existing player (aka "eternal") variables
		for (const auto &player_var : Player->variables) {
			// if the active mission has a variable with the same name as a variable saved to the player file, override its initial value with the persistent value
			if ( !stricmp(Sexp_variables[j].variable_name, player_var.variable_name) ) {
				// the variable in the mission file must be persistent and marked eternal,
				// otherwise we assume that the player file variable is rogue and we do not use the persistent value
				if ((Sexp_variables[j].type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) && (Sexp_variables[j].type & SEXP_VARIABLE_IS_PERSISTENT)) {
					Sexp_variables[j].type = player_var.type;
					strcpy_s(Sexp_variables[j].text, player_var.text);
				} else {
					error_display(0, "Variable %s is not marked persistent and/or not marked eternal in the mission file but has the same name as another eternally persistent variable.  This may be an unintentional name collision, or the mission variable may need to be marked eternally persistent.", Sexp_variables[j].text);
				}
				break;
			}
		}
	}
}

void parse_sexp_containers()
{
	if (!optional_string("#Sexp_containers")) {
		return;
	}

	if (optional_string("$Lists")) {
		stuff_sexp_list_containers();
		required_string("$End Lists");
	}

	if (optional_string("$Maps")) {
		stuff_sexp_map_containers();
		required_string("$End Maps");
	}

	// jg18 - persistence-related checking
	// adapted from parse_variables()

	// do this stuff only when playing through a campaign
	if (Fred_running || !(Game_mode & GM_CAMPAIGN_MODE)) {
		return;
	}

	// first update this mission's containers from campaign-persistent containers
	for (const auto &current_pc : Campaign.persistent_containers) {
		auto *p_container = get_sexp_container(current_pc.container_name.c_str());
		if (p_container != nullptr) {
			auto &container = *p_container;

			// if the mission container is eternal and shares the same name as a non-eternal persistent container, warn but do nothing
			if (container.is_eternal()) {
				error_display(0,
					"SEXP container %s is marked eternal in the mission file but has the same name as another non-eternal persistent container in the campaign.  One of "
					"these should be renamed to avoid confusion.",
					container.container_name.c_str());
			} else if (container.is_persistent()) {
				if (container.type_matches(current_pc)) {
					// TODO: when network containers are supported, review whether replacement should occur
					// if one container is marked for network use and the other isn't

					// replace!
					container = current_pc;
				} else {
					error_display(0,
						"SEXP container %s is marked persistent in the mission file but its type (%x) doesn't match a similarly named "
						"persistent container's type (%x) in the campaign.  One of these should be renamed to avoid confusion.",
						container.container_name.c_str(),
						(int)container.get_non_persistent_type(),
						(int)current_pc.get_non_persistent_type());
				}
			} else {
				error_display(0,
					"SEXP container %s is not marked persistent in the mission file but has the same name as another persistent container in the campaign.  One of these should be "
					"renamed to avoid confusion, or the mission container should be marked persistent.",
					container.container_name.c_str());
			}
		}
	}

	// then update this mission's containers from player-persistent containers
	for (const auto &player_container : Player->containers) {
		auto *p_container = get_sexp_container(player_container.container_name.c_str());
		if (p_container != nullptr) {
			auto &container = *p_container;

			// the container in the mission file must be persistent and marked eternal,
			// otherwise we assume that the player file container is rogue and we do not use the persistent values
			if (container.is_eternal() && container.is_persistent()) {
				if (container.type_matches(player_container)) {
					// TODO: when network containers are supported, review whether replacement should occur
					// if one container is marked for network use and the other isn't

					// replace!
					container = player_container;
				} else {
					error_display(0,
						"SEXP container %s is marked persistent in the mission file but its type (%x) doesn't match a similarly named "
						"persistent container's type (%x) in the player file.  One of these should be renamed to avoid confusion.",
						container.container_name.c_str(),
						(int)container.get_non_persistent_type(),
						(int)player_container.get_non_persistent_type());
				}
			} else {
				error_display(0,
					"SEXP container %s is not marked persistent and/or not marked eternal in the mission file but has the same name as another eternally persistent container.  This "
					"may be an unintentional name collision, or the mission container may need to be marked eternally persistent.",
					container.container_name.c_str());
			}
		}
	}
}

void parse_custom_data(mission* pm)
{
	if (!optional_string("#Custom Data"))
		return;

	if (optional_string("$begin_data_map")) {

		parse_string_map(pm->custom_data, "$end_data_map", "+Val:");
	}

	if (optional_string("$begin_custom_strings")) {
		while (optional_string("$Name:")) {
			custom_string cs;

			// The name of the string
			stuff_string(cs.name, F_NAME);

			// Arbitrary string value used for grouping strings together
			required_string("+Value:");
			stuff_string(cs.value, F_NAME);

			// The string text itself
			required_string("+String:");
			stuff_string(cs.text, F_MULTITEXT);

			pm->custom_strings.push_back(std::move(cs));
		}

		required_string("$end_custom_strings");
	}
}

void apply_default_custom_data(mission* pm)
{
	for (const auto& def : Default_custom_data) {
		size_t count = 0;
		for (const auto& listed : pm->custom_data) {
			if (listed.first != def.key) {
				count++;
			}
		}
		if (count == pm->custom_data.size()) {
			pm->custom_data.emplace(def.key, def.value);
		}
	}
}

bool parse_mission(mission *pm, int flags)
{
	int saved_warning_count = Global_warning_count;
	int saved_error_count = Global_error_count;

	// Reset the parse-time warning queue so each load starts fresh (only consumed by QtFRED).
	Mission_parse_warnings.clear();

	// reset parse error stuff
	Num_unknown_ship_classes = 0;
	Num_unknown_prop_classes = 0;
	Num_unknown_weapon_classes = 0;
	Num_unknown_loadout_classes = 0;

	Warned_about_team_out_of_range = false;

	reset_parse();
	mission_init(pm, (flags & MPF_ONLY_MISSION_INFO) != 0);

	parse_mission_info(pm);

	Current_file_checksum = netmisc_calc_checksum(pm,MISSION_CHECKSUM_SIZE);

	if (flags & MPF_ONLY_MISSION_INFO)
		return true;

	parse_plot_info(pm);
	parse_variables();
	parse_sexp_containers();
	parse_briefing_info(pm);	// TODO: obsolete code, keeping so we don't obsolete existing mission files
	parse_cutscenes(pm);
	parse_fiction(pm);
	parse_cmd_briefs(pm);
	parse_briefing(pm, flags);
	parse_debriefing_new(pm);
	parse_player_info(pm);
	parse_objects(pm, flags);
	parse_wings(pm);
	parse_props(pm);
	parse_events(pm);
	parse_goals(pm);
	parse_waypoints_and_jumpnodes(pm);
	parse_messages(pm, flags);
	parse_reinforcements(pm);
	parse_bitmaps(pm);
	parse_asteroid_fields(pm);
	parse_music(pm, flags);
	parse_custom_data(pm);

	// if we couldn't load some mod data
	if ((Num_unknown_ship_classes > 0) || (Num_unknown_prop_classes > 0) || ( Num_unknown_loadout_classes > 0 )) {
		// if running on standalone server, just print to the log
		if (Game_mode & GM_STANDALONE_SERVER) {
			mprintf(("Warning!  Could not load %d ship classes!\n", Num_unknown_ship_classes));
			return false;
		}
		// don't do this in FRED; we will display a separate popup
		else if (!Fred_running) {
			// build up the prompt...
			SCP_string text;

			if (Num_unknown_ship_classes > 0) {
				sprintf(text, "Warning!\n\nFreeSpace was unable to find %d ship class%s while loading this mission.  This can happen if you try to play a %s that is incompatible with the current mod.\n\n", Num_unknown_ship_classes, (Num_unknown_ship_classes > 1) ? "es" : "", (Game_mode & GM_CAMPAIGN_MODE) ? "campaign" : "mission");
			}
			else if (Num_unknown_prop_classes > 0) {
				sprintf(text, "Warning!\n\nFreeSpace was unable to find %d prop class%s while loading this mission.  This can happen if you try to play a %s that is incompatible with the current mod.\n\n", Num_unknown_prop_classes, (Num_unknown_prop_classes > 1) ? "es" : "", (Game_mode & GM_CAMPAIGN_MODE) ? "campaign" : "mission");
			}
			else if (Num_unknown_loadout_classes > 0) {
				sprintf(text, "Warning!\n\nFreeSpace was unable to find %d weapon class%s while loading this mission.  This can happen if you try to play a %s that is incompatible with the current mod.\n\n", Num_unknown_loadout_classes, (Num_unknown_loadout_classes > 1) ? "es" : "", (Game_mode & GM_CAMPAIGN_MODE) ? "campaign" : "mission");
			}

			if (Game_mode & GM_CAMPAIGN_MODE) {
				text += "(The current campaign is \"";
				text += Campaign.name;
			} else {
				text += "(The current mission is \"";
				text += pm->name;
			}

			text += "\", and the current mod is \"";

			if (Cmdline_mod == NULL || *Cmdline_mod == 0) {
				text += "<retail default> ";
			} else {
				for (char *mod_token = Cmdline_mod; *mod_token != '\0'; mod_token += strlen(mod_token) + 1) {
					text += mod_token;
					text += " ";
				}
			}

			text.erase(text.length() - 1);	// trim last space
			text += "\".)\n\n  You can continue to load the mission, but it is quite likely that you will encounter a large number of mysterious errors.  It is recommended that you either select a ";
			text += (Game_mode & GM_CAMPAIGN_MODE) ? "campaign" : "mission";
			text += " that is compatible with your current mod, or else exit FreeSpace and select a different mod.\n\n";

			text += "Do you want to continue to load the mission?";

			// now display the popup
			int popup_rval = popup(PF_TITLE_BIG | PF_TITLE_RED, 2, POPUP_NO, POPUP_YES, text.c_str());
			if (popup_rval == 0) {
				return false;
			}
		}
	}

	if (!post_process_mission(pm))
		return false;

	// QtFRED surfaces parse issues through its own error checker, so skip this summary popup there; Fred2 and the game still show it.
	if (!Qtfred_running &&
		((saved_warning_count - Global_warning_count) > 10 || (saved_error_count - Global_error_count) > 0)) {
		char text[512];
		sprintf(text, "Warning!\n\nThe current mission has generated %d warnings and/or errors during load.  These are usually caused by corrupted ship models or syntax errors in the mission file.  While FreeSpace Open will attempt to compensate for these issues, it cannot guarantee a trouble-free gameplay experience.  Source Code Project staff cannot provide assistance or support for these problems, as they are caused by the mission's data files, not FreeSpace Open's source code.", (saved_warning_count - Global_warning_count) + (saved_error_count - Global_error_count));
		popup(PF_TITLE_BIG | PF_TITLE_RED | PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, text);
	}

	log_printf(LOGFILE_EVENT_LOG, "Mission %s loaded.\n", pm->name.c_str()); 

	// success
	return true;
}

bool post_process_mission(mission *pm)
{
	int i, objnum;
	ship_weapon *swp;
	ship_obj *so;

	post_process_mission_props();

	// Goober5000 - this must be done even before post_process_ships_wings because it is a prerequisite
	ship_clear_ship_type_counts();

	// Goober5000 - must be done before all other post processing
	post_process_ships_wings();

	// the player_start_shipname had better exist at this point!
	auto player_start_entry = ship_registry_get(Player_start_shipname);
	if (!player_start_entry) {
		Warning(LOCATION, "Player start ship '%s' does not exist!", Player_start_shipname);		// maybe the mission was hand-edited :P
		return false;
	}
	Player_start_shipnum = player_start_entry->shipnum;
	Assert( Player_start_shipnum != -1 );
	Player_start_pobject = player_start_entry->p_objp();
	Assert( Player_start_pobject != NULL );

	// Assign objnum, shipnum, etc. to the player structure
	objnum = Ships[Player_start_shipnum].objnum;
	Player_obj = &Objects[objnum];
	if (!Fred_running){
		Player->objnum = objnum;
	}

	Player_obj->flags.set(Object::Object_Flags::Player_ship);			// make this object a player controlled ship.
	Player_ship = &Ships[Player_start_shipnum];
	Player_ai = &Ai_info[Player_ship->ai_index];

	Player_ai->targeted_subsys = NULL;
	Player_ai->targeted_subsys_parent = -1;

	// determine if player start has initial velocity and set forward cruise percent to relect this
	// this should check prev_ramp_vel because that is in local coordinates --wookieejedi
	if ( Player_obj->phys_info.prev_ramp_vel.xyz.z > 0.0f )
		Player->ci.forward_cruise_percent = Player_obj->phys_info.prev_ramp_vel.xyz.z / Player_obj->phys_info.max_vel.xyz.z * 100.0f;

	// Kazan - player use AI at start?
	if (pm->flags[Mission::Mission_Flags::Player_start_ai])
		Player_use_ai = true;

	// Assign squadron information
	if (!Fred_running && (Player != nullptr) && (pm->squad_name[0] != '\0') && (Game_mode & GM_CAMPAIGN_MODE) && !(Game_mode & GM_MULTIPLAYER)) {
		mprintf(("Reassigning player to squadron %s\n", pm->squad_name));
		player_set_squad(Player, pm->squad_name);
		player_set_squad_bitmap(Player, pm->squad_filename, false);
	}

	init_ai_system();

	waypoint_create_game_objects();

	// Goober5000 - this needs to be called only once after parsing of objects and wings is complete
	// (for individual invalidation, see mission_parse_mark_non_arrival)
	mission_parse_mark_non_arrivals();

	// deal with setting up arrival location for all ships.  Must do this now after all ships are created
	mission_parse_set_arrival_locations();

	// clear out information about arriving support ships
	Arriving_support_ship = nullptr;
	Num_arriving_repair_targets = 0;

	// before doing anything else, we must validate all of the sexpressions that were loaded into the mission.
	// Loop through the Sexp_nodes array and send the top level functions to the check_sexp_syntax parser
	// Cyborg -- If you are a ingame joiner, your sexps will be taken care of by the server, and checking will 
	// actually crash mission load, since a list of objects present are also given to them only after this point.

	if (!((Game_mode & GM_MULTIPLAYER) && (Net_player->flags & NETINFO_FLAG_INGAME_JOIN))) {
		for (i = 0; i < Num_sexp_nodes; i++) {
			if (is_sexp_top_level(i) && (!Fred_running || (i != Sexp_clipboard))) {
				int result, bad_node, op;

				op = get_operator_index(i);

				// need to make sure it is an operator before we treat it like one..
				if (op < 0)
				{
					result = SEXP_CHECK_UNKNOWN_OP;
					bad_node = i;
				}
				else
					result = check_sexp_syntax(i, query_operator_return_type(op), 1, &bad_node);

				// entering this if statement will result in program termination!!!!!
				// print out an error based on the return value from check_sexp_syntax()
				// G5K: now entering this statement simply aborts the mission load
				if ( result ) {
					SCP_string location_str;
					SCP_string sexp_str;
					SCP_string error_msg;
					SCP_string bad_node_str;

					// it's helpful to point out the goal/event, so do that if we can
					int index;
					if ((index = mission_event_find_sexp_tree(i)) >= 0) {
						location_str = "Error in mission event: \"";
						location_str += Mission_events[index].name;
						location_str += "\": ";
					} else if ((index = mission_goal_find_sexp_tree(i)) >= 0) {
						location_str = "Error in mission goal: \"";
						location_str += Mission_goals[index].name;
						location_str += "\": ";
					}

					convert_sexp_to_string(sexp_str, i, SEXP_ERROR_CHECK_MODE);
					truncate_message_lines(sexp_str, 30);

					stuff_sexp_text_string(bad_node_str, bad_node, SEXP_ERROR_CHECK_MODE);
					if (!bad_node_str.empty()) {	// the previous function adds a space at the end
						bad_node_str.pop_back();
					}
				
					error_msg = location_str + sexp_error_message(result);
					error_msg += ".\n\nIn sexpression: ";
					error_msg += sexp_str;
					error_msg += "\n\n(Bad node appears to be: ";
					error_msg += bad_node_str;
					error_msg += ")\n";
					// QtFRED surfaces SEXP errors through ErrorChecker's fred_check_sexp; skip the popup there.
					if (!Qtfred_running)
						Warning(LOCATION, "%s", error_msg.c_str());

					// syntax errors are recoverable in Fred but not FS
					if (!Fred_running && !sexp_recoverable_error(result)) {
						return false;
					}
				}
			}
		}
	}

	// multiplayer missions are handled just before mission start
	if (!(Game_mode & GM_MULTIPLAYER) ){	
		ai_post_process_mission();
	}

	// [ship_clear_ship_type_counts() moved to top of function]

	// we must count all of the ships of particular types.  We count all of the ships that do not have
	// their SF_IGNORE_COUNT flag set.  We don't count ships in wings when the equivalent wing flag is set.
	// in counting ships in wings, we increment the count by the wing's wave count to account for everyone.
	for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
		// do not skip over should-be-dead ships (probably destroy-before-mission ships)
		// a) because previous versions didn't, and changing this would change percentages,
		// b) because it does no harm to access should-be-dead ships here

		int shipnum = Objects[so->objnum].instance;
		// pass over non-ship objects and player ship objects
		if ( Ships[shipnum].objnum == -1 || (Objects[Ships[shipnum].objnum].flags[Object::Object_Flags::Player_ship]) )
			continue;
		if ( Ships[shipnum].flags[Ship::Ship_Flags::Ignore_count] )
			continue;
		if ( (Ships[shipnum].wingnum != -1) && (Wings[Ships[shipnum].wingnum].flags[Ship::Wing_Flags::Ignore_count]) )
			continue;

		int num = 1;

		ship_add_ship_type_count( Ships[shipnum].ship_info_index, num );
	}

	// now go through the list of ships yet to arrive
	for (p_object *p_objp = GET_FIRST(&Ship_arrival_list); p_objp != END_OF_LIST(&Ship_arrival_list); p_objp = GET_NEXT(p_objp))
	{
		int num;

		// go through similar motions as above
		if ( p_objp->flags[Mission::Parse_Object_Flags::SF_Ignore_count] )
			continue;
		if ( (p_objp->wingnum != -1) && (Wings[p_objp->wingnum].flags[Ship::Wing_Flags::Ignore_count]) )
			continue;

		if ( p_objp->wingnum == -1 )
			num = 1;
		else
			num = Wings[p_objp->wingnum].num_waves - 1;			// subtract one since we already counted the first wave
		
		ship_add_ship_type_count( p_objp->ship_class, num );
	}

	// set player weapons that are selected by default
	// AL 09/17/97: I added this code to select the first primary/secondary weapons, 
	// since I noticed the player ship sometimes doesn't get default weapons selected		

	// DB: modified 4/23/98 to take multiplayer into account. Under certain circumstances, multiplayer netplayer ships
	//     had their current_primary_bank and current_secondary_bank set to -1 (from ship_set()) and left there since
	//     Player_ship is not the only one we need to need about.
	for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {		
		if (Objects[so->objnum].flags[Object::Object_Flags::Should_be_dead])
			continue;
		ship *shipp = &Ships[Objects[so->objnum].instance];

		// don't process non player wing ships
		if ( !(shipp->flags[Ship::Ship_Flags::From_player_wing] ) )
			continue;			

		swp = &shipp->weapons;
	
		if ( swp->num_primary_banks > 0 ) {
			swp->current_primary_bank = 0;			// currently selected primary bank
		}

		if ( swp->num_secondary_banks > 0 ) {
			swp->current_secondary_bank = 0;			// currently selected secondary bank
		}
	}

	ets_init_ship(Player_obj);	// init ETS data for the player

	// Goober5000
	stars_load_first_valid_background();

	// put the timestamp stuff here for now
	Mission_end_time = -1;

	Allow_arrival_music_timestamp=timestamp(0);
	Allow_arrival_message_timestamp=timestamp(0);
	Allow_backup_message_timestamp=timestamp(0);
	Arrival_message_delay_timestamp = timestamp(-1);
	Arrival_message_subject = -1;

	int idx;
	for(idx=0; idx<2; idx++){
		Allow_arrival_music_timestamp_m[idx]=timestamp(0);
		Allow_arrival_message_timestamp_m[idx]=timestamp(0);
		Arrival_message_delay_timestamp_m[idx] = timestamp(-1);
	}	

	if(Game_mode & GM_MULTIPLAYER){ 
		multi_respawn_build_points();
	}	

	// maybe reset hotkey defaults when loading new mission
	if ( Last_file_checksum != Current_file_checksum ){
		mission_hotkey_reset_saved();
	}
	Last_file_checksum = Current_file_checksum;

	// Skip the volumetric noise bake under FRED. Only the gameplay deferred
	// pass and the Lab consume the baked 3D bitmap, and the bake can take
	// minutes in a debug build for higher quality nebulae.
	if (pm->volumetrics && !Fred_running)
		pm->volumetrics->renderVolumeBitmap();

	apply_default_custom_data(pm);

	if (Preload_briefing_icon_models) {
		int team = 0;
		if (MULTI_TEAM) {
			team = Net_player->p_info.team;
		}
		const auto &br = Briefings[team].stages;
		for (i = 0; i < Briefings[team].num_stages; i++) {
			const auto &stage = br[i];
			for (int j = 0; j < stage.num_icons; j++) {
				const auto& icon = stage.icons[j];

				bool valid = SCP_vector_inbounds(Ship_info, icon.ship_class);
				Assertion(valid, "Invalid ship class %d for briefing icon in stage %d", icon.ship_class, i);

				if (valid) {
					ship_info* sip = &Ship_info[icon.ship_class];
					int modelnum = model_load(sip->pof_file, sip);
					stage.icons[j].modelnum = modelnum;
					sip->model_num = modelnum;
				}
			}
		}
	}

	// success
	return true;
}

int get_mission_info(const char *filename, mission *mission_p, bool basic, bool filename_is_full_path)
{
	static SCP_string real_fname_buf;
	const char *real_fname = nullptr;

	if ( !filename || !strlen(filename) ) {
		return -1;
	}

	if (filename_is_full_path) {
		real_fname = filename;
	} else {
		// replace any extension with the standard one
		auto p = strrchr(filename, '.');
		if (p == nullptr) {
			real_fname_buf = filename;
			real_fname_buf += FS_MISSION_FILE_EXT;
			real_fname = real_fname_buf.c_str();
		} else if (stricmp(p, FS_MISSION_FILE_EXT) == 0) {
			real_fname = filename;
		} else {
			real_fname_buf.assign(filename, p - filename);
			real_fname_buf += FS_MISSION_FILE_EXT;
			real_fname = real_fname_buf.c_str();
		}
	}

	// if mission_p is NULL, make it point to The_mission
	if ( mission_p == NULL )
		mission_p = &The_mission;

	CFILE *ftemp = cfopen(real_fname, "rt");
	if (!ftemp) {
		return -1;
	}

	// 7/9/98 -- MWA -- check for 0 length file.
	auto filelength = cfilelength(ftemp);
	cfclose(ftemp);
	if (filelength == 0) {
		return -1;
	}

	try
	{
		read_file_text(real_fname, CF_TYPE_MISSIONS);
		mission_p->Reset();
		reset_parse();
		parse_mission_info(mission_p, basic);
	}
	catch (const parse::ParseException& e)
	{
		mprintf(("MISSIONS: Unable to parse '%s'!  Error message = %s.\n", real_fname, e.what()));
		return -1;
	}
	catch (const parse::VersionException& e)
	{
		mprintf(("MISSIONS: Unable to parse '%s'!  %s", real_fname, e.what()));
		return -1;
	}

	return 0;
}

void mission::Reset()
{
	name.clear();
	author.clear();
	required_fso_version = LEGACY_MISSION_VERSION;
	created[ 0 ] = '\0';
	modified[ 0 ] = '\0';
	notes[ 0 ] = '\0';
	mission_desc[ 0 ] = '\0';
	game_type = MISSION_TYPE_SINGLE;				// default to single player only
	flags.reset();

	num_players = 1;
	num_respawns = 3;
	max_respawn_delay = -1;
	memset(&Ignored_keys, 0, sizeof(int)*CCFG_MAX);

	support_ships.reset();

	squad_filename[ 0 ] = '\0';
	squad_name[ 0 ] = '\0';
	for ( int i = 0; i < GR_NUM_RESOLUTIONS; i++ )
		loading_screen[ i ][ 0 ] = '\0';

	skybox_model[ 0 ] = '\0';
	vm_set_identity(&skybox_orientation);
	skybox_flags = DEFAULT_NMODEL_FLAGS;

	envmap_name[ 0 ] = '\0';
	contrail_threshold = CONTRAIL_THRESHOLD_DEFAULT;
	large_ship_no_collide_collision_group = DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP;
	ambient_light_level = DEFAULT_AMBIENT_LIGHT_LEVEL;
	sound_environment.id = -1;

	command_persona = Default_command_persona;
	strcpy_s(command_sender, DEFAULT_COMMAND);
	debriefing_persona = debrief_find_persona_index();
	traitor_override = nullptr;

	event_music_name[ 0 ] = '\0';
	briefing_music_name[ 0 ] = '\0';
	substitute_event_music_name[ 0 ] = '\0';
	substitute_briefing_music_name[ 0 ] = '\0';

	ai_profile = &Ai_profiles[Default_ai_profile];
	lighting_profile_name = lighting_profiles::default_name();

	cutscenes.clear( );

	gravity = vmd_zero_vector;
	HUD_timer_padding = 0;
	volumetrics.reset();

	custom_data.clear();
	custom_strings.clear();
	fred_layers.clear();
	fred_layers.emplace_back("Default");
}

void support_ship_info::reset()
{
	arrival_location = ArrivalLocation::AT_LOCATION;
	arrival_anchor = anchor_t::invalid();
	departure_location = DepartureLocation::AT_LOCATION;
	departure_anchor = anchor_t::invalid();
	max_hull_repair_val = 0.0f;        // hull cannot be repaired
	max_subsys_repair_val = 100.0f;    //ASSUMPTION: full repair capabilities
	max_support_ships = -1;            // infinite
	max_concurrent_ships = 1;
	ship_class = -1;                   // ship class will be determined by the summoning ship's species
	tally = 0;
	support_available_for_species = 0; // will be filled in by the next loop
	for (auto& team_pool : rearm_weapon_pool) {
		std::fill(std::begin(team_pool), std::end(team_pool), -1); // -1 == unlimited per-weapon
	}
	disallow_rearm = false;
	allow_rearm_weapon_precedence = false;
	rearm_pool_from_loadout = false;

	// for each species, store whether support is available
	for (int species = 0; species < sz2i(Species_info.size()); species++) {
		if (Species_info[species].support_ship_index >= 0) {
			support_available_for_species |= (1 << species);
		}
	}
}

/**
 * Initialize the mission and related data structures.
 */
void mission_init(mission *pm, bool quick_init)
{
	pm->Reset();

	Player_starts = 0;
	Player_start_shipnum = -1;
	*Player_start_shipname = 0;		// make the string 0 length for checking later
	Entry_delay_time = 0;

	Mission_all_attack = 0;
	Num_teams = 1;				// assume 1

	// sometimes we don't need to run through the entire initialization,
	// e.g. if we're just checking mission info
	if (quick_init)
		return;

	init_sexp();
	mission_goals_and_events_init();

	fiction_viewer_reset();
	cmd_brief_reset();
	brief_reset();
	debrief_reset();
	messages_init();

	mission_parse_reset_alt();
	mission_parse_reset_callsign();
	ai_lua_reset_general_orders();

	Parse_names.clear();
	Num_path_restrictions = 0;
	Num_ai_dock_names = 0;
	ai_clear_goal_target_names();

	for (int i = 0; i < MAX_CARGO; i++)
		Cargo_names[i] = Cargo_names_buf[i]; // make a pointer array for compatibility
	strcpy(Cargo_names[0], "Nothing");
	Num_cargo = 1;

	jumpnode_level_close();
	waypoint_level_close();
	props_level_close();

	red_alert_invalidate_timestamp();
	event_music_reset_choices();
	clear_texture_replacements();

	// initialize the initially_docked array.
	for (int i = 0; i < MAX_SHIPS; i++) {
		Initially_docked[i].docker[0] = '\0';
		Initially_docked[i].dockee[0] = '\0';
		Initially_docked[i].docker_point[0] = '\0';
		Initially_docked[i].dockee_point[0] = '\0';
	}
	Total_initially_docked = 0;

	Parse_objects.clear();
	list_init(&Ship_arrival_list);	// init list for arrival ships

	Subsys_index = 0;
	Subsys_status_size = 0;

	if (Subsys_status != nullptr) {
		vm_free( Subsys_status );
		Subsys_status = nullptr;
	}

	Num_wings = 0;
	for (int i = 0; i < MAX_WINGS; i++)
		Wings[i].clear();
	
	Reinforcements.clear();

	Parse_props.clear();

	Asteroid_field.num_initial_asteroids = 0;

	// This could be set with a sexp, so we should reset it here
	Motion_debris_override = false;

	// reset background bitmaps and suns
	stars_pre_level_init();
	neb2_pre_level_init();

	ENVMAP = -1;

	// FS1 nebula
	nebula_close();
	Nebula_pitch = (int)((float)(Random::next() & 0x0fff) * 360.0f / 4096.0f);
	Nebula_bank = (int)((float)(Random::next() & 0x0fff) * 360.0f / 4096.0f);
	Nebula_heading = (int)((float)(Random::next() & 0x0fff) * 360.0f / 4096.0f);
	Nebula_index = -1;
	Mission_palette = 1;
}

// Main parse routine for parsing a mission.  The default parameter flags tells us which information
// to get when parsing the mission.  0 means get everything (default).  Other flags just gets us basic
// info such as game type, number of players etc. or whether we are importing from a different format.
bool parse_main(const char *mission_name, int flags)
{
	int i;
	bool rval;

	Assert(Ship_info.size() <= MAX_SHIP_CLASSES);

	// fill in Ship_class_names array with the names from the ship_info struct
	i = 0;
	for (auto it = Ship_info.begin(); it != Ship_info.end(); i++, ++it)
		Ship_class_names[i] = it->name;
	
	do {
		// don't do this for imports
		if (!(flags & MPF_IMPORT_FSM)) {
			CFILE *ftemp = cfopen(mission_name, "rt", CF_TYPE_MISSIONS);

			// fail situation.
			if (!ftemp) {
				if (!Fred_running)
					Error( LOCATION, "Couldn't open mission '%s'\n", mission_name );

				Current_file_length = -1;
				Current_file_checksum = 0;
	
				rval = false;
				break;
			}

			Current_file_length = cfilelength(ftemp);
			cfclose(ftemp);
		}

		try
		{
			// import FS1 mission
			if (flags & MPF_IMPORT_FSM) {
				read_file_text(mission_name, CF_TYPE_ANY);
				convertFSMtoFS2();
				rval = parse_mission(&The_mission, flags);
			}
			// regular mission load
			else {
				read_file_text(mission_name, CF_TYPE_MISSIONS);
				rval = parse_mission(&The_mission, flags);
			}

			display_parse_diagnostics();
		}
		catch (const parse::ParseException& e)
		{
			mprintf(("MISSIONS: Unable to parse '%s'!  Error message = %s.\n", mission_name, e.what()));
			rval = false;
			break;
		}
		catch (const parse::VersionException& e)
		{
			mprintf(("MISSIONS: Unable to parse '%s'!  %s", mission_name, e.what()));
			rval = false;
			break;
		}
	} while (0);

	if (!Fred_running)
		strcpy_s(Mission_filename, mission_name);

	return rval;
}

// Note, this is currently only called from game_shutdown()
void mission_parse_close()
{
	// free subsystems
	if (Subsys_status != NULL)
	{
		vm_free(Subsys_status);
		Subsys_status = NULL;
	}

	// the destructor for each p_object will clear its dock list
	Parse_objects.clear();
}

/**
 * Sets the arrival location of the ships in wingp.  
 *
 * @param wingp Pointer to wing
 * @param num_to_set The threshold value for wings may have us create more ships in the wing when there are still some remaining
 */
void mission_set_wing_arrival_location( wing *wingp, int num_to_set )
{
	int index;
	int anchor_objnum = -1;

	// get the starting index into the ship_index array of the first ship whose location we need set.

	index = wingp->current_count - num_to_set;
	if ( (wingp->arrival_location == ArrivalLocation::FROM_DOCK_BAY) || (wingp->arrival_location == ArrivalLocation::AT_LOCATION) ) {
		while ( index < wingp->current_count ) {
			object *objp;

			objp = &Objects[Ships[wingp->ship_index[index]].objnum];
			anchor_objnum = mission_set_arrival_location(wingp->arrival_anchor, wingp->arrival_location, wingp->arrival_distance, OBJ_INDEX(objp), wingp->arrival_path_mask, NULL, NULL);

			index++;
		}
	} else {
		object *leader_objp;
		vec3d pos;
		matrix orient;
		int wing_index;

		// wing is not arriving from a docking bay -- possibly move them based on arriving near
		// or in front of some other ship.
		index = wingp->current_count - num_to_set;
		leader_objp = &Objects[Ships[wingp->ship_index[index]].objnum];
		anchor_objnum = mission_set_arrival_location(wingp->arrival_anchor, wingp->arrival_location, wingp->arrival_distance, OBJ_INDEX(leader_objp), wingp->arrival_path_mask, &pos, &orient);
		if (anchor_objnum != -1) {
			// modify the remaining ships created
			index++;
			wing_index = 1;
			while ( index < wingp->current_count ) {
				object *objp;

				objp = &Objects[Ships[wingp->ship_index[index]].objnum];

				// change the position of the next ships in the wing.  Use the cool function in AiCode.cpp which
				// Mike K wrote to give new positions to the wing members.
				get_absolute_wing_pos( &objp->pos, leader_objp, WING_INDEX(wingp), wing_index++, false);
				memcpy( &objp->orient, &orient, sizeof(matrix) );

				index++;
			}
		}
	}

	if (Game_mode & GM_IN_MISSION) {
		for ( index = wingp->current_count - num_to_set; index < wingp->current_count; index ++ ) {
			object *objp = &Objects[Ships[wingp->ship_index[index]].objnum];
			object *anchor_objp = (anchor_objnum >= 0) ? &Objects[anchor_objnum] : nullptr;

			if (scripting::hooks::OnShipArrive->isActive()) {
				scripting::hooks::OnShipArrive->run(scripting::hooks::ShipArriveConditions{ &Ships[wingp->ship_index[index]], wingp->arrival_location, anchor_objp },
					scripting::hook_param_list(
						scripting::hook_param("Ship", 'o', objp),
						scripting::hook_param("Parent", 'o', anchor_objp, anchor_objp != nullptr)
					));
			}

			if (wingp->arrival_location != ArrivalLocation::FROM_DOCK_BAY) {
				shipfx_warpin_start(objp);
			}
		}
	}
}

/**
 * Called after a mission is parsed to set the arrival locations of all ships in the
 * mission to the apprioriate spot.  Mainly needed because ships might be in dock bays to start
 * the mission, so their AI mode must be set appropriately.
 */
void mission_parse_set_arrival_locations()
{
	int i;

	if ( Fred_running )
		return;

	obj_merge_created_list();
	for (auto so: list_range(&Ship_obj_list)) {
		// do not skip over should-be-dead ships (probably destroy-before-mission ships)
		// a) because previous versions didn't, and changing this could affect the location of should-be-dead ships
		// b) because it does no harm to access should-be-dead ships here

		auto shipp = &Ships[Objects[so->objnum].instance];
		// if the ship is in a wing -- ignore the info and let the wing info handle it
		if ( shipp->wingnum != -1 )
			continue;

		// call function to set arrival location for this ship.
		mission_set_arrival_location( shipp->arrival_anchor, shipp->arrival_location, shipp->arrival_distance, so->objnum, shipp->arrival_path_mask, NULL, NULL);
	}

	// do the wings
	for ( i = 0; i < Num_wings; i++ ) {

		// if wing has no ships, then don't process it.
		if ( Wings[i].current_count == 0 )
			continue;

		mission_set_wing_arrival_location( &Wings[i], Wings[i].current_count );
	}
}

// Goober5000
bool sexp_is_locked_false(int node)
{
	// dunno why these are different, but they are
	if (Fred_running)
		return (node == Locked_sexp_false);
	else
		return (Sexp_nodes[node].value == SEXP_KNOWN_FALSE);
}

void set_cue_to_false(int *cue)
{
	free_sexp2(*cue);
	*cue = Locked_sexp_false;
}

// function to set the arrival cue of a ship to false
void reset_arrival_to_false(p_object *pobjp, bool reset_wing)
{
	// falsify the ship cue
	mprintf(("Setting arrival cue of ship %s to false for initial docking purposes.\n", pobjp->name));
	set_cue_to_false(&pobjp->arrival_cue);

	// falsify the wing cue and all ships in that wing
	if (reset_wing && pobjp->wingnum >= 0)
	{
		wing *wingp = &Wings[pobjp->wingnum];
		mprintf(("Setting arrival cue of wing %s to false for initial docking purposes.\n", wingp->name));
		set_cue_to_false(&wingp->arrival_cue);

		for (SCP_vector<p_object>::iterator ii = Parse_objects.begin(); ii != Parse_objects.end(); ++ii)
		{
			if ((&(*ii) != pobjp) && (ii->wingnum == pobjp->wingnum))
				reset_arrival_to_false(&(*ii), false);
		}
	}
}

/**
 * In both retail and SCP, the dock "leader" is defined as the only guy in his
 * group with a non-false arrival cue.
 *
 * If we are forcing a leader, that means *none* of the ships in this dock group
 * have a non-false arrival cue, so we just need to pick one
 */
void parse_object_mark_dock_leader_sub(p_object *pobjp, p_dock_function_info *infop, bool force_a_leader)
{
	int cue_to_check;

	// if this guy is part of a wing, he uses his wing's arrival cue
	if (pobjp->wingnum >= 0)
	{
		cue_to_check = Wings[pobjp->wingnum].arrival_cue;
	}
	// check the object's arrival cue
	else
	{
		cue_to_check = pobjp->arrival_cue;
	}

	// is he a leader (using the definition above)?
	if (!sexp_is_locked_false(cue_to_check) || force_a_leader)
	{
		p_object *existing_leader;

		// increment number of leaders found
		infop->maintained_variables.int_value++;

		// see if we already found a leader
		existing_leader = infop->maintained_variables.objp_value;
		if (existing_leader != NULL)
		{
			// keep existing leader if he has a higher priority than us
			if (ship_class_compare(pobjp->ship_class, existing_leader->ship_class) >= 0)
			{
				// set my arrival cue to false
				reset_arrival_to_false(pobjp, true);
				return;
			}

			// otherwise, unmark the existing leader and set his arrival cue to false
            existing_leader->flags.remove(Mission::Parse_Object_Flags::SF_Dock_leader);
			reset_arrival_to_false(existing_leader, true);
		}

		// mark and save me as the leader
        pobjp->flags.set(Mission::Parse_Object_Flags::SF_Dock_leader);
		infop->maintained_variables.objp_value = pobjp;
	}
}

void parse_object_mark_dock_leader_helper(p_object *pobjp, p_dock_function_info *infop)
{
	parse_object_mark_dock_leader_sub(pobjp, infop, false);
}

void parse_object_choose_arbitrary_dock_leader_helper(p_object *pobjp, p_dock_function_info *infop)
{
	parse_object_mark_dock_leader_sub(pobjp, infop, true);
}

// Goober5000
void parse_object_set_handled_flag_helper(p_object *pobjp, p_dock_function_info * /*infop*/)
{
    pobjp->flags.set(Mission::Parse_Object_Flags::Already_handled);
}

// Goober5000
void parse_object_clear_handled_flag_helper(p_object *pobjp, p_dock_function_info * /*infop*/)
{
    pobjp->flags.remove(Mission::Parse_Object_Flags::Already_handled);
}

// Goober5000
void parse_object_clear_all_handled_flags()
{
	// clear flag for all ships
	for (SCP_vector<p_object>::iterator ii = Parse_objects.begin(); ii != Parse_objects.end(); ++ii)
	{
		p_object *pobjp = &(*ii);
		p_dock_function_info dfi;

		// since we're going through all objects, this object may not be docked
		if (!object_is_docked(pobjp))
			continue;

		// has this object (by extension, this group of docked objects) been cleared already?
		if (!(pobjp->flags[Mission::Parse_Object_Flags::Already_handled]))
			continue;

		// clear the handled flag for this group
		dock_evaluate_all_docked_objects(pobjp, &dfi, parse_object_clear_handled_flag_helper);
	}
}

// Goober5000
// This function iterates through the Initially_docked array and builds the dock trees
// for each parse object.  This can only be done after all objects and wings have been parsed.
void mission_parse_set_up_initial_docks()
{
	int i;

	// build trees
	for (i = 0; i < Total_initially_docked; i++)
	{
		// resolve the docker and dockee
		auto docker_entry = ship_registry_get(Initially_docked[i].docker);
		auto docker = docker_entry ? docker_entry->p_objp_or_null() : nullptr;
		if (docker == nullptr)
		{
			Warning(LOCATION, "Could not resolve initially docked object '%s'!", Initially_docked[i].docker);
			continue;
		}
		auto dockee_entry = ship_registry_get(Initially_docked[i].dockee);
		auto dockee = dockee_entry ? dockee_entry->p_objp_or_null() : nullptr;
		if (dockee == nullptr)
		{
			Warning(LOCATION, "Could not resolve docking target '%s' of initially docked object '%s'!", Initially_docked[i].dockee, Initially_docked[i].docker);
			continue;
		}

		// skip docking if they're already docked
		// (in FSO, we list all initially docked pairs for all ships,
		// so we end up with twice as many docking entries as we need)
		if (dock_check_find_direct_docked_object(docker, dockee))
			continue;

		// resolve the dockpoints
		auto docker_point = Initially_docked[i].docker_point;
		auto dockee_point = Initially_docked[i].dockee_point;

		// docker point in use?
		if (dock_find_object_at_dockpoint(docker, docker_point) != NULL)
		{
			Warning(LOCATION, "Trying to initially dock '%s' and '%s', but the former's dockpoint is already in use!", Initially_docked[i].docker, Initially_docked[i].dockee);
			continue;
		}

		// dockee point in use?
		if (dock_find_object_at_dockpoint(dockee, dockee_point) != NULL)
		{
			Warning(LOCATION, "Trying to initially dock '%s' and '%s', but the latter's dockpoint is already in use!", Initially_docked[i].docker, Initially_docked[i].dockee);
			continue;
		}

		dock_dock_objects(docker, docker_point, dockee, dockee_point);
	}

	// now resolve the leader of each tree
	for (SCP_vector<p_object>::iterator ii = Parse_objects.begin(); ii != Parse_objects.end(); ++ii)
	{
		p_object *pobjp = &(*ii);
		p_dock_function_info dfi;

		// since we're going through all objects, this object may not be docked
		if (!object_is_docked(pobjp))
			continue;

		// has this object (by extension, this group of docked objects) been handled already?
		if (pobjp->flags[Mission::Parse_Object_Flags::Already_handled])
			continue;

		// find the dock leader(s)
		dock_evaluate_all_docked_objects(pobjp, &dfi, parse_object_mark_dock_leader_helper);

		// display an error if necessary
		if (dfi.maintained_variables.int_value == 0)
		{
			if (!Qtfred_running)
				Warning(LOCATION, "In the docking group containing %s, every ship has an arrival cue set to false.  The group will not appear in-mission!\n", pobjp->name);

			// for FRED, we must arbitrarily choose a dock leader, otherwise the entire docked group will not be loaded
			if (Fred_running)
				dock_evaluate_all_docked_objects(pobjp, &dfi, parse_object_choose_arbitrary_dock_leader_helper);
		}
		else if (dfi.maintained_variables.int_value > 1)
		{
			if (!Qtfred_running)
				Warning(LOCATION, "In the docking group containing %s, there is more than one ship with a non-false arrival cue!  There can only be one such ship.  Setting all arrival cues except %s to false...\n", dfi.maintained_variables.objp_value->name, dfi.maintained_variables.objp_value->name);
		}

		// clear dfi stuff
		dfi.maintained_variables.int_value = 0;
		dfi.maintained_variables.objp_value = NULL;

		// set the handled flag for this group
		dock_evaluate_all_docked_objects(pobjp, &dfi, parse_object_set_handled_flag_helper);
	}

	// now clear the handled flags
	parse_object_clear_all_handled_flags();
}

/**
 * Returns true or false if the given mission support multiplayers
 */
int mission_parse_is_multi(const char *filename, char *mission_name)
{
	int game_type;
	int filelength;
	CFILE *ftemp;

	// new way of getting information.  Open the file, and just get the name and the game_type flags.
	// return the flags if a multiplayer mission

	ftemp = cfopen(filename, "rt");
	if (!ftemp)
		return 0;

	// 7/9/98 -- MWA -- check for 0 length file.
	filelength = cfilelength(ftemp);
	cfclose(ftemp);
	if ( filelength == 0 )
		return 0;

	game_type = 0;
	do {
		try
		{
			read_file_text(filename, CF_TYPE_MISSIONS);
			reset_parse();

			if (skip_to_string("$Name:") != 1) {
				nprintf(("Network", "Unable to process %s because we couldn't find $Name:", filename));
				break;
			}
			stuff_string(mission_name, F_NAME, NAME_LENGTH);

			if (skip_to_string("+Game Type Flags:") != 1) {
				nprintf(("Network", "Unable to process %s because we couldn't find +Game Type Flags:\n", filename));
				break;
			}
			stuff_int(&game_type);
		}
		catch (const parse::ParseException& e)
		{
			mprintf(("MISSIONS: Unable to parse '%s'!  Error message = %s.\n", filename, e.what()));
			break;
		}
		catch (const parse::VersionException& e)
		{
			mprintf(("MISSIONS: Unable to parse '%s'!  %s", filename, e.what()));
			break;
		}
	} while (0);

	return (game_type & MISSION_TYPE_MULTI) ? game_type : 0;
}

/**
 * Called to retrieve useful information about a mission.  
 *
 * We will get the name, description, and number of players for a mission. Probably used for multiplayer only?
 * The calling function can use the information in The_mission to get the name/description of the mission
 * if needed.
 */
int mission_parse_get_multi_mission_info( const char *filename )
{
	if ( get_mission_info(filename, &The_mission) )
		return -1;

	Assert( The_mission.game_type & MISSION_TYPE_MULTI );		// assume multiplayer only for now?

	// return the number of parse_players.  later, we might want to include (optionally?) the number
	// of other ships in the main players wing (usually wing 'alpha') for inclusion of number of
	// players allowed.

	return The_mission.num_players;
}

/**
 * @brief				Returns the parse object on the ship arrival list associated with the given name.
 * @param[in] name		The name of the object
 * @returns				The parse object, or NULL if no object with the given name is on the arrival list
 * @remarks				This function is used to determine whether a ship has arrived. Ships on the arrival list
 *						are considered to not be in the game; In order to make respawns work in multiplayer,
 *						player ships (those marked with the P_OF_PLAYER_START flag) are never removed from it.
 */
p_object *mission_parse_get_arrival_ship(const char *name)
{
	p_object *p_objp;

	if (name == nullptr)
		return nullptr;

	for (p_objp = GET_FIRST(&Ship_arrival_list); p_objp != END_OF_LIST(&Ship_arrival_list); p_objp = GET_NEXT(p_objp))
	{
		if (!stricmp(p_objp->name, name)) 
		{
			return p_objp;	// still on the arrival list
		}
	}

	return nullptr;
}

/**
 * @brief					Returns the parse object on the ship arrival list associated with the given net signature.
 * @param[in] net_signature	The net signature of the object
 * @returns					The parse object, or NULL if no object with the given signature is on the arrival list
 * @remarks					This function is used to determine whether a ship has arrived. Ships on the arrival list
 *							are considered to not be in the game; In order to make respawns work in multiplayer,
 *							player ships (those marked with the P_OF_PLAYER_START flag) are never removed from it.
 */
p_object *mission_parse_get_arrival_ship(ushort net_signature)
{
	p_object *p_objp;

	for (p_objp = GET_FIRST(&Ship_arrival_list); p_objp !=END_OF_LIST(&Ship_arrival_list); p_objp = GET_NEXT(p_objp))
	{
		if (p_objp->net_signature == net_signature) 
		{
			return p_objp;	// still on the arrival list
		}
	}

	return NULL;
}

/**
 * Because player ships remain on the arrival list (see parse_wing_create_ships), testing for yet-to-arrive by merely
 * checking the list will produce false positives for player ships.  So this function also checks whether the object was created.
 */
bool mission_check_ship_yet_to_arrive(const char *name)
{
	p_object *p_objp = mission_parse_get_arrival_ship(name);
	if (p_objp == nullptr)
		return false;

	if (p_objp->created_object != nullptr)
		return false;

	return true;
}

/**
 * Sets the arrival location of a parse object according to the arrival location of the object.
 * @return objnum of anchor ship if there is one, -1 otherwise.
 */
int mission_set_arrival_location(anchor_t anchor, ArrivalLocation location, int dist, int objnum, int path_mask, vec3d *new_pos, matrix *new_orient)
{
	int shipnum, anchor_objnum;
	vec3d anchor_pos, rand_vec, new_fvec;
	matrix orient;

	if ( location == ArrivalLocation::AT_LOCATION )
		return -1;

	Assert(anchor.isValid());
	if (!anchor.isValid())
		return -1;	// should never happen, but if it does, fail gracefully

	// this ship might possibly arrive at another location.  The location is based on the
	// proximity of some ship (and some other special tokens)
	if (anchor.value() & ANCHOR_SPECIAL_ARRIVAL)
	{
		bool get_players = (anchor.value() & ANCHOR_SPECIAL_ARRIVAL_PLAYER) > 0;

		// filter out iff
		int iff_index = anchor.value();
		iff_index &= ~ANCHOR_SPECIAL_ARRIVAL;
		iff_index &= ~ANCHOR_SPECIAL_ARRIVAL_PLAYER;

		// get ship
		shipnum = ship_get_random_team_ship(iff_get_mask(iff_index), get_players ? SHIP_GET_ONLY_PLAYERS : SHIP_GET_ANY_SHIP);
	}
	// if we didn't find the arrival anchor in the list of special nodes, then it must be a ship registry index
	else
	{
		auto anchor_entry = ship_registry_get(anchor);
		shipnum = anchor_entry ? anchor_entry->shipnum : -1;
	}

	// if we didn't get an object from one of the above functions, then make the object
	// arrive at its placed location
	if (shipnum < 0)
	{
		Assert ( location != ArrivalLocation::FROM_DOCK_BAY );		// bogus data somewhere!!!  get mwa
		nprintf (("allender", "couldn't find ship for arrival anchor -- using location ship created at"));
		return -1;
	}

	// take the shipnum and get the position.  once we have positions, we can determine where
	// to make this ship appear
	Assert ( shipnum != -1 );
	anchor_objnum = Ships[shipnum].objnum;
	anchor_pos = Objects[anchor_objnum].pos;

	// if arriving from docking bay, then set ai mode and call function as per AL's instructions.
	if ( location == ArrivalLocation::FROM_DOCK_BAY ) {
		// if we get an error, just let the ship arrive(?)
		if ( ai_acquire_emerge_path(&Objects[objnum], anchor_objnum, path_mask) == -1 ) {
			// get MWA or AL -- not sure what to do here when we cannot acquire a path
			mprintf(("Unable to acquire arrival path on anchor ship %s\n", Ships[shipnum].ship_name));
			return -1;
		}
	} else {

		// AL: ensure dist > 0 (otherwise get errors in vecmat)
		// TODO: maybe set distance to 2x ship radius of ship appearing in front of?
		if ( dist <= 0 )
		{
			// Goober5000 - default to 100
			Warning(LOCATION, "Distance of %d is invalid in mission_set_arrival_location.  Defaulting to 100.\n", dist);
			dist = 100;
		}
		
		// get a vector which is the ships arrival position based on the type of arrival
		// this ship should have.  Arriving near a ship we use a random normalized vector
		// scaled by the distance given by the designer.  Arriving in front of a ship means
		// entering the battle in the view cone.
		if ( location == ArrivalLocation::NEAR_SHIP ) {
			// get a random vector -- use static randvec if in multiplayer
			if ( Game_mode & GM_NORMAL )
				vm_vec_rand_vec_quick(&rand_vec);
			else
				static_randvec( Objects[objnum].net_signature, &rand_vec );
		} else if ( location == ArrivalLocation::IN_FRONT_OF_SHIP || location == ArrivalLocation::IN_BACK_OF_SHIP
			     || location == ArrivalLocation::ABOVE_SHIP || location == ArrivalLocation::BELOW_SHIP
			     || location == ArrivalLocation::TO_LEFT_OF_SHIP || location == ArrivalLocation::TO_RIGHT_OF_SHIP ) {
			vec3d t1, t2, t3;
			int r1, r2;
			float x;

			// cool function by MK to give a reasonable random vector "in front" of a ship
			// rvec and uvec are the right and up vectors.
			// If these are not available, this would be an expensive method.
			x = cosf(fl_radians(45.0f));
			if ( Game_mode & GM_NORMAL ) {
				r1 = Random::flip_coin() ? -1 : 1;
				r2 = Random::flip_coin() ? -1 : 1;
			} else {
				// in multiplayer, use the static rand functions so that all clients can get the
				// same information.
				r1 = static_rand(Objects[objnum].net_signature) < STATIC_RAND_MAX / 2 ? -1 : 1;
				r2 = static_rand(Objects[objnum].net_signature+1) < STATIC_RAND_MAX / 2 ? -1 : 1;
			}

			vm_vec_copy_scale(&t1, &(Objects[anchor_objnum].orient.vec.fvec), x);
			vm_vec_copy_scale(&t2, &(Objects[anchor_objnum].orient.vec.rvec), (1.0f - x) * r1);
			vm_vec_copy_scale(&t3, &(Objects[anchor_objnum].orient.vec.uvec), (1.0f - x) * r2);

			vm_vec_add(&rand_vec, &t1, &t2);
			vm_vec_add2(&rand_vec, &t3);
			vm_vec_normalize(&rand_vec);

			// vertical axis: rotate to up (around X axis)
			if (location == ArrivalLocation::ABOVE_SHIP || location == ArrivalLocation::BELOW_SHIP)
				vm_rot_point_around_line(&rand_vec, &rand_vec, PI_2, &vmd_zero_vector, &vmd_x_vector);
			// lateral axis: rotate to right (backwards around Y axis)
			else if (location == ArrivalLocation::TO_LEFT_OF_SHIP || location == ArrivalLocation::TO_RIGHT_OF_SHIP)
				vm_rot_point_around_line(&rand_vec, &rand_vec, -PI_2, &vmd_zero_vector, &vmd_y_vector);

			// for the opposite directions, just flip the vector around
			if (location == ArrivalLocation::IN_BACK_OF_SHIP || location == ArrivalLocation::BELOW_SHIP || location == ArrivalLocation::TO_LEFT_OF_SHIP)
				vm_vec_negate(&rand_vec);
		} else {
			UNREACHABLE("Unknown location type discovered when trying to parse %s -- Please let an SCP coder know!", Ships[shipnum].ship_name);
		}

		// add in the radius of the two ships involved.  This will make the ship arrive further than
		// specified, but will appear more accurate since we are pushing the edge of the model to the
		// specified distance.  large objects appears to be a lot closer without the following line because
		// the object centers were at the correct distance, but the model itself was much closer to the
		// target ship.
		dist += (int)Objects[objnum].radius + (int)Objects[anchor_objnum].radius;
		vm_vec_scale_add(&Objects[objnum].pos, &anchor_pos, &rand_vec, (float)dist);

		// I think that we will always want to orient the ship that is arriving to face towards
		// the ship it is arriving near/in front of.  The effect will be cool!
		//
		// calculate the new fvec of the ship arriving and use only that to get the matrix.  isn't a big
		// deal not getting bank.
		vm_vec_normalized_dir(&new_fvec, &anchor_pos, &Objects[objnum].pos );
		vm_vector_2_matrix_norm( &orient, &new_fvec, nullptr, nullptr );
		Objects[objnum].orient = orient;
	}

	// set the new_pos parameter since it might be used outside the function (i.e. when dealing with wings).
	if ( new_pos )
		memcpy(new_pos, &Objects[objnum].pos, sizeof(vec3d) );

	if ( new_orient )
		memcpy( new_orient, &Objects[objnum].orient, sizeof(matrix) );

	return anchor_objnum;
}

/**
 * Mark a reinforcement as available
 */
void mission_parse_mark_reinforcement_available(const char *name)
{
	int i = find_item_with_string(Reinforcements, &reinforcements::name, name);
	if (i >= 0)
	{
		auto &r = Reinforcements[i];
		if (!(r.flags & RF_IS_AVAILABLE))
		{
			r.flags |= RF_IS_AVAILABLE;

			// tell all of the clients.
			if (MULTIPLAYER_MASTER)
				send_reinforcement_avail(i);
		}
		return;
	}

	Assertion(false, "Reinforcement '%s' not found!", name);
}

/**
 * Takes a parse object and checks the arrival cue, delay and destruction of object it is arriving from then creates the object if necessary.  
 *
 * @return -1 if not created.  
 * @return objnum of created ship otherwise
 */
int mission_did_ship_arrive(p_object *objp, bool force_arrival)
{
	// find out if the arrival cue became true
	bool should_arrive = force_arrival || eval_sexp(objp->arrival_cue);

	// we must first check to see if this ship is a reinforcement or not.  If so, then don't
	// process
	if ( objp->flags[Mission::Parse_Object_Flags::SF_Reinforcement] ) {

		// if this ship did arrive, mark the reinforcement as available, and tell clients if in multiplayer
		// mode
		if ( should_arrive ) {
			mission_parse_mark_reinforcement_available(objp->name);
		}

		// if we're forcing the arrival, then "use" the reinforcement; otherwise don't process anything else
		if (force_arrival) {
			int i = find_item_with_string(Reinforcements, &reinforcements::name, objp->name);
			if (i >= 0) {
				Reinforcements[i].num_uses++;
			}
		} else {
			return -1;
		}
	}

	if ( should_arrive ) { 		// has the arrival criteria been met?
		// check to see if the delay field <= 0.  if so, then create a timestamp and then maybe
		// create the object
		if ( objp->arrival_delay <= 0 ) {
			objp->arrival_delay = timestamp( -objp->arrival_delay * 1000 );

			// make sure we have a valid timestamp
			Assert( objp->arrival_delay > 0 );
		}
		
		// if the timestamp hasn't elapsed, move onto the next ship.
		if ( !timestamp_elapsed(objp->arrival_delay) )
			return -1;

		// check to see if this ship is to arrive via a docking bay.  If so, and the ship to arrive from
		// doesn't exist, don't create.
		if ( objp->arrival_location == ArrivalLocation::FROM_DOCK_BAY ) {
			Assert( objp->arrival_anchor.isValid() );
			auto anchor_ship_entry = ship_registry_get(objp->arrival_anchor);

			// see if ship is yet to arrive.  If so, then return -1 so we can evaluate again later.
			if (!anchor_ship_entry || anchor_ship_entry->status == ShipStatus::NOT_YET_PRESENT)
				return -1;

			// see if ship is in mission.
			if (!anchor_ship_entry->has_shipp()) {
				mission_parse_mark_non_arrival(objp);	// Goober5000
				WarningEx(LOCATION, "Warning: Ship %s cannot arrive from docking bay of destroyed or departed %s.\n", objp->name, anchor_ship_entry->name);
				return -1;
			}

			// Goober5000 - check status of fighterbays - if they're destroyed, we can't launch - but we want to reeval later
			if (ship_fighterbays_all_destroyed(anchor_ship_entry->shipp())) {
				WarningEx(LOCATION, "Warning: Ship %s cannot arrive from destroyed docking bay of %s.\n", objp->name, anchor_ship_entry->name);
				return -1;
			}
		}

		if ( objp->flags[Mission::Parse_Object_Flags::SF_Cannot_arrive] ) {
			WarningEx(LOCATION, "Warning: Ship %s cannot arrive. Ship not created.\n", objp->name);
			return -1;
		}

		// create the ship
		int object_num = parse_create_object(objp);
		Assert(object_num >= 0 && object_num < MAX_OBJECTS);
		
		// Play the music track for an arrival
		if ( !(Ships[Objects[object_num].instance].flags[Ship::Ship_Flags::No_arrival_music]) )
			if ( timestamp_elapsed(Allow_arrival_music_timestamp) ) {
				Allow_arrival_music_timestamp = timestamp(ARRIVAL_MUSIC_MIN_SEPARATION);
				event_music_arrival(Ships[Objects[object_num].instance].team);
			}
		return object_num;
	}

	return -1;
}

// Goober5000
bool mission_maybe_make_ship_arrive(p_object *p_objp, bool force_arrival)
{
	if (p_objp->wingnum >= 0)
	{
		Warning(LOCATION, "Parse objects (%s) belonging to wings must arrive through the wing code!", p_objp->name);
		return false;
	}

	if (p_objp->created_object != nullptr)
	{
		Warning(LOCATION, "Cannot create a parse object (%s) more than once!", p_objp->name);
		return false;
	}

	// try to create ship
	int objnum = mission_did_ship_arrive(p_objp, force_arrival);
	if (objnum < 0)
		return false;

	if (p_objp == Arriving_support_ship)
		mission_parse_support_arrived(objnum);		// support ships have some unique housekeeping and are never on the arrival list
	else if (parse_object_on_arrival_list(p_objp))
		list_remove(&Ship_arrival_list, p_objp);	// remove from arrival list

	return true;
}

// Goober5000
void mission_parse_mark_non_arrival(p_object *p_objp)
{
	// mark the flag
    p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_arrive);
}

// Goober5000
void mission_parse_mark_non_arrival(wing *wingp)
{
	int wingnum = WING_INDEX(wingp);

	// look through all ships yet to arrive...
	for (p_object *p_objp = GET_FIRST(&Ship_arrival_list); p_objp != END_OF_LIST(&Ship_arrival_list); p_objp = GET_NEXT(p_objp))
	{
		// ...and mark the ones in this wing
        if (p_objp->wingnum == wingnum)
            p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_arrive);
	}	
}

/**
 * Set a flag on all parse objects on ship arrival list which cannot arrive in the mission
 */
void mission_parse_mark_non_arrivals()
{
	for (p_object *p_objp = GET_FIRST(&Ship_arrival_list); p_objp != END_OF_LIST(&Ship_arrival_list); p_objp = GET_NEXT(p_objp))
	{
		if (p_objp->wingnum != -1)
		{
			if (!object_is_docked(p_objp) && (Sexp_nodes[Wings[p_objp->wingnum].arrival_cue].value == SEXP_KNOWN_FALSE))
				p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_arrive);
		}
		else
		{
			if (Sexp_nodes[p_objp->arrival_cue].value == SEXP_KNOWN_FALSE)
				p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cannot_arrive);
		}
	}
}

/**
 * Deal with support ship arrival. This function can get called from either single or multiplayer.  Needed to that clients
 * can know when to abort rearm.
 *
 * @param objnum is the object number of the arriving support ship
 */
void mission_parse_support_arrived( int objnum )
{
	int i;

	// when the support ship arrives, the shipname it is supposed to repair is in the 'misc'
	// field of the parse object.  If the ship still exists, call ai function which actually
	// issues the goal for the repair
	for ( i = 0; i < Num_arriving_repair_targets; i++ ) {
		int shipnum;

		shipnum = ship_name_lookup( Arriving_repair_targets[i] );

		if ( shipnum != -1 ) {
			object *requester_objp, *support_objp;

			support_objp = &Objects[objnum];
			requester_objp = &Objects[Ships[shipnum].objnum];
			ai_add_rearm_goal( requester_objp, support_objp );
		}
	}

	Ships[Objects[objnum].instance].flags.set(Ship::Ship_Flags::Warped_support);

	Arriving_support_ship = NULL;
	Num_arriving_repair_targets = 0;
}

// Goober5000
int parse_object_on_arrival_list(p_object *pobjp)
{
	return (pobjp->next != NULL) && (pobjp->prev != NULL);
}

/**
 * Check the lists of arriving ships and wings, creating new ships/wings if the arrival criteria have been met
 */
void mission_eval_arrivals() {
	int rship = -1;

	// before checking arrivals, check to see if we should play a message concerning arrivals
	// of other wings.  We use the timestamps to delay the arrival message slightly for
	// better effect
	if (timestamp_valid(Arrival_message_delay_timestamp) && timestamp_elapsed(Arrival_message_delay_timestamp) && !MULTI_TEAM) {
		bool use_terran_cmd = !The_mission.flags[Mission::Mission_Flags::No_builtin_command] && (Command_announces_enemy_arrival_chance >= 0) && (frand() < Command_announces_enemy_arrival_chance);

		rship = ship_get_random_player_wing_ship(SHIP_GET_UNSILENCED);
		ship* subject = (Arrival_message_subject < 0) ? nullptr : &Ships[Arrival_message_subject];
		if ((rship < 0) || use_terran_cmd) {
			message_send_builtin(MESSAGE_ARRIVE_ENEMY, nullptr, subject, -1, -1);
		} else if (rship >= 0) {
			message_send_builtin(MESSAGE_ARRIVE_ENEMY, &Ships[rship], subject, -1, -1);
		}

		Arrival_message_delay_timestamp = timestamp(-1);		// make the stamp invalid
		Arrival_message_subject = -1;
	}

	// check the arrival list
	// Goober5000 - we can't run through the list the usual way because we might
	// remove a bunch of objects and completely screw up the list linkage
	for (SCP_vector<p_object>::iterator ii = Parse_objects.begin(); ii != Parse_objects.end(); ++ii) {
		p_object* pobjp = &(*ii);

		// make sure we're on the arrival list
		if (!parse_object_on_arrival_list(pobjp)) {
			continue;
		}

		// if this object has a wing, don't create it -- let code for wings determine if it should be created
		if (pobjp->wingnum >= 0) {
			continue;
		}

		// make it arrive
		mission_maybe_make_ship_arrive(pobjp);
	}

	// check the support ship arrival list
	if (Arriving_support_ship) {
		// make it arrive (support ships are not put on the arrival list)
		mission_maybe_make_ship_arrive(Arriving_support_ship);
	}

	// we must also check to see if there are waves of a wing that must
	// reappear if all the ships of the current wing have been destroyed or
	// have departed. If this is the case, then create the next wave.
	for (int i = 0; i < Num_wings; i++) {
		// make it arrive
		mission_maybe_make_wing_arrive(i);
	}
}

bool mission_maybe_make_wing_arrive(int wingnum, bool force_arrival)
{
	int rship = -1;
	auto wingp = &Wings[wingnum];

	// should we process this wing anymore
	if (wingp->flags[Ship::Wing_Flags::Gone])
		return false;

	// if we have a reinforcement wing, then don't try to create new ships automatically.
	if (wingp->flags[Ship::Wing_Flags::Reinforcement])
	{
		// check to see in the wings arrival cue is true, and if so, then mark the reinforcement
		// as available
		if (force_arrival || eval_sexp(wingp->arrival_cue))
			mission_parse_mark_reinforcement_available(wingp->name);

		// if we're forcing the arrival, then "use" the reinforcement; otherwise don't process anything else
		if (force_arrival && wingp->current_count == 0) {
			int i = find_item_with_string(Reinforcements, &reinforcements::name, wingp->name);
			if (i >= 0) {
				Reinforcements[i].num_uses++;
			}
		} else {
			// reinforcement wings skip the rest of the function
			return false;
		}
	}
		
	// don't do evaluations for departing wings
	if (wingp->flags[Ship::Wing_Flags::Departing])
		return false;

	// must check to see if we are at the last wave.  Code above to determine when a wing is gone only
	// gets run when a ship is destroyed (not every N seconds like it used to).  Do a quick check here.
	if (wingp->current_wave == wingp->num_waves)
		return false;

	// If the current wave of this wing is 0, then we haven't created the ships in the wing yet.
	// If the threshold of the wing has been reached, then we need to create more ships.
	bool is_first_wave = wingp->current_wave == 0;
	if (is_first_wave || (wingp->current_count <= wingp->threshold))
	{
		// Call parse_wing_create_ships to try and create it.  That function will eval the arrival
		// cue of the wing and create the ships if necessary.
		int created = parse_wing_create_ships(wingp, wingp->wave_count, false, force_arrival);

		// if we didn't create any ships, nothing more to do for this wing
		if (created <= 0)
			return false;

		// If this wing was a reinforcement wing, then we need to reset the reinforcement flag for the wing
		// so the user can call in another set if need be.
		if (wingp->flags[Ship::Wing_Flags::Reset_reinforcement])
		{
            wingp->flags.remove(Ship::Wing_Flags::Reset_reinforcement);
            wingp->flags.set(Ship::Wing_Flags::Reinforcement);
		}

		// probably send a message to the player when this wing arrives.
		// if no message, nothing more to do for this wing
		if (wingp->flags[Ship::Wing_Flags::No_arrival_message])
			return true;
		if (wingp->flags[Ship::Wing_Flags::No_first_wave_message] && is_first_wave)
			return true;

		// multiplayer team vs. team
		if(MULTI_TEAM)
		{
			// send a hostile wing arrived message
			rship = wingp->ship_index[wingp->special_ship];

			int multi_team_filter = Ships[rship].team;

			// there are two timestamps at work here.  One to control how often the player receives
			// messages about incoming hostile waves, and the other to control how long after
			// the wing arrives does the player actually get the message.
			if (timestamp_elapsed(Allow_arrival_message_timestamp_m[multi_team_filter]))
			{
				if (!timestamp_valid(Arrival_message_delay_timestamp_m[multi_team_filter]))
				{
					Arrival_message_delay_timestamp_m[multi_team_filter] = timestamp_rand(ARRIVAL_MESSAGE_DELAY_MIN, ARRIVAL_MESSAGE_DELAY_MAX);
				}
				Allow_arrival_message_timestamp_m[multi_team_filter] = timestamp(Builtin_messages[MESSAGE_ARRIVE_ENEMY].min_delay);
						
				// send to the proper team
				message_send_builtin(MESSAGE_ARRIVE_ENEMY, nullptr, nullptr, -1, multi_team_filter);
			}
		}
		// does the player attack this ship?
		else if (iff_x_attacks_y(Player_ship->team, Ships[wingp->ship_index[0]].team))
		{
			// there are two timestamps at work here.  One to control how often the player receives
			// messages about incoming hostile waves, and the other to control how long after
			// the wing arrives does the player actually get the message.
			if (timestamp_elapsed(Allow_arrival_message_timestamp))
			{
				if (!timestamp_valid(Arrival_message_delay_timestamp))
				{
					Arrival_message_delay_timestamp = timestamp_rand(ARRIVAL_MESSAGE_DELAY_MIN, ARRIVAL_MESSAGE_DELAY_MAX);
					Arrival_message_subject = wingp->ship_index[0];
				}
				Allow_arrival_message_timestamp = timestamp(Builtin_messages[MESSAGE_ARRIVE_ENEMY].min_delay);
			}
		}
		// everything else
		else if (timestamp_elapsed(Allow_backup_message_timestamp)) {
			rship = ship_get_random_ship_in_wing(wingnum, SHIP_GET_UNSILENCED);
			if (rship >= 0) {
				auto sent = message_send_builtin(MESSAGE_BACKUP, &Ships[rship], nullptr, -1, -1);
				if (sent) {
					Allow_backup_message_timestamp = timestamp(Builtin_messages[MESSAGE_BACKUP].min_delay);
				}
			}
		}

		return true;
	}

	return false;
}


/**
 * Called to make object objp depart.  Rewritten and expanded by Goober5000.
 */
int mission_do_departure(object *objp, bool goal_is_to_warp)
{
	Assert(objp->type == OBJ_SHIP);
	bool beginning_departure;
	DepartureLocation location;
	anchor_t anchor;
	int path_mask;
	ship *shipp = &Ships[objp->instance];
	ai_info *aip = &Ai_info[shipp->ai_index];

	// this function is often called many times in a row for ships that are in the first stage of departing,
	// but some things should only be done when departure first starts
	if (aip->mode == AIM_WARP_OUT || shipp->is_departing())
	{
		beginning_departure = false;
	}
	else
	{
		beginning_departure = true;
		mprintf(("Entered mission_do_departure() for %s\n", shipp->ship_name));

	if (OnDepartureStartedHook->isActive())
	{
		// add scripting hook for 'On Departure Started' --wookieejedi
		// hook is placed at the beginning of this function to allow the scripter to
		// actually have access to the ship's departure decisions before they are all executed
		OnDepartureStartedHook->run(scripting::hooks::ShipDepartConditions{ shipp },
			scripting::hook_param_list(scripting::hook_param("Self", 'o', objp), scripting::hook_param("Ship", 'o', objp)));
	}

		// abort rearm, because if we entered this function we're either going to depart via hyperspace, depart via bay,
		// or revert to our default behavior
		ai_abort_rearm_request(objp);
	}

	// if our current goal is to warp, then we won't consider departing to a bay, because the goal explicitly says to warp out
	// (this sort of goal can be assigned in FRED, either in the ship's initial orders or as the ai-warp-out goal)
	if (goal_is_to_warp)
	{
		// aha, but not if we were ORDERED to depart, because the comms menu ALSO uses the goal code, and yet the comms menu means any departure method!
		if ((shipp->flags[Ship::Ship_Flags::Departure_ordered]) || ((shipp->wingnum >= 0) && (Wings[shipp->wingnum].flags[Ship::Wing_Flags::Departure_ordered])))
		{
			mprintf(("Looks like we were ordered to depart; initiating the standard departure logic\n"));
		}
		// since our goal is to warp, then if we can warp, jump directly to the warping part
		else if (ship_can_warp_full_check(shipp))
		{
			mprintf(("Our current goal is to warp!  Trying to warp...\n"));
			goto try_to_warp;
		}
		// otherwise, since we can't warp, we'll do the standard bay departure check, etc.
	}

	// if this ship belongs to a wing, then use the wing departure information
	if (beginning_departure && shipp->wingnum >= 0)
	{
		wing *wingp = &Wings[shipp->wingnum];

		// copy the wing's departure information to the ship
		// (needed because the bay departure code will check the ship's information again later on)
		shipp->departure_location = wingp->departure_location;
		shipp->departure_anchor = wingp->departure_anchor;
		shipp->departure_path_mask = wingp->departure_path_mask;
	}
	
	location = shipp->departure_location;
	anchor = shipp->departure_anchor;
	path_mask = shipp->departure_path_mask;

	// if departing to a docking bay, try to find the anchor ship to depart to.  If not found, then
	// just make it warp out like anything else.
	if (location == DepartureLocation::TO_DOCK_BAY)
	{
		auto anchor_ship_entry = ship_registry_get(anchor);

		// see if ship is yet to arrive.  If so, then warp.
		if (!anchor_ship_entry || anchor_ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		{
			mprintf(("Anchor ship %s hasn't arrived yet!  Trying to warp...\n", anchor_ship_entry ? anchor_ship_entry->name : "<unknown>"));
			goto try_to_warp;
		}

		// see if ship is in mission.  If not, then we can assume it was destroyed or departed since
		// it is not on the arrival list (as shown by above if statement).
		if (!anchor_ship_entry->has_shipp())
		{
			mprintf(("Anchor ship %s not found!  Trying to warp...\n", anchor_ship_entry->name));
			goto try_to_warp;
		}

		// see if we can actually depart to the ship
		if (!ship_useful_for_departure(anchor_ship_entry->shipnum, shipp->departure_path_mask))
		{
			mprintf(("Anchor ship %s not suitable for departure (dying, departing, bays destroyed, etc.).  Trying to warp...\n", anchor_ship_entry->name));
			goto try_to_warp;
		}

		// find a path
		if (ai_acquire_depart_path(objp, anchor_ship_entry->objnum, path_mask) >= 0)
		{
			MONITOR_INC(NumShipDepartures,1);

			mprintf(("Acquired departure path\n"));
			return 1;
		}
	}

try_to_warp:

	// make sure we can actually warp
	if (ship_can_warp_full_check(shipp))
	{
		if (aip->mode != AIM_WARP_OUT)
		{
			mprintf(("Setting mode to warpout\n"));

			ai_set_mode_warp_out(objp, aip);
			MONITOR_INC(NumShipDepartures, 1);
		}

		return 1;
	}
	// find something else to do
	else
	{
		// NOTE: this point should no longer be reached in the standard goal code, since the goal or comm order must be achievable
		// for this function to be called.  This point should only be reached if the ship has no subspace drive, AND either has no
		// mothership assigned (or departs to hyperspace) or the mothership was destroyed, AND falls into one of the following cases:
		// 1) The ship's departure cue evaluates to true in the mission
		// 2) A support ship has had its hull fall to 25% when it has no repair targets
		// 3) A fighter or bomber with an IFF that doesn't allow support ships has its warp_out_timestamp elapse (but this seems to not be a possibility anymore)
		// 4) An instructor in a training mission has been fired upon
		mprintf(("Can't warp!  Doing something else instead.\n"));

        shipp->flags.remove(Ship::Ship_Flags::Depart_dockbay);
        shipp->flags.remove(Ship::Ship_Flags::Depart_warp);
		ai_do_default_behavior(objp);

		return 0;
	}
}

/**
 * Put here because mission_eval_arrivals is here.
 * @todo Might move these to a better location later -- MWA
 */
void mission_eval_departures()
{
	int i, j;
	object *objp;
	wing *wingp;

	// scan through the active ships an evaluate their departure cues.  For those
	// ships whose time has come, set their departing flag.

	for (auto so: list_range(&Ship_obj_list)) {
		objp = &Objects[so->objnum];
		if (objp->flags[Object::Object_Flags::Should_be_dead])
			continue;
		if (objp->type == OBJ_SHIP) {
			ship	*shipp;

			Assert((objp->instance >= 0) && (objp->instance < MAX_SHIPS));

			shipp = &Ships[objp->instance];
			
			// don't process a ship that is already departing or dying or disabled
			// AL 12-30-97: Added SF_CANNOT_WARP to check
			// Goober5000 - fixed so that it WILL eval when SF_CANNOT_WARP if departing to dockbay
			// wookieejedi - fixed so it accounts for break and never warp too
			if ( shipp->is_dying_or_departing() || ( !(ship_can_warp_full_check(shipp)) && (shipp->departure_location != DepartureLocation::TO_DOCK_BAY)) || ship_subsys_disrupted(shipp, SUBSYSTEM_ENGINE) ) {
				continue;
			}

			// don't process ships that are part of a wing -- handled in seperate case
			if ( shipp->wingnum != -1 )
				continue;

			// when the departure cue becomes true, set off the departure delay timer.  We store the
			// timer as -seconds in FreeSpace which indicates that the timer has not been set.  If the timer
			// is not set, then turn it into a valid timer and keep evaluating the timer until it is elapsed
			if ( eval_sexp(shipp->departure_cue) ) {
				if ( shipp->departure_delay <= 0 )
					shipp->departure_delay = timestamp(-shipp->departure_delay * 1000 );
				if ( timestamp_elapsed(shipp->departure_delay) )
					mission_do_departure( objp );
			}
		}
	}

	// now scan through the list of wings and check their departure cues.  For wings with
	// that cue being true, we must update internal variables to indicate that the wing is
	// departed and that no further waves of this wing will appear

	for ( i = 0; i < Num_wings; i++ ) {
		wingp = &Wings[i];

		// should we process this wing anymore
		if ( wingp->flags[Ship::Wing_Flags::Departing] )
			continue;

		// evaluate the sexpression.  If true, mark all the ships in this wing as departing and increment
		// the num departed in the wing structure.  Then add number of remaining waves * ships/wave to
		// departed count to get total count of ships in the wing which departed.  (We are counting ships
		// that have not yet arrived as departed if they never arrive -- this may be bad, but for some reason
		// seems like the right thing to do).

		if ( eval_sexp(wingp->departure_cue) ) {
			// if we haven't set up the departure timer yet (would be <= 0) setup the timer to pop N seconds
			// later
			if ( wingp->departure_delay <= 0 )
				wingp->departure_delay = timestamp( -wingp->departure_delay * 1000 );
			if ( !timestamp_elapsed(wingp->departure_delay) )
				continue;

            wingp->flags.set(Ship::Wing_Flags::Departing);
			for ( j = 0; j < wingp->current_count; j++ ) {
				ship *shipp;

				shipp = &Ships[wingp->ship_index[j]];
				if ( (shipp->is_departing()) || (shipp->flags[Ship::Ship_Flags::Dying]) )
					continue;

				Assert ( shipp->objnum != -1 );
				objp = &Objects[shipp->objnum];

				mission_do_departure( objp );
				// don't add to wingp->total_departed here -- this is taken care of in ship code.
			}
		}
	}
}

/**
 * Called from high level game loop to do mission evaluation stuff
 */
void mission_parse_eval_stuff()
{
	mission_eval_arrivals();
	mission_eval_departures();
}

int allocate_subsys_status()
{
	int i;
	// set primary weapon ammunition here, but does it actually matter? - Goober5000

	Assert(Subsys_index >= 0);

	// we allocate in blocks of MIN_SUBSYS_STATUS_SIZE so if we need more then make more
	if ( (Subsys_status == NULL) || (Subsys_index >= (Subsys_status_size - 1)) ) {
		Assert( MIN_SUBSYS_STATUS_SIZE > 0 );

		Subsys_status_size += MIN_SUBSYS_STATUS_SIZE;
		Subsys_status = (subsys_status*)vm_realloc(Subsys_status, sizeof(subsys_status) * Subsys_status_size );
	}

	Verify( Subsys_status != NULL );

	// the memset is redundant to the below assignments
	//memset( &Subsys_status[Subsys_index], 0, sizeof(subsys_status) );

	Subsys_status[Subsys_index].name[0] = '\0';

	Subsys_status[Subsys_index].percent = 0.0f;

	Subsys_status[Subsys_index].primary_banks[0] = SUBSYS_STATUS_NO_CHANGE;
	Subsys_status[Subsys_index].primary_ammo[0] = 100; // *
	
	for (i=1; i<MAX_SHIP_PRIMARY_BANKS; i++)
	{
		Subsys_status[Subsys_index].primary_banks[i] = -1;  // none
		Subsys_status[Subsys_index].primary_ammo[i] = 100;	// *
	}

	Subsys_status[Subsys_index].secondary_banks[0] = SUBSYS_STATUS_NO_CHANGE;
	Subsys_status[Subsys_index].secondary_ammo[0] = 100;
	
	for (i=1; i<MAX_SHIP_SECONDARY_BANKS; i++)
	{
		Subsys_status[Subsys_index].secondary_banks[i] = -1;
		Subsys_status[Subsys_index].secondary_ammo[i] = 100;
	}

	Subsys_status[Subsys_index].ai_class = SUBSYS_STATUS_NO_CHANGE;

	Subsys_status[Subsys_index].subsys_cargo_name = 0;	// "Nothing"
	Subsys_status[Subsys_index].subsys_cargo_title[0] = '\0';

	return Subsys_index++;
}

// Goober5000
int insert_subsys_status(p_object *pobjp)
{
	int i, new_index;

	// this is not good; we have to allocate another slot, but then bump all the
	// slots upward so that this particular parse object's subsystems are contiguous
	new_index = allocate_subsys_status();

	// only bump the subsystems if this isn't the very last subsystem
	if (new_index != pobjp->subsys_index + pobjp->subsys_count)
	{
		// copy the new blank entry for future reference
		subsys_status temp_entry;
		memcpy(&temp_entry, &Subsys_status[new_index], sizeof(subsys_status));

		// shift elements upward
		for (i = Subsys_index - 1; i > (pobjp->subsys_index + pobjp->subsys_count); i--)
		{
			memcpy(&Subsys_status[i], &Subsys_status[i-1], sizeof(subsys_status));
		}

		// correct the index so that the new subsystem belongs to the proper p_object
		new_index = pobjp->subsys_index + pobjp->subsys_count;

		// put the blank entry in the p_object
		memcpy(&Subsys_status[new_index], &temp_entry, sizeof(subsys_status));
	}

	// make the p_object aware of its new subsystem
	pobjp->subsys_count++;

	// we also have to adjust all the indexes in existing parse objects
	// (each p_object's subsys_index points to subsystem 0 in its list)
	for (SCP_vector<p_object>::iterator ii = Parse_objects.begin(); ii != Parse_objects.end(); ++ii)
	{
		// bump up base index to accommodate inserted subsystem
		if (ii->subsys_index >= new_index)
			ii->subsys_index++;
	}

	return new_index;
}

// Goober5000
subsys_status *parse_get_subsys_status(p_object *pobjp, const char *subsys_name)
{
	int i;
	subsys_status *sssp;

	for (i = 0; i < pobjp->subsys_count; i++)
	{
		sssp = &Subsys_status[pobjp->subsys_index + i];

		if (!subsystem_stricmp(sssp->name, subsys_name))
			return sssp;
	}

	return NULL;
}

// find (or add) the name in the list and return an index to it.
int get_parse_name_index(const char *name)
{
	int i, count = static_cast<int>(Parse_names.size());

	for (i=0; i<count; ++i)
		if (!stricmp(name, Parse_names[i].c_str()))
			return i;

	Assert(strlen(name) < NAME_LENGTH);
	Parse_names.push_back(name);
	return i;
}

// Goober5000
int add_path_restriction()
{
	int i, j;

	// parse it
	path_restriction_t temp;
	temp.cached_mask = (1 << MAX_SHIP_BAY_PATHS);	// uninitialized value (too high)
	temp.num_paths = (int)stuff_string_list(temp.path_names, MAX_SHIP_BAY_PATHS);

	// no restriction?
	if (temp.num_paths == 0)
		return -1;

	// first, see if it's duplicated anywhere
	for (i = 0; i < Num_path_restrictions; i++)
	{
		// must have same number of allowed paths
		if (temp.num_paths != Path_restrictions[i].num_paths)
			continue;

		// see if path names match
		for (j = 0; j < temp.num_paths; j++)
		{
			// no match, so skip this
			if (stricmp(temp.path_names[j], Path_restrictions[i].path_names[j]) != 0)
				goto continue_outer_loop;
		}

		// match!
		return i;

continue_outer_loop:
		;
	}

	// no match, so add a new restriction

	// check limit
	if (Num_path_restrictions >= MAX_PATH_RESTRICTIONS)
	{
		Warning(LOCATION, "Maximum number of path restrictions reached");
		return -1;
	}

	// add this restriction at the new index
	int index = Num_path_restrictions++;
	Path_restrictions[index] = temp;

	return index;
}

/**
 * Look for \<any friendly\>, \<any hostile player\>, etc.
 */
anchor_t get_special_anchor(const char *name)
{
	char tmp[NAME_LENGTH + 15];
	const char *iff_name;
	int iff_index;
	
	if (strnicmp(name, "<any ", 5) != 0)
		return anchor_t::invalid();

	strcpy_s(tmp, name+5);
	iff_name = strtok(tmp, " >");
	if (iff_name == nullptr)
		return anchor_t::invalid();

	// hack substitute "hostile" for "enemy"
	if (!stricmp(iff_name, "enemy"))
		iff_name = "hostile";

	iff_index = iff_lookup(iff_name);
	if (iff_index < 0)
		return anchor_t::invalid();

	// restrict to players?
	if (stristr(name+5, "player") != NULL)
		return anchor_t(iff_index | ANCHOR_SPECIAL_ARRIVAL | ANCHOR_SPECIAL_ARRIVAL_PLAYER);
	else
		return anchor_t(iff_index | ANCHOR_SPECIAL_ARRIVAL);
}

anchor_t get_anchor(const char *name)
{
	auto special_anchor = get_special_anchor(name);

	if (special_anchor.isValid())
		return special_anchor;

	return anchor_t(get_parse_name_index(name) | ANCHOR_IS_PARSE_NAMES_INDEX);
}

/**
 * See if an arrival/departure anchor is missing a hangar bay.  If it is, the message parameter will be populated with an appropriate error.
 */
void check_anchor_for_hangar_bay(SCP_string &message, SCP_set<anchor_t> &anchors_checked, anchor_t anchor, const char *other_name, bool other_is_ship, bool is_arrival)
{
	message.clear();

	if (anchors_checked.contains(anchor))
		return;
	anchors_checked.insert(anchor);

	auto anchor_ship_entry = ship_registry_get(anchor);
	if (anchor_ship_entry)
	{
		// Load the anchor ship model with subsystems and all; it'll need to be done for this mission anyway
		auto anchor_sip = anchor_ship_entry->sip();
		anchor_sip->model_num = model_load(anchor_sip->pof_file, anchor_sip);

		// Check if this model has a hangar bay
		if (!model_has_hangar_bay(anchor_sip->model_num))
		{
			sprintf(message, "%s (%s) is used as a%s anchor by %s %s (and possibly elsewhere too), but it does not have a hangar bay!", anchor_ship_entry->name,
				anchor_sip->name, is_arrival ? "n arrival" : " departure", other_is_ship ? "ship" : "wing", other_name);
		}
	}
};

/**
 * Fixup the goals/ai references for player objects in the mission
 */
void mission_parse_fixup_players()
{
	for (auto so: list_range(&Ship_obj_list)) {
		auto objp = &Objects[so->objnum];
		if (objp->flags[Object::Object_Flags::Should_be_dead])
			continue;

		if ( (objp->type == OBJ_SHIP) && (objp->flags[Object::Object_Flags::Player_ship]) ) {
			game_busy( NOX("** fixing up player/ai stuff **") );	// animate the loading screen, doesn't nothing if the screen is not active
			ai_clear_ship_goals( &Ai_info[Ships[objp->instance].ai_index] );
			init_ai_object( OBJ_INDEX(objp) );
		}
	}
}

// code to warp in a new support ship.  It works by finding the average position of all ships
// in the mission, creating a vector from that position to the player, and scaling out behind the
// player some distance.  Should be sufficient.

#define WARP_IN_MIN_DISTANCE	1000.0f
#define WARP_IN_TIME_MIN		3000				// warps in min 3 seconds later
#define WARP_IN_TIME_MAX		6000				// warps in max 6 seconds later

/**
 * Adds requester_objp onto the queue of ships for the arriving support ship to service
 */
void mission_add_to_arriving_support( object *requester_objp )
{
	int i;
	ship *shipp;

	Assert ( Arriving_support_ship );

	if ( Num_arriving_repair_targets == MAX_AI_GOALS ) {
		mprintf(("Reached MAX_AI_GOALS trying to add repair request!\n"));
		return;
	}

	shipp = &Ships[requester_objp->instance];
	// check for duplicates before adding
	for (i = 0; i < Num_arriving_repair_targets; i++ ) {
		if ( !stricmp(Arriving_repair_targets[i], shipp->ship_name) ){
			break;
		}
	}
	if ( i != Num_arriving_repair_targets ){		// found the ship before reaching the end -- ignore it!
		return;
	}

	strcpy_s( Arriving_repair_targets[Num_arriving_repair_targets], Ships[requester_objp->instance].ship_name );
	Num_arriving_repair_targets++;

	if ( MULTIPLAYER_MASTER ){
		multi_maybe_send_repair_info( requester_objp, NULL, REPAIR_INFO_WARP_ADD );
	}	
}

extern int pp_collide_any(vec3d *curpos, vec3d *goalpos, float radius, object *ignore_objp1, object *ignore_objp2, int big_only_flag);

/**
 * Set the warp in position for a support ship relative to an object.
 * Caller tries several positions, passing vector in x, y, z.
 */
int get_warp_in_pos(vec3d *pos, object *objp, float x, float y, float z)
{
	float	rand_val;

	if ( Game_mode & GM_NORMAL )
		rand_val = frand();
	else
		rand_val = static_randf(objp->net_signature);

	rand_val = 1.0f + (rand_val - 0.5f)*0.2f;

	*pos = objp->pos;

	vm_vec_scale_add2( pos, &objp->orient.vec.rvec, x*rand_val*800.0f);
	vm_vec_scale_add2( pos, &objp->orient.vec.uvec, y*rand_val*800.0f);
	vm_vec_scale_add2( pos, &objp->orient.vec.fvec, z*rand_val*800.0f);

	return pp_collide_any(&objp->pos, pos, objp->radius, objp, NULL, 1);
}

/**
 * Modified by Goober5000 to allow more flexibility in support ships
 */
void mission_bring_in_support_ship( object *requester_objp )
{
	vec3d center, warp_in_pos;
	p_object *pobj;
	ship *requester_shipp;
	int i;

	Assert ( requester_objp->type == OBJ_SHIP );
	requester_shipp = &Ships[requester_objp->instance];	//	MK, 10/23/97, used to be ->type, bogus, no?

	// if the support ship is already arriving, add the requester to the list
	if ( Arriving_support_ship ) {
		mission_add_to_arriving_support( requester_objp );
		return;
	}

	// If the support ship class was set via SEXP, use it. Otherwise, look it up by the requester's species.
	int ship_class = The_mission.support_ships.ship_class;
	if (ship_class < 0) {
		int requester_species = Ship_info[requester_shipp->ship_info_index].species;
		ship_class = Species_info[requester_species].support_ship_index;
		if ( ship_class < 0 ) {
			Warning(LOCATION, "Couldn't determine the support ship class to bring in\n");
			return;
		}
	}
	
	// create a parse object, and put it onto the ship arrival list.  This whole thing kind of stinks.
	// I want to put it into a parse object since it needs to arrive just a little later than
	// this function is called.  I have to make some assumptions in the code about values for the parse
	// object since I'm no longer working with a mission file.  These exceptions will be noted with
	// comments

	Support_ship_pobj = p_object();		// get a fresh p_object with default fields
	Arriving_support_ship = &Support_ship_pobj;
	pobj = Arriving_support_ship;
	pobj->ship_class = ship_class;
	auto sip = &Ship_info[ship_class];

	// initialize class-specific fields
	pobj->ai_class = sip->ai_class;
	pobj->warpin_params_index = sip->warpin_params_index;
	pobj->warpout_params_index = sip->warpout_params_index;
	pobj->ship_max_shield_strength = sip->max_shield_strength;
	pobj->ship_max_hull_strength = sip->max_hull_strength;
	Assert(pobj->ship_max_hull_strength > 0.0f);	// Goober5000: div-0 check (not shield because we might not have one)
	pobj->max_shield_recharge = sip->max_shield_recharge;
	pobj->replacement_textures = sip->replacement_textures;	// initialize our set with the ship class set, which may be empty
	pobj->score = sip->score;

	// get average position of all ships
	obj_get_average_ship_pos( &center );
	vm_vec_sub( &warp_in_pos, &center, &(requester_objp->pos) );

	//	Choose position to warp in ship.
	//	Temporary, but changed by MK because it used to be exactly behind the player.
	//	This could cause an Assert if the player immediately targeted it (before moving).
	//	Tend to put in front of the player to aid him in flying towards the ship.

	if (!get_warp_in_pos(&warp_in_pos, requester_objp, 1.0f, 0.1f, 1.0f))
		if (!get_warp_in_pos(&warp_in_pos, requester_objp, 1.0f, 0.2f, -1.0f))
			if (!get_warp_in_pos(&warp_in_pos, requester_objp, -1.0f, -0.2f, -1.0f))
				if (!get_warp_in_pos(&warp_in_pos, requester_objp, -1.0f, -0.1f, 1.0f))
					get_warp_in_pos(&warp_in_pos, requester_objp, 0.1f, 1.0f, 0.2f);

	// position for ship if it warps in
	pobj->pos = warp_in_pos;

	// tally the ship
	The_mission.support_ships.tally++;

	// create a name for the ship.  use "Support #".  look for collisions until one isn't found anymore
	i = 1;
	do {
		sprintf(pobj->name, NOX("Support %d"), i);
		if ( (ship_name_lookup(pobj->name) == -1) && (ship_find_exited_ship_by_name(pobj->name) == -1) )
			break;
		i++;
	} while(true);

	// create a ship registry entry for the support ship
	{
		ship_registry_entry entry(pobj->name);
		entry.status = ShipStatus::NOT_YET_PRESENT;
		entry.pobj_num = -1;	// since it's not in Parse_objects

		Ship_registry.push_back(entry);
		Ship_registry_map[pobj->name] = static_cast<int>(Ship_registry.size() - 1);
	}

	pobj->team = requester_shipp->team;

	// We will put the requester object shipname in repair target array and then take
	// care of setting up the goal when creating the ship!!!!
	Num_arriving_repair_targets = 0;
	mission_add_to_arriving_support( requester_objp );

	// Goober5000 - take some stuff from mission flags
	pobj->arrival_location = The_mission.support_ships.arrival_location;
	pobj->arrival_anchor = The_mission.support_ships.arrival_anchor;
	pobj->departure_location = The_mission.support_ships.departure_location;
	pobj->departure_anchor = The_mission.support_ships.departure_anchor;

	pobj->arrival_delay = timestamp_rand(WARP_IN_TIME_MIN, WARP_IN_TIME_MAX);
	pobj->arrival_cue = Locked_sexp_true;
	pobj->departure_cue = Locked_sexp_false;

	pobj->initial_velocity = 100;		// start at 100% velocity

    if (Player_obj->flags[Object::Object_Flags::No_shields])  {
		pobj->flags.set(Mission::Parse_Object_Flags::OF_No_shields);	// support ships have no shields when player has not shields
	}

	pobj->net_signature = multi_assign_network_signature(MULTI_SIG_SHIP);
}

/**
 * Returns true if a support ship is currently in the process of warping in.
 */
int mission_is_support_ship_arriving()
{
	if ( Arriving_support_ship )
		return 1;
	else
		return 0;
}

/**
 * Returns true if the given ship is scheduled to be repaired by the arriving support ship
 */
int mission_is_repair_scheduled( object *objp )
{
	char *name;
	int i;

	if ( !Arriving_support_ship )
		return 0;

	Assert ( objp->type == OBJ_SHIP );
	name = Ships[objp->instance].ship_name;
	for (i = 0; i < Num_arriving_repair_targets; i++ ) {
		if ( !strcmp( name, Arriving_repair_targets[i]) )
			return 1;
	}

	return 0;
}

/**
 * Removed the given ship from the list of ships that are to get repair by arriving support ship
 */
int mission_remove_scheduled_repair( object *objp )
{
	char *name;
	int i, index;

	if ( !Arriving_support_ship )
		return 0;

	// itereate through the target list looking for this ship name.  If not found, we
	// can simply return.
	Assert ( objp->type == OBJ_SHIP );
	name = Ships[objp->instance].ship_name;
	for (index = 0; index < Num_arriving_repair_targets; index++ ) {
		if ( !strcmp( name, Arriving_repair_targets[index]) )
			break;
	}
	if ( index == Num_arriving_repair_targets )
		return 0;

	// ship is found -- compress the array
	for ( i = index; i < Num_arriving_repair_targets - 1; i++ )
		strcpy_s( Arriving_repair_targets[i], Arriving_repair_targets[i+1] );

	Num_arriving_repair_targets--;

	if ( MULTIPLAYER_MASTER )
		multi_maybe_send_repair_info( objp, NULL, REPAIR_INFO_WARP_REMOVE );

	return 1;
}

/**
 * Alternate name stuff
 */
int mission_parse_lookup_alt(const char *name)
{
	int idx;

	// sanity
	if(name == NULL)
		return -1;

	// lookup
	for(idx=0; idx<Mission_alt_type_count; idx++)
	{
		if(!strcmp(Mission_alt_types[idx], name))
			return idx;
	}

	// could not find
	return -1;
}

static int mission_parse_lookup_alt_index_warn = 1;
const char *mission_parse_lookup_alt_index(int index)
{
	if((index < 0) || (index >= Mission_alt_type_count))
	{
		if (mission_parse_lookup_alt_index_warn)
		{
			Warning(LOCATION, "Ship with invalid alt_name.  Get a programmer");
			mission_parse_lookup_alt_index_warn = 0;
		}
		return "";
	}

	// get it
	return Mission_alt_types[index];
}

int mission_parse_add_alt(const char *name)
{
	// sanity
	if(name == NULL)
		return -1;

	// maybe add
	if(Mission_alt_type_count < MAX_ALT_TYPE_NAMES)
	{
		// stuff the name
		strcpy_s(Mission_alt_types[Mission_alt_type_count++], name);

		// done
		return Mission_alt_type_count - 1;
	}

	return -1;
}

void mission_parse_remove_alt(const char *name)
{
	// sanity
	if(name == NULL)
		return;

	// maybe remove
	for (int i = 0; i < Mission_alt_type_count; ++i)
	{
		if (!strcmp(Mission_alt_types[i], name))
		{
			// remove this name by overwriting it with the last name
			if (i < Mission_alt_type_count - 1)
				strcpy_s(Mission_alt_types[i], Mission_alt_types[Mission_alt_type_count - 1]);

			Mission_alt_type_count--;
			break;
		}
	}
}

void mission_parse_reset_alt()
{
	Mission_alt_type_count = 0;
}

// For compatibility purposes some mods want alt names to truncate at the hash symbol.  But we can't actually do that at mission load,
// since we have to save them again in FRED.  So this function processes the names just before the mission starts.
// To further complicate things, some mods actually want the hash to be displayed.  So this function uses the double-hash as an escape sequence for a single hash.
void mission_process_alt_types()
{
	for (int i = 0; i < Mission_alt_type_count; ++i)
	{
		// truncate at a single hash
		end_string_at_first_hash_symbol(Mission_alt_types[i], true);

		// ## -> #
		consolidate_double_characters(Mission_alt_types[i], '#');
	}
}

/**
 * Callsign stuff
 */
int mission_parse_lookup_callsign(const char *name)
{
	int idx;

	// sanity
	if(name == NULL)
		return -1;

	// lookup
	for(idx=0; idx<Mission_callsign_count; idx++)
	{
		if(!strcmp(Mission_callsigns[idx], name))
			return idx;
	}

	// could not find
	return -1;
}

static int mission_parse_lookup_callsign_index_warn = 1;
const char *mission_parse_lookup_callsign_index(int index)
{
	if((index < 0) || (index >= Mission_callsign_count))
	{
		if (mission_parse_lookup_callsign_index_warn)
		{
			Warning(LOCATION, "Ship with invalid callsign.  Get a programmer");
			mission_parse_lookup_callsign_index_warn = 0;
		}
		return "";
	}

	// get it
	return Mission_callsigns[index];
}

int mission_parse_add_callsign(const char *name)
{
	// sanity
	if(name == NULL)
		return -1;

	// maybe add
	if(Mission_callsign_count < MAX_CALLSIGNS)
	{
		// stuff the name
		strcpy_s(Mission_callsigns[Mission_callsign_count++], name);

		// done
		return Mission_callsign_count - 1;
	}

	return -1;
}

void mission_parse_remove_callsign(const char *name)
{
	// sanity
	if(name == NULL)
		return;

	// maybe remove
	for (int i = 0; i < Mission_callsign_count; ++i)
	{
		if (!strcmp(Mission_callsigns[i], name))
		{
			// remove this callsign by overwriting it with the last callsign
			if (i < Mission_callsign_count - 1)
				strcpy_s(Mission_callsigns[i], Mission_callsigns[Mission_callsign_count - 1]);

			Mission_callsign_count--;
			break;
		}
	}
}

void mission_parse_reset_callsign()
{
	Mission_callsign_count = 0;
}

int is_training_mission()
{
	return (The_mission.game_type & MISSION_TYPE_TRAINING);
}

/**
 * Go through all the displayed text in one section and fix the section and text delimiters should all be different
 */
void conv_fix_punctuation_section(char *str, const char *section_start, const char *section_end, const char *text_start,
								  const char *text_end)
{
	char *s1, *s2, *t1, *t2;

	s1 = strstr(str, section_start);
	s2 = strstr(s1, section_end);

	t1 = s1;
	
	while (1)
	{
		t1 = strstr(t1+1, text_start);
		if (!t1 || t1 > s2) return;

		t2 = strstr(t1, text_end);
		if (!t2 || t2 > s2) return;

		replace_all(t1, "\"", "$quote", PARSE_TEXT_SIZE - (str - Parse_text) - 1, (t2 - t1));
	}	
}
	
// Goober5000
void conv_fix_punctuation()
{
	// command briefings
	conv_fix_punctuation_section(Parse_text, "#Command Briefing", "#Briefing", "$Stage Text:", "$end_multi_text");

	// briefings
	conv_fix_punctuation_section(Parse_text, "#Briefing", "#Debriefing_info", "$multi_text", "$end_multi_text");

	// debriefings
	conv_fix_punctuation_section(Parse_text, "#Debriefing_info", "#Players", "$Multi text", "$end_multi_text");

	// messages
	conv_fix_punctuation_section(Parse_text, "#Messages", "#Reinforcements", "$Message:", "\n");
}

// Goober5000
void convertFSMtoFS2()
{
	// fix punctuation
	conv_fix_punctuation();
}

void clear_texture_replacements() 
{
	Fred_texture_replacements.clear();
}

bool check_for_23_3_data()
{
	auto e_list = ai_lua_get_general_orders(true);
	if (!e_list.empty())
		return true;
	auto v_list = ai_lua_get_general_orders(false, true);
	if (!v_list.empty())
		return true;

	for (const auto& msg : Messages)
	{
		if (!msg.note.empty())
			return true;
	}

	if (!The_mission.custom_data.empty()) {
		return true;
	}

	if (!The_mission.custom_strings.empty()) {
		return true;
	}

	for (const auto& so : list_range(&Ship_obj_list))
	{
		auto shipp = &Ships[Objects[so->objnum].instance];

		auto shipp_params = &Warp_params[shipp->warpin_params_index];
		auto sip_params = &Warp_params[Ship_info[shipp->ship_info_index].warpin_params_index];
		if (shipp_params->supercap_warp_physics != sip_params->supercap_warp_physics)
			return true;

		shipp_params = &Warp_params[shipp->warpout_params_index];
		sip_params = &Warp_params[Ship_info[shipp->ship_info_index].warpout_params_index];
		if (shipp_params->supercap_warp_physics != sip_params->supercap_warp_physics)
			return true;
	}

	for (int t = 0; t < Num_teams; t++) {
		if (Team_data[t].do_not_validate) {
			return true;
		}
	}

	return false;
}

bool check_for_24_1_data()
{
	for (int wingnum = 0; wingnum < Num_wings; wingnum++)
	{
		if (Wings[wingnum].arrival_location == ArrivalLocation::IN_BACK_OF_SHIP || Wings[wingnum].arrival_location == ArrivalLocation::ABOVE_SHIP || Wings[wingnum].arrival_location == ArrivalLocation::BELOW_SHIP
			|| Wings[wingnum].arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP || Wings[wingnum].arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP)
			return true;
	}

	for (const auto& so : list_range(&Ship_obj_list))
	{
		auto shipp = &Ships[Objects[so->objnum].instance];

		if (shipp->arrival_location == ArrivalLocation::IN_BACK_OF_SHIP || shipp->arrival_location == ArrivalLocation::ABOVE_SHIP || shipp->arrival_location == ArrivalLocation::BELOW_SHIP
			|| shipp->arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP || shipp->arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP)
			return true;

		if (shipp->cargo_title[0] != '\0')
			return true;
		for (const auto& ss : list_range(&shipp->subsys_list))
		{
			if (ss->subsys_cargo_title[0] != '\0')
				return true;
		}
	}

	return (Asteroid_field.debris_genre == DG_DEBRIS && !Asteroid_field.field_debris_type.empty()) ||
		   (Asteroid_field.debris_genre == DG_ASTEROID && !Asteroid_field.field_asteroid_type.empty());
}

bool check_for_24_3_data()
{
	for (int i = 0; i < Num_teams; i++) {
		for (int j = 0; j < Briefings[i].num_stages; j++) {
			if (!gr_compare_color_values(Briefings[i].stages[j].grid_color, Color_briefing_grid)) {
				return true;
			}
		}
	}
	return false;
}

bool check_for_25_1_data()
{
	if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default])
		return true;

	if (count_items_with_value(Props) > 0)
		return true;

	if (The_mission.flags[Mission::Mission_Flags::Fullneb] && !Neb2_fog_save_legacy_values)
		return true;

	if (The_mission.flags[Mission::Mission_Flags::Limited_support_rearm_pool]) {
		return true;
	}

	if (The_mission.support_ships.disallow_rearm || The_mission.support_ships.allow_rearm_weapon_precedence ||
		The_mission.support_ships.rearm_pool_from_loadout) {
		return true;
	}

	for (int team = 0; team < Num_teams; ++team) {
		for (int pool_wep = 0; pool_wep < MAX_WEAPON_TYPES; ++pool_wep) {
			if (The_mission.support_ships.rearm_weapon_pool[team][pool_wep] != -1) {
				return true;
			}
		}
	}

	constexpr auto defaultLayer = "Default";

	for (const auto& so : list_range(&Ship_obj_list))
	{
		auto shipp = &Ships[Objects[so->objnum].instance];
		if (!shipp->fred_layer.empty() && !lcase_equal(shipp->fred_layer, defaultLayer))
			return true;
	}

	for (const auto& wl : Waypoint_lists)
	{
		if (wl.get_no_draw_lines() || wl.get_has_custom_color())
			return true;
		const auto& layer = wl.get_fred_layer();
		if (!layer.empty() && !lcase_equal(layer, defaultLayer))
			return true;
	}

	if (std::any_of(Jump_nodes.begin(), Jump_nodes.end(), [defaultLayer](const auto& jn) {
		const auto& layer = jn.GetFredLayer();
		return !layer.empty() && !lcase_equal(layer, defaultLayer);
	}))
		return true;

	for (int wingnum = 0; wingnum < Num_wings; wingnum++)
	{
		if (Wings[wingnum].has_display_name())
			return true;
	}

	return false;
}
