/*
 * 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.
 *
*/

//	Parse a symbolic expression.
//	These are identical to Lisp functions.
//	It uses a very baggy format, allocating 16 characters per token, regardless
//	of how many are used.

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <cassert>
#include <climits>
#include <cstdint>

#include "ai/aigoals.h"
#include "ai/ailua.h"
#include "asteroid/asteroid.h"
#include "autopilot/autopilot.h"
#include "camera/camera.h"
#include "camera/photomode.h"
#include "cmdline/cmdline.h"
#include "debris/debris.h"
#include "debugconsole/console.h"
#include "fireball/fireballs.h"		// for explosion stuff
#include "freespace.h"
#include "gamesequence/gamesequence.h"
#include "gamesnd/eventmusic.h"		// for change-soundtrack
#include "gamesnd/gamesnd.h"
#include "globalincs/alphacolors.h"
#include "globalincs/linklist.h"
#include "globalincs/systemvars.h"
#include "globalincs/utility.h"
#include "globalincs/version.h"
#include "graphics/2d.h"
#include "graphics/font.h"
#include "graphics/light.h"
#include "hud/hud.h"
#include "hud/hudartillery.h"
#include "hud/hudescort.h"
#include "hud/hudets.h"
#include "hud/hudmessage.h"
#include "hud/hudparse.h"
#include "hud/hudshield.h"
#include "hud/hudsquadmsg.h"		// for the order sexp
#include "hud/hudwingmanstatus.h"
#include "iff_defs/iff_defs.h"
#include "io/keycontrol.h"
#include "io/timer.h"
#include "jumpnode/jumpnode.h"
#include "localization/localize.h"
#include "math/fvi.h"
#include "menuui/techmenu.h"		// for intel stuff
#include "mission/missionbriefcommon.h"
#include "mission/missioncampaign.h"
#include "mission/missiongoals.h"
#include "mission/missionlog.h"
#include "mission/missionmessage.h"
#include "mission/missionparse.h"		// for p_object definition
#include "mission/missiontraining.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_obj.h"
#include "network/multi_sexp.h"
#include "network/multi_team.h"
#include "network/multimsgs.h"
#include "network/multiutil.h"
#include "object/objcollide.h"
#include "object/objectdock.h"
#include "object/objectshield.h"
#include "object/objectsnd.h"
#include "object/waypoint.h"
#include "parse/generic_log.h"
#include "parse/parselo.h"
#include "scripting/scripting.h"
#include "parse/sexp.h"
#include "parse/sexp_container.h"
#include "playerman/player.h"
#include "prop/prop.h"
#include "render/3d.h"
#include "scripting/global_hooks.h"
#include "ship/afterburner.h"
#include "ship/awacs.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "ship/shiphit.h"
#include "ship/ship_flags.h"
#include "ship/subsysdamage.h"		// for sensor effect sexp
#include "sound/audiostr.h"
#include "sound/ds.h"
#include "sound/sound.h"
#include "utils/unicode.h"
#include "starfield/starfield.h"
#include "starfield/supernova.h"
#include "stats/medals.h"
#include "utils/Random.h"
#include "weapon/beam.h"
#include "weapon/emp.h"
#include "weapon/shockwave.h"
#include "weapon/weapon.h"

#include "parse/sexp/sexp_lookup.h"

#ifndef NDEBUG
#include "hud/hudmessage.h"
#endif

// Stupid windows workaround...
#ifdef MessageBox
#undef MessageBox
#endif

// legacy references
// --------------------------------------------------------------------------------
#define SEXP_CONDITIONAL_OPERATOR sexp_oper_type::CONDITIONAL
#define SEXP_ARGUMENT_OPERATOR    sexp_oper_type::ARGUMENT
#define SEXP_ACTION_OPERATOR      sexp_oper_type::ACTION
#define SEXP_ARITHMETIC_OPERATOR  sexp_oper_type::ARITHMETIC
#define SEXP_BOOLEAN_OPERATOR     sexp_oper_type::BOOLEAN
#define SEXP_INTEGER_OPERATOR     sexp_oper_type::INTEGER
#define SEXP_GOAL_OPERATOR        sexp_oper_type::GOAL

#define OSWPT_TYPE_NONE				oswpt_type::NONE
#define OSWPT_TYPE_SHIP				oswpt_type::SHIP
#define OSWPT_TYPE_WING				oswpt_type::WING
#define OSWPT_TYPE_WAYPOINT			oswpt_type::WAYPOINT
#define OSWPT_TYPE_SHIP_ON_TEAM		oswpt_type::SHIP_ON_TEAM
#define OSWPT_TYPE_WHOLE_TEAM		oswpt_type::WHOLE_TEAM
#define OSWPT_TYPE_PARSE_OBJECT		oswpt_type::PARSE_OBJECT
#define OSWPT_TYPE_EXITED			oswpt_type::EXITED
#define OSWPT_TYPE_WING_NOT_PRESENT	oswpt_type::WING_NOT_PRESENT
// --------------------------------------------------------------------------------


SCP_vector<sexp_oper> Operators = {
//   Operator, Identity, Min / Max arguments
	//Arithmetic Category
	{ "+",								OP_PLUS,								2,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},
	{ "-",								OP_MINUS,								2,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},
	{ "*",								OP_MUL,									2,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},
	{ "/",								OP_DIV,									2,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},
	{ "mod",							OP_MOD,									2,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},
	{ "rand",							OP_RAND,								2,	3,			SEXP_ARITHMETIC_OPERATOR,	},
	{ "rand-multiple",					OP_RAND_MULTIPLE,						2,	3,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "abs",							OP_ABS,									1,	1,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "min",							OP_MIN,									1,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "max",							OP_MAX,									1,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "avg",							OP_AVG,									1,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "pow",							OP_POW,									2,	2,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "signum",							OP_SIGNUM,								1,	1,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "is-nan",							OP_IS_NAN,								1,	1,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "nan-to-number",					OP_NAN_TO_NUMBER,						1,	1,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "set-bit",						OP_SET_BIT,								2,	2,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "unset-bit",						OP_UNSET_BIT,							2,	2,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "is-bit-set",						OP_IS_BIT_SET,							2,	2,			SEXP_BOOLEAN_OPERATOR,		},	// Goober5000
	{ "bitwise-and",					OP_BITWISE_AND,							2,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "bitwise-or",						OP_BITWISE_OR,							2,	INT_MAX,	SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "bitwise-not",					OP_BITWISE_NOT,							1,	1,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "bitwise-xor",					OP_BITWISE_XOR,							2,	2,			SEXP_ARITHMETIC_OPERATOR,	},	// Goober5000
	{ "angle-vectors",					OP_ANGLE_VECTORS,						6,  6,			SEXP_ARITHMETIC_OPERATOR,   },  // Lafiel

	//Logical Category
	{ "true",							OP_TRUE,								0,	0,			SEXP_BOOLEAN_OPERATOR,	},
	{ "false",							OP_FALSE,								0,	0,			SEXP_BOOLEAN_OPERATOR,	},
	{ "and",							OP_AND,									2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "and-in-sequence",				OP_AND_IN_SEQUENCE,						2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "or",								OP_OR,									2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "not",							OP_NOT,									1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "xor",							OP_XOR,									2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "=",								OP_EQUALS,								2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "!=",								OP_NOT_EQUAL,							2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ ">",								OP_GREATER_THAN,						2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ ">=",								OP_GREATER_OR_EQUAL,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "<",								OP_LESS_THAN,							2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "<=",								OP_LESS_OR_EQUAL,						2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "string-equals",					OP_STRING_EQUALS,						2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "string-greater-than",			OP_STRING_GREATER_THAN,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "string-less-than",				OP_STRING_LESS_THAN,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "perform-actions-bool-first",		OP_PERFORM_ACTIONS_BOOL_FIRST,			2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "perform-actions-bool-last",		OP_PERFORM_ACTIONS_BOOL_LAST,			2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "has-time-elapsed",				OP_HAS_TIME_ELAPSED,					1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "has-time-elapsed-msecs",			OP_HAS_TIME_ELAPSED_MSECS,				1,	1,			SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "is-true-for-duration",			OP_IS_TRUE_FOR_DURATION,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000

	//Event/Goals Category
	{ "is-goal-true-delay",				OP_GOAL_TRUE_DELAY,						2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-goal-false-delay",			OP_GOAL_FALSE_DELAY,					2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-goal-incomplete",				OP_GOAL_INCOMPLETE,						1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-event-true",					OP_EVENT_TRUE,							1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-event-true-delay",			OP_EVENT_TRUE_DELAY,					2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-event-true-msecs-delay",		OP_EVENT_TRUE_MSECS_DELAY,				2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-event-false",					OP_EVENT_FALSE,							1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-event-false-delay",			OP_EVENT_FALSE_DELAY,					2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-event-false-msecs-delay",		OP_EVENT_FALSE_MSECS_DELAY,				2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-event-incomplete",			OP_EVENT_INCOMPLETE,					1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-previous-goal-true",			OP_PREVIOUS_GOAL_TRUE,					2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-previous-goal-false",			OP_PREVIOUS_GOAL_FALSE,					2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-previous-goal-incomplete",	OP_PREVIOUS_GOAL_INCOMPLETE,			2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-previous-event-true",			OP_PREVIOUS_EVENT_TRUE,					2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-previous-event-false",		OP_PREVIOUS_EVENT_FALSE,				2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-previous-event-incomplete",	OP_PREVIOUS_EVENT_INCOMPLETE,			2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "reset-event",					OP_RESET_EVENT,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "reset-goal",						OP_RESET_GOAL,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000

	//Objectives Category
	{ "is-destroyed",					OP_IS_DESTROYED,						1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-destroyed-delay",				OP_IS_DESTROYED_DELAY,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "was-destroyed-by-delay",			OP_WAS_DESTROYED_BY_DELAY,				3,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,  },
	{ "is-subsystem-destroyed",			OP_IS_SUBSYSTEM_DESTROYED,				2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-subsystem-destroyed-delay",	OP_IS_SUBSYSTEM_DESTROYED_DELAY,		3,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-disabled",					OP_IS_DISABLED,							1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-disabled-delay",				OP_IS_DISABLED_DELAY,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-disarmed",					OP_IS_DISARMED,							1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-disarmed-delay",				OP_IS_DISARMED_DELAY,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "has-docked",						OP_HAS_DOCKED,							3,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "has-docked-delay",				OP_HAS_DOCKED_DELAY,					4,	4,			SEXP_BOOLEAN_OPERATOR,	},
	{ "has-undocked",					OP_HAS_UNDOCKED,						3,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "has-undocked-delay",				OP_HAS_UNDOCKED_DELAY,					4,	4,			SEXP_BOOLEAN_OPERATOR,	},
	{ "has-arrived",					OP_HAS_ARRIVED,							1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "has-arrived-delay",				OP_HAS_ARRIVED_DELAY,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "has-departed",					OP_HAS_DEPARTED,						1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "has-departed-delay",				OP_HAS_DEPARTED_DELAY,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "are-waypoints-done",				OP_WAYPOINTS_DONE,						2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "are-waypoints-done-delay",		OP_WAYPOINTS_DONE_DELAY,				3,	4,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-nav-visited",					OP_NAV_IS_VISITED,						1,	1,			SEXP_BOOLEAN_OPERATOR,	}, // Kazan
	{ "ship-type-destroyed",			OP_SHIP_TYPE_DESTROYED,					2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "percent-ships-destroyed",		OP_PERCENT_SHIPS_DESTROYED,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "percent-ships-disabled",			OP_PERCENT_SHIPS_DISABLED,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "percent-ships-disarmed",			OP_PERCENT_SHIPS_DISARMED,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "percent-ships-departed",			OP_PERCENT_SHIPS_DEPARTED,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "percent-ships-arrived",			OP_PERCENT_SHIPS_ARRIVED,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "percent-ships-scanned",			OP_PERCENT_SHIPS_SCANNED,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "depart-node-delay",				OP_DEPART_NODE_DELAY,					3,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "destroyed-or-departed-delay",	OP_DESTROYED_DEPARTED_DELAY,			2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	

	//Status Category
	//Mission Sub-Category
	{ "num-ships-in-battle",			OP_NUM_SHIPS_IN_BATTLE,					0,	INT_MAX,	SEXP_INTEGER_OPERATOR,	},	//phreak modified by FUBAR
	{ "num-ships-in-wing",				OP_NUM_SHIPS_IN_WING,					1,	INT_MAX,	SEXP_INTEGER_OPERATOR,	},	// Karajorma
	{ "directive-value",				OP_DIRECTIVE_VALUE,						1,	2,			SEXP_INTEGER_OPERATOR,	},	// Karajorma
	{ "get-hotkey",						OP_GET_HOTKEY,							1,	1,			SEXP_INTEGER_OPERATOR,	},	// wookieejedi

	//Player Sub-Category
	{ "was-promotion-granted",			OP_WAS_PROMOTION_GRANTED,				0,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "was-medal-granted",				OP_WAS_MEDAL_GRANTED,					0,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "skill-level-at-least",			OP_SKILL_LEVEL_AT_LEAST,				1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "num_kills",						OP_NUM_KILLS,							1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "num_assists",					OP_NUM_ASSISTS,							1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "num_type_kills",					OP_NUM_TYPE_KILLS,						2,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "num_class_kills",				OP_NUM_CLASS_KILLS,						2,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "ship_score",						OP_SHIP_SCORE,							1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "time-elapsed-last-order",		OP_LAST_ORDER_TIME,						2,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "player-is-cheating",				OP_PLAYER_IS_CHEATING_BASTARD,			0,  0,			SEXP_BOOLEAN_OPERATOR,  },
	{ "is-language",					OP_IS_LANGUAGE,							1,	1,			SEXP_BOOLEAN_OPERATOR, }, // Goober5000
	{ "used-cheat",						OP_USED_CHEAT,							1,	1,			SEXP_BOOLEAN_OPERATOR, }, // Kiloku

	//Multiplayer Sub-Category
	{ "num-players",					OP_NUM_PLAYERS,							0,	0,			SEXP_INTEGER_OPERATOR,	},
	{ "team-score",						OP_TEAM_SCORE,							1,	1,			SEXP_INTEGER_OPERATOR,	}, 
	{ "ship-deaths",					OP_SHIP_DEATHS,							1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "respawns-left",					OP_RESPAWNS_LEFT,						1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "is-player",						OP_IS_PLAYER,							2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Karajorma

	//Ship Status Sub-Category
	{ "is-in-mission",					OP_IS_IN_MISSION,						1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "is-docked",						OP_IS_DOCKED,							1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	{ "is-ship-visible",				OP_IS_SHIP_VISIBLE,						1,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-ship-stealthy",				OP_IS_SHIP_STEALTHY,					1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-friendly-stealth-visible",	OP_IS_FRIENDLY_STEALTH_VISIBLE,			1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-iff",							OP_IS_IFF,								2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-species",						OP_IS_SPECIES,							2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-ai-class",					OP_IS_AI_CLASS,							2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-ship-type",					OP_IS_SHIP_TYPE,						2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-ship-class",					OP_IS_SHIP_CLASS,						2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-facing",						OP_IS_FACING,							3,	4,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is_tagged",						OP_IS_TAGGED,							1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "has-been-tagged-delay",			OP_HAS_BEEN_TAGGED_DELAY,				2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "are-ship-flags-set",				OP_ARE_SHIP_FLAGS_SET,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Karajorma
	{ "are-wing-flags-set",				OP_ARE_WING_FLAGS_SET,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR, },	// Goober5000
	{ "has-armor-type",					OP_HAS_ARMOR_TYPE,						3,	3,			SEXP_BOOLEAN_OPERATOR, },	// MjnMixael
	{ "is-ship-emp-active",				OP_IS_SHIP_EMP_ACTIVE,					1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// MjnMixael

	//Shields, Engines and Weapons Sub-Category
	{ "has-primary-weapon",				OP_HAS_PRIMARY_WEAPON,					3,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Karajorma
	{ "has-secondary-weapon",			OP_HAS_SECONDARY_WEAPON,				3,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// Karajorma
	{ "is-primary-selected",			OP_IS_PRIMARY_SELECTED,					2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "is-secondary-selected",			OP_IS_SECONDARY_SELECTED,				2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "primary-fired-since",			OP_PRIMARY_FIRED_SINCE,					3,	3,			SEXP_INTEGER_OPERATOR,	},	// Karajorma
	{ "turret-fired-since",				OP_TURRET_FIRED_SINCE,					3,	3,			SEXP_INTEGER_OPERATOR, },	// Asteroth
	{ "secondary-fired-since",			OP_SECONDARY_FIRED_SINCE,				3,	3,			SEXP_INTEGER_OPERATOR,	},	// Karajorma
	{ "primary-ammo-pct",				OP_PRIMARY_AMMO_PCT,					2,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "secondary-ammo-pct",				OP_SECONDARY_AMMO_PCT,					2,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "get-primary-ammo",				OP_GET_PRIMARY_AMMO,					2,	2,			SEXP_INTEGER_OPERATOR,	}, // Karajorma
	{ "get-secondary-ammo",				OP_GET_SECONDARY_AMMO,					2,	2,			SEXP_INTEGER_OPERATOR,	}, // Karajorma
	{ "turret-get-primary-ammo",		OP_TURRET_GET_PRIMARY_AMMO,				3,	3,			SEXP_INTEGER_OPERATOR,	},	// DahBlount
	{ "turret-get-secondary-ammo",		OP_TURRET_GET_SECONDARY_AMMO,			3,	3,			SEXP_INTEGER_OPERATOR,	},	// DahBlount
	{ "turret-has-primary-weapon",		OP_TURRET_HAS_PRIMARY_WEAPON,			4,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// MjnMixael
	{ "turret-has-secondary-weapon",	OP_TURRET_HAS_SECONDARY_WEAPON,			4,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},	// MjnMixael
	{ "get-num-countermeasures",		OP_GET_NUM_COUNTERMEASURES,				1,	1,			SEXP_INTEGER_OPERATOR,	}, // Karajorma
	{ "weapon-energy-pct",				OP_WEAPON_ENERGY_LEFT,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "afterburner-energy-pct",			OP_AFTERBURNER_LEFT,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "shield-recharge-pct",			OP_SHIELD_RECHARGE_PCT,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "weapon-recharge-pct",			OP_WEAPON_RECHARGE_PCT,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "engine-recharge-pct",			OP_ENGINE_RECHARGE_PCT,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "shield-quad-low",				OP_SHIELD_QUAD_LOW,						2,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "get-throttle-speed",				OP_GET_THROTTLE_SPEED,					1,	1,			SEXP_INTEGER_OPERATOR,	}, // Karajorma
	{ "current-speed",					OP_CURRENT_SPEED,						1,	1,			SEXP_INTEGER_OPERATOR,	},

	//Cargo Sub-Category
	{ "is-cargo-known",					OP_IS_CARGO_KNOWN,						1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-cargo-known-delay",			OP_CARGO_KNOWN_DELAY,					2,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "cap-subsys-cargo-known-delay",	OP_CAP_SUBSYS_CARGO_KNOWN_DELAY,		3,	INT_MAX,	SEXP_BOOLEAN_OPERATOR,	},
	{ "is-cargo",						OP_IS_CARGO,							2,	3,			SEXP_BOOLEAN_OPERATOR,	},

	//Damage Sub-Category
	{ "shields-left",					OP_SHIELDS_LEFT,						1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "hits-left",						OP_HITS_LEFT,							1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "hits-left-subsystem",			OP_HITS_LEFT_SUBSYSTEM,					2,	3,			SEXP_INTEGER_OPERATOR,	},
	{ "hits-left-subsystem-generic",	OP_HITS_LEFT_SUBSYSTEM_GENERIC,			2,	2,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "hits-left-subsystem-specific",	OP_HITS_LEFT_SUBSYSTEM_SPECIFIC,		2,	2,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "sim-hits-left",					OP_SIM_HITS_LEFT,						1,	1,			SEXP_INTEGER_OPERATOR,	}, // Turey
	{ "get-damage-caused",				OP_GET_DAMAGE_CAUSED,					2,	INT_MAX,	SEXP_INTEGER_OPERATOR,	},

	//Distance and Coordinates Sub-Category
	{ "distance",						OP_DISTANCE,							2,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "distance-to-center",				OP_DISTANCE_CENTER,						2,	2,			SEXP_INTEGER_OPERATOR, },	// Goober5000
	{ "distance-to-bbox",				OP_DISTANCE_BBOX,						2,	2,			SEXP_INTEGER_OPERATOR, },	// Goober5000
	{ "distance-center-to-subsystem",	OP_DISTANCE_CENTER_SUBSYSTEM,			3,	3,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "distance-bbox-to-subsystem",		OP_DISTANCE_BBOX_SUBSYSTEM,				3,	3,			SEXP_INTEGER_OPERATOR, },	// Goober5000
	{ "distance-to-nav",				OP_NAV_DISTANCE,						1,	1,			SEXP_INTEGER_OPERATOR,	},	// Kazan
	{ "num-within-box",					OP_NUM_WITHIN_BOX,						7,	INT_MAX,	SEXP_INTEGER_OPERATOR,	},	//WMC
	{ "is-in-box",						OP_IS_IN_BOX,							7,	8,			SEXP_INTEGER_OPERATOR,	},	//Sushi
	{ "special-warp-dist",				OP_SPECIAL_WARP_DISTANCE,				1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "get-object-x",					OP_GET_OBJECT_X,						1,	5,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "get-object-y",					OP_GET_OBJECT_Y,						1,	5,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "get-object-z",					OP_GET_OBJECT_Z,						1,	5,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "get-object-pitch",				OP_GET_OBJECT_PITCH,					1,	1,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "get-object-bank",				OP_GET_OBJECT_BANK,						1,	1,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "get-object-heading",				OP_GET_OBJECT_HEADING,					1,	1,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "get-object-speed-x",				OP_GET_OBJECT_SPEED_X,					1,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "get-object-speed-y",				OP_GET_OBJECT_SPEED_Y,					1,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "get-object-speed-z",				OP_GET_OBJECT_SPEED_Z,					1,	2,			SEXP_INTEGER_OPERATOR,	},
	{ "angle-facing-object",			OP_ANGLE_FVEC_TARGET,					2,  2,			SEXP_INTEGER_OPERATOR,  }, // Lafiel 

	//Variables Sub-Category
	{ "string-to-int",					OP_STRING_TO_INT,						1,	1,			SEXP_INTEGER_OPERATOR,	}, // Karajorma
	{ "string-get-length",				OP_STRING_GET_LENGTH,					1,	1,			SEXP_INTEGER_OPERATOR,	}, // Goober5000

	//Containers Sub-Category
	{ "is-container-empty",				OP_IS_CONTAINER_EMPTY,					1,	1,			SEXP_INTEGER_OPERATOR, }, // Karajorma
	{ "get-container-size",				OP_GET_CONTAINER_SIZE,					1,	1,			SEXP_INTEGER_OPERATOR, }, // Karajorma
	{ "list-has-data",					OP_LIST_HAS_DATA,						2,	INT_MAX,	SEXP_INTEGER_OPERATOR, }, // Karajorma
	{ "list-data-index",				OP_LIST_DATA_INDEX,						2,	2,			SEXP_INTEGER_OPERATOR, }, // Karajorma
	{ "map-has-key",					OP_MAP_HAS_KEY,							2,	INT_MAX,	SEXP_INTEGER_OPERATOR, }, // Karajorma
	{ "map-has-data-item",				OP_MAP_HAS_DATA_ITEM,					2,	3,			SEXP_INTEGER_OPERATOR, }, // Karajorma

	//Other Sub-Category
	{ "script-eval-bool",				OP_SCRIPT_EVAL_BOOL,					1,	INT_MAX,	SEXP_BOOLEAN_OPERATOR, },
	{ "script-eval-num",				OP_SCRIPT_EVAL_NUM,						1,	INT_MAX,	SEXP_INTEGER_OPERATOR, },

	//Time Category
	{ "time-ship-destroyed",			OP_TIME_SHIP_DESTROYED,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "time-ship-arrived",				OP_TIME_SHIP_ARRIVED,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "time-ship-departed",				OP_TIME_SHIP_DEPARTED,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "time-wing-destroyed",			OP_TIME_WING_DESTROYED,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "time-wing-arrived",				OP_TIME_WING_ARRIVED,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "time-wing-departed",				OP_TIME_WING_DEPARTED,					1,	1,			SEXP_INTEGER_OPERATOR,	},
	{ "mission-time",					OP_MISSION_TIME,						0,	0,			SEXP_INTEGER_OPERATOR,	},
	{ "mission-time-msecs",				OP_MISSION_TIME_MSECS,					0,	0,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "time-docked",					OP_TIME_DOCKED,							3,	3,			SEXP_INTEGER_OPERATOR,	},
	{ "time-undocked",					OP_TIME_UNDOCKED,						3,	3,			SEXP_INTEGER_OPERATOR,	},
	{ "time-to-goal",					OP_TIME_TO_GOAL,						1,	1,			SEXP_INTEGER_OPERATOR,	},	// tcrayford
	{ "set-hud-timer-padding",			OP_SET_HUD_TIME_PAD,					1,	1,			SEXP_ACTION_OPERATOR,	},  // MjnMixael

	//Conditionals Category
	{ "cond",							OP_COND,								1,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR,},
	{ "when",							OP_WHEN,								2,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR,},
	{ "when-argument",					OP_WHEN_ARGUMENT,						3,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR,},	// Goober5000
	{ "every-time",						OP_EVERY_TIME,							2,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR,},	// Goober5000
	{ "every-time-argument",			OP_EVERY_TIME_ARGUMENT,					3,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR,},	// Goober5000
	{ "on-mission-skip",				OP_ON_MISSION_SKIP,						1,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR, },	// Goober5000
	{ "functional-when",				OP_FUNCTIONAL_WHEN,						4,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR, },	// Goober5000
	{ "if-then-else",					OP_IF_THEN_ELSE,						3,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR,},	// Goober5000
	{ "functional-if-then-else",		OP_FUNCTIONAL_IF_THEN_ELSE,				3,	3,			SEXP_CONDITIONAL_OPERATOR, },	// Goober5000
	{ "switch",							OP_SWITCH,								2,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR, },	// Goober5000
	{ "functional-switch",				OP_FUNCTIONAL_SWITCH,					2,	INT_MAX,	SEXP_CONDITIONAL_OPERATOR, },	// Goober5000
	{ "any-of",							OP_ANY_OF,								1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR,	},	// Goober5000
	{ "every-of",						OP_EVERY_OF,							1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR,	},	// Goober5000
	{ "random-of",						OP_RANDOM_OF,							1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR,	},	// Goober5000
	{ "random-multiple-of",				OP_RANDOM_MULTIPLE_OF,					1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR,	},	// Karajorma
	{ "number-of",						OP_NUMBER_OF,							2,	INT_MAX,	SEXP_ARGUMENT_OPERATOR,	},	// Goober5000
	{ "first-of",						OP_FIRST_OF,							2,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// MageKing17
	{ "in-sequence",					OP_IN_SEQUENCE,							1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR,	},	// Karajorma
	{ "for-counter",					OP_FOR_COUNTER,							2,	3,			SEXP_ARGUMENT_OPERATOR,	},	// Goober5000
	{ "for-ship-class",					OP_FOR_SHIP_CLASS,						1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// Goober5000
	{ "for-ship-type",					OP_FOR_SHIP_TYPE,						1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// Goober5000
	{ "for-ship-team",					OP_FOR_SHIP_TEAM,						1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// Goober5000
	{ "for-ship-species",				OP_FOR_SHIP_SPECIES,					1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// Goober5000
	{ "for-players",					OP_FOR_PLAYERS,							0,	0,			SEXP_ARGUMENT_OPERATOR, },	// Goober5000
	{ "for-subsystems",					OP_FOR_SUBSYSTEMS,						1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// Goober5000
	{ "for-container-data",				OP_FOR_CONTAINER_DATA,					1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// jg18
	{ "for-map-container-keys",			OP_FOR_MAP_CONTAINER_KEYS,				1,	INT_MAX,	SEXP_ARGUMENT_OPERATOR, },	// jg18
	{ "invalidate-argument",			OP_INVALIDATE_ARGUMENT,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,		},	// Goober5000
	{ "invalidate-all-arguments",		OP_INVALIDATE_ALL_ARGUMENTS,			0,	0,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "validate-argument",				OP_VALIDATE_ARGUMENT,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "validate-all-arguments",			OP_VALIDATE_ALL_ARGUMENTS,				0,	0,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "do-for-valid-arguments",			OP_DO_FOR_VALID_ARGUMENTS,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "num-valid-arguments",			OP_NUM_VALID_ARGUMENTS,					0,	0,			SEXP_ACTION_OPERATOR,	},	// Karajorma

	//Change Category
	//Messaging Sub-Category
	{ "send-message",					OP_SEND_MESSAGE,						3,	3,			SEXP_ACTION_OPERATOR,	},
	{ "send-builtin-message",					OP_SEND_BUILTIN_MESSAGE,						4,	INT_MAX,			SEXP_ACTION_OPERATOR,	},
	{ "send-message-list",				OP_SEND_MESSAGE_LIST,					4,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "send-message-chain",				OP_SEND_MESSAGE_CHAIN,					5,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "send-random-message",			OP_SEND_RANDOM_MESSAGE,					3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "scramble-messages",				OP_SCRAMBLE_MESSAGES,					0,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "unscramble-messages",			OP_UNSCRAMBLE_MESSAGES,					0,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "disable-builtin-messages",		OP_DISABLE_BUILTIN_MESSAGES,			0,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "enable-builtin-messages",		OP_ENABLE_BUILTIN_MESSAGES,				0,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-persona",					OP_SET_PERSONA,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-death-message",				OP_SET_DEATH_MESSAGE,					1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-mission-mood",				OP_SET_MISSION_MOOD,					1,	1,			SEXP_ACTION_OPERATOR,	},	// Karajorma


	//AI Control Sub-Category
	{ "add-goal",						OP_ADD_GOAL,							2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "remove-goal",					OP_REMOVE_GOAL,							2,	4,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "add-ship-goal",					OP_ADD_SHIP_GOAL,						2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "add-wing-goal",					OP_ADD_WING_GOAL,						2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "clear-goals",					OP_CLEAR_GOALS,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "clear-ship-goals",				OP_CLEAR_SHIP_GOALS,					1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "clear-wing-goals",				OP_CLEAR_WING_GOALS,					1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "good-rearm-time",				OP_GOOD_REARM_TIME,						2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "bad-rearm-time",					OP_BAD_REARM_TIME,						2,	2,			SEXP_ACTION_OPERATOR,   },	// Asteroth
	{ "good-primary-time",				OP_GOOD_PRIMARY_TIME,					4,	4,			SEXP_ACTION_OPERATOR,	},	// plieblang
	{ "good-secondary-time",			OP_GOOD_SECONDARY_TIME,					4,	4,			SEXP_ACTION_OPERATOR,	},
	{ "change-ai-class",				OP_CHANGE_AI_CLASS,						2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "player-use-ai",					OP_PLAYER_USE_AI,						0,	0,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "player-not-use-ai",				OP_PLAYER_NOT_USE_AI,					0,	0,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-player-orders",				OP_SET_PLAYER_ORDERS,					3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-order-allowed-for-target",	OP_SET_ORDER_ALLOWED_TARGET,			3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// MjnMixael
	{ "enable-general-orders",          OP_ENABLE_GENERAL_ORDERS,               2,  INT_MAX,    SEXP_ACTION_OPERATOR,   },  // MjnMixael
	{ "validate-general-orders",        OP_VALIDATE_GENERAL_ORDERS,             2,  INT_MAX,    SEXP_ACTION_OPERATOR,   },  // MjnMixael
	{ "cap-waypoint-speed",				OP_CAP_WAYPOINT_SPEED,					2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "set-wing-formation",				OP_SET_WING_FORMATION,					3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000

	//Ship Status Sub-Category
	{ "protect-ship",					OP_PROTECT_SHIP,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "unprotect-ship",					OP_UNPROTECT_SHIP,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "beam-protect-ship",				OP_BEAM_PROTECT_SHIP,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "beam-unprotect-ship",			OP_BEAM_UNPROTECT_SHIP,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-protect-ship",			OP_TURRET_PROTECT_SHIP,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "turret-unprotect-ship",			OP_TURRET_UNPROTECT_SHIP,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-invisible",					OP_SHIP_INVISIBLE,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-visible",					OP_SHIP_VISIBLE,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-stealthy",					OP_SHIP_STEALTHY,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-unstealthy",				OP_SHIP_UNSTEALTHY,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "friendly-stealth-invisible",		OP_FRIENDLY_STEALTH_INVISIBLE,			1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "friendly-stealth-visible",		OP_FRIENDLY_STEALTH_VISIBLE,			1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "primitive-sensors-set-range",	OP_PRIMITIVE_SENSORS_SET_RANGE,			2,	2,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-targetable-as-bomb",		OP_SHIP_BOMB_TARGETABLE,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-untargetable-as-bomb",		OP_SHIP_BOMB_UNTARGETABLE,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "kamikaze",						OP_KAMIKAZE,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Sesquipedalian
	{ "change-iff",						OP_CHANGE_IFF,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "change-iff-color",				OP_CHANGE_IFF_COLOR,					6,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "add-remove-escort",				OP_ADD_REMOVE_ESCORT,					2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "ship-change-alt-name",			OP_SHIP_CHANGE_ALT_NAME,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-change-display-name",		OP_SHIP_CHANGE_DISPLAY_NAME,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},  // DarkVisor
	{ "ship-change-callsign",			OP_SHIP_CHANGE_CALLSIGN,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "ship-tag",						OP_SHIP_TAG,							3,	8,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-untag",						OP_SHIP_UNTAG,							1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-arrival-info",				OP_SET_ARRIVAL_INFO,					2,	8,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-departure-info",				OP_SET_DEPARTURE_INFO,					2,	7,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "alter-ship-flag",				OP_ALTER_SHIP_FLAG,						3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "alter-wing-flag",				OP_ALTER_WING_FLAG,						2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "cancel-future-waves",			OP_CANCEL_FUTURE_WAVES,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	}, // naomimyselfandi

	//Shields, Engines and Weapons Sub-Category
	{ "set-weapon-energy",				OP_SET_WEAPON_ENERGY,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-shield-energy",				OP_SET_SHIELD_ENERGY,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-player-throttle-speed",		OP_SET_PLAYER_THROTTLE_SPEED,			2,	2,			SEXP_ACTION_OPERATOR,	},	// CommanderDJ
	{ "set-afterburner-energy",			OP_SET_AFTERBURNER_ENERGY,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-subspace-drive",				OP_SET_SUBSPACE_DRIVE,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "set-primary-weapon",				OP_SET_PRIMARY_WEAPON,					3,	5,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-secondary-weapon",			OP_SET_SECONDARY_WEAPON,				3,	5,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-primary-ammo",				OP_SET_PRIMARY_AMMO,					3,	4,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-secondary-ammo",				OP_SET_SECONDARY_AMMO,					3,	4,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-num-countermeasures",		OP_SET_NUM_COUNTERMEASURES,				2,	2,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "lock-primary-weapon",			OP_LOCK_PRIMARY_WEAPON,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "unlock-primary-weapon",			OP_UNLOCK_PRIMARY_WEAPON,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "lock-secondary-weapon",			OP_LOCK_SECONDARY_WEAPON,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "unlock-secondary-weapon",		OP_UNLOCK_SECONDARY_WEAPON,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "lock-afterburner",				OP_LOCK_AFTERBURNER,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// KeldorKatarn
	{ "unlock-afterburner",				OP_UNLOCK_AFTERBURNER,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// KeldorKatarn
	{ "shields-on",						OP_SHIELDS_ON,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Sesquipedalian
	{ "shields-off",					OP_SHIELDS_OFF,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Sesquipedalian
	{ "force-glide",					OP_FORCE_GLIDE,							2,	2,			SEXP_ACTION_OPERATOR,	},	// The E
	{ "disable-ets",					OP_DISABLE_ETS,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// The E
	{ "enable-ets",						OP_ENABLE_ETS,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// The E
	{ "break-warp",						OP_WARP_BROKEN,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "fix-warp",						OP_WARP_NOT_BROKEN,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "never-warp",						OP_WARP_NEVER,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "allow-warp",						OP_WARP_ALLOWED,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "special-warpout-name",			OP_SET_SPECIAL_WARPOUT_NAME,			2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "get-ets-value",					OP_GET_ETS_VALUE,						2,	2,			SEXP_ACTION_OPERATOR,	},	// niffiwan
	{ "set-ets-values",					OP_SET_ETS_VALUES,						4,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// niffiwan
	{ "get-power-output",				OP_GET_POWER_OUTPUT,					1,	1,			SEXP_ACTION_OPERATOR,	},	// The E

	//Subsystems and Health Sub-Category
	{ "ship-invulnerable",				OP_SHIP_INVULNERABLE,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-vulnerable",				OP_SHIP_VULNERABLE,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-guardian",					OP_SHIP_GUARDIAN,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-no-guardian",				OP_SHIP_NO_GUARDIAN,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-guardian-threshold",		OP_SHIP_GUARDIAN_THRESHOLD,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ship-subsys-guardian-threshold",	OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD,		3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "set-guard-range",                OP_SET_GUARD_RANGE,                     2,  INT_MAX,    SEXP_ACTION_OPERATOR,   },  // MjnMixael
	{ "self-destruct",					OP_SELF_DESTRUCT,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "destroy-instantly",				OP_DESTROY_INSTANTLY,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Admiral MS
	{ "destroy-instantly-with-debris",	OP_DESTROY_INSTANTLY_WITH_DEBRIS,		1,	INT_MAX,	SEXP_ACTION_OPERATOR,   },	// Asteroth
	{ "destroy-subsys-instantly",		OP_DESTROY_SUBSYS_INSTANTLY,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Admiral MS
	{ "sabotage-subsystem",				OP_SABOTAGE_SUBSYSTEM,					3,	3,			SEXP_ACTION_OPERATOR,	},
	{ "repair-subsystem",				OP_REPAIR_SUBSYSTEM,					3,	5,			SEXP_ACTION_OPERATOR,	},
	{ "ship-copy-damage",				OP_SHIP_COPY_DAMAGE,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-subsystem-strength",			OP_SET_SUBSYSTEM_STRNGTH,				3,	5,			SEXP_ACTION_OPERATOR,	},
	{ "subsys-set-random",				OP_SUBSYS_SET_RANDOM,					3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "lock-rotating-subsystem",		OP_LOCK_ROTATING_SUBSYSTEM,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "free-rotating-subsystem",		OP_FREE_ROTATING_SUBSYSTEM,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "reverse-rotating-subsystem",		OP_REVERSE_ROTATING_SUBSYSTEM,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "rotating-subsys-set-turn-time",	OP_ROTATING_SUBSYS_SET_TURN_TIME,		3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "lock-translating-subsystem",		OP_LOCK_TRANSLATING_SUBSYSTEM,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "free-translating-subsystem",		OP_FREE_TRANSLATING_SUBSYSTEM,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "reverse-translating-subsystem",	OP_REVERSE_TRANSLATING_SUBSYSTEM,		2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "translating-subsys-set-speed",	OP_TRANSLATING_SUBSYS_SET_SPEED,		3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "trigger-submodel-animation",		OP_TRIGGER_SUBMODEL_ANIMATION,			4,	6,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "change-subsystem-name",			OP_CHANGE_SUBSYSTEM_NAME,				3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "ship-subsys-targetable",			OP_SHIP_SUBSYS_TARGETABLE,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-subsys-untargetable",		OP_SHIP_SUBSYS_UNTARGETABLE,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-subsys-no-replace",			OP_SHIP_SUBSYS_NO_REPLACE,				3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "ship-subsys-no-live-debris",		OP_SHIP_SUBSYS_NO_LIVE_DEBRIS,			3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "ship-subsys-vanish",				OP_SHIP_SUBSYS_VANISHED,				3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "ship-subsys-ignore_if_dead",		OP_SHIP_SUBSYS_IGNORE_IF_DEAD,			3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "awacs-set-radius",				OP_AWACS_SET_RADIUS,					3,	3,			SEXP_ACTION_OPERATOR,	},

	//Cargo Sub-Category
	{ "transfer-cargo",					OP_TRANSFER_CARGO,						2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "exchange-cargo",					OP_EXCHANGE_CARGO,						2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "set-cargo",						OP_SET_CARGO,							2,	3,			SEXP_ACTION_OPERATOR,	},
	{ "jettison-cargo-delay",			OP_JETTISON_CARGO_DELAY,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "jettison-cargo",					OP_JETTISON_CARGO_NEW,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-docked",						OP_SET_DOCKED,							4,	4,			SEXP_ACTION_OPERATOR,	},	// Sushi
	{ "cargo-no-deplete",				OP_CARGO_NO_DEPLETE,					1,	2,			SEXP_ACTION_OPERATOR,	},
	{ "set-scanned",					OP_SET_SCANNED,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "set-unscanned",					OP_SET_UNSCANNED,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	
	//Armor and Damage Types Sub-Category
	{ "set-armor-type",					OP_SET_ARMOR_TYPE,						4,	INT_MAX,	SEXP_ACTION_OPERATOR,	},  // FUBAR
	{ "weapon-set-damage-type",			OP_WEAPON_SET_DAMAGE_TYPE,				4,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "ship-set-damage-type",			OP_SHIP_SET_DAMAGE_TYPE,				4,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "ship-set-shockwave-damage-type",	OP_SHIP_SHOCKWAVE_SET_DAMAGE_TYPE,		3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// FUBAR
	{ "field-set-damage-type",			OP_FIELD_SET_DAMAGE_TYPE,				2,	2,			SEXP_ACTION_OPERATOR,	},	// FUBAR

	//Beams and Turrets Sub-Category
	{ "fire-beam",						OP_BEAM_FIRE,							3,	5,			SEXP_ACTION_OPERATOR,	},
	{ "fire-beam-at-coordinates",		OP_BEAM_FIRE_COORDS,					5,	9,			SEXP_ACTION_OPERATOR,	},
	{ "beam-create",					OP_BEAM_FLOATING_FIRE,					7,	14,			SEXP_ACTION_OPERATOR,	},
	{ "beam-free",						OP_BEAM_FREE,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "beam-free-all",					OP_BEAM_FREE_ALL,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "beam-lock",						OP_BEAM_LOCK,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "beam-lock-all",					OP_BEAM_LOCK_ALL,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-free",					OP_TURRET_FREE,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-free-all",				OP_TURRET_FREE_ALL,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-lock",					OP_TURRET_LOCK,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-lock-all",				OP_TURRET_LOCK_ALL,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-tagged-only",				OP_TURRET_TAGGED_ONLY_ALL,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-tagged-clear",			OP_TURRET_TAGGED_CLEAR_ALL,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-tagged-specific",			OP_TURRET_TAGGED_SPECIFIC,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//phreak
	{ "turret-tagged-clear-specific",	OP_TURRET_TAGGED_CLEAR_SPECIFIC,		2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//phreak
	{ "turret-change-weapon",			OP_TURRET_CHANGE_WEAPON,				5,	5,			SEXP_ACTION_OPERATOR,	},	//WMC
	{ "turret-set-direction-preference",OP_TURRET_SET_DIRECTION_PREFERENCE,		3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//FUBAR
	{ "turret-set-rate-of-fire",		OP_TURRET_SET_RATE_OF_FIRE,				3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//FUBAR
	{ "turret-set-optimum-range",		OP_TURRET_SET_OPTIMUM_RANGE,			3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//FUBAR
	{ "turret-set-target-priorities",	OP_TURRET_SET_TARGET_PRIORITIES,		3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//FUBAR
	{ "turret-set-inaccuracy",	        OP_TURRET_SET_INACCURACY,		        2,	INT_MAX,	SEXP_ACTION_OPERATOR,   },	// Asteroth
	{ "turret-set-target-order",		OP_TURRET_SET_TARGET_ORDER,				2,	2+ NUM_TURRET_ORDER_TYPES,	SEXP_ACTION_OPERATOR,	},	//WMC
	{ "ship-turret-target-order",		OP_SHIP_TURRET_TARGET_ORDER,			1,	1+ NUM_TURRET_ORDER_TYPES,	SEXP_ACTION_OPERATOR,	},	//WMC
	{ "turret-subsys-target-disable",	OP_TURRET_SUBSYS_TARGET_DISABLE,		2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-subsys-target-enable",	OP_TURRET_SUBSYS_TARGET_ENABLE,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "turret-set-forced-target",	    OP_TURRET_SET_FORCED_TARGET,			3,	INT_MAX,	SEXP_ACTION_OPERATOR,   },  // Asteroth
	{ "turret-set-forced-subsys-target",OP_TURRET_SET_FORCED_SUBSYS_TARGET,		4,	INT_MAX,	SEXP_ACTION_OPERATOR, },  // Asteroth
	{ "turret-clear-forced-target",	    OP_TURRET_CLEAR_FORCED_TARGET,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,   },  // Asteroth
	{ "turret-set-primary-ammo",		OP_TURRET_SET_PRIMARY_AMMO,				4,	4,			SEXP_ACTION_OPERATOR,	},	// DahBlount
	{ "turret-set-secondary-ammo",		OP_TURRET_SET_SECONDARY_AMMO,			4,	4,			SEXP_ACTION_OPERATOR,	},	// DahBlount
	{ "is-in-turret-fov",				OP_IS_IN_TURRET_FOV,					3,	4,			SEXP_BOOLEAN_OPERATOR,	},	// Goober5000
	
	//Models and Textures Sub-Category
	{ "change-ship-class",				OP_CHANGE_SHIP_CLASS,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "deactivate-glow-maps",			OP_DEACTIVATE_GLOW_MAPS,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//-Bobboau
	{ "activate-glow-maps",				OP_ACTIVATE_GLOW_MAPS,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//-Bobboau
	{ "deactivate-glow-points",			OP_DEACTIVATE_GLOW_POINTS,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//-Bobboau
	{ "activate-glow-points",			OP_ACTIVATE_GLOW_POINTS,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//-Bobboau
	{ "deactivate-glow-point-bank",		OP_DEACTIVATE_GLOW_POINT_BANK,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//-Bobboau
	{ "activate-glow-point-bank",		OP_ACTIVATE_GLOW_POINT_BANK,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//-Bobboau
	{ "set-thrusters-status",			OP_SET_THRUSTERS,						2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// The E
	{ "don't-collide-invisible",		OP_DONT_COLLIDE_INVISIBLE,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "collide-invisible",				OP_COLLIDE_INVISIBLE,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "add-to-collision-group",			OP_ADD_TO_COLGROUP,						2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// The E
	{ "remove-from-collision-group",	OP_REMOVE_FROM_COLGROUP,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// The E
	{ "add-to-collision-group-new",		OP_ADD_TO_COLGROUP_NEW,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "remove-from-collision-group-new",	OP_REMOVE_FROM_COLGROUP_NEW,		2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "get-collision-group",			OP_GET_COLGROUP_ID,						1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "change-team-color",				OP_CHANGE_TEAM_COLOR,					3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// The E
	{ "replace-texture",				OP_REPLACE_TEXTURE,						3,  INT_MAX,	SEXP_ACTION_OPERATOR,   },  // Lafiel
	{ "replace-skybox-texture",				OP_REPLACE_TEXTURE_SKYBOX,						2, 2,	SEXP_ACTION_OPERATOR,   },  // Lafiel
	{ "set-alpha-multiplier",			OP_SET_ALPHA_MULT,						2,	INT_MAX,	SEXP_ACTION_OPERATOR,   }, //Lafiel
	{ "trigger-ship-animation",			OP_TRIGGER_ANIMATION_NEW,				3,	7,			SEXP_ACTION_OPERATOR,	}, //Lafiel
	{ "stop-looping-animation",			OP_STOP_LOOPING_ANIMATION,				3,  3,			SEXP_ACTION_OPERATOR,   }, //Lafiel
	{ "update-moveable-animation",		OP_UPDATE_MOVEABLE,						2,	INT_MAX,	SEXP_ACTION_OPERATOR,	}, //Lafiel
	{ "advance-moveable-animation",		OP_ADVANCE_MOVEABLE,					2,	2,			SEXP_ACTION_OPERATOR,	}, //Lafiel

	//Coordinate Manipulation Sub-Category
	{ "set-object-position",			OP_SET_OBJECT_POSITION,					4,	4,			SEXP_ACTION_OPERATOR,	},	// WMC
	{ "set-object-orientation",			OP_SET_OBJECT_ORIENTATION,				4,	4,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-object-facing",				OP_SET_OBJECT_FACING,					4,	6,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-object-facing-object",		OP_SET_OBJECT_FACING_OBJECT,			2,	4,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-object-speed-x",				OP_SET_OBJECT_SPEED_X,					2,	3,			SEXP_ACTION_OPERATOR,	},	// WMC // Deprecated by wookieejedi
	{ "set-object-speed-y",				OP_SET_OBJECT_SPEED_Y,					2,	3,			SEXP_ACTION_OPERATOR,	},	// WMC // Deprecated by wookieejedi
	{ "set-object-speed-z",				OP_SET_OBJECT_SPEED_Z,					2,	3,			SEXP_ACTION_OPERATOR,	},	// WMC // Deprecated by wookieejedi
	{ "ship-maneuver",					OP_SHIP_MANEUVER,						10, 11,			SEXP_ACTION_OPERATOR,	},	// Wanderer
	{ "ship-rot-maneuver",				OP_SHIP_ROT_MANEUVER,					6,	7,			SEXP_ACTION_OPERATOR,	},	// Wanderer
	{ "ship-lat-maneuver",				OP_SHIP_LAT_MANEUVER,					6,	7,			SEXP_ACTION_OPERATOR,	},	// Wanderer
	{ "set-immobile",					OP_SET_IMMOBILE,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-mobile",						OP_SET_MOBILE,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000

	//Mission and Campaign Sub-Category
	{ "invalidate-goal",				OP_INVALIDATE_GOAL,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "validate-goal",					OP_VALIDATE_GOAL,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "red-alert",						OP_RED_ALERT,							0,	0,			SEXP_ACTION_OPERATOR,	},
	{ "end-mission",					OP_END_MISSION,							0,	3,			SEXP_ACTION_OPERATOR,	},	//-Sesquipedalian
	{ "force-jump",						OP_FORCE_JUMP,							0,	0,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "next-mission",					OP_NEXT_MISSION,						1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "end-campaign",					OP_END_CAMPAIGN,						0,	1,			SEXP_ACTION_OPERATOR,	},
	{ "end-of-campaign",				OP_END_OF_CAMPAIGN,						0,	0,			SEXP_ACTION_OPERATOR,	},
	{ "set-debriefing-toggled",			OP_SET_DEBRIEFING_TOGGLED,				1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-debriefing-persona",			OP_SET_DEBRIEFING_PERSONA,				1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-traitor-override",			OP_SET_TRAITOR_OVERRIDE,				1,	1,			SEXP_ACTION_OPERATOR,	},	// MjnMixael
	{ "set-friendly-damage-caps",		OP_SET_FRIENDLY_DAMAGE_CAPS,			1,	3,			SEXP_ACTION_OPERATOR,	},	// Kestrellius
	{ "allow-treason",					OP_ALLOW_TREASON,						1,	1,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "grant-promotion",				OP_GRANT_PROMOTION,						0,	0,			SEXP_ACTION_OPERATOR,	},
	{ "grant-medal",					OP_GRANT_MEDAL,							1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "allow-ship",						OP_ALLOW_SHIP,							1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "allow-weapon",					OP_ALLOW_WEAPON,						1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "tech-add-ships",					OP_TECH_ADD_SHIP,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "tech-add-weapons",				OP_TECH_ADD_WEAPON,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "tech-add-intel",					OP_TECH_ADD_INTEL,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "tech-remove-intel",				OP_TECH_REMOVE_INTEL,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// wookieejedi
	{ "tech-add-intel-xstr",			OP_TECH_ADD_INTEL_XSTR,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "tech-remove-intel-xstr",			OP_TECH_REMOVE_INTEL_XSTR,				2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// wookieejedi
	{ "tech-reset-to-default",			OP_TECH_RESET_TO_DEFAULT,				0,	0,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "change-player-score",			OP_CHANGE_PLAYER_SCORE,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "change-team-score",				OP_CHANGE_TEAM_SCORE,					2,	2,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "set-respawns",					OP_SET_RESPAWNS,						2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "add-remove-hotkey",				OP_ADD_REMOVE_HOTKEY,					3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// wookieejedi

	//Music and Sound Sub-Category
	{ "change-soundtrack",				OP_CHANGE_SOUNDTRACK,					1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000	
	{ "play-sound-from-table",			OP_PLAY_SOUND_FROM_TABLE,				4,	4,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "play-sound-from-file",			OP_PLAY_SOUND_FROM_FILE,				1,	4,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "close-sound-from-file",			OP_CLOSE_SOUND_FROM_FILE,				0,	2,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "pause-sound-from-file",			OP_PAUSE_SOUND_FROM_FILE,				1,	2,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-sound-environment",			OP_SET_SOUND_ENVIRONMENT,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Taylor
	{ "update-sound-environment",		OP_UPDATE_SOUND_ENVIRONMENT,			2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Taylor
	{ "adjust-audio-volume",			OP_ADJUST_AUDIO_VOLUME,					1,	3,			SEXP_ACTION_OPERATOR,	},

	//HUD Sub-Category
	{ "hud-disable",					OP_HUD_DISABLE,							1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "hud-disable-except-messages",	OP_HUD_DISABLE_EXCEPT_MESSAGES,			1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "hud-set-custom-gauge-active",	OP_HUD_SET_CUSTOM_GAUGE_ACTIVE,			2, 	INT_MAX, 	SEXP_ACTION_OPERATOR,	},
	{ "hud-set-builtin-gauge-active",	OP_HUD_SET_BUILTIN_GAUGE_ACTIVE,		2, 	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "hud-set-text",					OP_HUD_SET_TEXT,						2,	2,			SEXP_ACTION_OPERATOR,	},	//WMCoolmon
	{ "hud-set-xstr",                   OP_HUD_SET_XSTR,                        3,  3,          SEXP_ACTION_OPERATOR,   },  //MjnMixael
	{ "hud-set-text-num",				OP_HUD_SET_TEXT_NUM,					2,	2,			SEXP_ACTION_OPERATOR,	},	//WMCoolmon
	{ "hud-set-message",				OP_HUD_SET_MESSAGE,						2,	2,			SEXP_ACTION_OPERATOR,	},	//The E
	{ "hud-set-directive",				OP_HUD_SET_DIRECTIVE,					2,	2,			SEXP_ACTION_OPERATOR,	},	//The E
	{ "hud-set-frame",					OP_HUD_SET_FRAME,						2,	2,			SEXP_ACTION_OPERATOR,	},	//WMCoolmon
	{ "hud-set-coords",					OP_HUD_SET_COORDS,						3,	3,			SEXP_ACTION_OPERATOR,	},	//WMCoolmon
	{ "hud-set-color",					OP_HUD_SET_COLOR,						4,	4,			SEXP_ACTION_OPERATOR,	},	//WMCoolmon
	{ "hud-reset-color",				OP_HUD_RESET_COLOR,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "hud-display-gauge",				OP_HUD_DISPLAY_GAUGE,					2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "hud-gauge-set-active",			OP_HUD_GAUGE_SET_ACTIVE,				2,	2,			SEXP_ACTION_OPERATOR,	},	//Deprecated
	{ "hud-activate-gauge-type",		OP_HUD_ACTIVATE_GAUGE_TYPE,				2,	2,			SEXP_ACTION_OPERATOR,	},	//Deprecated
	{ "hud-clear-messages",				OP_HUD_CLEAR_MESSAGES,					0,	0,			SEXP_ACTION_OPERATOR,	},	// swifty
	{ "hud-set-max-targeting-range",	OP_HUD_SET_MAX_TARGETING_RANGE,			1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "hud-force-sensor-static",		OP_HUD_FORCE_SENSOR_STATIC,				1,	1,			SEXP_ACTION_OPERATOR,	},	// MjnMixael
	{ "hud-force-emp-effect",			OP_HUD_FORCE_EMP_EFFECT,				2,	3,			SEXP_ACTION_OPERATOR,	},	// MjnMixael
	{ "set-squadron-wings",				OP_SET_SQUADRON_WINGS,			1,	MAX_SQUADRON_WINGS,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-player-target",				OP_SET_PLAYER_TARGET,					1,	2,			SEXP_ACTION_OPERATOR,	},	// LuytenKy
	{ "clear-player-target",			OP_CLEAR_PLAYER_TARGET,					0,	0,			SEXP_ACTION_OPERATOR,	},	// LuytenKy

	//Nav Sub-Category
	{ "add-nav-waypoint",				OP_NAV_ADD_WAYPOINT,					3,	4,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "add-nav-ship",					OP_NAV_ADD_SHIP,						2,	2,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "del-nav",						OP_NAV_DEL,								1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "hide-nav",						OP_NAV_HIDE,							1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "restrict-nav",					OP_NAV_RESTRICT,						1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "unhide-nav",						OP_NAV_UNHIDE,							1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "unrestrict-nav",					OP_NAV_UNRESTRICT,						1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "set-nav-visited",				OP_NAV_SET_VISITED,						1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "unset-nav-visited",				OP_NAV_UNSET_VISITED,					1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "set-nav-carry",					OP_NAV_SET_CARRY,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//kazan
	{ "unset-nav-carry",				OP_NAV_UNSET_CARRY,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//kazan
	{ "set-nav-needslink",				OP_NAV_SET_NEEDSLINK,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//kazan
	{ "unset-nav-needslink",			OP_NAV_UNSET_NEEDSLINK,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//kazan
	{ "is-nav-linked",					OP_NAV_ISLINKED,						1,	1,			SEXP_BOOLEAN_OPERATOR,	},	//kazan
	{ "use-nav-cinematics",				OP_NAV_USECINEMATICS,					1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "use-autopilot",					OP_NAV_USEAP,							1,	1,			SEXP_ACTION_OPERATOR,	},	//kazan
	{ "select-nav",						OP_NAV_SELECT,							1,	1,			SEXP_ACTION_OPERATOR,	},	//Talon1024
	{ "unselect-nav",					OP_NAV_UNSELECT,						0,	0,			SEXP_ACTION_OPERATOR,	},	//Talon1024
	{ "set-nav-color",					OP_NAV_SET_COLOR,						4,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//Goober5000
	{ "set-nav-visited-color",			OP_NAV_SET_VISITED_COLOR,				4,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//Goober5000

	//Cutscene Sub-Category
	{ "set-cutscene-bars",				OP_CUTSCENES_SET_CUTSCENE_BARS,			0,	1,			SEXP_ACTION_OPERATOR,	},
	{ "unset-cutscene-bars",			OP_CUTSCENES_UNSET_CUTSCENE_BARS,		0,	1,			SEXP_ACTION_OPERATOR,	},
	{ "fade-in",						OP_CUTSCENES_FADE_IN,					0,	4,			SEXP_ACTION_OPERATOR,	},
	{ "fade-out",						OP_CUTSCENES_FADE_OUT,					0,	4,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera",						OP_CUTSCENES_SET_CAMERA,				0,	1,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-position",			OP_CUTSCENES_SET_CAMERA_POSITION,		3,	6,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-facing",				OP_CUTSCENES_SET_CAMERA_FACING,			3,	6,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-facing-object",		OP_CUTSCENES_SET_CAMERA_FACING_OBJECT,	1,	4,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-rotation",			OP_CUTSCENES_SET_CAMERA_ROTATION,		3,	6,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-host",				OP_CUTSCENES_SET_CAMERA_HOST,			1,	2,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-target",				OP_CUTSCENES_SET_CAMERA_TARGET,			1,	2,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-fov",					OP_CUTSCENES_SET_CAMERA_FOV,			1,	5,			SEXP_ACTION_OPERATOR,	},
	{ "set-fov",						OP_CUTSCENES_SET_FOV,					1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "get-fov",						OP_CUTSCENES_GET_FOV,					0,	0,			SEXP_INTEGER_OPERATOR,	},
	{ "reset-fov",						OP_CUTSCENES_RESET_FOV,					0,	0,			SEXP_ACTION_OPERATOR,	},
	{ "reset-camera",					OP_CUTSCENES_RESET_CAMERA,				0,	1,			SEXP_ACTION_OPERATOR,	},
	{ "show-subtitle",					OP_CUTSCENES_SHOW_SUBTITLE,				4,	14,			SEXP_ACTION_OPERATOR,	},
	{ "show-subtitle-text",				OP_CUTSCENES_SHOW_SUBTITLE_TEXT,		6,	15,			SEXP_ACTION_OPERATOR,	},
	{ "show-subtitle-image",			OP_CUTSCENES_SHOW_SUBTITLE_IMAGE,		8,	11,			SEXP_ACTION_OPERATOR,	},
	{ "clear-subtitles",				OP_CLEAR_SUBTITLES,						0,	0,			SEXP_ACTION_OPERATOR,	},
	{ "lock-perspective",				OP_CUTSCENES_FORCE_PERSPECTIVE,			1,	3,			SEXP_ACTION_OPERATOR,	},
	{ "allow-photo-mode",				OP_ALLOW_PHOTO_MODE,					1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "set-camera-shudder",				OP_SET_CAMERA_SHUDDER,					2,	4,			SEXP_ACTION_OPERATOR,	},
	{ "supernova-start",				OP_SUPERNOVA_START,						1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "supernova-stop",					OP_SUPERNOVA_STOP,						0,	0,			SEXP_ACTION_OPERATOR,	},	//CommanderDJ
	{ "get-supernova-stage",			OP_GET_SUPERNOVA_STAGE,					0,	0,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "set-motion-debris-override",		OP_OVERRIDE_MOTION_DEBRIS,				1,  1,			SEXP_ACTION_OPERATOR,	},	// The E

	//Background and Nebula Sub-Category
	{ "mission-set-nebula",				OP_MISSION_SET_NEBULA,					1,	2,			SEXP_ACTION_OPERATOR,	},	// Sesquipedalian
	{ "mission-set-subspace",			OP_MISSION_SET_SUBSPACE,				1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "change-background",				OP_CHANGE_BACKGROUND,					1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "add-background-bitmap",			OP_ADD_BACKGROUND_BITMAP,				8,	9,			SEXP_ACTION_OPERATOR,	},	// phreak
	{ "add-background-bitmap-new",		OP_ADD_BACKGROUND_BITMAP_NEW,			8,	9,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "remove-background-bitmap",		OP_REMOVE_BACKGROUND_BITMAP,			1,	1,			SEXP_ACTION_OPERATOR,	},	// phreak
	{ "add-sun-bitmap",					OP_ADD_SUN_BITMAP,						5,	6,			SEXP_ACTION_OPERATOR,	},	// phreak
	{ "add-sun-bitmap-new",				OP_ADD_SUN_BITMAP_NEW,					5,	6,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "remove-sun-bitmap",				OP_REMOVE_SUN_BITMAP,					1,	1,			SEXP_ACTION_OPERATOR,	},	// phreak
	{ "nebula-change-pattern",			OP_NEBULA_CHANGE_PATTERN,				1,	1,			SEXP_ACTION_OPERATOR,	},	// Axem
	{ "nebula-change-fog-color",		OP_NEBULA_CHANGE_FOG_COLOR,				3,	3,			SEXP_ACTION_OPERATOR,   },	// Asteroth
	{ "nebula-change-storm",			OP_NEBULA_CHANGE_STORM,					1,	1,			SEXP_ACTION_OPERATOR,	},	// phreak
	{ "nebula-toggle-poof",				OP_NEBULA_TOGGLE_POOF,					2,	2,			SEXP_ACTION_OPERATOR,	},	// phreak
	{ "nebula-set-poofs",				OP_NEBULA_SET_POOFS,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "nebula-fade-poof",				OP_NEBULA_FADE_POOF,					3,	3,			SEXP_ACTION_OPERATOR,	},	// MjnMixael
	{ "nebula-fade-poofs",				OP_NEBULA_FADE_POOFS,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "nebula-set-range",				OP_NEBULA_SET_RANGE,					1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "volumetrics-toggle", 			OP_VOLUMETRICS_TOGGLE, 					1,	1,			SEXP_ACTION_OPERATOR,	},	// Lafiel
	{ "set-skybox-model",				OP_SET_SKYBOX_MODEL,					1,	8,			SEXP_ACTION_OPERATOR,	},	// taylor
	{ "set-skybox-orientation",			OP_SET_SKYBOX_ORIENT,					3,	3,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-skybox-alpha",				OP_SET_SKYBOX_ALPHA,					1,	1,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-ambient-light",				OP_SET_AMBIENT_LIGHT,					3,	3,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "toggle-asteroid-field",			OP_TOGGLE_ASTEROID_FIELD,				1,	1,			SEXP_ACTION_OPERATOR,	},	// MjnMixael
	{ "set-asteroid-field",				OP_SET_ASTEROID_FIELD,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// MjnMixael - Deprecated
	{ "set-debris-field",				OP_SET_DEBRIS_FIELD,					1,	12,			SEXP_ACTION_OPERATOR,	},	// MjnMixael - Deprecated
	{ "config-asteroid-field",			OP_CONFIG_ASTEROID_FIELD,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},  // MjnMixael
	{ "config-debris-field",			OP_CONFIG_DEBRIS_FIELD,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},  // MjnMixael
	{ "config-field-targets",			OP_CONFIG_FIELD_TARGETS,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},  // MjnMixael
	{ "set-motion-debris",			    OP_SET_MOTION_DEBRIS,					1,	1,			SEXP_ACTION_OPERATOR,	},	// MjnMixael

	//Jump Node Sub-Category
	{ "set-jumpnode-name",				OP_JUMP_NODE_SET_JUMPNODE_NAME,			2,	2,			SEXP_ACTION_OPERATOR,	},	//CommanderDJ
	{ "set-jumpnode-display-name",      OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME, 2,  2,          SEXP_ACTION_OPERATOR,   },  //MjnMixael
	{ "set-jumpnode-color",				OP_JUMP_NODE_SET_JUMPNODE_COLOR,		5,	5,			SEXP_ACTION_OPERATOR,	},
	{ "set-jumpnode-model",				OP_JUMP_NODE_SET_JUMPNODE_MODEL,		3,	3,			SEXP_ACTION_OPERATOR,	},
	{ "show-jumpnode",					OP_JUMP_NODE_SHOW_JUMPNODE,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "hide-jumpnode",					OP_JUMP_NODE_HIDE_JUMPNODE,				1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},

	//Special Effects Sub-Category
	{ "set-post-effect",				OP_SET_POST_EFFECT,						2,	5,			SEXP_ACTION_OPERATOR,	},	// Hery
	{ "reset-post-effects",				OP_RESET_POST_EFFECTS,					0,	0,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-effect",					OP_SHIP_EFFECT,							3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Valathil
	{ "ship-create",					OP_SHIP_CREATE,							5,	10,			SEXP_ACTION_OPERATOR,	},	// WMC
	{ "prop-create",                    OP_PROP_CREATE,                         5,  8,          SEXP_ACTION_OPERATOR,   },  // MjnMixael
	{ "weapon-create",					OP_WEAPON_CREATE,						5,	10,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-vanish",					OP_SHIP_VANISH,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "prop-vanish",					OP_PROP_VANISH,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},  // MjnMixael
	{ "ship-vaporize",					OP_SHIP_VAPORIZE,						1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "ship-no-vaporize",				OP_SHIP_NO_VAPORIZE,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-explosion-option",			OP_SET_EXPLOSION_OPTION,				3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "create-bolt",					OP_CREATE_BOLT,							7,	8,			SEXP_ACTION_OPERATOR,	},	// MjnMixael
	{ "explosion-effect",				OP_EXPLOSION_EFFECT,					11,	14,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "warp-effect",					OP_WARP_EFFECT,							12, 14,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "clear-weapons",					OP_CLEAR_WEAPONS,						0,	1,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "clear-debris",					OP_CLEAR_DEBRIS,						0,	1,			SEXP_ACTION_OPERATOR, },	// Goober5000
	{ "set-time-compression",			OP_CUTSCENES_SET_TIME_COMPRESSION,		1,	3,			SEXP_ACTION_OPERATOR,	},
	{ "reset-time-compression",			OP_CUTSCENES_RESET_TIME_COMPRESSION,	0,	0,			SEXP_ACTION_OPERATOR,	},
	{ "call-ssm-strike",				OP_CALL_SSM_STRIKE,						3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// X3N0-Life-Form
	{ "set-gravity-accel",				OP_SET_GRAVITY_ACCEL,					1,	1,			SEXP_ACTION_OPERATOR, },	// Asteroth
	{ "force-rearm",					OP_FORCE_REARM,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},  // MjnMixael
	{ "abort-rearm",					OP_ABORT_REARM,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// MjnMixael

	//Variable Category
	{ "modify-variable",				OP_MODIFY_VARIABLE,						2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "get-variable-by-index",			OP_GET_VARIABLE_BY_INDEX,				1,	1,			SEXP_INTEGER_OPERATOR,	},	// Goober5000
	{ "set-variable-by-index",			OP_SET_VARIABLE_BY_INDEX,				2,	2,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "copy-variable-from-index",		OP_COPY_VARIABLE_FROM_INDEX,			2,	2,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "copy-variable-between-indexes",	OP_COPY_VARIABLE_BETWEEN_INDEXES,		2,	2,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "int-to-string",					OP_INT_TO_STRING,						2,	2,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "string-concatenate",				OP_STRING_CONCATENATE,					3,	3,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "string-concatenate-block",		OP_STRING_CONCATENATE_BLOCK,			3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "string-get-substring",			OP_STRING_GET_SUBSTRING,				4,	4,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "string-set-substring",			OP_STRING_SET_SUBSTRING,				5,	5,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "modify-variable-xstr",			OP_MODIFY_VARIABLE_XSTR,				3,	3,			SEXP_ACTION_OPERATOR,	},	// m!m

	//Containers Category
	{ "add-to-list",					OP_CONTAINER_ADD_TO_LIST,				3,	INT_MAX,	SEXP_ACTION_OPERATOR, },	// Karajorma
	{ "remove-from-list",				OP_CONTAINER_REMOVE_FROM_LIST,			2,	INT_MAX,	SEXP_ACTION_OPERATOR, },	// Karajorma
	{ "add-to-map",						OP_CONTAINER_ADD_TO_MAP,				3,	INT_MAX,	SEXP_ACTION_OPERATOR, },	// Karajorma
	{ "remove-from-map",				OP_CONTAINER_REMOVE_FROM_MAP,			2,	INT_MAX,	SEXP_ACTION_OPERATOR, },	// Karajorma
	{ "get-map-keys",					OP_CONTAINER_GET_MAP_KEYS,				2,	3,			SEXP_ACTION_OPERATOR, },	// Karajorma
	{ "clear-container",				OP_CLEAR_CONTAINER,						1,	INT_MAX,	SEXP_ACTION_OPERATOR, },	// Karajorma
	{ "copy-container",					OP_COPY_CONTAINER,						2,	3,			SEXP_ACTION_OPERATOR, },	// jg18
	{ "apply-container-filter",			OP_APPLY_CONTAINER_FILTER,				2,	2,			SEXP_ACTION_OPERATOR, },	// jg18

	//Other Sub-Category
	{ "damaged-escort-priority",		OP_DAMAGED_ESCORT_LIST,					3,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	//phreak
	{ "damaged-escort-priority-all",	OP_DAMAGED_ESCORT_LIST_ALL,				1,	MAX_COMPLETE_ESCORT_LIST,	SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "set-support-ship",				OP_SET_SUPPORT_SHIP,					6,	7,			SEXP_ACTION_OPERATOR,	},	// Goober5000
	{ "script-eval",					OP_SCRIPT_EVAL,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "script-eval-block",				OP_SCRIPT_EVAL_BLOCK,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "multi-eval",						OP_SCRIPT_EVAL_MULTI,					2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "script-eval-string",				OP_SCRIPT_EVAL_STRING,					2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "debug",							OP_DEBUG,								2,	2,			SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "do-nothing",						OP_NOP,									0,	0,			SEXP_ACTION_OPERATOR,	},
	
	//AI Goals Category
	{ "ai-chase",						OP_AI_CHASE,							2,	4,			SEXP_GOAL_OPERATOR,	},
	{ "ai-chase-wing",					OP_AI_CHASE_WING,						2,	4,			SEXP_GOAL_OPERATOR,	},
	{ "ai-chase-ship-class",			OP_AI_CHASE_SHIP_CLASS,					2,	4,			SEXP_GOAL_OPERATOR, },
	{ "ai-chase-ship-type",				OP_AI_CHASE_SHIP_TYPE,                  2,  4,          SEXP_GOAL_OPERATOR, }, // LuytenKy
	{ "ai-chase-any",					OP_AI_CHASE_ANY,						1,	2,			SEXP_GOAL_OPERATOR,	},
	{ "ai-guard",						OP_AI_GUARD,							2,	3,			SEXP_GOAL_OPERATOR,	},
	{ "ai-guard-wing",					OP_AI_GUARD_WING,						2,	3,			SEXP_GOAL_OPERATOR,	},
	{ "ai-destroy-subsystem",			OP_AI_DESTROY_SUBSYS,					3,	5,			SEXP_GOAL_OPERATOR,	},
	{ "ai-disable-ship",				OP_AI_DISABLE_SHIP,						2,	4,			SEXP_GOAL_OPERATOR,	},
	{ "ai-disable-ship-tactical",		OP_AI_DISABLE_SHIP_TACTICAL,			2,	4,			SEXP_GOAL_OPERATOR, },
	{ "ai-disarm-ship",					OP_AI_DISARM_SHIP,						2,	4,			SEXP_GOAL_OPERATOR,	},
	{ "ai-disarm-ship-tactical",		OP_AI_DISARM_SHIP_TACTICAL,				2,	4,			SEXP_GOAL_OPERATOR, },
	{ "ai-warp",						OP_AI_WARP,								2,	2,			SEXP_GOAL_OPERATOR,	},
	{ "ai-warp-out",					OP_AI_WARP_OUT,							1,	1,			SEXP_GOAL_OPERATOR,	},
	{ "ai-dock",						OP_AI_DOCK,								4,	5,			SEXP_GOAL_OPERATOR,	},
	{ "ai-undock",						OP_AI_UNDOCK,							1,	2,			SEXP_GOAL_OPERATOR,	},
	{ "ai-rearm-repair",				OP_AI_REARM_REPAIR,						2,	2,			SEXP_GOAL_OPERATOR, },
	{ "ai-waypoints",					OP_AI_WAYPOINTS,						2,	5,			SEXP_GOAL_OPERATOR,	},
	{ "ai-waypoints-once",				OP_AI_WAYPOINTS_ONCE,					2,	5,			SEXP_GOAL_OPERATOR,	},
	{ "ai-ignore",						OP_AI_IGNORE,							2,	2,			SEXP_GOAL_OPERATOR,	},
	{ "ai-ignore-new",					OP_AI_IGNORE_NEW,						2,	2,			SEXP_GOAL_OPERATOR,	},
	{ "ai-form-on-wing",				OP_AI_FORM_ON_WING,						1,	5,			SEXP_GOAL_OPERATOR, },
	{ "ai-fly-to-ship",					OP_AI_FLY_TO_SHIP,						2,	5,			SEXP_GOAL_OPERATOR, },
	{ "ai-stay-near-ship",				OP_AI_STAY_NEAR_SHIP,					2,	5,			SEXP_GOAL_OPERATOR,	},
	{ "ai-evade-ship",					OP_AI_EVADE_SHIP,						2,	2,			SEXP_GOAL_OPERATOR,	},
	{ "ai-keep-safe-distance",			OP_AI_KEEP_SAFE_DISTANCE,				1,	1,			SEXP_GOAL_OPERATOR,	},
	{ "ai-stay-still",					OP_AI_STAY_STILL,						2,	3,			SEXP_GOAL_OPERATOR,	},
	{ "ai-play-dead",					OP_AI_PLAY_DEAD,						1,	1,			SEXP_GOAL_OPERATOR,	},
	{ "ai-play-dead-persistent",		OP_AI_PLAY_DEAD_PERSISTENT,				1,	1,			SEXP_GOAL_OPERATOR, },

	{ "goals",							OP_GOALS_ID,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},

	//Training Category
	{ "key-pressed",					OP_KEY_PRESSED,							1,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "key-reset",						OP_KEY_RESET,							1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "key-reset-multiple",				OP_KEY_RESET_MULTIPLE,					1,	INT_MAX,	SEXP_ACTION_OPERATOR,	},
	{ "ignore-key",						OP_IGNORE_KEY,							2,	INT_MAX,	SEXP_ACTION_OPERATOR,	},	// Karajorma
	{ "targeted",						OP_TARGETED,							1,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "node-targeted",					OP_NODE_TARGETED,						1,	2,			SEXP_BOOLEAN_OPERATOR,	},	// FUBAR
	{ "missile-locked",					OP_MISSILE_LOCKED,						1,	3,			SEXP_BOOLEAN_OPERATOR,	},	// Sesquipedalian
	{ "speed",							OP_SPEED,								1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "facing",							OP_FACING,								2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "facing-waypoint",				OP_FACING2,								2,	2,			SEXP_BOOLEAN_OPERATOR,	},
	{ "order",							OP_ORDER,								2,	3,			SEXP_BOOLEAN_OPERATOR,	},
	{ "query-orders",					OP_QUERY_ORDERS,						3,	6,			SEXP_BOOLEAN_OPERATOR,	}, // Karajorma
	{ "reset-orders",					OP_RESET_ORDERS,						0,	0,			SEXP_ACTION_OPERATOR,	}, // Karajorma
	{ "waypoint-missed",				OP_WAYPOINT_MISSED,						0,	0,			SEXP_BOOLEAN_OPERATOR,	},
	{ "waypoint-twice",					OP_WAYPOINT_TWICE,						0,	0,			SEXP_BOOLEAN_OPERATOR,	},
	{ "path-flown",						OP_PATH_FLOWN,							0,	0,			SEXP_BOOLEAN_OPERATOR,	},
	{ "training-msg",					OP_TRAINING_MSG,						1,	4,			SEXP_ACTION_OPERATOR,	},
	{ "flash-hud-gauge",				OP_FLASH_HUD_GAUGE,						1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "primaries-depleted",				OP_PRIMARIES_DEPLETED,					1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "secondaries-depleted",			OP_SECONDARIES_DEPLETED,				1,	1,			SEXP_BOOLEAN_OPERATOR,	},
	{ "special-check",					OP_SPECIAL_CHECK,						1,	1,			SEXP_ACTION_OPERATOR,	},
	{ "set-training-context-fly-path",	OP_SET_TRAINING_CONTEXT_FLY_PATH,		2,	2,			SEXP_ACTION_OPERATOR,	},
	{ "set-training-context-speed",		OP_SET_TRAINING_CONTEXT_SPEED,			2,	2,			SEXP_ACTION_OPERATOR,	},
};
SCP_vector<int> Sorted_operator_indexes;
size_t Max_operator_length = 0;

sexp_ai_goal_link Sexp_ai_goal_links[] = {
	{ AI_GOAL_CHASE, OP_AI_CHASE },
	{ AI_GOAL_CHASE_WING, OP_AI_CHASE_WING },
	{ AI_GOAL_CHASE_SHIP_CLASS, OP_AI_CHASE_SHIP_CLASS },
	{ AI_GOAL_CHASE_SHIP_TYPE, OP_AI_CHASE_SHIP_TYPE},
	{ AI_GOAL_CHASE_ANY, OP_AI_CHASE_ANY },
	{ AI_GOAL_DOCK, OP_AI_DOCK },
	{ AI_GOAL_UNDOCK, OP_AI_UNDOCK },
	{ AI_GOAL_WARP, OP_AI_WARP_OUT },
	{ AI_GOAL_WARP, OP_AI_WARP },
	{ AI_GOAL_WAYPOINTS, OP_AI_WAYPOINTS },
	{ AI_GOAL_WAYPOINTS_ONCE, OP_AI_WAYPOINTS_ONCE },
	{ AI_GOAL_DESTROY_SUBSYSTEM, OP_AI_DESTROY_SUBSYS },
	{ AI_GOAL_DISABLE_SHIP, OP_AI_DISABLE_SHIP },
	{ AI_GOAL_DISABLE_SHIP_TACTICAL, OP_AI_DISABLE_SHIP_TACTICAL },
	{ AI_GOAL_DISARM_SHIP, OP_AI_DISARM_SHIP },
	{ AI_GOAL_DISARM_SHIP_TACTICAL, OP_AI_DISARM_SHIP_TACTICAL },
	{ AI_GOAL_GUARD, OP_AI_GUARD },
	{ AI_GOAL_GUARD_WING, OP_AI_GUARD_WING },
	{ AI_GOAL_EVADE_SHIP, OP_AI_EVADE_SHIP },
	{ AI_GOAL_STAY_NEAR_SHIP, OP_AI_STAY_NEAR_SHIP },
	{ AI_GOAL_KEEP_SAFE_DISTANCE, OP_AI_KEEP_SAFE_DISTANCE },
	{ AI_GOAL_IGNORE, OP_AI_IGNORE },
	{ AI_GOAL_IGNORE_NEW, OP_AI_IGNORE_NEW },
	{ AI_GOAL_STAY_STILL, OP_AI_STAY_STILL },
	{ AI_GOAL_PLAY_DEAD, OP_AI_PLAY_DEAD },
	{ AI_GOAL_PLAY_DEAD_PERSISTENT, OP_AI_PLAY_DEAD_PERSISTENT },
	{ AI_GOAL_FORM_ON_WING, OP_AI_FORM_ON_WING },
	{ AI_GOAL_FLY_TO_SHIP, OP_AI_FLY_TO_SHIP },
	{ AI_GOAL_REARM_REPAIR, OP_AI_REARM_REPAIR },
};

SCP_vector<dynamic_sexp_enum_list> Dynamic_enums;

SCP_vector<dynamic_sexp_parameter_list> Dynamic_parameters;

SCP_vector<dynamic_sexp_child_enum_suffixes> Dynamic_enum_suffixes;

int get_dynamic_parameter_index(const std::string& op_name, int param)
{
	auto it = std::find_if(Dynamic_parameters.begin(), Dynamic_parameters.end(), [&op_name](const auto& this_param) {
		return lcase_equal(this_param.operator_name, op_name);
	});

	if (it != Dynamic_parameters.end()) {
		for (const auto& param_pair : it->parameter_map) {
			if (param == param_pair.first) {
				// If it's a pos number return it, else return the previous param inde
				return param_pair.second >= 0 ? param_pair.second : param - 1;
			}
		}

		// If we don't have an exact match then we're probably in $Repeat territory so let's return the last parent
		// parameter in the list.
		const auto& last_param_pair = it->parameter_map.back();
		// If it's a pos number return it, else return the previous param inde
		return last_param_pair.second >= 0 ? last_param_pair.second : param - 1;
	}

	// Didn't find anything.
	return -1;
}

// Gets the custom suffix to append when using child enums. If we have a match
// for operator name and parameter index, then return the suffix. Otherwise return
// an empty string
SCP_string get_child_enum_suffix(const SCP_string& op_name, int param_index)
{
	for (size_t i = 0; i < Dynamic_enum_suffixes.size(); i++) {
		if (lcase_equal(Dynamic_enum_suffixes[i].operator_name, op_name)) {
			if (Dynamic_enum_suffixes[i].param_index == param_index) {
				return Dynamic_enum_suffixes[i].suffix;
			}
		}
	}
	return "";
}

int get_dynamic_enum_position(const SCP_string &enum_name)
{
	for (int i = 0; i < (int)Dynamic_enums.size(); i++) {
		if (lcase_equal(enum_name, Dynamic_enums[i].name)) {
			return i;
		}
	}

	// Didn't find anything.
	return -1;
}

void sexp_set_skybox_model_preload(const char *name); // taylor
int Num_skybox_flags = 6;
const char *Skybox_flags[] = {
	"force-clamp",
	"add-lighting",
	"no-transparency",
	"add-zbuffer",
	"add-culling",
	"no-glowmaps",
};

int	Directive_count;
int	Sexp_useful_number = 1;  // a variable to pass useful info in from external modules
bool Assume_event_is_current = true;
int	Locked_sexp_true = -1;
int	Locked_sexp_false = -1;
int	Num_sexp_ai_goal_links = sizeof(Sexp_ai_goal_links) / sizeof(sexp_ai_goal_link);
int	Sexp_clipboard = -1;  // used by Fred

// If you edit this, make sure this is greater than zero,
// so that we don't have to write pointless asserts. :)
#define SEXP_NODE_INCREMENT	250

int Num_sexp_nodes = 0;
sexp_node *Sexp_nodes = nullptr;

sexp_variable Sexp_variables[MAX_SEXP_VARIABLES];
sexp_variable Block_variables[MAX_SEXP_VARIABLES];			// used for compatibility with retail. 

int Num_special_expl_blocks;

SCP_vector<int> Current_sexp_operator;

// for sexp_fade
static int Fade_out_r = 0;
static int Fade_out_g = 0;
static int Fade_out_b = 0;

// for is-true-for-duration
SCP_vector<fix> Sexp_is_true_for_duration_times;

// for play-music - Goober5000
SCP_vector<int>	Sexp_music_handles;		// All handles used by all invocations of play-sound-from-file.  The default handle is in index 0.

// for sound environments - Goober5000/Taylor
#define SEO_VOLUME		0
#define SEO_DECAY_TIME	1
#define SEO_DAMPING		2
int sexp_sound_environment_option_lookup(const char *text);
const char *Sound_environment_option[] = { "volume", "decay time", "damping" };
int Num_sound_environment_options = 3;

// for adjust-audio-volume - The E
const char *Adjust_audio_options[] = { "Music", "Voice", "Effects" };
int Num_adjust_audio_options = 3;
int audio_volume_option_lookup(const char *text);

int hud_gauge_type_lookup(const char* name);

// for explosions - Goober5000
#define EO_DAMAGE			0
#define EO_BLAST			1
#define EO_INNER_RADIUS		2
#define EO_OUTER_RADIUS		3
#define EO_SHOCKWAVE_SPEED	4
#define EO_DEATH_ROLL_TIME	5
int sexp_explosion_option_lookup(const char *text);
const char *Explosion_option[] = { "damage", "blast", "inner radius", "outer radius", "shockwave speed", "death roll time" };
int Num_explosion_options = 6;

#define FWET_RETVAL_FIRST	0
#define FWET_RETVAL_LAST	1
int sexp_functional_when_eval_type_lookup(const char *text);
const char *Functional_when_eval_type[] = { "return-value-first", "return-value-last" };
int Num_functional_when_eval_types = 2;

int get_sexp();
void build_extended_sexp_string(SCP_string &accumulator, int cur_node, int level, int mode);
void update_sexp_references(const char *old_name, const char *new_name, int format, int node);
int sexp_determine_team(const char *subj);
void init_sexp_vars();

// for handling variables
void add_block_variable(const char *text, const char *var_name, int type, int index);
void sexp_modify_variable(int node);
int sexp_get_variable_by_index(int node);
void sexp_set_variable_by_index(int node);
void sexp_copy_variable_from_index(int node);
void sexp_copy_variable_between_indexes(int node);

int verify_vector(const char *text);
bool is_descendant_of_when_argument_op(int node);


#define ARG_ITEM_F_DUP	(1<<0)

// Goober5000 - adapted from sexp_list_item in Sexp_tree.h
struct arg_item
{
	const char *text = nullptr;
	int node = -1;

	arg_item *next = nullptr;
	int flags = 0;
	int nesting_level = 0;

	arg_item() = default;

	void add_data(const char *str, int n);
	void add_data(const std::pair<const char*, int> &data);
	void add_data_dup(const char *str, int n);
	void add_data_dup(const std::pair<const char*, int> &data);
	void add_data_set_dup(const char *str, int n);
	void add_data_set_dup(const std::pair<const char*, int> &data);
	void expunge();
	int is_empty();
	arg_item *get_next();
	void clear_nesting_level(); 
};

arg_item Sexp_applicable_argument_list;
SCP_vector<std::pair<const char*, int>> Sexp_replacement_arguments;
int Sexp_current_argument_nesting_level;


//Karajorma
int get_generic_subsys(const char *subsys_name);
bool ship_class_unchanged(const ship_registry_entry *ship_entry);
void multi_sexp_modify_variable();

// jg18
// container_value_index is for using one specific value from a container
int copy_node_to_replacement_args(int node, int container_value_index = -1);
// checks whether the node's value can change over the course of the mission
bool is_node_value_dynamic(int node);
// returns 0 on success or a SEXP_CHECK_* value on failure
int check_dynamic_value_node_type(int node, bool is_string, bool is_number);

#define NO_OPERATOR_INDEX_DEFINED		-2
#define NOT_A_SEXP_OPERATOR				-1

// hud-display-gauge magic values
#define SEXP_HUD_GAUGE_WARPOUT "warpout"

// event log stuff
SCP_vector<SCP_string> *Current_event_log_buffer;
SCP_vector<SCP_string> *Current_event_log_variable_buffer;
SCP_vector<SCP_string> *Current_event_log_container_buffer;
SCP_vector<SCP_string> *Current_event_log_argument_buffer;

// Goober5000 - arg_item class stuff, borrowed from sexp_list_item class stuff -------------
void arg_item::add_data(const char *str, int n)
{
	arg_item *item, *ptr;

	// create item
	item = new arg_item();
	item->text = str;
	item->node = n;
	item->nesting_level = Sexp_current_argument_nesting_level;

	// prepend item to existing list
	ptr = this->next;
	this->next = item;
	item->next = ptr;
}

void arg_item::add_data(const std::pair<const char*, int> &data)
{
	add_data(data.first, data.second);
}

void arg_item::add_data_dup(const char *str, int n)
{
	arg_item *item, *ptr;

	// create item
	item = new arg_item();
	item->text = vm_strdup(str);
	item->flags |= ARG_ITEM_F_DUP;
	item->node = n;
	item->nesting_level = Sexp_current_argument_nesting_level;

	// prepend item to existing list
	ptr = this->next;
	this->next = item;
	item->next = ptr;
}

void arg_item::add_data_dup(const std::pair<const char*, int> &data)
{
	add_data_dup(data.first, data.second);
}

void arg_item::add_data_set_dup(const char *str, int n)
{
	arg_item *item, *ptr;

	// create item
	item = new arg_item();
	item->text = str;
	item->flags |= ARG_ITEM_F_DUP;
	item->node = n;
	item->nesting_level = Sexp_current_argument_nesting_level;

	// prepend item to existing list
	ptr = this->next;
	this->next = item;
	item->next = ptr;
}

void arg_item::add_data_set_dup(const std::pair<const char*, int> &data)
{
	add_data_set_dup(data.first, data.second);
}

arg_item* arg_item::get_next()
{
	if (this->next != nullptr) {
		if (this->next->nesting_level >= Sexp_current_argument_nesting_level) {
			return this->next; 
		}
	}

	return nullptr;
}

void arg_item::expunge()
{
	arg_item *ptr;

	// contiually delete first item of list
	while (this->next != nullptr)
	{
		ptr = this->next->next;

		if (this->next->flags & ARG_ITEM_F_DUP)
			vm_free(const_cast<char*>(this->next->text));
		delete this->next;

		this->next = ptr;
	}
}

void arg_item::clear_nesting_level()
{
	arg_item *ptr;

	// contiually delete first item of list
	while (this->next != nullptr && this->next->nesting_level >= Sexp_current_argument_nesting_level )
	{
		ptr = this->next->next;

		if (this->next->flags & ARG_ITEM_F_DUP)
			vm_free(const_cast<char*>(this->next->text));
		delete this->next;

		this->next = ptr;
	}
}

int arg_item::is_empty()
{
	return (this->next == nullptr);
}
//-------------------------------------------------------------------------------------------------

void clear_cache(int node)
{
	// free anything cached
	if (Sexp_nodes[node].cache)
	{
		delete Sexp_nodes[node].cache;
		Sexp_nodes[node].cache = nullptr;
	}

	// note that cached_variable_index is not reset here because it is a parallel cache (c.f. sexp_get_variable_index)
}

void sexp_nodes_init()
{
	if (Num_sexp_nodes == 0 || Sexp_nodes == nullptr)
		return;

	nprintf(("SEXP", "Reinitializing sexp nodes...\n"));
	nprintf(("SEXP", "Entered function with %d nodes.\n", Num_sexp_nodes));

	// usually, the persistent nodes are grouped at the beginning of the array;
	// so we ought to be able to free all the subsequent nodes
	int i, last_persistent_node = -1;

	for (i = 0; i < Num_sexp_nodes; i++)
	{
		if (Sexp_nodes[i].type & SEXP_FLAG_PERSISTENT)
			last_persistent_node = i;					// keep track of it
		else
			Sexp_nodes[i].type = SEXP_NOT_USED;			// it's not needed

		// free anything cached
		clear_cache(i);
	}

	nprintf(("SEXP", "Last persistent node index is %d.\n", last_persistent_node));

	// if all the persistent nodes are gone, free all the nodes
	if (last_persistent_node == -1)
	{
		vm_free(Sexp_nodes);
		Sexp_nodes = nullptr;
		Num_sexp_nodes = 0;
	}
	// if there's enough of a difference to make it worthwhile, free some nodes
	else if (Num_sexp_nodes - (last_persistent_node + 1) > 2 * SEXP_NODE_INCREMENT)
	{
		// round it up to the next evenly divisible size
		Num_sexp_nodes = (last_persistent_node + 1);
		Num_sexp_nodes += SEXP_NODE_INCREMENT - (Num_sexp_nodes % SEXP_NODE_INCREMENT);

		Sexp_nodes = (sexp_node *) vm_realloc(Sexp_nodes, sizeof(sexp_node) * Num_sexp_nodes);
		Verify(Sexp_nodes != nullptr);
	}

	nprintf(("SEXP", "Exited function with %d nodes.\n", Num_sexp_nodes));
}

static void sexp_nodes_close()
{
	// free all sexp nodes... should only be done on game shutdown
	if (Sexp_nodes != nullptr)
	{
		// free anything cached
		for (int i = 0; i < Num_sexp_nodes; i++)
			clear_cache(i);

		vm_free(Sexp_nodes);
		Sexp_nodes = nullptr;
		Num_sexp_nodes = 0;
	}
}

// done at the beginning of each mission
void init_sexp()
{
	// Goober5000
	Sexp_replacement_arguments.clear();
	Sexp_applicable_argument_list.expunge();
	Sexp_current_argument_nesting_level = 0;
	Current_sexp_network_packet.initialize();

	sexp_nodes_init();
	init_sexp_vars();
	init_sexp_containers();
	Locked_sexp_false = Locked_sexp_true = -1;

	Locked_sexp_false = alloc_sexp("false", SEXP_LIST, SEXP_ATOM_OPERATOR, -1, -1);
	Assert(Locked_sexp_false != -1);
	Sexp_nodes[Locked_sexp_false].type = SEXP_ATOM;  // fix bypassing value
	Sexp_nodes[Locked_sexp_false].value = SEXP_KNOWN_FALSE;

	Locked_sexp_true = alloc_sexp("true", SEXP_LIST, SEXP_ATOM_OPERATOR, -1, -1);
	Assert(Locked_sexp_true != -1);
	Sexp_nodes[Locked_sexp_true].type = SEXP_ATOM;  // fix bypassing value
	Sexp_nodes[Locked_sexp_true].value = SEXP_KNOWN_TRUE;

	// init fade_out colors
	Fade_out_r = 0;
	Fade_out_g = 0;
	Fade_out_b = 0;

	// init data structures used by certain operators
	// (note, Sexp_music_handles are not cleared here because sexp_music_close() handled that at the end of the previous mission)
	Sexp_is_true_for_duration_times.clear();
}

// done at the beginning of the game
void sexp_startup()
{
#ifndef NDEBUG
	// sanity check categories and subcategories for all operators
	for (const auto& op : Operators)
	{
		int subcategory = get_subcategory(op.value);

		if (subcategory != OP_SUBCATEGORY_NONE)
		{
			int category = get_category(op.value);
			int implied_category = category_of_subcategory(subcategory);

			Assertion(category == implied_category, "Operator %s has a category that is not a parent of its subcategory!", op.text.c_str());
		}
	}
#endif

	sexp::dynamic_sexp_init();

	// sort all operators case-insensitively by name
	Sorted_operator_indexes.clear();
	Sorted_operator_indexes.reserve(Operators.size());
	Max_operator_length = 0;

	for (int i = 0; i < (int)Operators.size(); ++i)
	{
		Sorted_operator_indexes.push_back(i);
		auto len = Operators[i].text.length();
		if (len > Max_operator_length)
			Max_operator_length = len;
	}

	std::sort(Sorted_operator_indexes.begin(), Sorted_operator_indexes.end(), [](const int &index_a, const int &index_b)
		{
			const auto &op_a = Operators[index_a];
			const auto &op_b = Operators[index_b];
			return lcase_lessthan(op_a.text, op_b.text);
		});
}

void sexp_shutdown()
{
	sexp_nodes_close();

	sexp::dynamic_sexp_shutdown();
}

/**
 * Allocate an sexp node.
 */
int alloc_sexp(const char *text, int type, int subtype, int first, int rest)
{
	int node;
	int sexp_const = get_operator_const(text);

	if ((sexp_const == OP_TRUE) && (type == SEXP_ATOM) && (subtype == SEXP_ATOM_OPERATOR))
		return Locked_sexp_true;

	else if ((sexp_const == OP_FALSE) && (type == SEXP_ATOM) && (subtype == SEXP_ATOM_OPERATOR))
		return Locked_sexp_false;

	node = find_free_sexp();

	// need more sexp nodes?
	if (node == Num_sexp_nodes || node == -1)
	{
		int old_size = Num_sexp_nodes;

		// allocate in blocks of SEXP_NODE_INCREMENT
		Num_sexp_nodes += SEXP_NODE_INCREMENT;
		Sexp_nodes = (sexp_node *) vm_realloc(Sexp_nodes, sizeof(sexp_node) * Num_sexp_nodes);

		Verify(Sexp_nodes != nullptr);
		nprintf(("SEXP", "Bumping dynamic sexp node limit from %d to %d...\n", old_size, Num_sexp_nodes));

		// clear all the new sexp nodes we just allocated
		memset(&Sexp_nodes[old_size], 0, sizeof(sexp_node) * SEXP_NODE_INCREMENT); //-V512

		// our new sexp is the first out of the ones we just created
		node = old_size;
	}

	Assert(node != Locked_sexp_true);
	Assert(node != Locked_sexp_false);
	Assert(strlen(text) < TOKEN_LENGTH);
	Assert(type >= 0);

	strcpy_s(Sexp_nodes[node].text, text);
	Sexp_nodes[node].type = type;
	Sexp_nodes[node].subtype = subtype;
	Sexp_nodes[node].first = first;
	Sexp_nodes[node].rest = rest;
	Sexp_nodes[node].value = SEXP_UNKNOWN;
	Sexp_nodes[node].flags = SNF_DEFAULT_VALUE;
	Sexp_nodes[node].op_index = NO_OPERATOR_INDEX_DEFINED;
	Sexp_nodes[node].cache = nullptr;
	Sexp_nodes[node].cached_variable_index = -1;
	Sexp_nodes[node].duration_index = -1;

	// special-arg?
	if (type == SEXP_ATOM && !strcmp(text, SEXP_ARGUMENT_STRING))
		Sexp_nodes[node].flags |= SNF_SPECIAL_ARG_IN_NODE;

	return node;
}

static int Sexp_hwm = 0;

int count_free_sexp_nodes()
{
	int i, f = 0, p = 0;

	for (i = 0; i < Num_sexp_nodes; i++)
	{
		if (Sexp_nodes[i].type == SEXP_NOT_USED)
			f++;
		else if (Sexp_nodes[i].type & SEXP_FLAG_PERSISTENT)
			p++;
	}

	if (Num_sexp_nodes - f > Sexp_hwm)
	{
		nprintf(("Sexp", "Sexp nodes: Free=%d, Used=%d, Persistent=%d\n", f, Num_sexp_nodes - f, p));
		Sexp_hwm = Num_sexp_nodes - f;
	}

	return f;
}

/**
 * Find the next free sexp and return its index.
 */
int find_free_sexp()
{
	int i;

	// sanity
	if (Num_sexp_nodes == 0 || Sexp_nodes == nullptr)
		return -1;

	for (i = 0; i < Num_sexp_nodes; i++)
	{
		if (Sexp_nodes[i].type == SEXP_NOT_USED)
			return i;
	}

	return -1;
}

/**
 * Mark a whole sexp tree with the persistent flag so that it won't get re-used between missions
 */
void sexp_mark_persistent(int n)
{
	if (n == -1){
		return;
	}

	// total hack because of the true/false locked sexps -- we should make those persistent as well
	if ( (n == Locked_sexp_true) || (n == Locked_sexp_false) ){
		return;
	}

	Assert( !(Sexp_nodes[n].type & SEXP_FLAG_PERSISTENT) );
	Sexp_nodes[n].type |= SEXP_FLAG_PERSISTENT;

	sexp_mark_persistent(Sexp_nodes[n].first);
	sexp_mark_persistent(Sexp_nodes[n].rest);

}

/**
 * Remove the persistent flag from all nodes in the tree
 */
void sexp_unmark_persistent(int n)
{
	if (n == -1){
		return;
	}

	// see sexp_mark_persistent
	if ( (n == Locked_sexp_true) || (n == Locked_sexp_false) ){
		return;
	}

	Assert( Sexp_nodes[n].type & SEXP_FLAG_PERSISTENT );
	Sexp_nodes[n].type &= ~SEXP_FLAG_PERSISTENT;

	sexp_unmark_persistent(Sexp_nodes[n].first);
	sexp_unmark_persistent(Sexp_nodes[n].rest);
}

/**
 * Free up the specified sexp node,  Leaves link chains untouched.
 */
int free_one_sexp(int num)
{
	Assert(Fred_running);
	Assert((num >= 0) && (num < Num_sexp_nodes));
	Assert(Sexp_nodes[num].type != SEXP_NOT_USED);  // make sure it is actually used
	Assert(!(Sexp_nodes[num].type & SEXP_FLAG_PERSISTENT));

	// never free these nodes
	if ((num == Locked_sexp_true) || (num == Locked_sexp_false))
		return 0;

	Sexp_nodes[num].type = SEXP_NOT_USED;
	clear_cache(num);
	return 1;
}

/**
 * Free a used sexp node, so it can be reused later.  
 *
 * Should only be called on an atom or a list, and not an operator.  If on a list, the 
 * list and everything in it will be freed (including the operator).
 * calling_node defaults to -1
 */
int free_sexp(int num, int calling_node)
{
	int i, rest, count = 0;

	Assert((num >= 0) && (num < Num_sexp_nodes));
	Assert(Sexp_nodes[num].type != SEXP_NOT_USED);  // make sure it is actually used
	Assert(!(Sexp_nodes[num].type & SEXP_FLAG_PERSISTENT));

	// never free these nodes
	if ((num == -1) || (num == Locked_sexp_true) || (num == Locked_sexp_false))
		return 0;

	Sexp_nodes[num].type = SEXP_NOT_USED;
	clear_cache(num);
	count++;

	i = Sexp_nodes[num].first;
	while (i != -1) 
	{
		rest = Sexp_nodes[i].rest;
		count += free_sexp(i, num);
		i = rest;
	}

	rest = Sexp_nodes[num].rest;
	if (calling_node >= 0) 
	{
		if (Sexp_nodes[calling_node].first == num)
			Sexp_nodes[calling_node].first = rest;

		if (Sexp_nodes[calling_node].rest == num)
			Sexp_nodes[calling_node].rest = rest;
	}

	return count;  // total elements freed up.
}

/**
 * Free up an entire sexp tree.  
 * 
 * Because the root node is an operator, instead of a list, we can't simply call free_sexp().  
 * This function should only be called on the root node of an sexp, otherwise the linking will get screwed up.
 */
int free_sexp2(int num, int calling_node)
{	
	int i, rest, count = 0;

	Assert((num >= 0) && (num < Num_sexp_nodes));
	Assert(Sexp_nodes[num].type != SEXP_NOT_USED);  // make sure it is actually used
	Assert(!(Sexp_nodes[num].type & SEXP_FLAG_PERSISTENT));

	// never free these nodes
	if ((num == -1) || (num == Locked_sexp_true) || (num == Locked_sexp_false)){
		return 0;
	}

	i = Sexp_nodes[num].rest;
	while (i != -1) {
		rest = Sexp_nodes[i].rest;
		count += free_sexp(i, num);
		i = rest;
	}

	count += free_sexp(num, calling_node);
	return count;
}

/**
 * Reset the status of all the nodes in a tree, forcing them to all be evaluated again.
 */
void flush_sexp_tree(int node)
{
	if (node < 0){
		return;
	}

	Sexp_nodes[node].value = SEXP_UNKNOWN;
	clear_cache(node);
	Sexp_nodes[node].cached_variable_index = -1;
	Sexp_nodes[node].duration_index = -1;

	flush_sexp_tree(Sexp_nodes[node].first);
	flush_sexp_tree(Sexp_nodes[node].rest);
}

int verify_sexp_tree(int node)
{
	if (node == -1){
		return 0;
	}

	if ((Sexp_nodes[node].type == SEXP_NOT_USED) ||
		(Sexp_nodes[node].first == node) ||
		(Sexp_nodes[node].rest == node)) {
		Error(LOCATION, "Sexp node is corrupt");
		return -1;
	}

	if (Sexp_nodes[node].first != -1){
		verify_sexp_tree(Sexp_nodes[node].first);
	}
	if (Sexp_nodes[node].rest != -1){
		verify_sexp_tree(Sexp_nodes[node].rest);
	}

	return 0;
}

/**
 * @todo CASE OF SEXP VARIABLES - ONLY 1 COPY OF VARIABLE
 */
int dup_sexp_chain(int node)
{
	int cur, first, rest;

	if (node == -1){
		return -1;
	}

	// TODO - CASE OF SEXP VARIABLES - ONLY 1 COPY OF VARIABLE
	first = dup_sexp_chain(Sexp_nodes[node].first);
	rest = dup_sexp_chain(Sexp_nodes[node].rest);
	cur = alloc_sexp(Sexp_nodes[node].text, Sexp_nodes[node].type, Sexp_nodes[node].subtype, first, rest);

	if (cur == -1) {
		if (first != -1){
			free_sexp(first);
		}
		if (rest != -1){
			free_sexp(rest);
		}
	}

	return cur;
}

/**
 * Compare SEXP chains
 * @return 1 if they are the same, 0 if different
 */
int cmp_sexp_chains(int node1, int node2)
{
	if ((node1 == -1) && (node2 == -1)){
		return 1;
	}

	if ((node1 == -1) || (node2 == -1)){
		return 0;
	}

	// DA: 1/7/99 Need to check the actual Sexp_node.text, not possible variable, which can be equal
	if (stricmp(Sexp_nodes[node1].text, Sexp_nodes[node2].text) != 0){
		return 0;
	}

	if (!cmp_sexp_chains(Sexp_nodes[node1].first, Sexp_nodes[node2].first)){
		return 0;
	}

	if (!cmp_sexp_chains(Sexp_nodes[node1].rest, Sexp_nodes[node2].rest)){
		return 0;
	}

	return 1;
}

/**
 * Determine if an sexp node is within the given sexp chain.
 */
int query_node_in_sexp(int node, int sexp)
{
	if (sexp == -1){
		return 0;
	}
	if (node == sexp){
		return 1;
	}

	if (query_node_in_sexp(node, Sexp_nodes[sexp].first)){
		return 1;
	}
	if (query_node_in_sexp(node, Sexp_nodes[sexp].rest)){
		return 1;
	}

	return 0;
}

/**
 * Find the index of the list associated with an operator
 */
int find_sexp_list(int num)
{
	int i;

	for (i = 0; i < Num_sexp_nodes; i++)
	{
		if (Sexp_nodes[i].first == num)
			return i;
	}

	// not found
	return -1;
}

/**
 * Find node of operator that item is an argument of.
 */
int find_parent_operator(int node)
{
	int i;
	Assert((node >= 0) && (node < Num_sexp_nodes));

	if (Sexp_nodes[node].subtype == SEXP_ATOM_OPERATOR)
	{
		node = find_sexp_list(node);

		// are we already at the top of the list?  this will happen for non-standard sexps
		// (sexps that fire instantly instead of using a conditional) such as:
		// $Formula: ( do-nothing ) 
		if (node < 0)
			return -1;
	}

	// iterate backwards through the sexps nodes (i.e. do the inverse of CDR)
	while (Sexp_nodes[node].subtype != SEXP_ATOM_OPERATOR)
	{
		for (i = 0; i < Num_sexp_nodes; i++)
		{
			if (Sexp_nodes[i].rest == node)
				break;
		}

		if (i == Num_sexp_nodes)
			return -1;  // not found, probably at top node already.

		node = i;
	}

	return node;
}

/**
 * Determine if an sexpression node is the top level node of an sexpression tree.
 *
 * Top level nodes do not have their node id in anyone elses first or rest index.
 */
int is_sexp_top_level( int node )
{
	int i;

	Assert((node >= 0) && (node < Num_sexp_nodes));

	if (Sexp_nodes[node].type == SEXP_NOT_USED)
		return 0;

	for (i = 0; i < Num_sexp_nodes; i++)
	{
		if ((Sexp_nodes[i].type == SEXP_NOT_USED) || (i == node ))				// don't check myself or unused nodes
			continue;

		if ((Sexp_nodes[i].first == node) || (Sexp_nodes[i].rest == node))
			return 0;
	}

	return 1;
}

/**
 * Find argument number
 */
int find_argnum(int parent_node, int arg_node)
{
	int n, tally;
	Assertion((parent_node >= 0) && (parent_node < Num_sexp_nodes), "find_argnum was passed an invalid parent!");
	Assertion((arg_node >= 0) && (arg_node < Num_sexp_nodes), "find_argnum was passed an invalid child!");

	n = CDR(parent_node);
	tally = 0;

	while (n >= 0)
	{
		// check if there is an operator node at this position that matches our expected node
		if (CAR(n) == arg_node)
			return tally;

		// check if there is a regular node at this position that matches our expected node
		if (n == arg_node)
			return tally;

		tally++;
		n = CDR(n);
	}

	// argument node not found
	return -1;
}

/**
 * From an operator name, return its index in the array Operators
 */
int get_operator_index(const char *token)
{
	Assertion(token != nullptr, "get_operator_index(char*) called with a null token; get a coder!\n");

	for (size_t i=0; i < Operators.size(); i++){
		if (Operators[i].text == token){
			return (int)i;
		}
	}

	return NOT_A_SEXP_OPERATOR;
}

/**
 * From a sexp node, return the index in the array Operators or NOT_A_SEXP_OPERATOR if not an operator
 */
int get_operator_index(int node)
{
	Assertion(node >= 0 && node < Num_sexp_nodes, "Passed an out-of-range node index (%d) to get_operator_index(int)!", node);
	if (node < 0 || node >= Num_sexp_nodes)
		return NOT_A_SEXP_OPERATOR;

	if (!Fred_running && (Sexp_nodes[node].op_index != NO_OPERATOR_INDEX_DEFINED) ) {
		return Sexp_nodes[node].op_index;
	}

	int index = get_operator_index(Sexp_nodes[node].text); 
	Sexp_nodes[node].op_index = index;
	return index;
}

/**
 * From an operator name, return its constant (the number it was define'd with)
 */
int get_operator_const(const char *token)
{
	int	idx = get_operator_index(token);

	if (idx == NOT_A_SEXP_OPERATOR)
		return OP_NOT_AN_OP;

	return Operators[idx].value;
}

int get_operator_const(int node)
{
	Assertion(node >= 0 && node < Num_sexp_nodes, "Passed an out-of-range node index (%d) to get_operator_const(int)!", node);
	if (node < 0 || node >= Num_sexp_nodes)
		return OP_NOT_AN_OP;

	if (!Fred_running && Sexp_nodes[node].op_index >= 0) {
		return Operators[Sexp_nodes[node].op_index].value;
	}

	int	idx = get_operator_index(node);

	if (idx == NOT_A_SEXP_OPERATOR)
		return OP_NOT_AN_OP;

	return Operators[idx].value;
}

int find_operator_index(int op_const)
{
	for (int i = 0; i < (int)Operators.size(); ++i)
		if (Operators[i].value == op_const)
			return i;

	return -1;
}

int query_sexp_args_count(int node, bool only_valid_args = false)
{
	int count = 0;
	int n = CDR(node);

	for ( ; n != -1; n = CDR(n))
	{
		if (only_valid_args && !(Sexp_nodes[n].flags & SNF_ARGUMENT_VALID))
			continue;

		count++;
	}

	return count;
}

/**
 * Needed to fix bug with sexps like send-message list which have arguments that need to be supplied as a block
 * 
 * @return whether the number of arguments for the supplied operation is correct
 */
static bool check_operator_argument_count(int count, int op_index)
{
	Assertion(op_index >= 0 && op_index < sz2i(Operators.size()), "op_index is out of range!");

	if (count < Operators[op_index].min || count > Operators[op_index].max)
		return false;

	int op_const = Operators[op_index].value;

	// send-message-list has arguments as blocks of 4
	// same with send-message-chain, but there's an extra argument

	if (op_const == OP_SEND_MESSAGE_CHAIN)
		count--;

	if (op_const == OP_SEND_MESSAGE_LIST || op_const == OP_SEND_MESSAGE_CHAIN)
		if (count % 4 != 0)
			return false;

	return true;
}

// helper functions for check_container_value_data_type()
bool check_container_data_sexp_arg_type(ContainerType con_type, bool is_string, bool is_number)
{
	if (any(con_type & ContainerType::STRING_DATA)) {
		return is_string;
	} else if (any(con_type & ContainerType::NUMBER_DATA)) {
		return is_number;
	} else {
		UNREACHABLE("Unknown container data type %d", (int)con_type);
		return false;
	}
}

bool check_map_container_key_sexp_arg_type(ContainerType con_type, bool is_string, bool is_number)
{
	if (any(con_type & ContainerType::STRING_KEYS)) {
		return is_string;
	} else if (any(con_type & ContainerType::NUMBER_KEYS)) {
		return is_number;
	} else {
		UNREACHABLE("Unknown map container key type %d", (int)con_type);
		return false;
	}
}

// Checks "container value" SEXP arg type
// follows return value conventions of check_sexp_syntax()
int check_container_value_data_type(int op, int argnum, ContainerType con_type, bool is_string, bool is_number)
{
	switch (op) {
		case OP_LIST_HAS_DATA:
		case OP_LIST_DATA_INDEX:
		case OP_MAP_HAS_DATA_ITEM:
		case OP_CONTAINER_ADD_TO_LIST:
		case OP_CONTAINER_REMOVE_FROM_LIST:
			if (!check_container_data_sexp_arg_type(con_type, is_string, is_number)) {
				return SEXP_CHECK_WRONG_CONTAINER_DATA_TYPE;
			}
			break;

		case OP_MAP_HAS_KEY:
		case OP_CONTAINER_REMOVE_FROM_MAP:
			Assertion(any(con_type & ContainerType::MAP),
				"Attempt to check map SEXP with non-map container type %d. Please report!",
				(int)con_type);
			if (!check_map_container_key_sexp_arg_type(con_type, is_string, is_number)) {
				return SEXP_CHECK_WRONG_MAP_KEY_TYPE;
			}
			break;

		case OP_CONTAINER_ADD_TO_MAP:
			Assertion(any(con_type & ContainerType::MAP),
				"Attempt to check map SEXP with non-map container type %d. Please report!",
				(int)con_type);
			// since the first arg is a map container name, the first key is at argnum 1
			if (argnum % 2 != 0) {
				// map key
				if (!check_map_container_key_sexp_arg_type(con_type, is_string, is_number)) {
					return SEXP_CHECK_WRONG_MAP_KEY_TYPE;
				}
			} else {
				// map data
				if (!check_container_data_sexp_arg_type(con_type, is_string, is_number)) {
					return SEXP_CHECK_WRONG_CONTAINER_DATA_TYPE;
				}
			}
			break;

		default:
			UNREACHABLE("Unhandled container value-based operator %d", op);
			break;
	}

	return 0;
}

// SEXP type checking for variables and container data
bool check_data_type(int type, bool is_string, bool is_number)
{
	switch (type) {
		case OPF_NUMBER:
		case OPF_POSITIVE:
			return is_number;

		case OPF_AMBIGUOUS:
		case OPF_GAME_SND:
		case OPF_FIREBALL:
		case OPF_WEAPON_BANK_NUMBER:
			// either type is ok
			return true;

		default:
			return is_string;
	}
}

bool check_container_data_type(int type, ContainerType con_type, int op, int argnum, const sexp_container *p_value_container)
{
	const bool is_string = any(con_type & ContainerType::STRING_DATA);
	const bool is_number = any(con_type & ContainerType::NUMBER_DATA);
	if (type == OPF_CONTAINER_VALUE) {
		Assertion(p_value_container,
			"Attempt to check data type of null container for SEXP operator %d at arg %d. Please report!",
			op,
			argnum);
		return (check_container_value_data_type(op, argnum, p_value_container->type, is_string, is_number) == 0);
	} else {
		return check_data_type(type, is_string, is_number);
	}
}

bool check_variable_data_type(int type, int var_type, int op, int argnum, const sexp_container *p_value_container) {
	const bool is_string = (var_type & SEXP_VARIABLE_STRING);
	const bool is_number = (var_type & SEXP_VARIABLE_NUMBER);
	if (type == OPF_CONTAINER_VALUE) {
		Assertion(p_value_container,
			"Attempt to check data type of null container for SEXP operator %d for variable at arg %d. Please "
			"report!",
			op,
			argnum);
		return (check_container_value_data_type(op, argnum, p_value_container->type, is_string, is_number) == 0);
	} else {
		return check_data_type(type, is_string, is_number);
	}
}

bool is_special_sender(const char* name) {
	return name[0] == '#';
}

/**
 * Check SEXP syntax
 * @return 0 if ok, negative if there's an error in expression..
 * See the returns types in sexp.h
 */
int check_sexp_syntax(int node, int desired_return_type, int recursive, int *bad_node, sexp_mode mode)
{
	int i = 0, z, argnum = 0, desired_argument_type = OPF_NONE, node_subtype = -1, node_return_type = OPR_NONE;
	size_t count;
	int op_node;
	int var_index = -1;
	size_t st;
	const sexp_container *p_container = nullptr; // for SEXPs that take container name as arg

	Assertion(node >= 0 && node < Num_sexp_nodes, "Node %d must be a valid SEXP node!", node);
	Assertion(Sexp_nodes[node].type != SEXP_NOT_USED, "Node %d must be in use!", node);

	op_node = node;		// save the node of the operator since we need to get to other args.
	if (bad_node)
		*bad_node = op_node;

	if (Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER && desired_return_type == OPR_BOOL) {
		// special case Mark seems to want supported
		Assert(Sexp_nodes[node].first == -1);  // only lists should have a first pointer
		if (Sexp_nodes[node].rest != -1)  // anything after the number?
			return SEXP_CHECK_NONOP_ARGS; // if so, it's a syntax error

		return 0;
	}

	if (Sexp_nodes[op_node].subtype != SEXP_ATOM_OPERATOR)
		return SEXP_CHECK_OP_EXPECTED;  // not an operator, which it should always be

	int op_index = get_operator_index(op_node);
	int op_const = SCP_vector_inbounds(Operators, op_index) ? Operators[op_index].value : OP_NOT_AN_OP;
	if (op_const == OP_NOT_AN_OP)
		return SEXP_CHECK_UNKNOWN_OP;  // unrecognized operator

	// check that types match - except that OPR_AMBIGUOUS matches everything
	if (desired_return_type != OPR_AMBIGUOUS)
	{
		// get the return type of the next thing
		z = query_operator_return_type(op_const);
		if (z == OPR_POSITIVE && desired_return_type == OPR_NUMBER)
		{
			// positive data type can map to number data type just fine
		}
		// Goober5000's number hack
		else if (z == OPR_NUMBER && desired_return_type == OPR_POSITIVE)
		{
			// this isn't kosher, but we hack it to make it work
		}
		else if (z != desired_return_type)
		{
			// anything else is a mismatch
			return SEXP_CHECK_TYPE_MISMATCH;
		}
	}

	count = query_sexp_args_count(op_node);

	if (!check_operator_argument_count(sz2i(count), op_index))
		return SEXP_CHECK_BAD_ARG_COUNT;  // incorrect number of arguments

	node = Sexp_nodes[op_node].rest;
	while (node != -1) {
		desired_argument_type = query_operator_argument_type(op_index, argnum);
		Assertion(Sexp_nodes[node].type != SEXP_NOT_USED, "Node %d must be in use!", node);
		if (bad_node)
			*bad_node = node;
		node_subtype = Sexp_nodes[node].subtype;

		if (node_subtype == SEXP_ATOM_LIST) {
			i = Sexp_nodes[node].first;
			if (bad_node)
				*bad_node = i;
			
			// be sure to check to see if this node is a list of stuff and not an actual operator type
			// thing.  (i.e. in the case of a cond statement, the conditional will fall into this if
			// statement.  MORE TO DO HERE!!!!
			if (Sexp_nodes[i].subtype == SEXP_ATOM_LIST)
				return 0;

			int op2_index = get_operator_index(i);
			int op2_const = SCP_vector_inbounds(Operators, op2_index) ? Operators[op2_index].value : OP_NOT_AN_OP;
			if (op2_const == OP_NOT_AN_OP)
				return SEXP_CHECK_UNKNOWN_OP;

			node_return_type = query_operator_return_type(op2_const);
			if (recursive) {
				sexp_opr_t opr;
				if (!map_opf_to_opr((sexp_opf_t)desired_argument_type, opr)) {
					return SEXP_CHECK_UNKNOWN_TYPE;
				}

				if ((z = check_sexp_syntax(i, (int)opr, recursive, bad_node)) != 0) {
					return z;
				}
			}

		} else if (node_subtype == SEXP_ATOM_NUMBER) {
			node_return_type = OPR_POSITIVE;
			auto ptr = CTEXT(node);
			if (*ptr == '-') {
				node_return_type = OPR_NUMBER;
				ptr++;
			} else if (*ptr == '+') {
				ptr++;
			}

			if (desired_argument_type == OPF_BOOL)  // allow numbers to be used where boolean is required.
				node_return_type = OPR_BOOL;

			// Only check that this is a number if it's not <argument>.
			if (!(Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)) {
				while (*ptr) {
					if (*ptr == '.' || *ptr == ',')
						return SEXP_CHECK_MUST_BE_INTEGER;
					if (!isdigit(*ptr))
						return SEXP_CHECK_INVALID_NUM;  // not a valid number

					ptr++;
				}

				i = atoi(CTEXT(node));
				if ( (op_const == OP_HAS_DOCKED_DELAY) || (op_const == OP_HAS_UNDOCKED_DELAY) )
					if ( (argnum == 2) && (i < 1) )
						return SEXP_CHECK_NUM_RANGE_INVALID;

				// valid color range 0 to 255 - FUBAR
				if ((op_const == OP_CHANGE_IFF_COLOR)  && ((argnum >= 2) && (argnum <= 4)))
				{
					if ( i < 0 || i > 255) 
					{
						return SEXP_CHECK_NUM_RANGE_INVALID;
					}
				}

				if ( (query_operator_return_type(op_const) == OPR_AI_GOAL) && (argnum == Operators[op_index].min - 1) )
					if ( (i < 0) || (i > 200) )
						return SEXP_CHECK_NUM_RANGE_INVALID;
			}

		} else if (node_subtype == SEXP_ATOM_STRING) {
			;	// no special handling

		} else if (node_subtype == SEXP_ATOM_CONTAINER_NAME) {
			;	// no special handling

		} else if (node_subtype == SEXP_ATOM_CONTAINER_DATA) {
			// this is an instance of "Replace Container Data"

			// can't be used in special argument list
			if (is_argument_provider_op(op_const)) {
				return SEXP_CHECK_TYPE_MISMATCH;
			}

			const int modifier_node = Sexp_nodes[node].first;
			if (modifier_node == -1) {
				return SEXP_CHECK_MISSING_CONTAINER_MODIFIER;
			}

			const auto *p_data_container = get_sexp_container(Sexp_nodes[node].text);
			// name should have already been checked in get_sexp()
			Assertion(p_data_container,
				"Attempt to check type of container data for SEXP operator %s at arg %d for non-existent container %s. "
				"Please report!",
				Operators[op_index].text.c_str(),
				argnum,
				Sexp_nodes[node].text);
			const auto &data_container = *p_data_container;

			if (!check_container_data_type(desired_argument_type,
					data_container.type,
					op_const,
					argnum,
					p_container)) {
				return SEXP_CHECK_WRONG_CONTAINER_DATA_TYPE;
			}

			// ignore nested "Replace" uses
			if (!(Sexp_nodes[modifier_node].type & SEXP_FLAG_VARIABLE) &&
					(Sexp_nodes[modifier_node].subtype != SEXP_ATOM_CONTAINER_DATA)) {
				Assertion(Sexp_nodes[modifier_node].subtype != SEXP_ATOM_CONTAINER_NAME,
					"Attempt to use container name %s as modifier for container %s. Please report!",
					Sexp_nodes[modifier_node].text,
					Sexp_nodes[node].text);
				if (data_container.is_list()) {
					const auto modifier = get_list_modifier(Sexp_nodes[modifier_node].text);
					if ((Sexp_nodes[modifier_node].subtype != SEXP_ATOM_STRING) ||
							(modifier == ListModifier::INVALID)) {
						if (bad_node)
							*bad_node = modifier_node;
						return SEXP_CHECK_INVALID_LIST_MODIFIER;
					}
					if (modifier == ListModifier::AT_INDEX) {
						const int list_index_node = CDR(modifier_node);
						if (list_index_node == -1) {
							if (bad_node)
								*bad_node = modifier_node;
							return SEXP_CHECK_INVALID_LIST_MODIFIER;
						}
						// we can't check that index < length because we don't know what the length will be then
						if (Sexp_nodes[list_index_node].subtype != SEXP_ATOM_NUMBER ||
								atoi(Sexp_nodes[list_index_node].text) < 0) {
							if (bad_node)
								*bad_node = list_index_node;
							return SEXP_CHECK_INVALID_LIST_MODIFIER;
						}
					}
				} else if (data_container.is_map()) {
					if ((any(data_container.type & ContainerType::NUMBER_KEYS) &&
							Sexp_nodes[modifier_node].subtype != SEXP_ATOM_NUMBER) ||
						(any(data_container.type & ContainerType::STRING_KEYS) &&
							Sexp_nodes[modifier_node].subtype != SEXP_ATOM_STRING)) {
						if (bad_node)
							*bad_node = modifier_node;
						return SEXP_CHECK_WRONG_MAP_KEY_TYPE;
					}
				} else {
					UNREACHABLE("Unknown container type %d", (int)data_container.type);
				}
			}

			// not much else we can check here, since validity depends on runtime values, so continue
			node = Sexp_nodes[node].rest;
			argnum++;
			continue;

		} else {
			UNREACHABLE("SEXP subtype is %d when it should be SEXP_ATOM_LIST, SEXP_ATOM_NUMBER, SEXP_ATOM_STRING, "
						"SEXP_ATOM_CONTAINER_NAME, or "
						"SEXP_ATOM_CONTAINER_DATA!",
				node_subtype);
		}

		// variables should only be typechecked. 
		if ((Sexp_nodes[node].type & SEXP_FLAG_VARIABLE) && (desired_argument_type != OPF_VARIABLE_NAME)) {
			var_index = sexp_get_variable_index(node);
			if (var_index < 0)
				return SEXP_CHECK_INVALID_VARIABLE;

			if (!check_variable_data_type(desired_argument_type,
					Sexp_variables[var_index].type,
					op_const,
					argnum,
					p_container)) {
				return SEXP_CHECK_INVALID_VARIABLE_TYPE;
			}
			node = Sexp_nodes[node].rest;
			argnum++;
			continue; 
		}

		// if this is the special argument, make sure it is being properly used
		if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE) {
			bool found = is_descendant_of_when_argument_op(node);

			// if there is no argument operator higher in the tree, we have a problem
			if (!found) {
				return SEXP_CHECK_MISPLACED_SPECIAL_ARGUMENT;
			} else {
				// we don't know what the argument will be at runtime,
				// so assume it's valid and continue to the next
				node = Sexp_nodes[node].rest;
				argnum++;
				continue;
			}
		}

		switch (desired_argument_type) {
			case OPF_NAV_POINT:
				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				break;

			case OPF_NUMBER:
				if ((node_return_type != OPR_NUMBER) && (node_return_type != OPR_POSITIVE)){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				break;

			case OPF_POSITIVE:
				if (node_return_type == OPR_NUMBER){
					// for numeric literals, check whether the number is negative
					if (node_subtype == SEXP_ATOM_NUMBER){
						if (*Sexp_nodes[node].text == '-')
							return SEXP_CHECK_NEGATIVE_NUM;
					}

					// Goober5000's number hack - for numeric return values of operators, skip the check (assume all return values will be positive)
					break;
					// return SEXP_CHECK_NEGATIVE_NUM;
				}

				if (node_return_type != OPR_POSITIVE){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				break;

			case OPF_SHIP_NOT_PLAYER:
				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (ship_name_lookup(CTEXT(node), 0) < 0)
				{
					if (Fred_running || !mission_check_ship_yet_to_arrive(CTEXT(node)))
					{
						return SEXP_CHECK_INVALID_SHIP;
					}
				}

				break;

			case OPF_SHIP_OR_NONE:
				if (node_subtype != SEXP_ATOM_STRING)
				{
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (stricmp(CTEXT(node), SEXP_NONE_STRING) != 0)		// none is okay
				{
					if (ship_name_lookup(CTEXT(node), 1) < 0)
					{
						if (Fred_running || !mission_check_ship_yet_to_arrive(CTEXT(node)))
						{
							return SEXP_CHECK_INVALID_SHIP;
						}
					}
				}

				break;

			case OPF_SHIP:
			case OPF_SHIP_POINT:
				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (ship_name_lookup(CTEXT(node), 1) < 0) {
					if (Fred_running || !mission_check_ship_yet_to_arrive(CTEXT(node)))
					{
						if (desired_argument_type == OPF_SHIP)
						{													// return invalid ship if not also looking for point
							return SEXP_CHECK_INVALID_SHIP;
						}

						if (find_matching_waypoint(CTEXT(node)) == nullptr)
						{
							if (verify_vector(CTEXT(node)))					// verify return non-zero on invalid point
							{
								return SEXP_CHECK_INVALID_SHIP_POINT;
							}
						}
					}
				}

				break;

			case OPF_PROP:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				if (prop_name_lookup(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_PROP;
				}
				break;

			case OPF_WING:
				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (wing_name_lookup(CTEXT(node), 1) < 0){
					return SEXP_CHECK_INVALID_WING;
				}

				break;

			case OPF_SHIP_WING:
			case OPF_SHIP_WING_WHOLETEAM:
			case OPF_SHIP_WING_SHIPONTEAM_POINT:
			case OPF_SHIP_WING_POINT:
			case OPF_SHIP_WING_POINT_OR_NONE:
			case OPF_ORDER_RECIPIENT:
				if ( node_subtype != SEXP_ATOM_STRING ){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (desired_argument_type == OPF_ORDER_RECIPIENT) {
					if (!strcmp ("<all fighters>", CTEXT(node))) {
						break;
					}
				}

				// all of these have ships and wings in common
				if (ship_name_lookup(CTEXT(node), 1) >= 0 || wing_name_lookup(CTEXT(node), 1) >= 0) {
					break;
				}
				// also check arrival list if we're running the game
				if (!Fred_running && mission_check_ship_yet_to_arrive(CTEXT(node))) {
					break;
				}

				// none is okay for _OR_NONE
				if (desired_argument_type == OPF_SHIP_WING_POINT_OR_NONE && !stricmp(CTEXT(node), SEXP_NONE_STRING))	{
					break;
				}

				// two different ways of checking teams
				if ((desired_argument_type == OPF_SHIP_WING_WHOLETEAM) && iff_lookup(CTEXT(node)) >= 0) {
					break;
				}
				if ((desired_argument_type == OPF_SHIP_WING_SHIPONTEAM_POINT) && sexp_determine_team(CTEXT(node)) >= 0)	{
					break;
				}

				// only other possibility is waypoints
				if (desired_argument_type == OPF_SHIP_WING_SHIPONTEAM_POINT || desired_argument_type == OPF_SHIP_WING_POINT || desired_argument_type == OPF_SHIP_WING_POINT_OR_NONE) {
					if (find_matching_waypoint(CTEXT(node)) == nullptr) {
						if (verify_vector(CTEXT(node))) {  // non-zero on verify vector mean invalid!
							return (desired_argument_type == OPF_SHIP_WING_SHIPONTEAM_POINT) ? SEXP_CHECK_INVALID_SHIP_WING_SHIPONTEAM_POINT : SEXP_CHECK_INVALID_SHIP_WING_POINT;
						}
					}
					break;
				}

				// nothing left
				if (desired_argument_type == OPF_ORDER_RECIPIENT)
					return SEXP_CHECK_INVALID_ORDER_RECIPIENT;
				else if (desired_argument_type == OPF_SHIP_WING_WHOLETEAM)
					return SEXP_CHECK_INVALID_SHIP_WING_WHOLETEAM;
				else
					return SEXP_CHECK_INVALID_SHIP_WING;

			case OPF_SHIP_PROP:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				if (ship_name_lookup(CTEXT(node), 1) >= 0) {
					break;
				}
				if (prop_name_lookup(CTEXT(node)) >= 0) {
					break;
				}

				// also check arrival list if we're running the game
				if (!Fred_running && mission_check_ship_yet_to_arrive(CTEXT(node))) {
					break;
				}
				
				return SEXP_CHECK_INVALID_SHIP_PROP;

			case OPF_AWACS_SUBSYSTEM:
			case OPF_ROTATING_SUBSYSTEM:
			case OPF_TRANSLATING_SUBSYSTEM:
			case OPF_SUBSYSTEM:
			case OPF_SUBSYSTEM_OR_NONE:
			case OPF_SUBSYS_OR_GENERIC:
			{
				int shipnum,ship_class;
				int ship_node;				

				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				// none is okay for subsys_or_none
				if (desired_argument_type == OPF_SUBSYSTEM_OR_NONE && !stricmp(CTEXT(node), SEXP_NONE_STRING))
				{
					break;
				}

				//  subsys_or_generic can also accept generic types
				if (desired_argument_type == OPF_SUBSYS_OR_GENERIC && get_generic_subsys(CTEXT(node)) != SUBSYSTEM_NONE) {
					break;
				}

				// we must get the model of the ship that is part of this sexpression and find a subsystem
				// with that name.  This code assumes by default that the ship is *always* the first name
				// in the sexpression.  If this is ever not the case, the code here must be changed to
				// get the correct ship name.
				switch(op_const)
				{
					case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY:
					case OP_DISTANCE_CENTER_SUBSYSTEM:
					case OP_DISTANCE_BBOX_SUBSYSTEM:
					case OP_SET_CARGO:
					case OP_IS_CARGO:
					case OP_CHANGE_AI_CLASS:
					case OP_IS_AI_CLASS:
					case OP_MISSILE_LOCKED:
					case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD:
					case OP_IS_IN_TURRET_FOV:
					case OP_TURRET_SET_FORCED_TARGET:
						ship_node = CDR(CDR(op_node));
						break;

					case OP_BEAM_FIRE:
					case OP_TURRET_SET_FORCED_SUBSYS_TARGET:
						if(argnum == 1){
							ship_node = CDR(op_node);
						} else {
							ship_node = CDR(CDR(CDR(op_node)));
						}
						break;

					case OP_BEAM_FLOATING_FIRE:
						ship_node = CDDDDDR(CDDR(op_node));
						break;

					case OP_QUERY_ORDERS:
						ship_node = CDR(CDR(CDR(CDR(op_node))));
						break;
	
					case OP_WEAPON_CREATE:
						ship_node = CDDDDDR(CDDDDR(op_node));
						break;

					default:
						if (op_const < First_available_operator_id) {
							ship_node = CDR(op_node);
						} else {
							int r_count = get_dynamic_parameter_index(Sexp_nodes[op_node].text, argnum);
							
							if (r_count < 0)
								error_display(1,
									"Expected to find a dynamic lua parent parameter for node %i in operator %s but "
									"found nothing!",
									argnum,
									Sexp_nodes[op_node].text);
							
							ship_node = op_node; //initialize it I guess
							while (r_count >= 0) {
								ship_node = CDR(ship_node);
								r_count--;
							}
						}
						break;
				}
				if (ship_node < 0) {
					Warning(LOCATION, "Could not find ship node for operator %s!", Operators[op_index].text.c_str());
					return SEXP_CHECK_INVALID_SHIP;
				}

				if (is_node_value_dynamic(ship_node)) {
					const int dyn_val_check = check_dynamic_value_node_type(ship_node, true, false);
					if (dyn_val_check) {
						return dyn_val_check;
					} else {
						// since the value changes at runtime
						// we can check only the type
						break;
					}
				}

				auto shipname = CTEXT(ship_node);
				shipnum = ship_name_lookup(shipname, 1);
				if (shipnum >= 0)
				{
					ship_class = Ships[shipnum].ship_info_index;
				}
				else
				{
					// must try to find the ship in the arrival list
					p_object *p_objp = mission_parse_get_arrival_ship(shipname);

					if (!p_objp)
					{
						if (desired_argument_type == OPF_SUBSYSTEM_OR_NONE)
							break;
						else
						{
							if (bad_node)
								*bad_node = ship_node;

							return SEXP_CHECK_INVALID_SHIP;
						}
					}

					ship_class = p_objp->ship_class;
				}

				// check for the special "hull" value
				if ( (op_const == OP_SABOTAGE_SUBSYSTEM) || (op_const == OP_REPAIR_SUBSYSTEM) || (op_const == OP_SET_SUBSYSTEM_STRNGTH) || (op_const == OP_SET_ARMOR_TYPE) || (op_const == OP_BEAM_FIRE)) {
					if ( !stricmp( CTEXT(node), SEXP_HULL_STRING) || !stricmp( CTEXT(node), SEXP_SIM_HULL_STRING) ){
						break;
					}
				}
				// check for special "shields" value for armor types
				if (op_const == OP_SET_ARMOR_TYPE) {
					if ( !stricmp( CTEXT(node), SEXP_SHIELD_STRING) || !stricmp( CTEXT(node), SEXP_SIM_HULL_STRING) ){
						break;
					}
				}

				for (i=0; i<Ship_info[ship_class].n_subsystems; i++)
				{
					if (!subsystem_stricmp(Ship_info[ship_class].subsystems[i].subobj_name, CTEXT(node)))
					{
						break;
					}
				}

				if (i == Ship_info[ship_class].n_subsystems)
				{
					return SEXP_CHECK_INVALID_SUBSYS;
				}

				if(Fred_running)
				{
					// if we're checking for an AWACS subsystem and this is not an awacs subsystem
					if((desired_argument_type == OPF_AWACS_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Awacs]))
					{
						return SEXP_CHECK_INVALID_AWACS_SUBSYS;
					}

					// rotating subsystem, like above - Goober5000
					if ((desired_argument_type == OPF_ROTATING_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Rotates]))
					{
						return SEXP_CHECK_INVALID_ROTATING_SUBSYS;
					}

					// translating subsystem, like above - Goober5000
					if ((desired_argument_type == OPF_TRANSLATING_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Translates]))
					{
						return SEXP_CHECK_INVALID_TRANSLATING_SUBSYS;
					}
				}

				break;
			}

			case OPF_ANIMATION_NAME: {
				// OP 1 is always the ship

				int shipnum,ship_class;
				int ship_node;

				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				ship_node = CDR(op_node);

				if (is_node_value_dynamic(ship_node)) {
					const int dyn_val_check = check_dynamic_value_node_type(ship_node, true, false);
					if (dyn_val_check) {
						return dyn_val_check;
					} else {
						// since the value changes at runtime
						// we can check only the type
						break;
					}
				}

				auto shipname = CTEXT(ship_node);
				shipnum = ship_name_lookup(shipname, 1);
				if (shipnum >= 0)
				{
					ship_class = Ships[shipnum].ship_info_index;
				}
				else
				{
					// must try to find the ship in the arrival list
					p_object *p_objp = mission_parse_get_arrival_ship(shipname);

					if (!p_objp)
					{
						if (desired_argument_type == OPF_SUBSYSTEM_OR_NONE)
							break;
						else
						{
							if (bad_node)
								*bad_node = ship_node;

							return SEXP_CHECK_INVALID_SHIP;
						}
					}

					ship_class = p_objp->ship_class;
				}

				const auto& animSet = Ship_info[ship_class].animations;
				switch(op_const) {
					case OP_TRIGGER_ANIMATION_NEW:
					case OP_STOP_LOOPING_ANIMATION: {
						//Second OP trigger type
						//Third OP triggered by
						auto triggerType = animation::anim_match_type(CTEXT(CDR(ship_node)));
						
						const auto& animations = animSet.getRegisteredTriggers();
						
						SCP_string triggeredBy = CTEXT(CDDR(ship_node));
						SCP_tolower(triggeredBy);
						
						if(std::find_if(animations.cbegin(), animations.cend(), [&triggerType, &triggeredBy](const std::remove_reference<decltype(animations)>::type::value_type& animation) -> bool {
							if (animation.type != triggerType)
								return false;

							// Since Dock Bay animations can be tables as NOT on door x, technically doors not tabled for can be valid targets. Just allow anything here.
							if(animation.type == animation::ModelAnimationTriggerType::DockBayDoor)
								return true;
							
							if(animation.subtype != animation::ModelAnimationSet::SUBTYPE_DEFAULT){
								if(!can_construe_as_integer(triggeredBy.c_str()))
									return false;
								
								int triggeredBySubtype = atoi(triggeredBy.c_str());
								int animationSubtype = animation.subtype;
								
								return triggeredBySubtype == animationSubtype;
							}
							else{
								return animation.name == triggeredBy;
							}
						}) == animations.cend())
							return SEXP_CHECK_INVALID_ANIMATION;
						
						break;
					}
					case OP_UPDATE_MOVEABLE:
					case OP_ADVANCE_MOVEABLE:{
						//Second OP name
						SCP_string name = CTEXT(CDR(ship_node));
						SCP_tolower(name);
						
						const auto& moveables = animSet.getRegisteredMoveables();
						
						if(std::find(moveables.cbegin(), moveables.cend(), name) == moveables.cend())
							return SEXP_CHECK_INVALID_ANIMATION;
						
						break;
					}
				}

				break;		
			}
			
			case OPF_SUBSYSTEM_TYPE:
				i = string_lookup(CTEXT(node), Subsystem_types, SUBSYSTEM_MAX);
				if (i < 0)
					return SEXP_CHECK_INVALID_SUBSYS_TYPE;
				break;

			case OPF_POINT:
				if (node_subtype != SEXP_ATOM_STRING)
				{
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (find_matching_waypoint(CTEXT(node)) == nullptr)
				{
					if (verify_vector(CTEXT(node)))
					{
						return SEXP_CHECK_INVALID_POINT;
					}
				}

				break;

			case OPF_IFF:
				if (node_subtype != SEXP_ATOM_STRING)
				{
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (iff_lookup(CTEXT(node)) < 0)
				{
					return SEXP_CHECK_INVALID_IFF;
				}

				break;

			case OPF_AI_CLASS:
				if (node_subtype != SEXP_ATOM_STRING)
				{
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				for (i=0; i<Num_ai_classes; i++)
				{
					if (!stricmp(Ai_class_names[i], CTEXT(node)))
					{
						break;
					}
				}

				if (i == Num_ai_classes)
				{
					return SEXP_CHECK_INVALID_AI_CLASS;
				}

				break;

			case OPF_ARRIVAL_LOCATION:
				if (node_subtype != SEXP_ATOM_STRING)
				{
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				for (i=0; i<MAX_ARRIVAL_NAMES; i++)
				{
					if (!stricmp(Arrival_location_names[i], CTEXT(node)))
					{
						break;
					}
				}

				if (i == MAX_ARRIVAL_NAMES)
				{
					return SEXP_CHECK_INVALID_ARRIVAL_LOCATION;
				}

				break;

			case OPF_DEPARTURE_LOCATION:
				if (node_subtype != SEXP_ATOM_STRING)
				{
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				for (i=0; i<MAX_DEPARTURE_NAMES; i++)
				{
					if (!stricmp(Departure_location_names[i], CTEXT(node)))
					{
						break;
					}
				}

				if (i == MAX_DEPARTURE_NAMES)
				{
					return SEXP_CHECK_INVALID_DEPARTURE_LOCATION;
				}

				break;

			case OPF_ARRIVAL_ANCHOR_ALL:
				if (node_subtype != SEXP_ATOM_STRING)
				{
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				else
				{
					int valid = 0;

					// <any friendly>, etc.
					if (get_special_anchor(CTEXT(node)).isValid())
					{
						valid = 1;
					}

					if (ship_name_lookup(CTEXT(node), 1) >= 0)
					{
						valid = 1;
					}

					if (!Fred_running && mission_check_ship_yet_to_arrive(CTEXT(node)))
					{
						valid = 1;
					}

					if (!valid)
					{
						return SEXP_CHECK_INVALID_ARRIVAL_ANCHOR_ALL;
					}
				}

				break;

			case OPF_SOUNDTRACK_NAME:
				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (!stricmp(CTEXT(node), "<No Music>"))
					break;

				if (Cmdline_freespace_no_music)
					break;

				if (event_music_get_soundtrack_index(CTEXT(node)) >= 0)
					break;

				return SEXP_CHECK_INVALID_SOUNDTRACK_NAME;

			case OPF_SHIP_WITH_BAY:
			{
				auto name = CTEXT(node);
				int shipnum = -1;

				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				if (!stricmp(name, "<no anchor>"))
					break;

				shipnum = ship_name_lookup(name, 1);
				if (shipnum < 0)
				{
					if (Fred_running)
						return SEXP_CHECK_INVALID_SHIP;

					if (!mission_check_ship_yet_to_arrive(name))
						return SEXP_CHECK_INVALID_SHIP;

					// Goober5000 - since we can't check POFs for ships which have yet to arrive
					// (not without a bit of work anyway), just assume they're okay
					break;
				}

				// ship exists at this point

				// now determine if this ship has a hangar bay
				if (!ship_has_hangar_bay(shipnum))
					return SEXP_CHECK_INVALID_SHIP_WITH_BAY;

				break;
			}

			case OPF_SUPPORT_SHIP_CLASS:
				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (!stricmp(CTEXT(node), "<species support ship class>"))
					break;

				if (!stricmp(CTEXT(node), "<any support ship class>"))
					break;

				i = -1;
				for (auto it = Ship_info.cbegin(); it != Ship_info.cend(); ++it) {
					if (!stricmp(CTEXT(node), it->name))
					{
						if (it->flags[Ship::Info_Flags::Support])
						{
							i = (int)std::distance(Ship_info.cbegin(), it);
							break;
						}
					}
				}

				if (i == -1)
					return SEXP_CHECK_INVALID_SUPPORT_SHIP_CLASS;

				break;

			case OPF_BOOL:
				if (node_return_type != OPR_BOOL){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				break;

			case OPF_AI_ORDER:
				if ( node_subtype != SEXP_ATOM_STRING ){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				break;

			case OPF_NULL:
				if (node_return_type != OPR_NULL){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				break;

			case OPF_SSM_CLASS:
				if ( node_subtype != SEXP_ATOM_STRING ) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (ssm_info_lookup(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_SSM_CLASS;
				}

				break;

			// Goober5000
			case OPF_FLEXIBLE_ARGUMENT:
				if (node_return_type != OPR_FLEXIBLE_ARGUMENT) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				break;

			// Goober5000
			case OPF_ANYTHING:
				if (node_subtype == SEXP_ATOM_CONTAINER_NAME) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				break;

			case OPF_AI_GOAL:
			{
				if (node_return_type != OPR_AI_GOAL){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				// we should check the syntax of the actual goal!!!!
				z = Sexp_nodes[node].first;
				if ((z = check_sexp_syntax(z, OPR_AI_GOAL, recursive, bad_node)) != 0){
					return z;
				}

				if (Fred_running) {
					int ship_num, ship2, wing_num = 0;

					// if it's the "goals" operator, this is part of initial orders, so we can't grab the ship from it
					if (op_const == OP_GOALS_ID) {
						break;
					}

					auto ship_node = Sexp_nodes[op_node].rest;
					Assert(ship_node >= 0);

					if (is_node_value_dynamic(ship_node)) {
						const int dyn_val_check = check_dynamic_value_node_type(ship_node, true, false);
						if (dyn_val_check) {
							return dyn_val_check;
						} else {
							// since the value changes at runtime
							// we can check only the type
							break;
						}
					}

					ship_num = ship_name_lookup(CTEXT(ship_node), 1);	// Goober5000 - include players
					if (ship_num < 0) {
						wing_num = wing_name_lookup(CTEXT(ship_node));
						if (wing_num < 0) {
							if (bad_node){
								*bad_node = ship_node;
							}

							return SEXP_CHECK_INVALID_SHIP;  // should have already been caught earlier, but just in case..
						}
					}

					Assert(node_subtype == SEXP_ATOM_LIST);
					z = Sexp_nodes[node].first;
					Assert(Sexp_nodes[z].subtype != SEXP_ATOM_LIST);
					z = get_operator_const(z);
					if (ship_num >= 0) {
						if (!query_sexp_ai_goal_valid(z, ship_num)){
							if (bad_node)
								*bad_node = ship_node;
							return SEXP_CHECK_ORDER_NOT_ALLOWED;
						}

					} else {
						for (i=0; i<Wings[wing_num].wave_count; i++){
							if (!query_sexp_ai_goal_valid(z, Wings[wing_num].ship_index[i])){
								if (bad_node)
									*bad_node = ship_node;
								return SEXP_CHECK_ORDER_NOT_ALLOWED;
							}
						}
					}

					if ((z == OP_AI_DOCK) && (Sexp_nodes[node].rest >= 0)) {
						ship2 = ship_name_lookup(CTEXT(Sexp_nodes[node].rest), 1);	// Goober5000 - include players
						if ((ship_num < 0) || !ship_docking_valid(ship_num, ship2)){
							if (bad_node)
								*bad_node = ship_node;
							return SEXP_CHECK_DOCKING_NOT_ALLOWED;
						}
					}
				}

				break;
			}

			case OPF_SHIP_TYPE: {
				if (node_subtype != SEXP_ATOM_STRING){
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				auto type_name = CTEXT(node);
				i = ship_type_name_lookup(type_name);

				if (i < 0){
					if (Fighter_bomber_valid && !stricmp(type_name, Fighter_bomber_type_name)) {
						// this is allowed even though it's not an explicit type
						break;
					}

					return SEXP_CHECK_INVALID_SHIP_TYPE;
				}

				break;
			}

			case OPF_WAYPOINT_PATH:
				if (find_matching_waypoint_list(CTEXT(node)) == nullptr) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				break;

			case OPF_MESSAGE:
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				if (Fred_running) {
					for (i=0; i<Num_messages; i++)
						if (!stricmp(Messages[i].name, CTEXT(node)))
							break;

					if (i == Num_messages)
						return SEXP_CHECK_UNKNOWN_MESSAGE;
				}
				
				break;

			case OPF_PRIORITY: {
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				if (Fred_running) {  // should still check in Fred though..
					auto name = CTEXT(node);
					if (!stricmp(name, "low") || !stricmp(name, "normal") || !stricmp(name, "high"))
						break;

					return SEXP_CHECK_INVALID_PRIORITY;
				}

				break;
			}

			case OPF_MISSION_NAME:
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				if (Fred_running) {
					if (mode == sexp_mode::CAMPAIGN) {
						for (i=0; i<Campaign.num_missions; i++)
							if (!stricmp(CTEXT(node), Campaign.missions[i].name)) {
								if ((i != Sexp_useful_number) && (Campaign.missions[i].level >= Campaign.missions[Sexp_useful_number].level))
									return SEXP_CHECK_INVALID_LEVEL;

								break;
							}

						if (i == Campaign.num_missions)
							return SEXP_CHECK_INVALID_MISSION_NAME;

					} else {
						// mwa -- put the following if statement to prevent Fred errors for possibly valid
						// conditions.  We should do something else here!!!
						if ( (op_const == OP_PREVIOUS_EVENT_TRUE) || (op_const == OP_PREVIOUS_EVENT_FALSE) || (op_const == OP_PREVIOUS_EVENT_INCOMPLETE)
							|| (op_const == OP_PREVIOUS_GOAL_TRUE) || (op_const == OP_PREVIOUS_GOAL_FALSE) || (op_const == OP_PREVIOUS_GOAL_INCOMPLETE) )
							break;

						if (!(*Mission_filename) || stricmp(Mission_filename, CTEXT(node)) != 0)
							return SEXP_CHECK_INVALID_MISSION_NAME;
					}
				}

				break;

			case OPF_GOAL_NAME:
			case OPF_EVENT_NAME:
			{
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				count = 0;

				// we only need to check the campaign list if running in Fred and are in campaign mode.
				// otherwise, check the set of current goals/events
				if (Fred_running && (mode == sexp_mode::CAMPAIGN)) {
					z = find_parent_operator(node);
					Assert(z >= 0);
					z = Sexp_nodes[z].rest;  // first argument of operator should be mission name
					Assert(z >= 0);

					if (is_node_value_dynamic(z)) {
						const int dyn_val_check = check_dynamic_value_node_type(z, true, false);
						if (dyn_val_check) {
							return dyn_val_check;
						} else {
							// since the value changes at runtime
							// we can check only the type
							break;
						}
					}

					// look for mission
					count = count_items_with_string(Campaign.missions, i2sz(Campaign.num_missions), &cmission::name, CTEXT(z));

					// only check for a missing mission -- it's ok if the same mission appears multiple times in the campaign
					if (count == 0) {
						if (bad_node)
							*bad_node = z;

						return SEXP_CHECK_INVALID_MISSION_NAME;
					}

					// read the goal/event list from the mission file if we need to
					if (Campaign.missions[i].flags & CMISSION_FLAG_FRED_LOAD_PENDING)
					{
						read_mission_goal_list(i);
						Campaign.missions[i].flags &= ~CMISSION_FLAG_FRED_LOAD_PENDING;
					}

					if (desired_argument_type == OPF_GOAL_NAME) {
						count = count_items_with_string(Campaign.missions[i].goals, &mgoal::name, CTEXT(node));
					} else if (desired_argument_type == OPF_EVENT_NAME) {
						count = count_items_with_string(Campaign.missions[i].events, &mevent::name, CTEXT(node));
					} else {
						UNREACHABLE("desired_argument_type == %d; expected OPF_GOAL_NAME or OPF_EVENT_NAME", desired_argument_type);
					}
				} else if (desired_argument_type == OPF_GOAL_NAME) {
					// neither the previous mission nor the previous goal is guaranteed to exist (missions can be developed out of sequence), so we don't need to check them
					if ((op_const == OP_PREVIOUS_GOAL_TRUE) || (op_const == OP_PREVIOUS_GOAL_FALSE) || (op_const == OP_PREVIOUS_GOAL_INCOMPLETE))
						break;

					count = count_items_with_string(Mission_goals, &mission_goal::name, CTEXT(node));
				} else if (desired_argument_type == OPF_EVENT_NAME) {
					// neither the previous mission nor the previous event is guaranteed to exist (missions can be developed out of sequence), so we don't need to check them
					if ((op_const == OP_PREVIOUS_EVENT_TRUE) || (op_const == OP_PREVIOUS_EVENT_FALSE) || (op_const == OP_PREVIOUS_EVENT_INCOMPLETE))
						break;

					count = count_items_with_string(Mission_events, &mission_event::name, CTEXT(node));
				} else {
					UNREACHABLE("desired_argument_type == %d; expected OPF_GOAL_NAME or OPF_EVENT_NAME", desired_argument_type);
				}

				if (count == 0)
					return (desired_argument_type == OPF_GOAL_NAME) ? SEXP_CHECK_INVALID_GOAL_NAME : SEXP_CHECK_INVALID_EVENT_NAME;
				else if (count > 1)
					return (desired_argument_type == OPF_GOAL_NAME) ? SEXP_CHECK_AMBIGUOUS_GOAL_NAME : SEXP_CHECK_AMBIGUOUS_EVENT_NAME;

				break;
			}

			case OPF_DOCKER_POINT:
			case OPF_DOCKEE_POINT:
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				// This makes massive assumptions about the structure of the SEXP using it. If you add any 
				// new SEXPs that use this OPF, you will probably need to edit this section to accommodate them.
				if (Fred_running) {
					int ship_num, ship_node = -1, model;

					// Look for the node containing the docker/dockee ship. In most cases, we want 
					// the current SEXP operator, but for ai-dock and the docker, we want its parent.
					if (op_const == OP_AI_DOCK && desired_argument_type == OPF_DOCKER_POINT) {
						z = find_parent_operator(op_node);

						// if it's the "goals" operator, this is part of initial orders, so we can't grab the ship from it
						if (get_operator_const(z) == OP_GOALS_ID) {
							break;
						}

						ship_node = CDR(z);
					} else {
						z = op_node;

						if (op_const == OP_AI_DOCK) { // ai-dock with dockee
							ship_node = CDR(z);
						} else if (desired_argument_type == OPF_DOCKER_POINT) {
							if (op_const >= First_available_operator_id) {
								int r_count = get_dynamic_parameter_index(Sexp_nodes[op_node].text, argnum);
								
								if (r_count < 0)
									error_display(1,
										"Expected to find a dynamic lua parent parameter for node %i in operator %s "
										"but found nothing!",
										argnum,
										Sexp_nodes[op_node].text);
								
								ship_node = op_node; // initialize it I guess
								while (r_count >= 0) {
									ship_node = CDR(ship_node);
									r_count--;
								}
								break;
							} else {
								ship_node = CDR(z);
							}
						} else if (desired_argument_type == OPF_DOCKEE_POINT) {
							ship_node = CDDDR(z);
						} else if (op_const >= First_available_operator_id) {
							int r_count = get_dynamic_parameter_index(Sexp_nodes[op_node].text, argnum);
							
							if (r_count < 0)
								error_display(1,
									"Expected to find a dynamic lua parent parameter for node %i in operator %s "
									"but found nothing!",
									argnum,
									Sexp_nodes[op_node].text);
							
							ship_node = op_node; // initialize it I guess
							while (r_count >= 0) {
								ship_node = CDR(ship_node);
								r_count--;
							}
							break;
						} else {
							UNREACHABLE("Unhandled case for OPF_DOCKER_POINT/OPF_DOCKEE_POINT");
						}
					}
					Assert(ship_node >= 0);

					if (is_node_value_dynamic(ship_node)) {
						const int dyn_val_check = check_dynamic_value_node_type(ship_node, true, false);
						if (dyn_val_check) {
							return dyn_val_check;
						} else {
							// since the value changes at runtime
							// we can check only the type
							break;
						}
					}

					// look for the ship that has this dockpoint
					ship_num = ship_name_lookup(CTEXT(ship_node), 1);
					if (ship_num < 0) {
						if (bad_node)
							*bad_node = ship_node;

						return SEXP_CHECK_INVALID_SHIP;  // should have already been caught earlier, but just in case..
					}

					model = Ship_info[Ships[ship_num].ship_info_index].model_num;
					z = model_get_num_dock_points(model);
					for (i=0; i<z; i++)
						if (!stricmp(CTEXT(node), model_get_dock_name(model, i)))
							break;

					if (i == z)
						return (desired_argument_type == OPF_DOCKER_POINT) ? SEXP_CHECK_INVALID_DOCKER_POINT : SEXP_CHECK_INVALID_DOCKEE_POINT;
				}

				break;

			case OPF_WHO_FROM:
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				if (!is_special_sender(CTEXT(node))) {  // not a manual source?
					if (stricmp(CTEXT(node), "<any wingman>") != 0)
						if (stricmp(CTEXT(node), "<none>") != 0 ) // not a special token?
							if ((ship_name_lookup(CTEXT(node), 1) < 0) && (wing_name_lookup(CTEXT(node), 1) < 0))  // is it in the mission?
								if (Fred_running || !mission_check_ship_yet_to_arrive(CTEXT(node)))
									return SEXP_CHECK_INVALID_MSG_SOURCE;
				}

				break;

			//Karajorma
			case OPF_PERSONA:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				for (i = 0; i < (int)Personas.size(); i++) {
					if (!strcmp(CTEXT(node), Personas[i].name)) {
						break;
					}
				}

				if (i == (int)Personas.size()) {
					return SEXP_CHECK_INVALID_PERSONA_NAME; 
				}
				break;

			case OPF_MISSION_MOOD:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				for (i = 0; i < (int)Builtin_moods.size(); i++) {
					if (!strcmp(Builtin_moods[i].c_str(), CTEXT(node))) {
						break;
					}
				}

				if (i == (int)Builtin_moods.size()) {
					return SEXP_CHECK_INVALID_MISSION_MOOD;
				}

				break;

			case OPF_SHIP_FLAG:
			{
				Object::Object_Flags object_flag = Object::Object_Flags::NUM_VALUES;
				Ship::Ship_Flags ship_flag = Ship::Ship_Flags::NUM_VALUES;
				Mission::Parse_Object_Flags parse_obj_flag = Mission::Parse_Object_Flags::NUM_VALUES;
				AI::AI_Flags ai_flag = AI::AI_Flags::NUM_VALUES;
				sexp_check_flag_arrays(CTEXT(node), object_flag, ship_flag, parse_obj_flag, ai_flag);

				if (object_flag == Object::Object_Flags::NUM_VALUES && ship_flag == Ship::Ship_Flags::NUM_VALUES
					&& parse_obj_flag == Mission::Parse_Object_Flags::NUM_VALUES && ai_flag == AI::AI_Flags::NUM_VALUES) {
					return SEXP_CHECK_INVALID_SHIP_FLAG;
				}

				break;
			}

			case OPF_WING_FLAG:
			{
				Ship::Wing_Flags wing_flag = Ship::Wing_Flags::NUM_VALUES;
				sexp_check_flag_array(CTEXT(node), wing_flag);

				if (wing_flag == Ship::Wing_Flags::NUM_VALUES) {
					return SEXP_CHECK_INVALID_WING_FLAG;
				}

				break;
			}

			case OPF_TEAM_COLOR:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (!stricmp(CTEXT(node), "none"))
					break;

				if (Team_Colors.find(CTEXT(node)) == Team_Colors.end())
					return SEXP_CHECK_INVALID_TEAM_COLOR;
				
				break;

			case OPF_FONT:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (font::FontManager::getFont(CTEXT(node)) == nullptr)
					return SEXP_CHECK_INVALID_FONT;

				break;
				
			case OPF_SOUND_ENVIRONMENT:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (stricmp(CTEXT(node), SEXP_NONE_STRING) != 0 && ds_eax_get_preset_id(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_SOUND_ENVIRONMENT;
				}
				break;

			case OPF_AUDIO_VOLUME_OPTION:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (audio_volume_option_lookup(CTEXT(node)) == -1)
					return SEXP_CHECK_INVALID_AUDIO_VOLUME_OPTION;
				break;

			case OPF_BUILTIN_HUD_GAUGE:
			{
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				auto gauge_name = CTEXT(node);

				// for compatibility, since this operator now uses a different set of parameters
				if (op_const == OP_FLASH_HUD_GAUGE) {
					bool found = false;
					for (int legacy_idx = 0; legacy_idx < NUM_HUD_GAUGES; legacy_idx++) {
						if (stricmp(gauge_name, Legacy_HUD_gauges[legacy_idx].hud_gauge_text) == 0) {
							found = true;
							break;
						}
					}
					if (found) {
						break;
					}
				}

				if (hud_gauge_type_lookup(gauge_name) == -1)
					return SEXP_CHECK_INVALID_BUILTIN_HUD_GAUGE;

				break;
			}

			case OPF_CUSTOM_HUD_GAUGE:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (hud_get_custom_gauge(CTEXT(node), true) == nullptr) {
					return SEXP_CHECK_INVALID_CUSTOM_HUD_GAUGE;
				}

				break;

			case OPF_ANY_HUD_GAUGE:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (hud_get_gauge(CTEXT(node), true) == nullptr) {
					return SEXP_CHECK_INVALID_ANY_HUD_GAUGE;
				}

				break;

			case OPF_SOUND_ENVIRONMENT_OPTION:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (sexp_sound_environment_option_lookup(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_SOUND_ENVIRONMENT_OPTION; 
				}
				break;

			case OPF_EXPLOSION_OPTION:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (sexp_explosion_option_lookup(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_EXPLOSION_OPTION; 
				}
				break;

			case OPF_KEYPRESS:
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				break;

			case OPF_CARGO:
			case OPF_STRING:
			case OPF_MESSAGE_OR_STRING:
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;
				break;

			case OPF_SKILL_LEVEL:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				for (i = 0; i < NUM_SKILL_LEVELS; i++) {
					if ( !stricmp(CTEXT(node), Skill_level_names(i, 0)) )
						break;
				}
				if ( i == NUM_SKILL_LEVELS )
					return SEXP_CHECK_INVALID_SKILL_LEVEL;
				break;

			case OPF_MEDAL_NAME:
				if ( node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				for (i = 0; i < (int)Medals.size(); i++) {
					if ( !stricmp(CTEXT(node), Medals[i].name) )
						break;
				}

				if (i == (int)Medals.size())
					return SEXP_CHECK_INVALID_MEDAL_NAME;
				break;

			case OPF_HUGE_WEAPON:
			case OPF_WEAPON_NAME:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				i = weapon_info_lookup(CTEXT(node));

				if ( i < 0 )
					return SEXP_CHECK_INVALID_WEAPON_NAME;

				// we need to be sure that for huge weapons, the WIF_HUGE flag is set
				if (desired_argument_type == OPF_HUGE_WEAPON ) {
					if ( !(Weapon_info[i].wi_flags[Weapon::Info_Flags::Huge]) )
						return SEXP_CHECK_INVALID_WEAPON_NAME;
				}

				break;

			// Goober5000
			case OPF_INTEL_NAME:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				for ( i = 0; i < intel_info_size(); i++ ) {
					if ( !stricmp(CTEXT(node), Intel_info[i].name) )
						break;
				}

				if ( i == intel_info_size() )
					return SEXP_CHECK_INVALID_INTEL_NAME;
				
				break;

			case OPF_TURRET_TARGET_ORDER:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				for (i = 0; i < NUM_TURRET_ORDER_TYPES; i++ ) {
					if ( !stricmp(CTEXT(node), Turret_target_order_names[i]) )
						break;
				}

				if ( i == NUM_TURRET_ORDER_TYPES )
					return SEXP_CHECK_INVALID_TURRET_TARGET_ORDER;
				
				break;

			case OPF_TURRET_TYPE:
				if (node_subtype != SEXP_ATOM_STRING)
					return SEXP_CHECK_TYPE_MISMATCH;

				for (i = 0; i < NUM_TURRET_TYPES; i++) {
					if (!stricmp(CTEXT(node), Turret_valid_types[i]))
						break;
				}

				if (i == NUM_TURRET_TYPES)
					return SEXP_CHECK_INVALID_TURRET_TYPE;

				break;

			case OPF_ARMOR_TYPE:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				if (!stricmp(CTEXT(node), SEXP_NONE_STRING))
					break;

				for (st = 0; st < Armor_types.size(); st++ ) {
					if ( !stricmp(CTEXT(node), Armor_types[st].GetNamePtr()) )
						break;
				}

				if ( st == Armor_types.size() )
					return SEXP_CHECK_INVALID_ARMOR_TYPE;
				
				break;

			case OPF_DAMAGE_TYPE:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				if (!stricmp(CTEXT(node), SEXP_NONE_STRING))
					break;

				for (st = 0; st < Damage_types.size(); st++ ) {
					if ( !stricmp(CTEXT(node), Damage_types[st].name) )
						break;
				}

				if ( st == Damage_types.size() )
					return SEXP_CHECK_INVALID_DAMAGE_TYPE;
				
				break;

			case OPF_ANIMATION_TYPE:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				if (animation::anim_match_type(CTEXT(node)) == animation::ModelAnimationTriggerType::None )
					return SEXP_CHECK_INVALID_ANIMATION_TYPE;

				break;

			case OPF_TARGET_PRIORITIES:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;
	
				for(st = 0; st < Ai_tp_list.size(); st++) {
					if ( !stricmp(CTEXT(node), Ai_tp_list[st].name) )
						break;
				}

				if ( st == Ai_tp_list.size() )
					return SEXP_CHECK_INVALID_TARGET_PRIORITIES;
				
				break;
	
			case OPF_SHIP_CLASS_NAME:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				if (ship_info_lookup(CTEXT(node)) < 0)
					return SEXP_CHECK_INVALID_SHIP_CLASS_NAME;

				break;

			case OPF_SKYBOX_MODEL_NAME:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				if ( stricmp(CTEXT(node), NOX("default")) != 0 && stricmp(CTEXT(node), NOX("none")) != 0 && !strstr(CTEXT(node), NOX(".pof")) )
					return SEXP_CHECK_INVALID_SKYBOX_NAME;

				break;

			case OPF_SKYBOX_FLAGS:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				for ( i = 0; i < Num_skybox_flags; ++i ) {
					if ( !stricmp( CTEXT(node), Skybox_flags[i]) )
						break;
				}

				// if we reached the end of the list, then the flag is invalid
				if ( i == Num_skybox_flags )
					return SEXP_CHECK_INVALID_SKYBOX_FLAG;

				break;

			case OPF_JUMP_NODE_NAME:
				if ( node_subtype != SEXP_ATOM_STRING )
					return SEXP_CHECK_TYPE_MISMATCH;

				if (jumpnode_get_by_name(CTEXT(node)) == nullptr)
					return SEXP_CHECK_INVALID_JUMP_NODE;

				break;


			case OPF_VARIABLE_NAME:
				var_index = sexp_get_variable_index(node);
				if (var_index < 0)
					return SEXP_CHECK_INVALID_VARIABLE;

				switch (op_const)
				{
					// some SEXPs demand a number variable
					case OP_ADD_BACKGROUND_BITMAP:
					case OP_ADD_BACKGROUND_BITMAP_NEW:
					case OP_ADD_SUN_BITMAP:
					case OP_ADD_SUN_BITMAP_NEW:
					case OP_PLAY_SOUND_FROM_FILE:
					case OP_PAUSE_SOUND_FROM_FILE:
					case OP_CLOSE_SOUND_FROM_FILE:
						if (!(Sexp_variables[var_index].type & SEXP_VARIABLE_NUMBER)) 
							return SEXP_CHECK_INVALID_VARIABLE_TYPE;
						break;

					// some demand a string variable
					case OP_STRING_CONCATENATE:
					case OP_STRING_CONCATENATE_BLOCK:
					case OP_INT_TO_STRING:
					case OP_STRING_GET_SUBSTRING:
					case OP_STRING_SET_SUBSTRING:
					case OP_SCRIPT_EVAL_STRING:
						if (!(Sexp_variables[var_index].type & SEXP_VARIABLE_STRING)) 
							return SEXP_CHECK_INVALID_VARIABLE_TYPE;
						break;

					default:
						break;
				}

				// otherwise anything goes
				break;

			case OPF_AMBIGUOUS:
				// type checking for modify-variable
				// string or number -- anything goes
				break;

			case OPF_BACKGROUND_BITMAP:
			case OPF_SUN_BITMAP:
			case OPF_NEBULA_STORM_TYPE:
			case OPF_NEBULA_POOF:
			case OPF_NEBULA_PATTERN:
			case OPF_POST_EFFECT:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				break;

			case OPF_HUD_ELEMENT:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				} else {
					auto gauge = CTEXT(node);
					if ( strcmp(SEXP_HUD_GAUGE_WARPOUT, gauge) == 0 ) {
						break;
					}
				}
				return SEXP_CHECK_INVALID_HUD_ELEMENT;

			case OPF_WEAPON_BANK_NUMBER:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (!stricmp(CTEXT(node), SEXP_ALL_BANKS_STRING)) {
					break;
				}

				// if we haven't specified all banks we need to check the number of the bank is legal
				else {
					int num_banks = atoi(CTEXT(node));
					if ((num_banks >= MAX_SHIP_PRIMARY_BANKS) && (num_banks >= MAX_SHIP_SECONDARY_BANKS)) { // NOLINT
						return SEXP_CHECK_NUM_RANGE_INVALID;
					}
				}
				break;

			case OPF_SHIP_EFFECT:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (get_effect_from_name(CTEXT(node)) == -1 ) {
					return SEXP_CHECK_INVALID_SHIP_EFFECT;
				}
				break;

			case OPF_GAME_SND:
				if (node_subtype == SEXP_ATOM_NUMBER)
				{
					int node_num = atoi(CTEXT(node));
					if (node_num == -1)
						break;	// explicitly allow a sound of -1, indicating either "no sound" or "default"
					if (!gamesnd_get_by_tbl_index(node_num).isValid())
						return SEXP_CHECK_INVALID_GAME_SND;
				}
				else if (node_subtype == SEXP_ATOM_STRING)
				{
					auto node_text = CTEXT(node);
					if (stricmp(node_text, SEXP_NONE_STRING) == 0)
						break;	// explicitly allow "no sound", although some sexps might interpret it as "default"
					if (!gamesnd_get_by_name(node_text).isValid())
						return SEXP_CHECK_INVALID_GAME_SND;
				}
				else if (node_subtype != SEXP_ATOM_LIST)
					return SEXP_CHECK_INVALID_GAME_SND;
				break;

			case OPF_FIREBALL:
				if (node_subtype == SEXP_ATOM_NUMBER || can_construe_as_integer(CTEXT(node)))
				{
					int num = atoi(CTEXT(node));
					if (!SCP_vector_inbounds(Fireball_info, num))
						return SEXP_CHECK_INVALID_FIREBALL;
				}
				else if (node_subtype == SEXP_ATOM_STRING)
				{
					if (fireball_info_lookup(CTEXT(node)) < 0)
						return SEXP_CHECK_INVALID_FIREBALL;
				}
				else if (node_subtype != SEXP_ATOM_LIST)
					return SEXP_CHECK_INVALID_FIREBALL;
				break;

			case OPF_SPECIES:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (species_info_lookup(CTEXT(node)) < 0 ) {
					return SEXP_CHECK_INVALID_SPECIES;
				}
				break;

			case OPF_LANGUAGE:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				// that's all we do, since there may be a language the game doesn't know about
				break;

			case OPF_FUNCTIONAL_WHEN_EVAL_TYPE:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (sexp_functional_when_eval_type_lookup(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_FUNCTIONAL_WHEN_EVAL_TYPE;
				}
				break;

			case OPF_CONTAINER_NAME:
			case OPF_LIST_CONTAINER_NAME:
			case OPF_MAP_CONTAINER_NAME:
			{
				if (node_subtype != SEXP_ATOM_CONTAINER_NAME) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				p_container = get_sexp_container(Sexp_nodes[node].text);
				if (!p_container) {
					Warning(LOCATION, "Attempt to use unknown container %s. Please report!", Sexp_nodes[node].text);
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if ((desired_argument_type == OPF_LIST_CONTAINER_NAME && !p_container->is_list()) ||
						(desired_argument_type == OPF_MAP_CONTAINER_NAME && !p_container->is_map())) {
					return SEXP_CHECK_WRONG_CONTAINER_TYPE;
				}
				break;
			}

			case OPF_CONTAINER_VALUE:
				if (!p_container) {
					Warning(LOCATION, "Attempt to check value arg for null container for SEXP operator %s at arg %d. Please report!", Operators[op_index].text.c_str(), argnum);
					return SEXP_CHECK_TYPE_MISMATCH;
				}
				z = check_container_value_data_type(op_const,
					argnum,
					p_container->type,
					(node_subtype == SEXP_ATOM_STRING),
					(node_return_type == OPR_NUMBER) || (node_return_type == OPR_POSITIVE));
				if (z) {
					return z;
				}
				break;

			case OPF_DATA_OR_STR_CONTAINER:
			{
				if (node_subtype == SEXP_ATOM_CONTAINER_NAME) {
					// only list containers of strings or map containers with string keys are allowed
					const auto *p_str_container = get_sexp_container(Sexp_nodes[node].text);
					if (!p_str_container) {
						Warning(LOCATION, "Attempt to use unknown container %s. Please report!", Sexp_nodes[node].text);
						return SEXP_CHECK_TYPE_MISMATCH;
					}

					const auto &str_container = *p_str_container;
					if (str_container.is_list() && none(str_container.type & ContainerType::STRING_DATA)) {
						return SEXP_CHECK_WRONG_CONTAINER_DATA_TYPE;
					} else if (str_container.is_map() && none(str_container.type & ContainerType::STRING_KEYS)) {
						return SEXP_CHECK_WRONG_MAP_KEY_TYPE;
					}
				}
				break;
			}

			case OPF_ASTEROID_TYPES:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				} else {
					auto list = get_list_valid_asteroid_subtypes();
					bool valid = false;
					for (const auto& item : list) {
						if (!stricmp(CTEXT(node), item.c_str())) {
							valid = true;
							break;
						}
					}

					if (stricmp(CTEXT(node), SEXP_NONE_STRING) && !valid) {
						return SEXP_CHECK_INVALID_ASTEROID;
					}
				}
				break;

			case OPF_DEBRIS_TYPES:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (stricmp(CTEXT(node), SEXP_NONE_STRING) && (get_asteroid_index(CTEXT(node))) < 0) {
					return SEXP_CHECK_INVALID_ASTEROID;
				}
				break;

			case OPF_WING_FORMATION:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (!stricmp(CTEXT(node), "Default")) {
					break;
				}

				if (wing_formation_lookup(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_WING_FORMATION;
				}
				break;

			case OPF_MOTION_DEBRIS:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (stricmp(CTEXT(node), SEXP_NONE_STRING) && (get_motion_debris_by_name(CTEXT(node)) < 0)) {
					return SEXP_CHECK_INVALID_MOTION_DEBRIS;
				}
				break;

			case OPF_BOLT_TYPE:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (get_bolt_type_by_name(CTEXT(node)) < 0) {
					return SEXP_CHECK_INVALID_BOLT_TYPE;
				}
				break;

			case OPF_TRAITOR_OVERRIDE:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (stricmp(CTEXT(node), SEXP_NONE_STRING) && (get_traitor_override_pointer(CTEXT(node)) == nullptr)) {
					return SEXP_CHECK_INVALID_TRAITOR_OVERRIDE;
				}
				break;

			case OPF_LUA_GENERAL_ORDER:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (stricmp(CTEXT(node), SEXP_NONE_STRING) && (ai_lua_find_general_order_id(CTEXT(node)) < 0)) {
					return SEXP_CHECK_INVALID_LUA_GENERAL_ORDER;
				}
				break;

			case OPF_MISSION_CUSTOM_STRING:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (get_custom_string_by_name(CTEXT(node)) == nullptr) {
					return SEXP_CHECK_INVALID_CUSTOM_STRING;
				}
				break;

			case OPF_MESSAGE_TYPE:
				if (node_subtype != SEXP_ATOM_STRING) {
					return SEXP_CHECK_TYPE_MISMATCH;
				}

				if (get_builtin_message_type(CTEXT(node)) == MESSAGE_NONE) {
					return SEXP_CHECK_INVALID_MESSAGE_TYPE;
				}
				break;

			default: //This handles OPF_CHILD_LUA_ENUM as well
				if (!Dynamic_enums.empty()) {
					if ((desired_argument_type - First_available_opf_id) < sz2i(Dynamic_enums.size())) {
						if (node_subtype != SEXP_ATOM_STRING)
							return SEXP_CHECK_TYPE_MISMATCH;
					} else {
						Error(LOCATION, "Unhandled argument format");
					}
				} else {
					Error(LOCATION, "Unhandled argument format");
				}
				
		}

		node = Sexp_nodes[node].rest;
		argnum++;
	}

	return 0;
}

int check_sexp_potential_issues(int node, int *bad_node, SCP_string &issue_msg)
{
	// empty tree
	if (node < 0)
		return 0;

	if (bad_node)
		*bad_node = node;

	// check operators for anything that might trip up a mission designer
	if (Sexp_nodes[node].subtype == SEXP_ATOM_OPERATOR)
	{
		auto op_num = get_operator_const(node);
		int first_arg_node = CDR(node);

		switch (op_num)
		{
			// examine all sexps that check for wing destruction
			case OP_IS_DESTROYED_DELAY:
			case OP_PERCENT_SHIPS_DESTROYED:
				first_arg_node = CDR(first_arg_node);
				FALLTHROUGH;
			case OP_IS_DESTROYED:
			case OP_TIME_WING_DESTROYED:
			{
				if (!The_mission.ai_profile->flags[AI::Profile_Flags::Cancel_future_waves_of_any_wing_launched_from_an_exited_ship])
				{
					for (int n = first_arg_node; n >= 0; n = CDR(n))
					{
						int wingnum = wing_lookup(CTEXT(n));
						if (wingnum >= 0)
						{
							auto wingp = &Wings[wingnum];
							if (wingp->num_waves > 1 && wingp->arrival_location == ArrivalLocation::FROM_DOCK_BAY)
							{
								issue_msg = "Wing ";
								issue_msg += wingp->name;
								issue_msg += " has more than one wave and arrives from a docking bay.  Be careful when checking for this wing's destruction.  If the "
									"mothership is destroyed before all waves have arrived, the wing will not be considered destroyed.  Note that the "
									"$cancel future waves of any wing launched from an exited ship: flag can be used to fix this.";
								return SEXP_CHECK_POTENTIAL_ISSUE;
							}
						}
					}
				}
				break;
			}

			// examine uses of alter-ship-flag and are-ship-flags-set with "immobile"
			case OP_ALTER_SHIP_FLAG:
			case OP_ARE_SHIP_FLAGS_SET:
			{
				Object::Object_Flags object_flag = Object::Object_Flags::NUM_VALUES;
				Ship::Ship_Flags ship_flag = Ship::Ship_Flags::NUM_VALUES;
				Mission::Parse_Object_Flags parse_obj_flag = Mission::Parse_Object_Flags::NUM_VALUES;
				AI::AI_Flags ai_flag = AI::AI_Flags::NUM_VALUES;
				bool immobile_used = false;

				// check to see the flag is specified by the sexp (don't check to see what the flag may be applied to)
				if (op_num == OP_ALTER_SHIP_FLAG)
				{
					auto flag_name = CTEXT(first_arg_node);
					sexp_check_flag_arrays(flag_name, object_flag, ship_flag, parse_obj_flag, ai_flag);
					if (object_flag == Object::Object_Flags::Immobile || parse_obj_flag == Mission::Parse_Object_Flags::OF_Immobile)
						immobile_used = true;
				}
				else
				{
					for (int n = CDR(first_arg_node); n >= 0; n = CDR(n))
					{
						auto flag_name = CTEXT(n);
						sexp_check_flag_arrays(flag_name, object_flag, ship_flag, parse_obj_flag, ai_flag);
						if (object_flag == Object::Object_Flags::Immobile || parse_obj_flag == Mission::Parse_Object_Flags::OF_Immobile)
						{
							immobile_used = true;
							break;
						}
					}
				}

				// now check if any ships are created with any of the flags
				if (immobile_used)
				{
					for (const auto so : list_range(&Ship_obj_list))
					{
						const auto &obj = Objects[so->objnum];
						if (obj.flags[Object::Object_Flags::Immobile, Object::Object_Flags::Dont_change_position, Object::Object_Flags::Dont_change_orientation])
						{
							issue_msg = "At least one ship (";
							issue_msg += Ships[obj.instance].ship_name;
							issue_msg += ") has \"Does Not Change Position\" and/or \"Does Not Change Orientation\" checked, while this ";
							issue_msg += Sexp_nodes[node].text;
							issue_msg += " operator uses the \"immobile\" flag.  Be aware that all three flags are independent and setting/checking one flag will not "
								"set/check another.  For convenience, the set-mobile and set-immobile operators will clear conflicting flags, but alter-ship-flag will not.";
							return SEXP_CHECK_POTENTIAL_ISSUE;
						}
					}
				}
				break;
			}

			default:
				break;
		}
	}

	// recurse down the tree
	int z = check_sexp_potential_issues(CAR(node), bad_node, issue_msg);
	if (z)
		return z;
	z = check_sexp_potential_issues(CDR(node), bad_node, issue_msg);
	if (z)
		return z;

	return 0;
}

// Goober5000
bool get_unformatted_sexp_variable_name(char *unformatted, const char *formatted)
{
	// first character should be @
	if (*formatted != SEXP_VARIABLE_CHAR)
		return false;

	auto intermediate = formatted + 1;
	size_t n;

	// get variable name (up to '[')
	auto l_bracket = strchr(intermediate, '[');
	if (l_bracket) {
		auto r_bracket = strchr(l_bracket, ']');
		if (!r_bracket) {
			error_display(0, "Malformed variable token: %s", formatted);
			return false;
		}
		n = l_bracket - intermediate;
	} else {
		n = strlen(intermediate);
	}

	if (n >= TOKEN_LENGTH - 1)
		return false;

	strncpy(unformatted, intermediate, n);
	unformatted[n] = 0;
	return true;
}

// Goober5000
// This function specifically checks the token that is parsed from a mission file.
// In this situation the variable would appear as @variable_name[variable_contents].
// See also check_sexp_node_text_for_sexp_variable().
int check_string_for_sexp_variable(const char *str, size_t n_chars)
{
	if (*str != SEXP_VARIABLE_CHAR)
		return -1;

	constexpr size_t var_token_size = 2 * (TOKEN_LENGTH - 1) + 4;		// @variable_token[contents_token]\0
	char variable_token[var_token_size];
	char variable_name[NAME_LENGTH];

	// token is too long?
	if (n_chars >= var_token_size)
		return -1;

	strncpy(variable_token, str, n_chars);
	variable_token[n_chars] = 0;

	// see if it's really a variable
	if (!get_unformatted_sexp_variable_name(variable_name, variable_token))
		return -1;

	return get_index_sexp_variable_name(variable_name);
}

/**
 * Get text to stuff into Sexp_node in case of variable
 *
 * If Fred_running - stuff Sexp_variables[].variable_name
 * otherwise - stuff index into Sexp_variables array.
 */
void get_sexp_text_for_variable(char *text, int sexp_var_index)
{
	Assertion(sexp_var_index >= 0 && sexp_var_index < MAX_SEXP_VARIABLES, "sexp_var_index out of range!");

	if (Fred_running) {
		strcpy(text, Sexp_variables[sexp_var_index].variable_name);
	} else {
		sprintf(text, "%d", sexp_var_index);
	}
}

// Goober5000
void do_preload_for_arguments(void (*preloader)(const char *), int arg_node, int arg_handler_node)
{
	if (arg_node < 0)
		return;

	// we have a special argument
	if (Sexp_nodes[arg_node].flags & SNF_SPECIAL_ARG_IN_NODE)
	{
		int n;

		// we might not be handling it, either because this is not a *_of operator
		// or because we've disabled preloading for special arguments
		if (arg_handler_node < 0)
			return;

		// preload for each argument
		for (n = CDR(arg_handler_node); n != -1; n = CDR(n))
		{
			preloader(CTEXT(n));
		}
	}
	// we don't
	else
	{
		// preload for just the one argument
		preloader(CTEXT(arg_node));
	}
}

// Goober5000
void preload_change_ship_class(const char *text)
{
	int idx;
	ship_info *sip;

	idx = ship_info_lookup(text);
	if (idx < 0)
		return;

	// preload the model, just in case there is no other ship of this class in the mission
	// (this eliminates the slight pause during a mission when changing to a previously unloaded model)
	sip = &Ship_info[idx];
	sip->model_num = model_load(sip->pof_file, sip);

	if (sip->model_num >= 0)
		model_page_in_textures(sip->model_num, idx);
}

// MjnMixael
void preload_asteroid_class(const char* text)
{
	const auto& list = get_list_valid_asteroid_subtypes();

	bool valid = std::any_of(list.begin(), list.end(), [&](const SCP_string& item) { return !stricmp(text, item.c_str()); });

	if (!valid)
		return;

	asteroid_load(ASTEROID_TYPE_SMALL, get_asteroid_subtype_index_by_name(text, ASTEROID_TYPE_SMALL));
	asteroid_load(ASTEROID_TYPE_MEDIUM, get_asteroid_subtype_index_by_name(text, ASTEROID_TYPE_MEDIUM));
	asteroid_load(ASTEROID_TYPE_LARGE, get_asteroid_subtype_index_by_name(text, ASTEROID_TYPE_LARGE));

}	

// MjnMixael
void preload_debris_class(const char* text)
{
	auto idx = get_asteroid_index(text);
	if (idx < 0)
		return;

	asteroid_load(idx, 0);
}

// MjnMixael
void preload_change_prop_class(const char* text)
{
	int idx;
	prop_info* pip;

	idx = prop_info_lookup(text);
	if (idx < 0)
		return;

	pip = &Prop_info[idx];
	pip->model_num = model_load(pip->pof_file.c_str());

	if (pip->model_num >= 0)
		model_page_in_textures(pip->model_num, idx);
}

// Goober5000
void preload_turret_change_weapon(const char *text)
{
	int idx;

	idx = weapon_info_lookup(text);
	if (idx < 0)
		return;

	weapon_mark_as_used(idx);
}

void preload_texture(const char* text)
{
	if (!stricmp(text, "invisible"))
		return;

	int texture = bm_load_either(text);

	if (texture < 0) {
		Warning(LOCATION, "Could not preload texture %s", text);
		return;
	}

	bm_page_in_texture(texture);
}

// Goober5000
// we don't use the do_preload function because the preloader needs to access two nodes at a time
// also we're not using CTEXT or eval_num here because XSTR should really be constant, and
// also because we can't really run sexp stuff in a preloader
void localize_sexp(int text_node, int id_node)
{
	// we need both a name and an id
	if (text_node < 0 || id_node < 0)
		return;

	int id = atoi(Sexp_nodes[id_node].text);
	Assert(id < 10000000);
	SCP_string xstr;
	sprintf(xstr, "XSTR(\"%s\", %d)", Sexp_nodes[text_node].text, id);

	memset(Sexp_nodes[text_node].text, 0, TOKEN_LENGTH * sizeof(char));
	lcl_ext_localize(xstr.c_str(), Sexp_nodes[text_node].text, TOKEN_LENGTH - 1);
}

// Advance to and consume the closing parenthesis of a sexp, in case of a parse error.
// If a closing parenthesis is not found, this advances to the end of the string.
void skip_sexp(bool within_quotes = false)
{
	int level = 1;

	while (*Mp != '\0')
	{
		if (*Mp == '\"')
			within_quotes = !within_quotes;

		if (!within_quotes)
		{
			if (*Mp == ')')
			{
				level--;
				if (level == 0)
				{
					Mp++;
					return;
				}
			}
			else if (*Mp == '(')
				level++;
		}

		Mp++;
	}
}

/**
 * Returns the first sexp index of data this function allocates. (start of this sexp)
 *
 * NOTE: On entry into this function, Mp points to the first character past the opening parenthesis.
 */
int get_sexp()
{
	int start, node, last, op;
	char token[TOKEN_LENGTH];
	bool prune_extra_args = false;

	Assert(*(Mp-1) == '(');
	auto starting_Mp = Mp;

	// start - the node allocated in first instance of function
	// node - the node allocated in current instance of function
	// variable - whether string or number is a variable referencing Sexp_variables

	// initialization
	start = last = -1;

	ignore_white_space();
	while (*Mp != ')') {
		// end of string or end of file
		if (*Mp == '\0') {
			char buf[512];
			error_display(0, "Unexpected end of sexp!\n%s", three_dot_truncate(buf, starting_Mp, 512));
			return Locked_sexp_false;
		}

		// Sexp list
		if (*Mp == '(') {
			Mp++;
			node = alloc_sexp("", SEXP_LIST, SEXP_ATOM_LIST, get_sexp(), -1);
		}

		// Sexp string
		else if (*Mp == '\"') {
			auto startp = Mp;
			auto len = strcspn(Mp + 1, "\"");
			// was closing quote not found?
			if (*(Mp + 1 + len) != '\"') {
				char buf[512];
				error_display(0, "Unexpected end of quoted string embedded in sexp!\n%s", three_dot_truncate(buf, starting_Mp, 512));
				skip_sexp(true);	// this will have the effect of skipping to the end of the file or string
				return Locked_sexp_false;
			}
			// bump past closing quote
			Mp += (len + 2);

			// it could be a string variable
			int sexp_var_index = check_string_for_sexp_variable(startp + 1, len);
			if (sexp_var_index >= 0) {
				get_sexp_text_for_variable(token, sexp_var_index);
				node = alloc_sexp(token, (SEXP_ATOM | SEXP_FLAG_VARIABLE), SEXP_ATOM_STRING, -1, -1);
			}
			// it's a regular string
			else {
				// token is too long?
				if (len >= TOKEN_LENGTH) {
					SCP_string long_token(startp + 1, len);
					error_display(0, "Token is too long. Needs to be %d characters or shorter:\n%s", TOKEN_LENGTH - 1, long_token.c_str());
					// here we can just truncate; we don't need to return
					len = TOKEN_LENGTH - 1;
				}

				strncpy(token, startp + 1, len);
				token[len] = 0;
				node = alloc_sexp(token, SEXP_ATOM, SEXP_ATOM_STRING, -1, -1);
			}
		}

		// Sexp container
		else if (*Mp == sexp_container::DELIM) {
			auto startp = Mp;
			size_t len = 0;
			while (!is_parenthesis(*Mp) && !is_white_space(*Mp)) {
				// end of string or end of file
				if (*Mp == '\0') {
					char buf[512];
					error_display(0, "Unexpected end of sexp!\n%s", three_dot_truncate(buf, starting_Mp, 512));
					return Locked_sexp_false;
				}
				Mp++;
				len++;
			}

			// token is too long?
			if (len >= TOKEN_LENGTH) {
				SCP_string long_token(startp, len);
				error_display(0, "Token is too long. Needs to be %d characters or shorter:\n%s", TOKEN_LENGTH - 1, long_token.c_str());
				skip_sexp();
				return Locked_sexp_false;
			}

			strncpy(token, startp, len);
			token[len] = 0;

			// container name
			if (token[1] == sexp_container::DELIM) {
				auto container_name = token + 2;

				if (get_sexp_container(container_name) == nullptr) {
					error_display(0, "Attempt to use unknown container '%s'", token);
					skip_sexp();
					return Locked_sexp_false;
				}

				node = alloc_sexp(container_name, SEXP_ATOM, SEXP_ATOM_CONTAINER_NAME, -1, -1);
			}
			// container data
			else {
				if (token[len - 1] != sexp_container::DELIM) {
					error_display(0, "Malformed container data token: %s", token);
					skip_sexp();
					return Locked_sexp_false;
				}

				char container_name[TOKEN_LENGTH];
				strcpy_s(container_name, token + 1);	// skip first delimiter
				container_name[len - 2] = 0;			// truncate last delimiter

				if (get_sexp_container(container_name) == nullptr) {
					error_display(0, "Attempt to use data from unknown container '%s'", token);
					skip_sexp();
					return Locked_sexp_false;
				}

				// advance to the container modifier, since we'll read them when calling get_sexp() below
				ignore_gray_space();
				// watch out for malformed input
				if (*Mp != '(')
					break;
				Mp++;

				node = alloc_sexp(container_name, SEXP_ATOM, SEXP_ATOM_CONTAINER_DATA, get_sexp(), -1);
			}
		}

		// Sexp operator or number
		else {
			auto startp = Mp;
			size_t len = 0;
			while (!is_parenthesis(*Mp) && !is_white_space(*Mp)) {
				// end of string or end of file
				if (*Mp == '\0') {
					char buf[512];
					error_display(0, "Unexpected end of sexp!\n%s", three_dot_truncate(buf, starting_Mp, 512));
					return Locked_sexp_false;
				}
				Mp++;
				len++;
			}

			// it could be a numeric variable
			int sexp_var_index = check_string_for_sexp_variable(startp, len);
			if (sexp_var_index >= 0) {
				get_sexp_text_for_variable(token, sexp_var_index);
				node = alloc_sexp(token, (SEXP_ATOM | SEXP_FLAG_VARIABLE), SEXP_ATOM_NUMBER, -1, -1);
			}
			// it could be an operator
			else {
				// token is too long?
				if (len >= TOKEN_LENGTH) {
					SCP_string long_token(startp, len);
					error_display(0, "Token is too long. Needs to be %d characters or shorter:\n%s", TOKEN_LENGTH - 1, long_token.c_str());
					skip_sexp();
					return Locked_sexp_false;
				}

				strncpy(token, startp, len);
				token[len] = 0;

				// maybe replace deprecated names
				if (!stricmp(token, "set-ship-position"))
					strcpy_s(token, "set-object-position");
				else if (!stricmp(token, "set-ship-facing"))
					strcpy_s(token, "set-object-facing");
				else if (!stricmp(token, "set-ship-facing-object"))
					strcpy_s(token, "set-object-facing-object");
				else if (!stricmp(token, "ai-chase-any-except")) {
					strcpy_s(token, "ai-chase-any");
					prune_extra_args = true;
				} else if (!stricmp(token, "change-ship-model"))
					strcpy_s(token, "change-ship-class");
				else if (!stricmp(token, "radar-set-max-range"))
					strcpy_s(token, "hud-set-max-targeting-range");
				else if (!stricmp(token, "ship-subsys-vanished"))
					strcpy_s(token, "ship-subsys-vanish");
				else if (!stricmp(token, "directive-is-variable"))
					strcpy_s(token, "directive-value");
				else if (!stricmp(token, "variable-array-get"))
					strcpy_s(token, "get-variable-by-index");
				else if (!stricmp(token, "variable-array-set"))
					strcpy_s(token, "set-variable-by-index");
				else if (!stricmp(token, "distance-ship-subsystem"))
					strcpy_s(token, "distance-center-to-subsystem");
				else if (!stricmp(token, "remove-weapons"))
					strcpy_s(token, "clear-weapons");
				else if (!stricmp(token, "hud-set-retail-gauge-active"))
					strcpy_s(token, "hud-set-builtin-gauge-active");
				else if (!stricmp(token, "perform-actions"))
					strcpy_s(token, "perform-actions-bool-first");
				else if (!stricmp(token, "add-to-collision-group2"))
					strcpy_s(token, "add-to-collision-group-new");
				else if (!stricmp(token, "remove-from-collision-group2"))
					strcpy_s(token, "remove-from-collision-group-new");

				op = get_operator_index(token);
				if (op >= 0) {
					node = alloc_sexp(token, SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, -1);
				}
				// it's not an operator, and we've checked for variables, so treat it as a number
				else {
					node = alloc_sexp(token, SEXP_ATOM, SEXP_ATOM_NUMBER, -1, -1);
				}
			}
		}

		// update links
		if (start == -1) {
			start = node;
		} else {
			const char *message = nullptr;
			Assert(last != -1);

			// Locked_sexp_true and Locked_sexp_false are only meant to represent operator
			// nodes with no arguments, i.e. (true) and (false). If they appear as "bare"
			// arguments to another operator, or have arguments of their own, the global
			// SEXP node structure can become corrupted; we hack around this by always
			// wrapping them in their own list in these cases and warning the user.
			if (last == start && (start == Locked_sexp_false || start == Locked_sexp_true)) {
				message = "Found true or false operator at the start of a list, likely in a handwritten SEXP.";
				start = alloc_sexp("", SEXP_LIST, SEXP_ATOM_LIST, start, -1);
				last = start;
			}
			if (node == Locked_sexp_false || node == Locked_sexp_true) {
				message = "Found true or false operator in non-operator position, likely in a handwritten SEXP.";
				node = alloc_sexp("", SEXP_LIST, SEXP_ATOM_LIST, node, -1);
			}
			Sexp_nodes[last].rest = node;

			if (message != nullptr) {
				SCP_string context;
				convert_sexp_to_string(context, start, SEXP_ERROR_CHECK_MODE);
				Warning(LOCATION, "%s\n\nCurrently parsed SEXP, with fixed operator:\n%s\n", message, context.c_str());
			}
		}

		Assert(node != -1);  // ran out of nodes.  Time to raise the MAX!
		last = node;
		ignore_white_space();
	}

	Mp++;  // skip past the ')'

	
	// Goober5000 - backwards compatibility for removed ai-chase-any-except
	if (get_operator_const(start) == OP_AI_CHASE_ANY && prune_extra_args)
	{
		// if there is more than one argument, free the extras
		int n = CDR(CDR(start));
		if (n >= 0)
		{
			// free the entire rest of the argument list
			free_sexp2(n, CDR(start));
		}
	}
	
	// Goober5000 - preload stuff for certain sexps
	if (!Fred_running)
	{
		int n, parent, arg_handler = -1;

		// see if we're using special arguments
		parent = find_parent_operator(start);
		if (parent >= 0 && is_when_argument_op(get_operator_const(parent)))
		{
			// get the first op of the parent, which should be a *_of operator
			arg_handler = CADR(parent);
			if (arg_handler >= 0 && !is_argument_provider_op(get_operator_const(arg_handler)))
				arg_handler = -1;
		}

		// DISABLE ARGUMENT PRELOADING FOR NOW
		// see Mantis #925 for discussion
		// Also, the preloader will have to be moved to a different function (after the parsing is finished)
		// in order to work properly with special arguments.  The current node is an orphan until get_sexp
		// completes, at which time it will be linked into the sexp node list; this means that it is
		// impossible to get the parent node.
		arg_handler = -1;

		// preload according to sexp
		op = get_operator_const(start);
		switch (op)
		{
			case OP_CHANGE_SHIP_CLASS:
				// ship class is argument #1
				n = CDR(start);
				do_preload_for_arguments(preload_change_ship_class, n, arg_handler);
				break;

			case OP_SHIP_CREATE:
				// ship class is argument #2
				n = CDDR(start);
				// page in ship classes of dynamically created ships
				// preload_change_ship_class doesn't require a class change, so we can use that here -zookeeper
				do_preload_for_arguments(preload_change_ship_class, n, arg_handler);
				break;

			case OP_PROP_CREATE:
				// prop class is argument #2
				n = CDDR(start);
				// page in prop classes of dynamically created props
				do_preload_for_arguments(preload_change_prop_class, n, arg_handler);
				break;

			case OP_SET_SPECIAL_WARPOUT_NAME:
				// set flag for taylor
				Knossos_warp_ani_used = true;
				break;

			case OP_MISSION_SET_NEBULA:
				// set flag for WMC
				Nebula_sexp_used = true;
				break;

			case OP_MISSION_SET_SUBSPACE:
				// set flag for Goober5000
				The_mission.flags.set(Mission::Mission_Flags::Preload_subspace);
				break;

			case OP_WARP_EFFECT:
				// type of warp is argument #11
				n = CDDDDDR(start);
				n = CDDDDDR(n);
				n = CDR(n);

				// set flag for taylor
				if (CAR(n) != -1 || Sexp_nodes[n].flags & SNF_SPECIAL_ARG_IN_NODE)		// if it's evaluating a sexp or a special argument
					Knossos_warp_ani_used = true;											// set flag just in case
				else if (atoi(CTEXT(n)) != 0)											// if it's not the default 0
					Knossos_warp_ani_used = true;											// set flag just in case
				break;

			case OP_SET_SKYBOX_MODEL:
				// model is argument #1
				n = CDR(start);
				do_preload_for_arguments(sexp_set_skybox_model_preload, n, arg_handler);
				break;

			case OP_CONFIG_ASTEROID_FIELD:
				// asteroid types start at argument #17
				n = CDDDDDR(start);
				n = CDDDDDR(n);
				n = CDDDDDR(n);
				n = CDDR(n);

				// loop through all remaining arguments
				for (int arg = n; arg >= 0; arg = CDR(arg)) {
					do_preload_for_arguments(preload_asteroid_class, arg, arg_handler);
				}
				break;

			case OP_CONFIG_DEBRIS_FIELD:
				// debris types start at argument #10
				n = CDDDDDR(start);
				n = CDDDDDR(n);
				
				// loop through all remaining arguments
				for (int arg = n; arg >= 0; arg = CDR(arg)) {
					do_preload_for_arguments(preload_debris_class, arg, arg_handler);
				}
				break;

			case OP_TURRET_CHANGE_WEAPON:
				// weapon to change to is arg #3
				n = CDDDR(start);
				do_preload_for_arguments(preload_turret_change_weapon, n, arg_handler);
				break;

			case OP_WEAPON_CREATE:
				// weapon class is argument #2
				n = CDDR(start);
				// just as with change-ship-class/ship-create, we can use the turret-change-weapon preloader for weapon-create
				do_preload_for_arguments(preload_turret_change_weapon, n, arg_handler);
				break;

			case OP_BEAM_FLOATING_FIRE:
				// beam weapon class is argument #1
				n = CDR(start);
				// see above for weapon-create
				do_preload_for_arguments(preload_turret_change_weapon, n, arg_handler);
				break;

			case OP_CHANGE_BACKGROUND:
				n = CDR(start);
				do_preload_for_arguments(stars_preload_background, n, arg_handler);
				break;

			case OP_ADD_SUN_BITMAP:
			case OP_ADD_SUN_BITMAP_NEW:
				n = CDR(start);
				do_preload_for_arguments(stars_preload_sun_bitmap, n, arg_handler);
				break;

			case OP_ADD_BACKGROUND_BITMAP:
			case OP_ADD_BACKGROUND_BITMAP_NEW:
				n = CDR(start);
				do_preload_for_arguments(stars_preload_background_bitmap, n, arg_handler);
				break;

			case OP_TECH_ADD_INTEL_XSTR:
			case OP_TECH_REMOVE_INTEL_XSTR:
				// do XSTR translation for each entry in the list
				// we don't use the do_preload function because the preloader needs to access two nodes at a time
				// also we're not using CTEXT or eval_num here because XSTR should really be constant, and
				// also because we can't really run sexp stuff in a preloader
				n = CDR(start);
				while (n >= 0 && CDR(n) >= 0)
				{
					localize_sexp(n, CDR(n));
					n = CDDR(n);
				}
				break;

			case OP_MODIFY_VARIABLE_XSTR:
				n = CDDR(start); // First parameter is the variable name so we need to get the second parameter
				localize_sexp(n, CDR(n));
				break;

			case OP_REPLACE_TEXTURE:
			case OP_REPLACE_TEXTURE_SKYBOX:
				//Texture name is argument 2
				n = CDDR(start);
				do_preload_for_arguments(preload_texture, n, arg_handler);
				break;
		}
	}

	return start;
}


/**
 * Stuffs a list of sexp variables
 */
int stuff_sexp_variable_list()
{	
	int count;
	char var_name[TOKEN_LENGTH];
	char default_value[TOKEN_LENGTH];
	char str_type[TOKEN_LENGTH];
	char persistent[TOKEN_LENGTH];
	char network[TOKEN_LENGTH];
	int index;
	int type;

	count = 0;
	required_string("$Variables:");
	ignore_white_space();

	// check for start of list
	if (*Mp != '(') {
		error_display(1, "Reading sexp variable list.  Found [%c].  Expecting '('.\n", *Mp);
		throw parse::ParseException("Syntax error");
	}

	Mp++;
	ignore_white_space();

	while (*Mp != ')') {
		Assert(count < MAX_SEXP_VARIABLES);

		// get index - for debug
		stuff_int(&index);
		ignore_gray_space();

		// get var_name
		get_string(var_name);
		ignore_gray_space();

		// get default_value;
		get_string(default_value);
		ignore_gray_space();

		// get type
		get_string(str_type);
		ignore_white_space();

		// determine type
		if (!stricmp(str_type, "number")) {
			type = SEXP_VARIABLE_NUMBER;
		} else if (!stricmp(str_type, "string")) {
			type = SEXP_VARIABLE_STRING;
		} else if (!stricmp(str_type, "block")) {
			// Goober5000 - This looks dangerous... these flags are needed for certain things, but it
			// looks like BLOCK_*_SIZE is the only thing that keeps a block from running off the end
			// of its boundary.
			type = SEXP_VARIABLE_BLOCK;
		} else {
			type = SEXP_VARIABLE_UNKNOWN;
			error_display(1, "SEXP variable '%s' is an unknown type!", var_name);
			throw parse::ParseException("Unknown variable type");
		}

		// possibly get network-variable
		if (check_for_string("\"network-variable\"")) {
			// eat it
			get_string(network);
			ignore_white_space();

			// set type
			type |= SEXP_VARIABLE_NETWORK;
		}

		// maybe this is an eternal persistent variable of some type
		if (check_for_string("\"eternal\"")) {
			// eat it
			get_string(persistent);
			ignore_white_space();

			// set type
			type |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE;
		}
		
		// maybe this is a persistent variable of some type
		if (check_for_string("\"player-persistent\"") || check_for_string("\"save-on-mission-close\"")) {
			// eat it
			get_string(persistent);
			ignore_white_space();

			// set type
			type |= SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE;
		} else if (check_for_string("\"campaign-persistent\"") || check_for_string("\"save-on-mission-progress\"")) {
			// eat it
			get_string(persistent);
			ignore_white_space();

			// set type
			type |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS;
		// trap error
		} else if (check_for_string("\"")) {
			// eat garbage
			get_string(persistent);
			ignore_white_space();

			// notify of error
			error_display(0, "Error parsing sexp variables - unknown persistence type encountered.  You can continue from here without trouble.");
		}

		// finished getting info; now add the variable
		if ( type & SEXP_VARIABLE_BLOCK ) {
			add_block_variable(default_value, var_name, type, index);
		}
		else {
			// check if variable name already exists
			// (block variables can be duplicated, but regular ones can't)
			if (get_index_sexp_variable_name(var_name) >= 0) {
				error_display(0, "Variable '%s' already exists!  Skipping duplicate.", var_name);
			} else {
				count++;
				sexp_add_variable(default_value, var_name, type, index);
			}
		}
	}

	Mp++;

	return count;
}

bool has_special_explosion_block_index(ship *shipp, int *index)
{
	int current_index;

	for ( current_index = (MAX_SEXP_VARIABLES - BLOCK_EXP_SIZE) ; current_index >= (MAX_SEXP_VARIABLES - (BLOCK_EXP_SIZE * Num_special_expl_blocks)) ; current_index = (current_index - BLOCK_EXP_SIZE) ) {
		if ( 
			(atoi(Block_variables[current_index+INNER_RAD].text) == shipp->special_exp_inner) &&
			(atoi(Block_variables[current_index+OUTER_RAD].text) == shipp->special_exp_outer) &&
			(atoi(Block_variables[current_index+DAMAGE].text) == shipp->special_exp_damage) &&
			(atoi(Block_variables[current_index+BLAST].text) == shipp->special_exp_blast) &&
			(atoi(Block_variables[current_index+PROPAGATE].text) == (shipp->use_shockwave ? 1:0) ) &&
			(atoi(Block_variables[current_index+SHOCK_SPEED].text) == shipp->special_exp_shockwave_speed)
			) {
				*index = current_index;
				return true;
		}
	}	

	// if we got here, this ship's special explosions aren't represented in the Block_variables array
	*index = current_index;
	return false;
}

bool generate_special_explosion_block_variables()
{
	ship *shipp; 
	int current_index;
	bool already_added = false; 
	int num_sexp_variables;
	int i;

	// since we're (re)generating the block variable index, we don't start off with any block variables 
	Num_special_expl_blocks = 0;

	// get the number of sexp_variables we currently have. We must not try to add a block variable with an index below this.
	num_sexp_variables = sexp_variable_count();

	for ( auto sop = GET_FIRST(&Ship_obj_list); sop != END_OF_LIST(&Ship_obj_list); sop = GET_NEXT(sop) ) {
		if (Objects[sop->objnum].flags[Object::Object_Flags::Should_be_dead])
			continue;

		shipp=&Ships[Objects[sop->objnum].instance];

		if (!(shipp->use_special_explosion)) {
			continue;
		}

		already_added = has_special_explosion_block_index(shipp, &current_index);

		// if we can't add an index for this add this ship to the list of those which failed
		if (current_index < num_sexp_variables) {
			// fail list code goes here
			continue;
		}

		//if we haven't added this entry already, do so
		if (!already_added) {
			sprintf(Block_variables[current_index+INNER_RAD].text, "%d", shipp->special_exp_inner);
			sprintf(Block_variables[current_index+OUTER_RAD].text, "%d", shipp->special_exp_outer);
			sprintf(Block_variables[current_index+DAMAGE].text, "%d", shipp->special_exp_damage);
			sprintf(Block_variables[current_index+BLAST].text, "%d", shipp->special_exp_blast);
			sprintf(Block_variables[current_index+PROPAGATE].text, "%d", (shipp->use_shockwave ? 1:0) );
			sprintf(Block_variables[current_index+SHOCK_SPEED].text, "%d", shipp->special_exp_shockwave_speed);

			// add the names
			for (i = current_index; i < (current_index + BLOCK_EXP_SIZE); i++ ) {
				sprintf(Block_variables[i].variable_name, "%s", shipp->ship_name); 
			}

			Num_special_expl_blocks++;
		}
	}

	return true;
}

int num_block_variables()
{
	return Num_special_expl_blocks * BLOCK_EXP_SIZE;
}

/**
 * Stuff this particular SEXP node (just the node, not the tree) into a string representation
 */
void stuff_sexp_text_string(SCP_string &dest, int node, int mode)
{
	Assert((node >= 0) && (node < Num_sexp_nodes));

	if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_NAME) {
		Assertion(get_sexp_container(Sexp_nodes[node].text) != nullptr,
			"Couldn't find container: %s\n",
			Sexp_nodes[node].text);

		sprintf(dest, "%s%s ", sexp_container::NAME_NODE_PREFIX.c_str(), Sexp_nodes[node].text);
	}
	else if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_DATA) {
		Assertion(get_sexp_container(Sexp_nodes[node].text) != nullptr,
			"Couldn't find container: %s\n",
			Sexp_nodes[node].text);

		sprintf(dest, "%c%s%c ", sexp_container::DELIM, Sexp_nodes[node].text, sexp_container::DELIM);
	}
	else if (Sexp_nodes[node].type & SEXP_FLAG_VARIABLE)
	{
		int sexp_variables_index = get_index_sexp_variable_name(Sexp_nodes[node].text);
		// during the last pass through error-reporting mode, sexp variables have already been transcoded to their indexes
		if (mode == SEXP_ERROR_CHECK_MODE && sexp_variables_index < 0)
		{
			if (can_construe_as_integer(Sexp_nodes[node].text))
				sexp_variables_index = atoi(Sexp_nodes[node].text);
		}

		const char *var_name, *var_contents;
		if (sexp_variables_index < 0)
		{
			Warning(LOCATION, "Couldn't find variable: %s\n", Sexp_nodes[node].text);
			var_name = Sexp_nodes[node].text;
			var_contents = "undefined";
		}
		else
		{
			var_name = (Fred_running) ? Sexp_nodes[node].text : Sexp_variables[sexp_variables_index].variable_name;
			var_contents = Sexp_variables[sexp_variables_index].text;
			Assertion((Sexp_variables[sexp_variables_index].type & SEXP_VARIABLE_NUMBER) || (Sexp_variables[sexp_variables_index].type & SEXP_VARIABLE_STRING), "Variable %s must be either a number or a string!", var_name);
		}

		// number
		if (Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER)
		{
			sprintf(dest, "@%s[%s] ", var_name, var_contents);
		}
		// string
		else if (Sexp_nodes[node].subtype == SEXP_ATOM_STRING)
		{
			sprintf(dest, "\"@%s[%s]\" ", var_name, var_contents);
		}
		else
			UNREACHABLE("SEXP variable nodes must be SEXP_ATOM_NUMBER or SEXP_ATOM_STRING!");
	}
	// not a variable
	else
	{
		const char *ctext_string = CTEXT(node);

		// strings are enclosed in quotes
		if (Sexp_nodes[node].subtype == SEXP_ATOM_STRING)
		{
			sprintf(dest, "\"%s\" ", ctext_string);
		}
		// numbers and operators are printed as-is
		else
		{
			// do some sanity checking based on Github issue #2314
			if (Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER)
			{
				// if (for whatever reason) we have an empty string or an invalid number, print 0
				// do not do this trick in error mode, because we want to know what the text actually is
				// note that we let <argument> strings pass here
				if ((mode != SEXP_ERROR_CHECK_MODE) && 
					(!(Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)) &&
					(*ctext_string == '\0' || !can_construe_as_integer(ctext_string)))
				{
					mprintf(("SEXP: '%s' is not a number; using '0' instead\n", ctext_string));
					ctext_string = "0";
				}
			}
			sprintf(dest, "%s ", ctext_string);
		}
	}
}

int build_sexp_string(SCP_string &accumulator, int cur_node, int level, int mode)
{
	SCP_string buf;
	int node;
	auto old_length = accumulator.length();

	accumulator += "( ";
	node = cur_node;
	while (node != -1) {
		Assert(node >= 0 && node < Num_sexp_nodes);
		if (Sexp_nodes[node].first == -1) {
			// build text to string
			stuff_sexp_text_string(buf, node, mode);
			accumulator += buf;

		} else if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_DATA) {
			// build text to string
			stuff_sexp_text_string(buf, node, mode);
			accumulator += buf;

			build_sexp_string(accumulator, Sexp_nodes[node].first, level + 1, mode);
		} else {
			build_sexp_string(accumulator, Sexp_nodes[node].first, level + 1, mode);
		}

		node = Sexp_nodes[node].rest;
	}

	accumulator += ") ";
	if ((accumulator.length() - old_length) > 40) {
		accumulator.resize(old_length);
		build_extended_sexp_string(accumulator, cur_node, level, mode);
		return 1;
	}

	return 0;
}

void build_extended_sexp_string(SCP_string &accumulator, int cur_node, int level, int mode)
{
	SCP_string buf;
	int i, flag = 0, node;

	accumulator += "( ";
	node = cur_node;
	while (node != -1) {
		// not the first line?
		if (flag) {
			for (i=0; i<level + 1; i++)
				accumulator += "   ";
		}

		flag = 1;
		Assert(node >= 0 && node < Num_sexp_nodes);
		if (Sexp_nodes[node].first == -1) {
			stuff_sexp_text_string(buf, node, mode);
			accumulator += buf;

		} else if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_DATA) {
			// build text to string
			stuff_sexp_text_string(buf, node, mode);
			accumulator += buf;

			build_sexp_string(accumulator, Sexp_nodes[node].first, level + 1, mode);
		} else {
			build_sexp_string(accumulator, Sexp_nodes[node].first, level + 1, mode);
		}

		accumulator += "\n";
		node = Sexp_nodes[node].rest;
	}

	for (i=0; i<level; i++)
		accumulator += "   ";

	accumulator += ")";
}

void convert_sexp_to_string(SCP_string &dest, int cur_node, int mode)
{
	if (cur_node >= 0) {
		dest = "";
		build_sexp_string(dest, cur_node, 0, mode);
	} else {
		dest = "( )";
	}
}


// ----------------------------------------------------------------------------------- 
// Helper methods for getting data from nodes. Cause it's stupid to keep re-rolling this stuff for every single SEXP
// -----------------------------------------------------------------------------------

bool is_node_opf_positive(int node)
{
	if (node < 0)
		return false;

	if (!(Sexp_nodes[node].flags & SNF_CHECKED_NODE_FOR_OPF_POSITIVE))
	{
		int parent_node = find_parent_operator(node);

		// if the SEXP has no parent, the point is moot
		if (parent_node >= 0)
		{
			int arg_num = find_argnum(parent_node, node);
			Assertion(arg_num >= 0, "Error finding sexp argument.  The SEXP is not listed among its parent's children.");

			if (query_operator_argument_type(get_operator_index(parent_node), arg_num) == OPF_POSITIVE)
				Sexp_nodes[node].flags |= SNF_NODE_IS_OPF_POSITIVE;
		}

		Sexp_nodes[node].flags |= SNF_CHECKED_NODE_FOR_OPF_POSITIVE;
	}

	return (Sexp_nodes[node].flags & SNF_NODE_IS_OPF_POSITIVE);
}

// Goober5000's number hack - ensure negative numbers aren't sent to parameters that expect OPF_POSITIVE
void ensure_opf_positive_is_positive(int node, int &val)
{
	if (is_node_opf_positive(node) && (val < 0) && (val > SEXP_UNLIKELY_RETURN_VALUE_BOUND))
	{
		// warn about it, but only once
		static bool Warned_about_opf_positive = false;
		if (!Warned_about_opf_positive)
		{
			int parent_node = find_parent_operator(node);	// these calls are known to be valid because
			int arg_num = find_argnum(parent_node, node);	// they are prerequisites to marking the node

			Warning(LOCATION, "Parent node \"%s\", argument %d (token \"%s\", value %d) is negative, but is required to be positive!", Sexp_nodes[parent_node].text, arg_num + 1, Sexp_nodes[node].text, val);
			Warned_about_opf_positive = true;
		}

		// clamp it to avoid an underflow
		val = 0;
	}
}

/**
 * Evaluate number which may result from an operator or may be text
 */
int eval_num(int n, bool &is_nan, bool &is_nan_forever)
{
	is_nan = false;
	is_nan_forever = false;

	// see precedent in eval_sexp
	if (n == -1)
		return 0;
	Assert(n >= 0);

	int op_n = CAR(n);
	// CAR() of a container data node is the container modifier
	if (op_n >= 0 && Sexp_nodes[n].subtype != SEXP_ATOM_CONTAINER_DATA) // if argument is a sexp
	{
		int val = eval_sexp(op_n);

		// NaNs will propagate through operations, so let the calling function know
		if (Sexp_nodes[op_n].value == SEXP_NAN)
		{
			val = 0;
			is_nan = true;
		}
		else if (Sexp_nodes[op_n].value == SEXP_NAN_FOREVER)
		{
			val = 0;
			is_nan_forever = true;
		}
		else
			ensure_opf_positive_is_positive(op_n, val);

		return val;
	}
	else
		return sexp_atoi(n);		// otherwise, just get the number (this function already contains an OPF_POSITIVE check)
}

template <typename T>
int eval_nums(int &n, bool &is_nan, bool &is_nan_forever, T &arg)
{
	if (n >= 0)
	{
		int val = eval_num(n, is_nan, is_nan_forever);
		n = CDR(n);

		arg = (T)val;
		return 1;
	}
	else
	{
		is_nan = false;
		is_nan_forever = false;

		arg = (T)0;
		return 0;
	}
}

/**
 * Populate variadic arguments by running eval_num repeatedly.  No custom converter function is used; all numbers are cast from int to the desired type.
 * The count of numbers actually found (which depending on the sexp may not be the count of parameters) is returned.
 * NOTE: in contrast to eval_num, the *n* parameter will be advanced along the CDR path
 */
template <typename T, typename... Args>
int eval_nums(int &n, bool &is_nan, bool &is_nan_forever, T& first, Args&... rest)
{
	bool temp_nan, temp_nan_forever;
	int count = 0;

	is_nan = false;
	is_nan_forever = false;

	count += eval_nums(n, temp_nan, temp_nan_forever, first);
	if (temp_nan)
		is_nan = true;
	if (temp_nan_forever)
		is_nan_forever = true;

	count += eval_nums(n, temp_nan, temp_nan_forever, rest...);
	if (temp_nan)
		is_nan = true;
	if (temp_nan_forever)
		is_nan_forever = true;

	return count;
}

/**
 * Populate a numeric array by running eval_num repeatedly.  The converter function/lambda can be used to adapt the numbers returned from eval_num, such as casting (the default)
 * or restricting to a range.  The count of numbers actually found (which depending on the sexp may not be the size of the array) is returned.
 * NOTE: in contrast to eval_num, the *n* parameter will be advanced along the CDR path
 */
template <typename T, std::size_t SIZE>
int eval_array(std::array<T, SIZE> &numbers, int &n, bool &is_nan, bool &is_nan_forever, T(*converter)(int), const T &value_if_missing = (T)0)
{
	bool temp_nan, temp_nan_forever;
	int count = 0;

	is_nan = false;
	is_nan_forever = false;

	// fill up the array
	for (std::size_t i = 0; i < SIZE; ++i)
	{
		// see if we have a number
		if (n >= 0)
		{
			int num = eval_num(n, temp_nan, temp_nan_forever);
			n = CDR(n);
			++count;

			if (temp_nan)
				is_nan = true;
			if (temp_nan_forever)
				is_nan_forever = true;

			// populate the array with that number
			numbers[i] = converter(num);
		}
		// use the default
		else
			numbers[i] = value_if_missing;
	}

	return count;
}

/**
 * Certain compilers don't like a lambda as a default argument, so here's an extra function definition that supplies a standard converter.  Thanks to Gasbow for the insight.
 */
template <typename T, std::size_t SIZE>
int eval_array(std::array<T, SIZE> &numbers, int &n, bool &is_nan, bool &is_nan_forever)
{
	return eval_array<T>(numbers, n, is_nan, is_nan_forever, [](int num) -> T { return (T)num; });
}

/**
 * Populate a vector by running eval_num on up to three consecutive nodes.  Returns the number of nodes processed.
 * NOTE: in contrast to eval_num, the *n* parameter will be advanced along the CDR path
 */
int eval_vec3d(vec3d *vec, int &n, bool &is_nan, bool &is_nan_forever)
{
	Assertion(vec != nullptr, "Vec must not be null!");

	// this is slightly more verbose than ideal, because one can't cast between C and C++ arrays
	std::array<float, 3> a1d;
	int count = eval_array(a1d, n, is_nan, is_nan_forever);
	std::copy(a1d.begin(), a1d.end(), std::begin(vec->a1d));

	return count;
}

/**
 * Populate an angles struct by running eval_num on up to three consecutive nodes.  Returns the number of nodes processed.
 * NOTE: in contrast to eval_num, the *n* parameter will be advanced along the CDR path
 */
int eval_angles(angles *a, int &n, bool &is_nan, bool &is_nan_forever)
{
	Assertion(a != nullptr, "a must not be null!");

	std::array<int, 3> a1d;
	int count = eval_array(a1d, n, is_nan, is_nan_forever);

	a->p = fl_radians(a1d[0] % 360);
	a->b = fl_radians(a1d[1] % 360);
	a->h = fl_radians(a1d[2] % 360);

	return count;
}

/**
 * Takes a ship entry and returns the player for that ship or NULL if it is an AI ship.
 * In singleplayer mode, this is *the* player, but in multiplayer mode it could be any player.
 */
player *get_player_from_ship_entry(const ship_registry_entry *ship_entry, bool test_respawns = false, int *netplayer_index = nullptr)
{
	Assertion(ship_entry, "ship_entry cannot be null!");

	if (netplayer_index)
		*netplayer_index = -1;

	// singleplayer
	if (!(Game_mode & GM_MULTIPLAYER)) {
		if (ship_entry->objnum >= 0) {
			if (OBJ_INDEX(Player_obj) == ship_entry->objnum) {
				return Player;
			}
		}
		if (ship_entry->has_p_objp()) {
			if (ship_entry->p_objp()->flags[Mission::Parse_Object_Flags::OF_Player_start]) {
				return Player;
			}
		}
		return nullptr;
	}
	// multiplayer
	else {
		int np_index = -1;

		if (ship_entry->has_objp()) {
			// try and find the player
			np_index = multi_find_player_by_object(ship_entry->objp());
		}
		if (test_respawns && np_index < 0) {
			// Respawning ships don't have an objnum so we need to take a different approach 
			if (ship_entry->has_p_objp()) {
				np_index = multi_find_player_by_parse_object(ship_entry->p_objp());
			}
		}

		if ((np_index >= 0) && (np_index < MAX_PLAYERS)) {
			if (netplayer_index)
				*netplayer_index = np_index;

			return Net_players[np_index].m_player; 
		}

		return nullptr; 
	}
}

/**
 * Forwards to get_player_from_ship_entry using a ship_entry generated from this SEXP node.
 */
player *get_player_from_ship_node(int node, bool test_respawns = false, int *netplayer_index = nullptr)
{
	auto ship_entry = eval_ship(node);
	if (!ship_entry)
		return nullptr;

	return get_player_from_ship_entry(ship_entry, test_respawns, netplayer_index);
}

enum class ProcessSubsystemType : uint8_t { SUBSYSTEM, HULL, SIM_HULL, SHIELDS };

const char *special_subsystem_string(ProcessSubsystemType type)
{
	switch (type)
	{
		case ProcessSubsystemType::HULL:
			return SEXP_HULL_STRING;
		case ProcessSubsystemType::SIM_HULL:
			return SEXP_SIM_HULL_STRING;
		case ProcessSubsystemType::SHIELDS:
			return SEXP_SHIELD_STRING;
		case ProcessSubsystemType::SUBSYSTEM:
		default:
			return nullptr;
	}
}

/*
 * Iterates through a subsystem list and applies the given function to each subsystem, handling the "hull" subsystem as well as (optionally) generic subsystems.  The function's prototype should be:
 * 
 * void f(ProcessSubsystemType type, ship_subsys *ss);
 * 
 * If any of the booleans are true, ss will be nullptr.
 */
template <typename Function>
void process_ship_subsystems(const ship_registry_entry *ship_entry, bool allow_generic_subsystems, int node, bool iterate_nodes, const char *sexp_operator_name, Function f)
{
	Assertion(sexp_operator_name, "sexp_operator_name must not be null!");
	if (node < 0)
		return;

	if (!ship_entry || !ship_entry->has_shipp())
		return;
	auto shipp = ship_entry->shipp();

	// handle the subsystem node(s)
	for (int n = node; n >= 0; n = CDR(n))
	{
		auto subsys_name = CTEXT(n);

		// check for HULL
		if (!stricmp(subsys_name, SEXP_HULL_STRING))
		{
			f(ProcessSubsystemType::HULL, nullptr);
		}
		// check for SIM_HULL
		else if (!stricmp(subsys_name, SEXP_SIM_HULL_STRING))
		{
			f(ProcessSubsystemType::SIM_HULL, nullptr);
		}
		// check for shields
		else if (!stricmp(subsys_name, SEXP_SHIELD_STRING))
		{
			f(ProcessSubsystemType::SHIELDS, nullptr);
		}
		// some kind of subsystem...
		else
		{
			// check for a generic subsystem like <all engines>
			int generic_type = get_generic_subsys(subsys_name);
			if (generic_type)
			{
				if (allow_generic_subsystems)
				{
					// search through all subsystems
					for (auto ss : list_range(&shipp->subsys_list))
					{
						if (generic_type == SUBSYSTEM_ALL || generic_type == ss->system_info->type)
							f(ProcessSubsystemType::SUBSYSTEM, ss);
					}
				}
				else
					Warning(LOCATION, "Generic subsystem %s passed to %s.  This operator does not support generic subsystems.", subsys_name, sexp_operator_name);
			}
			// it's a regular subsystem
			else
			{
				auto ss = ship_get_subsys(shipp, subsys_name);
				if (ss)
					f(ProcessSubsystemType::SUBSYSTEM, ss);
				else if (ship_class_unchanged(ship_entry))
					Warning(LOCATION, "Invalid subsystem passed to %s: %s does not have a %s subsystem", sexp_operator_name, ship_entry->name, subsys_name);
			}
		}

		// some operators have only one subsystem node
		if (!iterate_nodes)
			break;
	}
}

// ----------------------------------------------------------------------------------- 
// SEXP caching
// -----------------------------------------------------------------------------------

/**
 * Gets a ship from a sexp node.  Returns the ship registry entry, or NULL if the ship is unknown.
 */
const ship_registry_entry *eval_ship(int node)
{
	if (node < 0)
		return nullptr;

	// check cache
	if (Sexp_nodes[node].cache)
	{
		// have we cached something else?
		if (Sexp_nodes[node].cache->sexp_node_data_type != OPF_SHIP)
			return nullptr;

		return &Ship_registry[Sexp_nodes[node].cache->ship_registry_index];
	}

	// maybe forward to a special-arg node
	if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)
	{
		auto current_argument = Sexp_replacement_arguments.back();
		int arg_node = current_argument.second;

		if (arg_node >= 0)
			return eval_ship(arg_node);
	}

	auto ship_it = Ship_registry_map.find(CTEXT(node));
	if (ship_it != Ship_registry_map.end())
	{
		// cache the value if it can't change later
		if (!is_node_value_dynamic(node))
			Sexp_nodes[node].cache = new sexp_cached_data(OPF_SHIP, -1, ship_it->second);

		return &Ship_registry[ship_it->second];
	}

	// we know nothing about this ship, apparently
	return nullptr;
}

/**
 * Gets a prop from a sexp node.  Returns the prop entry, or nullptr if the prop is unknown.
 * May be overkill for props. Research required.
 */
const prop *eval_prop(int node)
{
	if (node < 0)
		return nullptr;

	// check cache
	if (Sexp_nodes[node].cache)
	{
		// have we cached something else?
		if (Sexp_nodes[node].cache->sexp_node_data_type != OPF_PROP)
			return nullptr;

		return prop_id_lookup(Sexp_nodes[node].cache->ship_registry_index);
	}

	// maybe forward to a special-arg node
	if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)
	{
		auto current_argument = Sexp_replacement_arguments.back();
		int arg_node = current_argument.second;

		if (arg_node >= 0)
			return eval_prop(arg_node);
	}

	auto prop_idx = prop_name_lookup(CTEXT(node));
	if (prop_idx >= 0)
	{
		// cache the value if it can't change later
		if (!is_node_value_dynamic(node))
			Sexp_nodes[node].cache = new sexp_cached_data(OPF_PROP, -1, prop_idx);

		return prop_id_lookup(prop_idx);
	}

	// we know nothing about this prop, apparently
	return nullptr;
}

/**
 * Gets a wing from a sexp node.  Returns the wing pointer, or NULL if the wing is unknown.
 */
wing *eval_wing(int node)
{
	if (node < 0)
		return nullptr;

	// check cache
	if (Sexp_nodes[node].cache)
	{
		// have we cached something else?
		if (Sexp_nodes[node].cache->sexp_node_data_type != OPF_WING)
			return nullptr;

		return &Wings[Sexp_nodes[node].cache->other_index];
	}

	// maybe forward to a special-arg node
	if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)
	{
		auto current_argument = Sexp_replacement_arguments.back();
		int arg_node = current_argument.second;

		if (arg_node >= 0)
			return eval_wing(arg_node);
	}

	int wing_num = wing_lookup(CTEXT(node));
	if (wing_num >= 0)
	{
		// cache the value if it can't change later
		if (!is_node_value_dynamic(node))
			Sexp_nodes[node].cache = new sexp_cached_data(OPF_WING, wing_num);

		return &Wings[wing_num];
	}

	// it must not be a wing
	return nullptr;
}

/**
 * Returns a number parsed from the sexp node text.
 * NOTE: sexp_atoi() should only replace atoi(CTEXT(n)) - it should not replace atoi(Sexp_nodes[node].text) - see commit 9923c87bc1
 */
int sexp_atoi(int node)
{
	if (node < 0)
		return 0;

	// SEXP caching is not set up to work in FRED, so bypass all the caching code in that case
	if (!Fred_running)
	{
		// check cache
		if (Sexp_nodes[node].cache)
		{
			// have we cached something else?
			if (Sexp_nodes[node].cache->sexp_node_data_type != OPF_NUMBER)
				return 0;

			return Sexp_nodes[node].cache->numeric_literal;
		}

		// maybe forward to a special-arg node
		if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)
		{
			auto current_argument = Sexp_replacement_arguments.back();
			int arg_node = current_argument.second;

			if (arg_node >= 0)
				return sexp_atoi(arg_node);
		}
	}

	int num = atoi(CTEXT(node));
	ensure_opf_positive_is_positive(node, num);

	if (!Fred_running)
	{
		// cache the value if it can't change later
		if (!is_node_value_dynamic(node))
			Sexp_nodes[node].cache = new sexp_cached_data(OPF_NUMBER, num, -1);
	}

	return num;
}

/**
 * A version of can_construe_as_integer that knows about number caching.
 */
bool sexp_can_construe_as_integer(int node)
{
	if (node < 0)
		return false;

	// SEXP caching is not set up to work in FRED, so bypass all the caching code in that case
	if (!Fred_running)
	{
		if (Sexp_nodes[node].cache && Sexp_nodes[node].cache->sexp_node_data_type == OPF_NUMBER)
			return true;

		// maybe forward to a special-arg node
		if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)
		{
			auto current_argument = Sexp_replacement_arguments.back();
			int arg_node = current_argument.second;

			if (arg_node >= 0)
				return sexp_can_construe_as_integer(arg_node);
		}
	}

	return can_construe_as_integer(CTEXT(node));
}

/*
 * This can be used by both FRED and FSO.  When in FSO, it incorporates caching that runs parallel to the main data caching.
 * Note that this function does not do special-arg forwarding because it operates on the node text, not the CTEXT, and thus
 * there is no possibility of processing a special argument.
 */
int sexp_get_variable_index(int node)
{
	if (node < 0)
		return -1;

	if (Sexp_nodes[node].cached_variable_index >= 0)
		return Sexp_nodes[node].cached_variable_index;

	if (!(Sexp_nodes[node].type & SEXP_FLAG_VARIABLE))
		return -1;

	Assert(Sexp_nodes[node].first == -1);

	if (Fred_running)
		return get_index_sexp_variable_name(Sexp_nodes[node].text);

	// parse it
	int index = atoi(Sexp_nodes[node].text);

	// verify variable set
	Assert(Sexp_variables[index].type & SEXP_VARIABLE_SET);

	// cache and return
	Sexp_nodes[node].cached_variable_index = index;
	return index;
}

// ----------------------------------------------------------------------------------- 
// SEXP implementations
// -----------------------------------------------------------------------------------

// arithmetic functions
int add_sexps(int n)
{
	int	val, sum = 0;

	if (n != -1) {
		if (CAR(n) != -1) {
			val = eval_sexp(CAR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CAR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum = val;
		}
		else
			sum = sexp_atoi(n);

		while (CDR(n) != -1) {
			val = eval_sexp(CDR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CDR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum += val;
			n = CDR(n);
		}
	}

	return sum;
}

int sub_sexps(int n)
{
	int	val, sum = 0;

	if (n != -1) {
		if (CAR(n) != -1) {
			val = eval_sexp(CAR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CAR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum = val;
		}
		else
			sum = sexp_atoi(n);

		while (CDR(n) != -1) {
			val = eval_sexp(CDR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CDR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum -= val;
			n = CDR(n);
		}
	}

	return sum;
}

int mul_sexps(int n)
{
	int	val, sum = 0;

	if (n != -1) {
		if (CAR(n) != -1) {
			val = eval_sexp(CAR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CAR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum = val;
		}
		else
			sum = sexp_atoi(n);

		while (CDR(n) != -1) {
			val = eval_sexp(CDR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CDR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum *= val;
			n = CDR(n);
		}
	}

	return sum;
}

int div_sexps(int n)
{
	int	val, sum = 0;

	if (n != -1) {
		if (CAR(n) != -1) {
			val = eval_sexp(CAR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CAR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum = val;
		}
		else
			sum = sexp_atoi(n);

		while (CDR(n) != -1) {
			val = eval_sexp(CDR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CDR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			if (val == 0) {
				Warning(LOCATION, "Division by zero in sexp. Please check all uses of the / operator for possible causes.\n");
				return SEXP_NAN;
			}

			sum /= val;
			n = CDR(n);
		}
	}

	return sum;
}

int mod_sexps(int n)
{
	int	val, sum = 0;

	if (n != -1) {
		if (CAR(n) != -1) {
			val = eval_sexp(CAR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CAR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			sum = val;
		}
		else
			sum = sexp_atoi(n);

		while (CDR(n) != -1) {
			val = eval_sexp(CDR(n));

			// be sure to check for the NAN value when doing arithmetic -- this value should
			// get propagated to the next highest function.
			if (Sexp_nodes[CDR(n)].value == SEXP_NAN)
				return SEXP_NAN;
			else if (Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER)
				return SEXP_NAN_FOREVER;

			if (val == 0) {
				Warning(LOCATION, "Modulo by zero in sexp. Please check all uses of the %% operator for possible causes.\n");
				return SEXP_NAN;
			}

			sum = sum % val;
			n = CDR(n);
		}
	}

	return sum;
}

int rand_internal(int low, int high, int seed = 0)
{
	// maybe seed it
	if (seed > 0)
		Random::seed(seed);

	return Random::next(low, high);
}

// Goober5000
int abs_sexp(int n)
{
	bool is_nan, is_nan_forever;
	int val = eval_num(n, is_nan, is_nan_forever);

	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	return abs(val);
}

// Goober5000
int min_sexp(int n)
{
	bool is_nan, is_nan_forever;
	int temp, min_val = INT_MAX;

	while (n != -1)
	{
		temp = eval_num(n, is_nan, is_nan_forever);

		if (is_nan)
			return SEXP_NAN;
		if (is_nan_forever)
			return SEXP_NAN_FOREVER;

		if (temp < min_val)
			min_val = temp;

		n = CDR(n);
	}

	return min_val;
}

// Goober5000
int max_sexp(int n)
{
	bool is_nan, is_nan_forever;
	int temp, max_val = INT_MIN;

	while (n != -1)
	{
		temp = eval_num(n, is_nan, is_nan_forever);

		if (is_nan)
			return SEXP_NAN;
		if (is_nan_forever)
			return SEXP_NAN_FOREVER;

		if (temp > max_val)
			max_val = temp;

		n = CDR(n);
	}

	return max_val;
}

// Goober5000
int avg_sexp(int n)
{
	bool is_nan, is_nan_forever;
	int num = 0, avg_val = 0;

	while (n != -1)
	{
		int val = eval_num(n, is_nan, is_nan_forever);

		if (is_nan)
			return SEXP_NAN;
		if (is_nan_forever)
			return SEXP_NAN_FOREVER;

		avg_val += val;
		num++;

		n = CDR(n);
	}

	return (int) floor(((double) avg_val / num) + 0.5);
}

// Goober5000
int pow_sexp(int node)
{
	int num_1, num_2;
	bool is_nan, is_nan_forever;

	eval_nums(node, is_nan, is_nan_forever, num_1, num_2);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	// this is disallowed in FRED, but can still happen through careless arithmetic
	if (num_2 < 0)
	{
		Warning(LOCATION, "Power function pow(%d, %d) attempted to raise to a negative power!", num_1, num_2);
		return 0;
	}

	// shortcuts
	if (num_1 == 0 && num_2 == 0)
	{
		Warning(LOCATION, "Zero raised to the power of zero in sexp. Please check all uses of the power operator for possible causes.\n");
		return SEXP_NAN;
	}
	else if (num_1 == 0 || num_1 == 1 || num_2 == 1)
		return num_1;
	else if (num_2 == 0)
		return 1;

	double pow_result = pow(static_cast<double>(num_1), num_2);

	if (pow_result > static_cast<double>(INT_MAX))
	{
		nprintf(("SEXP", "Power function pow(%d, %d) is greater than INT_MAX!  Returning INT_MAX.", num_1, num_2));
		return INT_MAX;
	}
	else if (pow_result < static_cast<double>(INT_MIN))
	{
		nprintf(("SEXP", "Power function pow(%d, %d) is less than INT_MIN!  Returning INT_MIN.", num_1, num_2));
		return INT_MIN;
	}

	return static_cast<int>(pow_result);
}

// is there a better place to put this?  seems useful...
// credit to http://stackoverflow.com/questions/1903954/is-there-a-standard-sign-function-signum-sgn-in-c-c
template <typename T>
T sign(T t)
{
	if (t == 0)
		return T(0);
	else
		return (t < 0) ? T(-1) : T(1);
}

// Goober5000
int signum_sexp(int node)
{
	bool is_nan, is_nan_forever;
	int num = eval_num(node, is_nan, is_nan_forever);

	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	return sign(num);
}

// Goober5000
int sexp_is_nan(int n)
{
	// if this sexp has an operator, evaluate it
	if (CAR(n) != -1) {
		eval_sexp(CAR(n));

		// see whether the result was NaN
		if (Sexp_nodes[CAR(n)].value == SEXP_NAN)
			return SEXP_TRUE;
		if (Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
			return SEXP_KNOWN_TRUE;

		// see whether the result won't change
		if (Sexp_nodes[CAR(n)].value == SEXP_KNOWN_TRUE || Sexp_nodes[CAR(n)].value == SEXP_KNOWN_FALSE)
			return SEXP_KNOWN_FALSE;

		// it's not NaN, for now
		return SEXP_FALSE;
	}
	// straight-up numeric or string arguments cannot be NaN
	else
		return SEXP_KNOWN_FALSE;
}

// Goober5000
int sexp_nan_to_number(int n)
{
	// if this sexp has an operator, evaluate it
	if (CAR(n) != -1) {
		int val = eval_sexp(CAR(n));

		// see whether the result was NaN
		if (Sexp_nodes[CAR(n)].value == SEXP_NAN || Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
			return 0;

		// if not NaN, result is acceptable
		return val;
	}
	// straight-up numeric or string arguments cannot be NaN
	else
		return sexp_atoi(n);
}

// Goober5000
int sexp_set_bit(int node, bool set_it)
{
	int val, bit_index;
	bool is_nan, is_nan_forever;

	eval_nums(node, is_nan, is_nan_forever, val, bit_index);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	if (bit_index < 0 || bit_index > 31)
	{
		// out of range, so just return the original value
		// (we used to warn, but it's more convenient to FREDders to fail gracefully)
		return val;
	}

	if (set_it)
		return val | (1<<bit_index);
	else
		return val & ~(1<<bit_index);
}

// Goober5000
int sexp_is_bit_set(int node)
{
	int val, bit_index;
	bool is_nan, is_nan_forever;

	eval_nums(node, is_nan, is_nan_forever, val, bit_index);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	if (bit_index < 0 || bit_index > 31)
	{
		// out of range, so just return false
		// (we used to warn, but it's more convenient to FREDders to fail gracefully)
		return SEXP_FALSE;
	}

	if (val & (1<<bit_index))
		return SEXP_TRUE;
	else
		return SEXP_FALSE;
}

// Goober5000
int sexp_bitwise_and(int node)
{
	bool is_nan, is_nan_forever;

	int result = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	for (int n = CDR(node); n != -1; n = CDR(n))
	{
		int val = eval_num(n, is_nan, is_nan_forever);
		if (is_nan)
			return SEXP_NAN;
		if (is_nan_forever)
			return SEXP_NAN_FOREVER;

		result &= val;
	}

	return result;
}

// Goober5000
int sexp_bitwise_or(int node)
{
	bool is_nan, is_nan_forever;

	int result = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	for (int n = CDR(node); n != -1; n = CDR(n))
	{
		int val = eval_num(n, is_nan, is_nan_forever);
		if (is_nan)
			return SEXP_NAN;
		if (is_nan_forever)
			return SEXP_NAN_FOREVER;

		result |= val;
	}

	return result;
}

// Goober5000
int sexp_bitwise_not(int node)
{
	bool is_nan, is_nan_forever;

	int val = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	// flip the bits, then clear the sign bit
	return (~val) & INT_MAX;
}

// Goober5000
int sexp_bitwise_xor(int node)
{
	int val1, val2;
	bool is_nan, is_nan_forever;

	eval_nums(node, is_nan, is_nan_forever, val1, val2);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	return val1 ^ val2;
}

// seeding added by Karajorma and Goober5000
int rand_sexp(int node, bool multiple)
{
	bool is_nan, is_nan_forever;
	int n = node, low, high, rand_num, seed;

	// when getting a saved value
	if (Sexp_nodes[node].value == SEXP_NUM_EVAL)
	{
		// don't regenerate new random number
		return sexp_atoi(node);
	}

	// get low, high, and (optional) seed - seed will be 0, per eval_nums, if not specified
	eval_nums(n, is_nan, is_nan_forever, low, high, seed);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	if (low > high) {
		Warning(LOCATION, "rand%s was passed an invalid range [%d, %d]!", multiple ? "-multiple" : "", low, high);
		// preserve old behavior from before Random class was introduced
		return low;
	}

	// get the random number
	rand_num = rand_internal(low, high, seed);

	// when saving the value
	if (!multiple)
	{
		// set .value and .text so random number is generated only once.
		Sexp_nodes[node].value = SEXP_NUM_EVAL;
		sprintf(Sexp_nodes[node].text, "%d", rand_num);

		// any cached value is no longer relevant because we just changed the text
		clear_cache(node);
	}
	// if this is multiple with a nonzero seed provided
	else if (seed > 0)
	{
		// Set the seed to a new seeded random value. This will ensure that the next time the method
		// is called it will return a predictable but different number from the previous time. 
		sprintf(Sexp_nodes[CDDR(node)].text, "%d", rand_internal(1, INT_MAX, seed));

		// any cached value is no longer relevant because we just changed the text
		clear_cache(CDDR(node));
	}

	return rand_num;
}

// boolean evaluation functions.  Evaluate all sexpressions in the 'or' operator.  Needed to mark
// entries in the mission log as essential so when pruning the log, we know which entries we might
// need to keep.
int sexp_or(int n)
{
	bool all_false = true;
	bool result = false;

	if (n != -1)
	{
		if (CAR(n) != -1)
		{
			result = is_sexp_true(CAR(n)) || result;
			if (Sexp_nodes[CAR(n)].value == SEXP_KNOWN_TRUE)
				return SEXP_KNOWN_TRUE;								// if one of the OR clauses is TRUE, whole clause is true
			if (Sexp_nodes[CAR(n)].value != SEXP_KNOWN_FALSE)		// if the value is still unknown, they all can't be false
				all_false = false;
		}
		// this should never happen, because all arguments which return logical values are operators
		else
			result = (sexp_atoi(n) != 0) || result;

		while (CDR(n) != -1)
		{
			result = is_sexp_true(CDR(n)) || result;
			if ( Sexp_nodes[CDR(n)].value == SEXP_KNOWN_TRUE )
				return SEXP_KNOWN_TRUE;								// if one of the OR clauses is TRUE, whole clause is true
			if ( Sexp_nodes[CDR(n)].value != SEXP_KNOWN_FALSE )		// if the value is still unknown, they all can't be false
				all_false = false;

			n = CDR(n);
		}
	}

	if (all_false)
		return SEXP_KNOWN_FALSE;

	return result ? SEXP_TRUE : SEXP_FALSE;
}

// this function does the 'and' operator.  It will short circuit evaluation  *but* it will still
// evaluate other members of the and construct.  I do this because I need events in the mission log
// to get marked as essential for goal purposes, and evaluation is pretty much the only way
int sexp_and(int n)
{
	bool all_true = true;
	bool result = true;

	if (n != -1)
	{
		if (CAR(n) != -1)
		{
			result = is_sexp_true(CAR(n)) && result;
			if ( Sexp_nodes[CAR(n)].value == SEXP_KNOWN_FALSE || Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER )
				return SEXP_KNOWN_FALSE;							// if one of the AND clauses is FALSE, whole clause is false
			if ( Sexp_nodes[CAR(n)].value != SEXP_KNOWN_TRUE )		// if the value is still unknown, they all can't be true
				all_true = false;
		}
		// this should never happen, because all arguments which return logical values are operators
		else
			result = (sexp_atoi(n) != 0) && result;

		while (CDR(n) != -1)
		{
			result = is_sexp_true(CDR(n)) && result;
			if ( Sexp_nodes[CDR(n)].value == SEXP_KNOWN_FALSE || Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER )
				return SEXP_KNOWN_FALSE;							// if one of the AND clauses is FALSE, whole clause is false
			if ( Sexp_nodes[CDR(n)].value != SEXP_KNOWN_TRUE )		// if the value is still unknown, they all can't be true
				all_true = false;

			n = CDR(n);
		}
	}

	if (all_true)
		return SEXP_KNOWN_TRUE;

	return result ? SEXP_TRUE : SEXP_FALSE;
}

// this version of the 'and' operator determines whether or not its arguments become true
// in the order in which they are specified in the when statement.  Should be a simple matter of 
// seeing if anything evaluates to true later than something that evaluated to false
int sexp_and_in_sequence(int n)
{
	bool all_true = true;									// represents whether or not all nodes we have seen so far are true
	bool result = true;

	if (n != -1)
	{
		if (CAR(n) != -1)
		{
			result = is_sexp_true(CAR(n)) && result;
			if ( Sexp_nodes[CAR(n)].value == SEXP_KNOWN_FALSE || Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER )
				return SEXP_KNOWN_FALSE;							// if one of the AND clauses is FALSE, whole clause is false
			if ( Sexp_nodes[CAR(n)].value != SEXP_KNOWN_TRUE )		// if value is true, mark our all_true variable for later checking
				all_true = false;
		}
		// this should never happen, because all arguments which return logical values are operators
		else
			result = (sexp_atoi(n) != 0) && result;

		// a little test -- if the previous sexpressions was true, then mark the node itself as always
		// true.  I did this because of the distance function.  It might become true, then when waiting for
		// the second evalation, it might become false, rendering this function false.  So, when one becomes
		// true -- mark it true forever.
		if ( result )
			Sexp_nodes[CAR(n)].value = SEXP_KNOWN_TRUE;

		while (CDR(n) != -1)
		{
			bool next_result = is_sexp_true(CDR(n));
			if ( next_result && !result )				// if current result is true, and our running result is false, things didn't become true in order
				return SEXP_KNOWN_FALSE;

			result = next_result && result;
			if ( Sexp_nodes[CDR(n)].value == SEXP_KNOWN_FALSE || Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER )
				return SEXP_KNOWN_FALSE;							// if one of the AND clauses is FALSE, whole clause is false
			if ( Sexp_nodes[CDR(n)].value != SEXP_KNOWN_TRUE )		// if the value is still unknown, they all can't be true
				all_true = false;

			// see comment above for explanation of next lines
			if ( result )
				Sexp_nodes[CDR(n)].value = SEXP_KNOWN_TRUE;

			n = CDR(n);
		}
	}

	if ( all_true )
		return SEXP_KNOWN_TRUE;

	return result ? SEXP_TRUE : SEXP_FALSE;
}

// for these four basic boolean operations (not, <, >, and =), we have special cases that we must deal
// with.  We have sexpressions operators that might return a NAN type return value (such as the distance
// between two ships when one of the ships is destroyed or departed).  These operations need to check for
// this special NAN value and adjust their return types accordingly.  NAN values represent false return values
int sexp_not(int n)
{
	bool result = false;

	if (n != -1)
	{
		if (CAR(n) != -1)
		{
			// The "known" property is sticky.  It's not known-vs-not-known, it is known-[true-or-false].
			// The opposite of known-true is not unknown, it is known-false.
			//
			// Think of it in the context of a ship exiting a mission.  If a ship's cargo was scanned,
			// then after the ship exits, that sexp will always return known-true.  There is no possibility
			// of it ever becoming something other than true, so it's known. Similarly, the inverse of that
			// is known-false, because there is no possibility of the inverse ever becoming something other
			// than false.

			result = is_sexp_true(CAR(n));
			if ( Sexp_nodes[CAR(n)].value == SEXP_KNOWN_FALSE || Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER )
				return SEXP_KNOWN_TRUE;												// not KNOWN_FALSE == KNOWN_TRUE;
			else if ( Sexp_nodes[CAR(n)].value == SEXP_KNOWN_TRUE )		// not KNOWN_TRUE == KNOWN_FALSE
				return SEXP_KNOWN_FALSE;
			else if ( Sexp_nodes[CAR(n)].value == SEXP_NAN )				// not NAN == TRUE (I think)
				return SEXP_TRUE;
		}
		// this should never happen, because all arguments which return logical values are operators
		else
			result = (sexp_atoi(n) != 0);
	}

	return result ? SEXP_FALSE : SEXP_TRUE;
}

int sexp_xor(int n)
{
	bool result;
	int num_true = 0, num_known_true = 0, num_known_false = 0, num_args = 0;

	if (n != -1)
	{
		if (CAR(n) != -1)
		{
			num_args++;

			result = is_sexp_true(CAR(n));
			if (Sexp_nodes[CAR(n)].value == SEXP_KNOWN_TRUE)
				num_known_true++;
			else if (Sexp_nodes[CAR(n)].value == SEXP_KNOWN_FALSE || Sexp_nodes[CAR(n)].value == SEXP_NAN_FOREVER)
				num_known_false++;
			else if (result)
				num_true++;
		}
		// this should never happen, because all arguments which return logical values are operators
		else
			result = (sexp_atoi(n) != 0);

		while (CDR(n) != -1)
		{
			num_args++;

			result = is_sexp_true(CDR(n));
			if (Sexp_nodes[CDR(n)].value == SEXP_KNOWN_TRUE)
				num_known_true++;
			else if (Sexp_nodes[CDR(n)].value == SEXP_KNOWN_FALSE || Sexp_nodes[CDR(n)].value == SEXP_NAN_FOREVER)
				num_known_false++;
			else if (result)
				num_true++;

			n = CDR(n);
		}
	}

	if (num_known_true + num_known_false == num_args)
	{
		if (num_known_true == 1)
			return SEXP_KNOWN_TRUE;
		else
			return SEXP_KNOWN_FALSE;
	}

	if (num_true + num_known_true == 1)
		return SEXP_TRUE;
	else
		return SEXP_FALSE;
}

// Goober5000
int sexp_number_compare(int n, int op)
{
	int first_node = n;
	int current_node;
	int first_number = eval_sexp(first_node);
	int current_number;

	// bail on NANs
	if (CAR(first_node) != -1)
	{
		if (Sexp_nodes[CAR(first_node)].value == SEXP_NAN) return SEXP_FALSE;
		if (Sexp_nodes[CAR(first_node)].value == SEXP_NAN_FOREVER) return SEXP_KNOWN_FALSE;
	}
	if (CDR(first_node) != -1)
	{
		if (Sexp_nodes[CDR(first_node)].value == SEXP_NAN) return SEXP_FALSE;
		if (Sexp_nodes[CDR(first_node)].value == SEXP_NAN_FOREVER) return SEXP_KNOWN_FALSE;
	}

	// compare first node with each of the others
	for (current_node = CDR(first_node); current_node != -1; current_node = CDR(current_node))
	{
		// bail on NANs
		if (CAR(current_node) != -1)
		{
			if (Sexp_nodes[CAR(current_node)].value == SEXP_NAN) return SEXP_FALSE;
			if (Sexp_nodes[CAR(current_node)].value == SEXP_NAN_FOREVER) return SEXP_KNOWN_FALSE;
		}
		if (CDR(current_node) != -1)
		{
			if (Sexp_nodes[CDR(current_node)].value == SEXP_NAN) return SEXP_FALSE;
			if (Sexp_nodes[CDR(current_node)].value == SEXP_NAN_FOREVER) return SEXP_KNOWN_FALSE;
		}

		current_number = eval_sexp(current_node);

		// must satisfy our particular operator
		switch(op)
		{
			case OP_EQUALS:
				if (first_number != current_number) return SEXP_FALSE;
				break;

			case OP_NOT_EQUAL:
				if (first_number == current_number) return SEXP_FALSE;
				break;

			case OP_GREATER_THAN:
				if (first_number <= current_number) return SEXP_FALSE;
				break;

			case OP_GREATER_OR_EQUAL:
				if (first_number < current_number) return SEXP_FALSE;
				break;

			case OP_LESS_THAN:
				if (first_number >= current_number) return SEXP_FALSE;
				break;

			case OP_LESS_OR_EQUAL:
				if (first_number > current_number) return SEXP_FALSE;
				break;

			default:
				Warning(LOCATION, "Unhandled comparison case!  Operator = %d", op);
				break;
		}
	}

	// it satisfies the operator for all the arguments
	return SEXP_TRUE;
}

// Goober5000
int sexp_string_compare(int n, int op)
{
	int first_node = n;
	int current_node;
	int val;
	auto first_string = CTEXT(first_node);

	// compare first node with each of the others
	for (current_node = CDR(first_node); current_node != -1; current_node = CDR(current_node))
	{
		val = strcmp(first_string, CTEXT(current_node));

		// must satisfy our particular operator
		switch(op)
		{
			case OP_STRING_EQUALS:
				if (val != 0) return SEXP_FALSE;
				break;

			case OP_STRING_GREATER_THAN:
				if (val <= 0) return SEXP_FALSE;
				break;

			case OP_STRING_LESS_THAN:
				if (val >= 0) return SEXP_FALSE;
				break;
		}
	}

	// it satisfies the operator for all the arguments
	return SEXP_TRUE;
}

object_ship_wing_point_team::object_ship_wing_point_team(ship* sp)
	: type(OSWPT_TYPE_SHIP), objnum(sp->objnum)
{
	strcpy_s(object_name, sp->ship_name);

	ship_registry_index = ship_registry_get_index(sp->ship_name);
	Assertion(ship_registry_index >= 0, "Ship %s was not found in the ship registry!", sp->ship_name);
	if (ship_registry_index < 0)
		type = OSWPT_TYPE_NONE;
	else if (Ship_registry[ship_registry_index].status == ShipStatus::EXITED)
		type = OSWPT_TYPE_EXITED;
}

object_ship_wing_point_team::object_ship_wing_point_team(p_object* pop)
	: type(OSWPT_TYPE_PARSE_OBJECT)
{
	strcpy_s(object_name, pop->name);

	ship_registry_index = ship_registry_get_index(pop->name);
	Assertion(ship_registry_index >= 0, "Parse object %s was not found in the ship registry!", pop->name);
	if (ship_registry_index < 0)
		type = OSWPT_TYPE_NONE;
}

object_ship_wing_point_team::object_ship_wing_point_team(ship_obj* sop)
	: object_ship_wing_point_team(&Ships[Objects[sop->objnum].instance])
{}

object_ship_wing_point_team::object_ship_wing_point_team(wing* wingp)
	: wingnum(WING_INDEX(wingp))
{
	strcpy_s(object_name, wingp->name);

	if (wingp->current_count > 0)
		type = OSWPT_TYPE_WING;
	else
		type = OSWPT_TYPE_WING_NOT_PRESENT;

	// point to wing leader if he is valid
	if ((wingp->special_ship >= 0) && (wingp->ship_index[wingp->special_ship] >= 0))
	{
		objnum = Ships[wingp->ship_index[wingp->special_ship]].objnum;
	}
	// boo... well, just point to ship at index 0
	else
	{
		objnum = Ships[wingp->ship_index[0]].objnum;
		Warning(LOCATION, "Substituting ship '%s' at index 0 for nonexistent wing leader at index %d!", Ships[Objects[objnum].instance].ship_name, wingp->special_ship);
	}
}

bool object_ship_wing_point_team::matches(const ship *shipp) const
{
	if (!shipp)
		return false;

	switch (type)
	{
		case oswpt_type::SHIP:
			return ship_entry()->shipnum == SHIP_INDEX(shipp);

		case oswpt_type::SHIP_ON_TEAM:
		case oswpt_type::WHOLE_TEAM:
			return team == shipp->team;

		case oswpt_type::WING:
			return wingnum == shipp->wingnum;

		default:
			return false;
	}
}

void object_ship_wing_point_team::clear()
{
	*object_name = '\0';
	type = oswpt_type::NONE;

	ship_registry_index = -1;
	objnum = -1;
	wingnum = -1;
	wp_list = -1;
	wp_index = -1;
	team = -1;
}

bool object_ship_wing_point_team::operator==(const object_ship_wing_point_team &other) const
{
	if (type != other.type)
		return false;

	switch (type)
	{
		case oswpt_type::NONE:
			return true;

		case oswpt_type::PARSE_OBJECT:
		case oswpt_type::SHIP:
		case oswpt_type::EXITED:
			return ship_registry_index == other.ship_registry_index;

		case oswpt_type::SHIP_ON_TEAM:
		case oswpt_type::WHOLE_TEAM:
			return team == other.team;

		case oswpt_type::WING:
		case oswpt_type::WING_NOT_PRESENT:
			return wingnum == other.wingnum;

		case oswpt_type::WAYPOINT:
			return wp_list == other.wp_list && wp_index == other.wp_index;

		default:
			return false;
	}
}

bool object_ship_wing_point_team::operator!=(const object_ship_wing_point_team &other) const
{
	return !(operator==(other));
}

const ship_registry_entry* object_ship_wing_point_team::ship_entry() const
{
	Assertion(has_ship_entry(), "object_ship_wing_point_team::ship_entry() was called on an oswpt that does not have a ship entry");
	return &Ship_registry[ship_registry_index];
}

object* object_ship_wing_point_team::objp() const
{
	Assertion(has_objp(), "object_ship_wing_point_team::objp() was called on an oswpt that does not have an object");
	return &Objects[objnum];
}

wing* object_ship_wing_point_team::wingp() const
{
	Assertion(has_wingp(), "object_ship_wing_point_team::wingp() was called on an oswpt that does not have a wing");
	return &Wings[wingnum];
}

waypoint* object_ship_wing_point_team::waypointp() const
{
	Assertion(has_waypointp(), "object_ship_wing_point_team::waypointp() was called on an oswpt that does not have a waypoint");
	return &Waypoint_lists[wp_list].get_waypoints()[wp_index];
}

const ship_registry_entry* object_ship_wing_point_team::ship_entry_or_null() const
{
	return (ship_registry_index < 0) ? nullptr : &Ship_registry[ship_registry_index];
}

object* object_ship_wing_point_team::objp_or_null() const
{
	return (objnum < 0) ? nullptr : &Objects[objnum];
}

wing* object_ship_wing_point_team::wingp_or_null() const
{
	return (wingnum < 0) ? nullptr : &Wings[wingnum];
}

waypoint* object_ship_wing_point_team::waypointp_or_null() const
{
	return (wp_list < 0 || wp_index < 0) ? nullptr : &Waypoint_lists[wp_list].get_waypoints()[wp_index];
}

bool object_ship_wing_point_team::has_p_objp() const
{
	return has_ship_entry() && ship_entry()->has_p_objp();
}

bool object_ship_wing_point_team::has_shipp() const
{
	return has_ship_entry() && ship_entry()->has_shipp();
}

p_object* object_ship_wing_point_team::p_objp() const
{
	Assertion(has_p_objp(), "object_ship_wing_point_team::p_objp() was called on an oswpt that does not have a parse object");
	return ship_entry()->p_objp();
}

ship* object_ship_wing_point_team::shipp() const
{
	Assertion(has_shipp(), "object_ship_wing_point_team::shipp() was called on an oswpt that does not have a ship");
	return ship_entry()->shipp();
}

p_object* object_ship_wing_point_team::p_objp_or_null() const
{
	return (ship_registry_index < 0) ? nullptr : Ship_registry[ship_registry_index].p_objp_or_null();
}

ship* object_ship_wing_point_team::shipp_or_null() const
{
	return (ship_registry_index < 0) ? nullptr : Ship_registry[ship_registry_index].shipp_or_null();
}

// Goober5000
void eval_object_ship_wing_point_team(object_ship_wing_point_team *oswpt, int node, const char *ctext_override)
{
	int ship_registry_index = -1;
	int wingnum = -1;
	const char *node_ctext = nullptr;

	Assert(oswpt != nullptr);
	Assert(node >= 0 || ctext_override);

	oswpt->clear(); 

	// this is mainly for multiplayer, where you can't cache something over the network
	if (ctext_override)
	{
		ship_registry_index = ship_registry_get_index(ctext_override);
		if (ship_registry_index < 0)
		{
			wingnum = wing_lookup(ctext_override);
			if (wingnum < 0)
				node_ctext = ctext_override;
		}
	}
	// check caching
	else if (Sexp_nodes[node].cache)
	{
		if (Sexp_nodes[node].cache->sexp_node_data_type == OPF_SHIP)
		{
			ship_registry_index = Sexp_nodes[node].cache->ship_registry_index;
		}
		else if (Sexp_nodes[node].cache->sexp_node_data_type == OPF_WING)
		{
			wingnum = Sexp_nodes[node].cache->other_index;
		}
		// TODO: other caching
		else
		{
			Assertion(false, "Only ship and wing caching are currently handled!");
		}
	}
	// evaluate from the node itself
	else
	{
		// check each possibility
		auto ship_entry = eval_ship(node);
		if (ship_entry)
			ship_registry_index = SHIP_REGISTRY_INDEX(ship_entry);
		else
		{
			auto wingp = eval_wing(node);
			if (wingp)
				wingnum = WING_INDEX(wingp);
			else
				node_ctext = CTEXT(node);
		}
	}

	// see if this is a ship
	if (ship_registry_index >= 0)
	{
		auto ship_entry = &Ship_registry[ship_registry_index];

		oswpt->ship_registry_index = ship_registry_index;
		strcpy_s(oswpt->object_name, ship_entry->name);
		oswpt->objnum = ship_entry->objnum;

		switch (ship_entry->status)
		{
			case ShipStatus::NOT_YET_PRESENT:
				oswpt->type = OSWPT_TYPE_PARSE_OBJECT;
				break;

			case ShipStatus::PRESENT:
			case ShipStatus::DEATH_ROLL:
				oswpt->type = OSWPT_TYPE_SHIP;
				break;

			case ShipStatus::EXITED:
				oswpt->type = OSWPT_TYPE_EXITED;
				break;

			default:
				UNREACHABLE("Unhandled ship registry entry status for %s: %d", ship_entry->name, (int)ship_entry->status);
		}

		return;
	}

	// see if this is a wing
	if (wingnum >= 0)
	{
		auto wingp = &Wings[wingnum];

		oswpt->wingnum = wingnum;
		strcpy_s(oswpt->object_name, wingp->name);

		if (wingp->flags[Ship::Wing_Flags::Gone])
		{
			oswpt->type = OSWPT_TYPE_EXITED;
		}
		// make sure that at least one ship exists
		else if (wingp->current_count > 0)
		{
			oswpt->type = OSWPT_TYPE_WING;

			// point to wing leader if he is valid
			if ((wingp->special_ship >= 0) && (wingp->ship_index[wingp->special_ship] >= 0))
			{
				oswpt->objnum = Ships[wingp->ship_index[wingp->special_ship]].objnum;
			}
			// boo... well, just point to ship at index 0
			else
			{
				oswpt->objnum = Ships[wingp->ship_index[0]].objnum;
				Warning(LOCATION, "Substituting ship '%s' at index 0 for nonexistent wing leader at index %d!", Ships[Objects[oswpt->objnum].instance].ship_name, wingp->special_ship);
			}
		}
		// it's still a valid wing even if nobody is here
		else
		{
			oswpt->type = OSWPT_TYPE_WING_NOT_PRESENT;
		}

		return;
	}


	// check if we have nothing
	// (node_ctext will be non-NULL if there is no ship or wing - but that's hard to determine, Coverity misses it, and it could easily change in the future if this function is modified)
	if (!node_ctext || !stricmp(node_ctext, SEXP_NONE_STRING))
	{
		oswpt->type = OSWPT_TYPE_NONE;
		return;
	}


	// check if we have a point for a target
	auto wpt = find_matching_waypoint(node_ctext);
	if ((wpt != nullptr) && (wpt->get_objnum() >= 0))
	{
		oswpt->type = OSWPT_TYPE_WAYPOINT;

		oswpt->wp_list = wpt->get_parent_list_index();
		oswpt->wp_index = wpt->get_index();
		oswpt->objnum = wpt->get_objnum();

		return;
	}


	// check if we have an "<any team>" type
	int team = sexp_determine_team(node_ctext);
	if (team >= 0)
	{
		oswpt->type = OSWPT_TYPE_SHIP_ON_TEAM;
		oswpt->team = team;
		return;
	}


	// check if we have a whole-team type
	team = iff_lookup(node_ctext);
	if (team >= 0)
	{
		oswpt->type = OSWPT_TYPE_WHOLE_TEAM;
		oswpt->team = team;
		return;
	}


	// we apparently don't have anything legal
	return;
}

/**
 * Return the number of ships of a given team in the area battle
 */
int sexp_num_ships_in_battle(int n)
{
	int count = 0;
	ship_obj* so;

	if (n == -1) {
		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;

			count++;
		}

		return count;
	}

	while (n != -1) {
		object_ship_wing_point_team oswpt1;
		eval_object_ship_wing_point_team(&oswpt1, n);

		switch (oswpt1.type) {
			case OSWPT_TYPE_WHOLE_TEAM:
				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;

					auto shipp = &Ships[Objects[so->objnum].instance];
					if (shipp->team == oswpt1.team) {
						count++;
					}
				}
				break;

			case OSWPT_TYPE_SHIP:
				count++;
				break;

			case OSWPT_TYPE_WING:
				count += oswpt1.wingp()->current_count;
				break;

			default:
				break;
		}

		n = CDR(n);
	}

	return count;
}

/** 
 * Return the number of ships of a given wing or wings in the battle area
 */
int sexp_num_ships_in_wing(int n)
{
	int num_ships = 0;

	// A wing name must be provided, Assert that there is one.
	Assert ( n != -1 );

	//Cycle through the list of ships given
	while (n != -1)
	{
		// Get the name of the wing
		auto wingp = eval_wing(n);

		//If the wing exists add the number of ships in it to the total
		if (wingp)
		{
			num_ships += wingp->current_count;
		}

		//get the next node
		n = CDR (n) ; 
	}

	return num_ships ;
}

/**
 * Gets the 'real' speed of an object, taking into account docking
 */
int sexp_get_real_speed(object *obj)
{
	return fl2i(dock_calc_docked_speed(obj));
}

/**
 * Gets the current speed of the specified object
 *
 * Uses a lot of code shamelessly ripped from get_object_coordinates
 */
int sexp_current_speed(int n)
{
	object_ship_wing_point_team oswpt;
	eval_object_ship_wing_point_team(&oswpt, n);

	switch (oswpt.type)
	{
		case OSWPT_TYPE_EXITED:
			return SEXP_NAN;

		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WING:
			return sexp_get_real_speed(oswpt.objp());

		default:
			break;
	}

	return 0;
}

int sexp_check_objective_delay(int delay_node, int objective_node, int(*objective_function)(int, fix*))
{
	fix delay, time;
	int val;
	bool is_nan, is_nan_forever;

	time = 0;

	delay = i2f(eval_num(delay_node, is_nan, is_nan_forever));
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	// check value of function
	val = objective_function(objective_node, &time);

	if ((val == SEXP_TRUE) || (val == SEXP_KNOWN_TRUE))
	{
		if ((Missiontime - time) >= delay)
			return val;
		else
			return SEXP_FALSE;
	}

	return val;
}

/**
 * Evaluate if given ship is destroyed.
 * @return true if the ship in the expression has been destroyed.
 */
int sexp_is_destroyed(int n, fix *latest_time)
{
	int	count, num_destroyed, wing_index;
	bool has_been_destroyed;
	fix	time = 0;

	count = 0;
	num_destroyed = 0;
	wing_index = -1;
	while (n != -1)
	{
		++count;

		auto ship_entry = eval_ship(n);
		wing *wingp = nullptr;
		has_been_destroyed = false;

		// this might be a ship
		if (ship_entry)
		{
			if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
				return SEXP_CANT_EVAL;

			if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
			{
				// check the mission log
				if (mission_log_get_time(LOG_SHIP_DESTROYED, ship_entry->name, nullptr, &time) || mission_log_get_time(LOG_SELF_DESTRUCTED, ship_entry->name, nullptr, &time))
				{
					has_been_destroyed = true;
				}
				// exited without being destroyed
				else
				{
					return SEXP_KNOWN_FALSE;
				}
			}
		}
		else
			wingp = eval_wing(n);

		// this might be a wing
		if (wingp)
		{
			if (wing_has_yet_to_arrive(wingp))
				return SEXP_CANT_EVAL;

			if (wingp->flags[Ship::Wing_Flags::Gone])
			{
				// check the mission log
				if (mission_log_get_time(LOG_WING_DESTROYED, wingp->name, nullptr, &time))
				{
					has_been_destroyed = true;
				}
				// exited without being destroyed
				else
				{
					return SEXP_KNOWN_FALSE;
				}
			}
		}

		if (has_been_destroyed)
		{
			++num_destroyed;
			if (latest_time && (time > *latest_time))
				*latest_time = time;
		}
		// at this point we assume the ship or wing is in-mission, even if no ship or wing actually exists
		else
		{
			// If a previous SEXP already had an empty wing then this code would expose the internal value as the
			// directive count. Instead, we reset the count to zero here to make sure that the wing or ship count is
			// correct.
			if (Directive_count == DIRECTIVE_WING_ZERO)
			{
#ifndef NDEBUG
				static bool wing_zero_warning_shown = false;
				if (!wing_zero_warning_shown)
				{
					mprintf(("SEXP: is-destroyed-delay was used multiple times in a directive event! This might have "
						"unintended effects and should be replaced by a single use of is-destroyed-delay.\n"));
					wing_zero_warning_shown = true;
				}
#endif
				Directive_count = 0;
			}

			// ship or wing isn't destroyed -- add to directive count
			if (wingp)
			{
				wing_index = WING_INDEX(wingp);
				Directive_count += Wings[wing_index].current_count;
			}
			else
				++Directive_count;
		}

		// move to next ship/wing in list
		n = CDR(n);
	}

	// special case to mark a directive for destroy wing objectives true after a short amount
	// of time when there are more waves for this wing.
	if ( (count == 1) && (wing_index >= 0) && (Directive_count == 0) ) {
		if ( Wings[wing_index].current_wave < Wings[wing_index].num_waves )
			Directive_count =	DIRECTIVE_WING_ZERO;
	}

	if ( count == num_destroyed )
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

int sexp_is_destroyed_delay(int n)
{
	return sexp_check_objective_delay(n, CDR(n), sexp_is_destroyed);
}

/**
 * Return true if the subsystem of the given ship has been destroyed
 */
int sexp_is_subsystem_destroyed(int n, fix *time)
{
	auto ship_entry = eval_ship(n);
	if (!ship_entry)
		return SEXP_FALSE;
	if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_CANT_EVAL;

	auto subsys_name = CTEXT(CDR(n));

	if ( mission_log_get_time(LOG_SHIP_SUBSYS_DESTROYED, ship_entry->name, subsys_name, time) )
		return SEXP_KNOWN_TRUE;

	// if the ship has exited, no way to destroy its subsystem.
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_KNOWN_FALSE;

	return SEXP_FALSE;
}

int sexp_is_subsystem_destroyed_delay(int n)
{
	return sexp_check_objective_delay(CDDR(n), n, sexp_is_subsystem_destroyed);
}

/**
 * Determine if a ship has arrived onto the scene
 */
int sexp_has_arrived(int n, fix *latest_time)
{
	int	count, num_arrived;
	bool has_arrived;
	fix	time = 0;

	count = 0;
	num_arrived = 0;
	while ( n != -1 )
	{
		++count;

		auto ship_entry = eval_ship(n);
		wing *wingp = nullptr;
		has_arrived = false;

		// this might be a ship
		if (ship_entry)
		{
			if (ship_entry->status != ShipStatus::NOT_YET_PRESENT)
			{
				// check the mission log
				if (mission_log_get_time(LOG_SHIP_ARRIVED, ship_entry->name, nullptr, &time))
					has_arrived = true;
			}
		}
		else
			wingp = eval_wing(n);

		// this might be a wing
		if (wingp)
		{
			if (!wing_has_yet_to_arrive(wingp))
			{
				// check the mission log
				if (mission_log_get_time(LOG_WING_ARRIVED, wingp->name, nullptr, &time))
					has_arrived = true;
			}
		}

		if (has_arrived)
		{
			++num_arrived;
			if (latest_time && (time > *latest_time))
				*latest_time = time;
		}

		// move to next ship/wing in list
		n = CDR(n);
	}

	if ( count == num_arrived )
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

int sexp_has_arrived_delay(int n)
{
	return sexp_check_objective_delay(n, CDR(n), sexp_has_arrived);
}

/**
 * Determine if a ship/wing has departed
 */
int sexp_has_departed(int n, fix *latest_time)
{
	int count, num_departed;
	bool has_departed;
	fix time = 0;

	count = 0;
	num_departed = 0;
	while ( n != -1 )
	{
		++count;

		auto ship_entry = eval_ship(n);
		wing *wingp = nullptr;
		has_departed = false;

		// this might be a ship
		if (ship_entry)
		{
			if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
				return SEXP_CANT_EVAL;

			if (ship_entry->status == ShipStatus::EXITED)
			{
				// check the mission log
				if (mission_log_get_time(LOG_SHIP_DEPARTED, ship_entry->name, nullptr, &time))
				{
					has_departed = true;
				}
				// exited without departing
				else
				{
					return SEXP_KNOWN_FALSE;
				}
			}
		}
		else
			wingp = eval_wing(n);

		// this might be a wing
		if (wingp)
		{
			if (wing_has_yet_to_arrive(wingp))
				return SEXP_CANT_EVAL;

			if (wingp->flags[Ship::Wing_Flags::Gone])
			{
				// check the mission log
				if (mission_log_get_time(LOG_WING_DEPARTED, wingp->name, nullptr, &time))
				{
					has_departed = true;
				}
				// exited without departing
				else
				{
					return SEXP_KNOWN_FALSE;
				}
			}
		}

		if (has_departed)
		{
			++num_departed;
			if (latest_time && (time > *latest_time))
				*latest_time = time;
		}

		// move to next ship/wing in list
		n = CDR(n);
	}

	if ( count == num_departed )
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

int sexp_has_departed_delay(int n)
{
	return sexp_check_objective_delay(n, CDR(n), sexp_has_departed);
}

/**
 * Determine if a ship is disabled or disarmed, but not both
 */
int sexp_is_disabled_xor_disarmed(int n, bool check_disabled, fix *latest_time)
{
	int count, num_satisfied;
	fix time;

	count = 0;
	num_satisfied = 0;
	while ( n != -1 )
	{
		++count;

		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			return SEXP_FALSE;
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_CANT_EVAL;

		// check mission log
		if (mission_log_get_time(check_disabled ? LOG_SHIP_DISABLED : LOG_SHIP_DISARMED, ship_entry->name, nullptr, &time))
		{
			++num_satisfied;
			if (latest_time && (time > *latest_time))
				*latest_time = time;
		}
		// if the ship has exited, no way to destroy its subsystem.
		else if (ship_entry->status == ShipStatus::EXITED)
			return SEXP_KNOWN_FALSE;

		// move to next ship in list
		n = CDR(n);
	}

	if ( count == num_satisfied )
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

int sexp_is_disabled_xor_disarmed_delay(int n, bool check_disabled)
{
	// since we can't use captured-value lambdas as function pointers, we need a stateless lambda for each value of the variable
	if (check_disabled)
		return sexp_check_objective_delay(n, CDR(n), [](int _n, fix *_time)->int { return sexp_is_disabled_xor_disarmed(_n, true, _time); });
	else
		return sexp_check_objective_delay(n, CDR(n), [](int _n, fix *_time)->int { return sexp_is_disabled_xor_disarmed(_n, false, _time); });
}

/**
 * Determine if a ship or wing is done flying waypoints
 */
int sexp_are_waypoints_done(int n, fix *time, int count = 1)
{
	auto ship_entry = eval_ship(n);
	if (ship_entry)
	{
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_CANT_EVAL;

		auto waypoint_name = CTEXT(CDR(n));

		if (mission_log_get_time_indexed(LOG_WAYPOINTS_DONE, ship_entry->name, waypoint_name, count, time))
			return SEXP_KNOWN_TRUE;

		// if the ship has exited, no way to complete waypoints
		if (ship_entry->status == ShipStatus::EXITED)
			return SEXP_KNOWN_FALSE;

		return SEXP_FALSE;
	}

	auto wingp = eval_wing(n);
	if (wingp)
	{
		if (wing_has_yet_to_arrive(wingp))
			return SEXP_CANT_EVAL;

		auto waypoint_name = CTEXT(CDR(n));

		if (mission_log_get_time_indexed(LOG_WAYPOINTS_DONE, wingp->name, waypoint_name, count, time))
			return SEXP_KNOWN_TRUE;

		// if the wing is gone, no way to complete waypoints
		if (wingp->flags[Ship::Wing_Flags::Gone])
			return SEXP_KNOWN_FALSE;

		return SEXP_FALSE;
	}

	// neither a ship nor a wing
	return SEXP_FALSE;
}

// since we can't use lambda-captures as function pointers (see also disabled_xor_disarmed), we need a lot of code duplication from sexp_check_objective_delay
int sexp_are_waypoints_done_delay(int n)
{
	int delay_node = CDDR(n), objective_node = n, count_node = CDDDR(n);
	fix delay, time;
	int count, val;
	bool is_nan, is_nan_forever;

	time = 0;

	delay = i2f(eval_num(delay_node, is_nan, is_nan_forever));
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	if (count_node >= 0)
	{
		count = i2f(eval_num(count_node, is_nan, is_nan_forever));
		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;

		if (count <= 0)
		{
			Warning(LOCATION, "Are-waypoints-done-delay count should be at least 1!  This has been automatically adjusted.");
			count = 1;
		}
	}
	else
		count = 1;

	// check value of function
	val = sexp_are_waypoints_done(objective_node, &time, count);

	if ((val == SEXP_TRUE) || (val == SEXP_KNOWN_TRUE))
	{
		if ((Missiontime - time) >= delay)
			return val;
		else
			return SEXP_FALSE;
	}

	return val;
}

// First ship is the destroyer, rest of the arguments are the destroyed ships.
int sexp_was_destroyed_by(int n, fix *latest_time)
{
	int count, num_destroyed;
	fix time;

	auto ship_entry = eval_ship(n);
	if (!ship_entry)
		return SEXP_FALSE;
	if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_CANT_EVAL;

	count = 0;
	num_destroyed = 0;
	for (n = CDR(n); n != -1; n = CDR(n))
	{
		++count;

		auto destroyed_ship_entry = eval_ship(n);
		if (!destroyed_ship_entry)
			return SEXP_FALSE;
		if (destroyed_ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_CANT_EVAL;

		// check the mission log
		if (mission_log_get_time(LOG_SHIP_DESTROYED, destroyed_ship_entry->name, ship_entry->name, &time))
		{
			++num_destroyed;
			if (latest_time && (time > *latest_time))
				*latest_time = time;
		}
		// if the ship has exited, no way to destroy it
		else if (destroyed_ship_entry->status == ShipStatus::EXITED)
			return SEXP_KNOWN_FALSE;
	}

	if (count == num_destroyed)
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

int sexp_was_destroyed_by_delay(int n)
{
	return sexp_check_objective_delay(n, CDR(n), sexp_was_destroyed_by);
}

int sexp_has_docked_or_undocked(int n, int op_num)
{
	bool is_nan, is_nan_forever;
	Assert(op_num == OP_HAS_DOCKED || op_num == OP_HAS_UNDOCKED || op_num == OP_HAS_DOCKED_DELAY || op_num == OP_HAS_UNDOCKED_DELAY);

	auto docker = eval_ship(n);
	if (!docker)
		return SEXP_FALSE;
	if (docker->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_CANT_EVAL;
	n = CDR(n);

	auto dockee = eval_ship(n);
	if (!dockee)
		return SEXP_FALSE;
	if (dockee->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_CANT_EVAL;
	n = CDR(n);

	// count of times that we should look for
	int count = eval_num(n, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	n = CDR(n);

	if (count <= 0)
	{
		Warning(LOCATION, "Has-%sdocked%s count should be at least 1!  This has been automatically adjusted.", (op_num == OP_HAS_UNDOCKED || op_num == OP_HAS_UNDOCKED_DELAY ? "un" : ""), (op_num == OP_HAS_DOCKED_DELAY || op_num == OP_HAS_UNDOCKED_DELAY ? "-delay" : ""));
		count = 1;
	}

	if (op_num == OP_HAS_DOCKED_DELAY || op_num == OP_HAS_UNDOCKED_DELAY)
	{
		fix delay = i2f(eval_num(n, is_nan, is_nan_forever));
		fix time;

		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;

		if (mission_log_get_time_indexed(op_num == OP_HAS_DOCKED_DELAY ? LOG_SHIP_DOCKED : LOG_SHIP_UNDOCKED, docker->name, dockee->name, count, &time))
		{
			if ((Missiontime - time) >= delay)
				return SEXP_KNOWN_TRUE;
		}
		// if either ship has exited, no way to dock
		else if (docker->status == ShipStatus::EXITED || dockee->status == ShipStatus::EXITED)
			return SEXP_KNOWN_FALSE;
	}
	else
	{
		if (mission_log_get_time_indexed(op_num == OP_HAS_DOCKED ? LOG_SHIP_DOCKED : LOG_SHIP_UNDOCKED, docker->name, dockee->name, count, nullptr))
		{
			return SEXP_KNOWN_TRUE;
		}
		// if either ship has exited, no way to dock
		else if (docker->status == ShipStatus::EXITED || dockee->status == ShipStatus::EXITED)
			return SEXP_KNOWN_FALSE;
	}

	return SEXP_FALSE;
}

/**
 * Determine is all of a given ship type are destroyed
 */
int sexp_ship_type_destroyed(int n)
{
	bool is_nan, is_nan_forever;

	int percent = eval_num(n, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	auto shiptype = CTEXT(CDR(n));
	int type = ship_type_name_lookup(shiptype);

	if (type < 0 && Fighter_bomber_valid && !stricmp(shiptype, Fighter_bomber_type_name))
		type = Ship_type_fighter_bomber;
	else if (!SCP_vector_inbounds(Ship_types, type))
	{
		// bogus if we reach the end of this array!!!!
		Warning(LOCATION, "Invalid shiptype '%s' passed to ship-type-destroyed", shiptype);
		return SEXP_FALSE;
	}

	int killed, total;
	if (type == Ship_type_fighter_bomber)
	{
		killed = Ship_type_counts[Ship_type_fighter].killed + Ship_type_counts[Ship_type_bomber].killed;
		total = Ship_type_counts[Ship_type_fighter].total + Ship_type_counts[Ship_type_bomber].total;
	}
	else
	{
		//We are safe from array indexing probs b/c of previous if.
		killed = Ship_type_counts[type].killed;
		total = Ship_type_counts[type].total;
	}

	if (total == 0)
		return SEXP_FALSE;

	// determine if the percentage of killed/total is >= percentage given in the expression
	if ((killed * 100 / total) >= percent)
		return SEXP_KNOWN_TRUE;
	
	return SEXP_FALSE;
}

// following are time based functions

/**
 * Returns the time into the mission
 */
int sexp_mission_time()
{
	return f2i(Missiontime);
}

/**
 * Returns the time into the mission, in milliseconds
 */
int sexp_mission_time_msecs()
{
	// multiplying by 1000 can go over the limit for LONG_MAX so cast to long long int first
	auto mission_time = (std::int64_t) Missiontime;
	// This hack is necessary since fix is a 32-bit integer which would overflow if f2i would be used
	return (int)((mission_time * 1000) / 65536);
}

int sexp_has_time_elapsed(int n, bool use_msecs)
{
	bool is_nan, is_nan_forever;
	int time = eval_num(n, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	int mission_time = use_msecs ? sexp_mission_time_msecs() : sexp_mission_time();
	if ( mission_time >= time )
		return SEXP_KNOWN_TRUE;

	return SEXP_FALSE;
}

/**
 * Returns percent of length of distance to special warpout plane
 */
int sexp_special_warp_dist( int n)
{
	vec3d center_pos, actual_local_center;
	float half_length, dist_to_plane;

	// get ship
	auto ship_entry = eval_ship(n);

	// ships which aren't present get NAN
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	auto shipp = ship_entry->shipp();

	// check that ship has warpout_objnum
	if (shipp->special_warpout_objnum < 0 || shipp->special_warpout_objnum >= MAX_OBJECTS) {
		return SEXP_NAN;
	}

	auto sip = &Ship_info[shipp->ship_info_index];
	auto objp = ship_entry->objp();
	object *knossos_objp = &Objects[shipp->special_warpout_objnum];

	// check the special warpout device is valid
	if (knossos_objp->type != OBJ_SHIP || !Ship_info[Ships[knossos_objp->instance].ship_info_index].flags[Ship::Info_Flags::Knossos_device]) {
		return SEXP_NAN;
	}

	// check if within 45 degree half-angle cone of facing 
	float dot = fl_abs(vm_vec_dot(&knossos_objp->orient.vec.fvec, &objp->orient.vec.fvec));
	if (dot < 0.707f) {
		return SEXP_NAN;
	}

	// 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);

	// get distance
	dist_to_plane = fvi_ray_plane(nullptr, &knossos_objp->pos, &knossos_objp->orient.vec.fvec, &center_pos, &objp->orient.vec.fvec, 0.0f);
	dist_to_plane -= half_length;

	// return as a percent of length -- simplified from 100*dist/(2*half_length)
	return (int) (50.0f * dist_to_plane / half_length);
}

int sexp_time_exited(int n, int op_num)
{
	bool ship = (op_num == OP_TIME_SHIP_DESTROYED || op_num == OP_TIME_SHIP_DEPARTED);
	bool destroyed = (op_num == OP_TIME_SHIP_DESTROYED || op_num == OP_TIME_WING_DESTROYED);
	fix time;

	if (ship)
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			return SEXP_NAN;
		if (ship_entry->status == ShipStatus::DEATH_ROLL)
		{
			if (!destroyed)
				return SEXP_NAN;
		}
		else if (ship_entry->status != ShipStatus::EXITED)
			return SEXP_NAN;

		if (destroyed)
		{
			if (!mission_log_get_time(LOG_SHIP_DESTROYED, ship_entry->name, nullptr, &time)
				&& !mission_log_get_time(LOG_SELF_DESTRUCTED, ship_entry->name, nullptr, &time)) {		// returns 0 when not found
				return SEXP_NAN_FOREVER;	// exited but not destroyed
			}
		}
		else
		{
			if (!mission_log_get_time(LOG_SHIP_DEPARTED, ship_entry->name, nullptr, &time)) {			// returns 0 when not found
				return SEXP_NAN_FOREVER;	// exited but not departed
			}
		}
	}
	else
	{
		auto wingp = eval_wing(n);
		if (!wingp || !wingp->flags[Ship::Wing_Flags::Gone])
			return SEXP_NAN;

		if (!mission_log_get_time(destroyed ? LOG_WING_DESTROYED : LOG_WING_DEPARTED, wingp->name, nullptr, &time)) {
			return SEXP_NAN_FOREVER;		// exited but not de[stroy|part]ed
		}
	}

	// if we're here we know that the ship/wing did its thing
	return f2i(time);
}

int sexp_time_arrived(int n, bool ship)
{
	fix time;

	if (ship)
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_NAN;

		if (!mission_log_get_time(LOG_SHIP_ARRIVED, CTEXT(n), nullptr, &time)) {
			return SEXP_NAN;
		}
	}
	else
	{
		auto wingp = eval_wing(n);
		if (!wingp || wing_has_yet_to_arrive(wingp))
			return SEXP_NAN;

		if (!mission_log_get_time(LOG_WING_ARRIVED, CTEXT(n), nullptr, &time)) {
			return SEXP_NAN;
		}
	}

	// if we're here we know that the ship/wing has arrived
	return f2i(time);
}

int sexp_time_docked_or_undocked(int n, bool docked)
{
	fix time;
	bool is_nan, is_nan_forever;

	auto docker = eval_ship(n);
	if (!docker || docker->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	n = CDR(n);

	auto dockee = eval_ship(n);
	if (!dockee || dockee->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	n = CDR(n);

	int count = eval_num(n, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	if (count <= 0) {
		Warning(LOCATION, "Time-%sdocked count should be at least 1!  This has been automatically adjusted.", docked ? "" : "un");
		count = 1;
	}

	if (mission_log_get_time_indexed(docked ? LOG_SHIP_DOCKED : LOG_SHIP_UNDOCKED, docker->name, dockee->name, count, &time))
	{
		return f2i(time);
	}
	// if either ship has exited, no way to dock
	else if (docker->status == ShipStatus::EXITED || dockee->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	return SEXP_NAN;
}

void sexp_set_energy_pct (int node, int op_num)
{
	float new_pct; 
	bool is_nan, is_nan_forever;

	Assert (node >= 0);

	new_pct = eval_num(node, is_nan, is_nan_forever) / 100.0f;
	if (is_nan || is_nan_forever)
		return;

	// deal with ridiculous percentages
	CLAMP(new_pct, 0.0f, 1.0f);
	
	// only need to send a packet for afterburners because shields and weapon energy are sent from server to clients
	if (MULTIPLAYER_MASTER && (op_num == OP_SET_AFTERBURNER_ENERGY)) {
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_float(new_pct);
	}

	node = CDR(node); 

	for (; node >= 0; node = CDR(node)) {
		// get the ship
		auto ship_entry = eval_ship(node);
		if (!ship_entry || !ship_entry->has_shipp()) {
			continue;
		}
		auto shipp = ship_entry->shipp();
		auto sip = &Ship_info[shipp->ship_info_index]; 

		switch (op_num) {
			case OP_SET_AFTERBURNER_ENERGY:
				shipp->afterburner_fuel = sip->afterburner_fuel_capacity * new_pct; 
				break;

			case OP_SET_WEAPON_ENERGY:
				if (!(ship_has_energy_weapons(shipp)) ) {
					continue;
				}
				
				shipp->weapon_energy = sip->max_weapon_reserve * new_pct;
				break; 

			case OP_SET_SHIELD_ENERGY:
				if (shipp->ship_max_shield_strength == 0.0f) {
					continue;
				}	

				shield_set_strength(ship_entry->objp(), (shield_get_max_strength(shipp) * new_pct));
				break;
		}

		if (MULTIPLAYER_MASTER && (op_num == OP_SET_AFTERBURNER_ENERGY)) {
			Current_sexp_network_packet.send_ship(shipp);
		}
	}

	if (MULTIPLAYER_MASTER && (op_num == OP_SET_AFTERBURNER_ENERGY)) {
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_set_energy_pct() 
{
	ship *shipp; 
	float new_pct; 
	ship_info * sip; 

	int op_num = Current_sexp_network_packet.get_operator();

	Current_sexp_network_packet.get_float(new_pct);
	while (Current_sexp_network_packet.get_ship(shipp)) {
		sip = &Ship_info[shipp->ship_info_index]; 

		switch (op_num) {
			case OP_SET_AFTERBURNER_ENERGY:
				shipp->afterburner_fuel = sip->afterburner_fuel_capacity * new_pct; 
				break;

			case OP_SET_WEAPON_ENERGY:
				shipp->weapon_energy = sip->max_weapon_reserve * new_pct;
				break; 

			case OP_SET_SHIELD_ENERGY:
				shield_set_strength(&Objects[shipp->objnum], (shield_get_max_strength(shipp) * new_pct));
				break;
		}
	}
}

int sexp_get_energy_pct (int node, int op_num)
{
	float maximum = 0.0f, current = 0.0f; 

	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;
	auto shipp = ship_entry->shipp();
	auto sip = &Ship_info[shipp->ship_info_index]; 

	switch (op_num) {
		case OP_AFTERBURNER_LEFT:
			maximum = sip->afterburner_fuel_capacity;
			current = shipp->afterburner_fuel;
			break; 
		case OP_WEAPON_ENERGY_LEFT:
			if (ship_has_energy_weapons(shipp)) {
				maximum = sip->max_weapon_reserve; 
				current = shipp->weapon_energy;
			}
			break; 
	}
	if (maximum < WEAPON_RESERVE_THRESHOLD || current < WEAPON_RESERVE_THRESHOLD) {
		return 0;
	}

	return (int)(100 * (current/maximum));
}

/**
 * Return the remaining shields as a percentage of the given ship.
 */
int sexp_shields_left(int node)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	// Goober5000: in case ship has no shields
	if (ship_entry->shipp()->ship_max_shield_strength == 0.0f)
	{
		return 0;
	}

	// now return the amount of shields left as a percentage of the whole.
	int percent = (int)std::lround(get_shield_pct(ship_entry) * 100.0f);
	return percent;
}

/**
 * Return the remaining hits left as a percentage of the whole.
 *
 * This hit amount counts for all hits on the ship (hull + subsystems).  Use hits_left_hull to find hull hits remaining.
 */
int sexp_hits_left(int node, bool sim_hull)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	float hull_pct = sim_hull ? get_sim_hull_pct(ship_entry) : get_hull_pct(ship_entry);

	// now return the amount of hits left as a percentage of the whole.
	int percent = (int)std::lround(hull_pct * 100.0f);
	return percent;
}

/**
 * Determine if ship visible on radar
 * 
 * @return 0 - not visible
 * @return 1 - marginally targetable (jiggly on radar)
 * @return 2 - fully targetable
 */
int sexp_is_ship_visible(int node)
{
	int n = node;

	// get the ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;
	n = CDR(n);

	// the second argument is the viewer
	if (n >= 0)
	{
		auto viewer_entry = eval_ship(n);
		return ship_check_visibility(ship_entry->shipp(), viewer_entry ? viewer_entry->shipp_or_null() : nullptr);
	}
	else
	{
		return ship_check_visibility(ship_entry->shipp(), nullptr);
	}
}

/**
 * Determine if a flag is set on this ship
 */
int sexp_check_ship_flag(int node, Ship::Ship_Flags flag)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	if (ship_entry->shipp()->flags[flag])
		return SEXP_TRUE;
	else
		return SEXP_FALSE;
}

// get multi team v team score
// if not multi team v team return 0
// if invalid team return 0
int sexp_team_score(int node)
{
	// if multi t vs t
	if (Game_mode & GM_MULTIPLAYER) {
		if (Netgame.type_flags & NG_TYPE_TEAM) {

			bool is_nan, is_nan_forever;
			int team = eval_num(node, is_nan, is_nan_forever);
			if (is_nan)
				return SEXP_NAN;
			if (is_nan_forever)
				return SEXP_NAN_FOREVER;

			// Teams can only be 1 or 2 at the moment but we should use Num_teams in case more become possible in the future
			if (team <= 0 || team > Num_teams)
			{
				// invalid team index
				Warning(LOCATION, "sexp-team-score: team %d is not a valid team #", team);
				return 0;
			}

			return Multi_team_score[team - 1];
		}
	}

	return 0;
}

/**
 * Return the remaining hits left on a subsystem as a percentage of the whole.
 *
 * Goober5000 - this sexp is DEPRECATED because it works just like the new hits-left-substem-generic
 */
int sexp_hits_left_subsystem(int n)
{
	bool single_subsystem = false;
	int percent, type;

	// get the ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	auto subsys_name = CTEXT(CDR(n));
	auto ss = ship_get_subsys(ship_entry->shipp(), subsys_name);
	if (ss)
		type = ss->system_info->type;
	else
		type = SUBSYSTEM_NONE;

	if ( (type >= 0) && (type < SUBSYSTEM_MAX) ) {
		// check for the optional argument
		n = CDDR(n); 
		if (n >= 0) {
			single_subsystem = is_sexp_true(n);
		}

		// if the third option is present or if this is an unknown subsystem type we only want to find the percentage of the 
		// named subsystem
		if (single_subsystem || (type == SUBSYSTEM_UNKNOWN)) {
			if (ss) {
				percent = (int)std::lround(ss->current_hits / ss->max_hits * 100.0f);
				return percent;
			}

			// we reached end of ship subsys list without finding subsys_name
			if (ship_class_unchanged(ship_entry)) {
				auto problem = (Game_mode & GM_IN_MISSION) ? "subsystem was not found" : "subsystem information is not available when not in-mission";
				Warning(LOCATION, "Attempted to check subsystem '%s' on ship '%s' in hits-left-subsystem -- %s", subsys_name, ship_entry->name, problem);
			}
			return SEXP_NAN;

		// by default we return as a percentage the hits remaining on the subsystem as a whole (i.e. for 3 engines,
		// we are returning the sum of the hits on the 3 engines)
		} else {
			percent = (int)std::lround(ship_get_subsystem_strength(ship_entry->shipp(), type) * 100.0f);
			return percent;
		}
	}
	return SEXP_NAN;			// if for some strange reason, the type field of the subsystem is bogus
}

// Goober5000
int sexp_hits_left_subsystem_generic(int node)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	auto subsys_type_name = CTEXT(CDR(node));

	// find subsystem type
	int subsys_type = string_lookup(subsys_type_name, Subsystem_types, SUBSYSTEM_MAX);

	// error checking
	if (subsys_type < 0) {
		Warning(LOCATION, "Subsystem type '%s' not recognized in hits-left-subsystem-generic!", subsys_type_name);
		return SEXP_NAN_FOREVER;
	} else if (subsys_type == SUBSYSTEM_NONE) {
		// as you wish...?
		return 0;
	} else if (subsys_type == SUBSYSTEM_UNKNOWN) {
		Warning(LOCATION, "Cannot use SUBSYSTEM_UNKNOWN in hits-left-subsystem-generic!");
		return SEXP_NAN_FOREVER;
	}

	// return as a percentage the hits remaining on the subsystem as a whole (i.e. for 3 engines,
	// we are returning the sum of the hits on the 3 engines)
	return (int)std::lround(ship_get_subsystem_strength(ship_entry->shipp(), subsys_type) * 100.0f);
}

// Goober5000
int sexp_hits_left_subsystem_specific(int node)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	auto subsys_name = CTEXT(CDR(node));

	// find subsystem
	auto ss = ship_get_subsys(ship_entry->shipp(), subsys_name);
	if (ss) {
		// return as a percentage the hits remaining on this subsystem only
		return (int)std::lround(ss->current_hits / ss->max_hits * 100.0f);
	}

	// we reached end of ship subsys list without finding subsys_name
	if (ship_class_unchanged(ship_entry)) {
		auto problem = (Game_mode & GM_IN_MISSION) ? "subsystem was not found" : "subsystem information is not available when not in-mission";
		Warning(LOCATION, "Attempted to check subsystem '%s' on ship '%s' in hits-left-subsystem-specific -- %s", subsys_name, ship_entry->name, problem);
	}
	return SEXP_NAN;
}

int sexp_directive_value(int n)
{	
	int replace_current_value = SEXP_TRUE; 
	int directive_value;
	bool is_nan, is_nan_forever;

	Assert(n >= 0);

	directive_value = eval_num(n, is_nan, is_nan_forever);

	if (is_nan || is_nan_forever) {
		directive_value = 0;
	}

	n = CDR(n);
	if (n > -1) {
		replace_current_value = eval_sexp(n);
	}

	if (replace_current_value == SEXP_FALSE) { // note: any SEXP_KNOWN_FALSE result will return SEXP_FALSE
		Directive_count += directive_value;
	}
	else {
		Directive_count = directive_value;
	}
		
	return SEXP_TRUE;
}

int sexp_determine_team(const char *subj)
{
	char team_name[NAME_LENGTH];

	// quick check
	if (strnicmp(subj, "<any ", 5) != 0)
		return -1;

	// grab IFF (rest of string except for closing angle bracket)
	auto len = strlen(subj + 5) - 1;
	strncpy(team_name, subj + 5, len);
	team_name[len] = '\0';

	// find it
	return iff_lookup(team_name);
}

/**
 * Check distance between the center of two given objects
 */
int sexp_center_distance3(object *objp1, object *objp2)
{
	Assertion(objp1, "First object should be non-NULL based on check in sexp_distance2");

	// if the object isn't present in the mission now
	if (!objp2)
		return SEXP_NAN;

	return (int)vm_vec_dist(&objp1->pos, &objp2->pos);
}

/**
 * Check distance between the bounding boxes of two given objects
 */
int sexp_bbox_distance3(object *objp1, object *objp2)
{
	Assertion(objp1, "First object should be non-NULL based on check in sexp_distance2");

	// if the object isn't present in the mission now
	if (!objp2)
		return SEXP_NAN;

	if (objp1->type == OBJ_SHIP && objp2->type == OBJ_SHIP)
	{
		vec3d temp1, temp2;
		int model_num1 = Ship_info[Ships[objp1->instance].ship_info_index].model_num;
		int model_num2 = Ship_info[Ships[objp2->instance].ship_info_index].model_num;
		return (int)model_find_closest_points(&temp1, model_num1, -1, &objp1->orient, &objp1->pos, &temp2, model_num2, -1, &objp2->orient, &objp2->pos);
	}
	else if (objp1->type == OBJ_SHIP)
	{
		return (int)hud_find_target_distance(objp1, &objp2->pos);
	}
	else if (objp2->type == OBJ_SHIP)
	{
		return (int)hud_find_target_distance(objp2, &objp1->pos);
	}
	else
	{
		return (int)vm_vec_dist(&objp1->pos, &objp2->pos);
	}
}

/**
 * Check distance between two given objects using the wonky retail method
 */
int sexp_retail_distance3(object *objp1, object *objp2)
{
	Assertion(objp1, "First object should be non-NULL based on check in sexp_distance2");

	// if the object isn't present in the mission now
	if (!objp2)
		return SEXP_NAN;

	if ((objp1->type == OBJ_SHIP) && (objp2->type == OBJ_SHIP))
	{
		if (Player_obj == objp1)
			return (int) hud_find_target_distance(objp2, objp1);
		else
			return (int) hud_find_target_distance(objp1, objp2);
	}
	else
	{
		return (int) vm_vec_dist_quick(&objp1->pos, &objp2->pos);
	}
}

/**
 * Check distance between a given ship and a given subject (ship, wing, any \<team\>).
 */
int sexp_distance2(object *objp1, object_ship_wing_point_team *oswpt2, int(*distance_method)(object*, object*))
{
	int dist, dist_min = 0;
	bool inited = false;

	// if the object isn't present in the mission now
	if (!objp1)
		return SEXP_NAN;

	switch (oswpt2->type)
	{
		// we have a ship-on-team type, so check all ships of that type
		case OSWPT_TYPE_SHIP_ON_TEAM:
		{
			for (auto 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;

				if (Ships[Objects[so->objnum].instance].team == oswpt2->team)
				{
					dist = distance_method(objp1, &Objects[so->objnum]);
					if (dist != SEXP_NAN)
					{
						if (!inited || (dist < dist_min))
						{
							dist_min = dist;
							inited = true;
						}
					}
				}
			}

			// no objects were checked
			if (!inited)
				return SEXP_NAN;

			return dist_min;
		}

		// check ships and points
		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WAYPOINT:
		{
			return distance_method(objp1, oswpt2->objp());
		}

		// check wings
		case OSWPT_TYPE_WING:
		{
			for (int i = 0; i < oswpt2->wingp()->current_count; i++)
			{
				dist = distance_method(objp1, &Objects[Ships[oswpt2->wingp()->ship_index[i]].objnum]);
				if (dist != SEXP_NAN)
				{
					if (!inited || (dist < dist_min))
					{
						dist_min = dist;
						inited = true;
					}
				}
			}

			// no objects were checked
			if (!inited)
				return SEXP_NAN;

			return dist_min;
		}

		default:
			break;
	}

	return SEXP_NAN;
}

/**
 * Returns the distance between two objects.
 *
 * If a wing is specified as one (or both) of the arguments to this function, we are looking for the closest distance
 */
int sexp_distance(int n, int(*distance_method)(object*, object*))
{
	int dist, dist_min = 0;
	bool inited = false;
	object_ship_wing_point_team oswpt1, oswpt2;

	eval_object_ship_wing_point_team(&oswpt1, n);
	eval_object_ship_wing_point_team(&oswpt2, CDR(n));

	// check to see if either object was destroyed or departed
	if (oswpt1.type == OSWPT_TYPE_EXITED || oswpt2.type == OSWPT_TYPE_EXITED)
		return SEXP_NAN_FOREVER;

	switch (oswpt1.type)
	{
		// we have a ship-on-team type, so check all ships of that type
		case OSWPT_TYPE_SHIP_ON_TEAM:
		{
			for (auto 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;

				if (Ships[Objects[so->objnum].instance].team == oswpt1.team)
				{
					dist = sexp_distance2(&Objects[so->objnum], &oswpt2, distance_method);
					if (dist != SEXP_NAN)
					{
						if (!inited || (dist < dist_min))
						{
							dist_min = dist;
							inited = true;
						}
					}
				}
			}

			// no objects were checked
			if (!inited)
				return SEXP_NAN;

			return dist_min;
		}

		// check ships and points
		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WAYPOINT:
		{
			return sexp_distance2(oswpt1.objp(), &oswpt2, distance_method);
		}

		// check wings
		case OSWPT_TYPE_WING:
		{
			for (int i = 0; i < oswpt1.wingp()->current_count; i++)
			{
				dist = sexp_distance2(&Objects[Ships[oswpt1.wingp()->ship_index[i]].objnum], &oswpt2, distance_method);
				if (dist != SEXP_NAN)
				{
					if (!inited || (dist < dist_min))
					{
						dist_min = dist;
						inited = true;
					}
				}
			}

			// no objects were checked
			if (!inited)
				return SEXP_NAN;

			return dist_min;
		}

		default:
			break;
	}

	return SEXP_NAN;
}

/**
 * Locate the subsystem on a ship - Goober5000
 * 
 * Switched to a boolean so that it can report failure to do so
 */
bool sexp_get_subsystem_world_pos(vec3d *subsys_world_pos, const ship_registry_entry *ship_entry, const char *subsys_name)
{
	Assert(subsys_world_pos && ship_entry && subsys_name);

	// find the ship subsystem
	ship_subsys *ss = ship_get_subsys(ship_entry->shipp(), subsys_name);
	if (ss)
	{
		// find world position of subsystem on this object (the ship)
		get_subsystem_world_pos(ship_entry->objp(), ss, subsys_world_pos);
		return true;
	}

	// we reached end of ship subsys list without finding subsys_name 
	if (ship_class_unchanged(ship_entry)) {
		// this ship should have had the subsystem named as it shouldn't have changed class
		Warning(LOCATION, "sexp_get_subsystem_world_pos could not find subsystem '%s'", subsys_name);
	}
	return false;
}

/**
 * Check distance between the center of an object and a position
 */
int sexp_center_distance_point(object *objp1, vec3d *pos)
{
	Assertion(objp1 && pos, "Parameters should be non-NULL!");

	return (int)vm_vec_dist(&objp1->pos, pos);
}

/**
 * Check distance between the bounding box of an object and a position
 */
int sexp_bbox_distance_point(object *objp1, vec3d *pos)
{
	Assertion(objp1 && pos, "Parameters should be non-NULL!");

	if (objp1->type == OBJ_SHIP)
	{
		return (int)hud_find_target_distance(objp1, pos);
	}
	else
	{
		return (int)vm_vec_dist(&objp1->pos, pos);
	}
}

/**
 * Returns the distance between an object and a ship subsystem.
 *
 * If a wing is specified as the object argument to this function, we are looking for the closest distance
 */
int sexp_distance_subsystem(int n, int(*distance_method)(object*, vec3d*))
{
	int dist, dist_min = 0;
	bool inited = false;
	vec3d subsys_pos;
	object_ship_wing_point_team oswpt;

	eval_object_ship_wing_point_team(&oswpt, n);

	// quick out
	if (oswpt.type == OSWPT_TYPE_EXITED)
		return SEXP_NAN_FOREVER;

	// get the ship
	auto ship_with_subsys = eval_ship(CDR(n));
	if (!ship_with_subsys || ship_with_subsys->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_with_subsys->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	auto subsys_name = CTEXT(CDR(CDR(n)));

	// get the subsystem's coordinates or bail if we can't
	if (!sexp_get_subsystem_world_pos(&subsys_pos, ship_with_subsys, subsys_name)) 
		return SEXP_NAN;

	switch (oswpt.type)
	{
		// we have a ship-on-team type, so check all ships of that type
		case OSWPT_TYPE_SHIP_ON_TEAM:
		{
			for (auto 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;

				if (Ships[Objects[so->objnum].instance].team == oswpt.team)
				{
					dist = distance_method(&Objects[so->objnum], &subsys_pos);

					if (!inited || (dist < dist_min))
					{
						dist_min = dist;
						inited = true;
					}
				}
			}

			// no objects were checked
			if (!inited)
				return SEXP_NAN;

			return dist_min;
		}

		// check ships and points
		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WAYPOINT:
		{
			return distance_method(oswpt.objp(), &subsys_pos);
		}

		// check wings
		case OSWPT_TYPE_WING:
		{
			for (int i = 0; i < oswpt.wingp()->current_count; i++)
			{
				dist = distance_method(&Objects[Ships[oswpt.wingp()->ship_index[i]].objnum], &subsys_pos);

				if (!inited || (dist < dist_min))
				{
					dist_min = dist;
					inited = true;
				}
			}

			// no objects were checked
			if (!inited)
				return SEXP_NAN;

			return dist_min;
		}

		default:
			break;
	}

	return SEXP_NAN;
}

bool sexp_helper_is_within_box(const std::array<float, 6> &box_vals, const vec3d *pos)
{
	int i;
	for(i = 0; i < 3; i++)
	{
		if(pos->a1d[i] < (box_vals[i] - box_vals[i+3])
			|| pos->a1d[i] > (box_vals[i] + box_vals[i+3]))
		{
			return false;
		}
	}

	return true;
}

int sexp_num_within_box(int n)
{
	std::array<float, 6> box_vals;//x,y,z,width,height,depth
	int retval = 0;
	bool is_nan, is_nan_forever;

	eval_array(box_vals, n, is_nan, is_nan_forever);

	// nobody can be within a NaN box
	if (is_nan || is_nan_forever)
		return 0;
	
	for(; n >= 0; n = CDR(n))
	{
		// this might be a ship
		auto ship_entry = eval_ship(n);
		if (ship_entry)
		{
			if (ship_entry->has_objp() && sexp_helper_is_within_box(box_vals, &ship_entry->objp()->pos))
				retval++;
		}
		else
		{
			// this might be a wing.  if it is, someone in the wing must be present
			auto wingp = eval_wing(n);
			if (wingp && wingp->current_count > 0)
			{
				// every present member in the wing must be in the box
				bool wing_check = true;
				for (int i = 0; i < wingp->current_count; i++)
				{
					if (!sexp_helper_is_within_box(box_vals, &Objects[Ships[wingp->ship_index[i]].objnum].pos))
					{
						wing_check = false;
						break;
					}
				}

				if (wing_check)
					retval++;
			}
		}
	}

	return retval;
}

// Goober5000
// wookieejedi - this sexp is deprecated in favor of sexp_set_ship_man
void sexp_set_object_speed(object *objp, int speed, int axis, bool subjective)
{
	Assert(axis >= 0 && axis <= 2);

	if (subjective)
	{
		vec3d subjective_vel;

		// translate objective into subjective velocity
		vm_vec_rotate(&subjective_vel, &objp->phys_info.vel, &objp->orient);

		// set it
		subjective_vel.a1d[axis] = i2fl(speed);

		// translate it back to objective
		vm_vec_unrotate(&objp->phys_info.vel, &subjective_vel, &objp->orient);
	}
	else
	{
		objp->phys_info.vel.a1d[axis] = i2fl(speed);
	}
}

// Goober5000
// wookieejedi - this sexp is deprecated in favor of sexp_set_ship_man 
void sexp_set_object_speed(int n, int axis)
{
	Assert(n >= 0);

	bool is_nan, is_nan_forever;
	int speed;
	bool subjective = false;
	object_ship_wing_point_team oswpt;

	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	speed = eval_num(n, is_nan, is_nan_forever);
	n = CDR(n);
	if (is_nan || is_nan_forever)
		return;

	if (n >= 0)
	{
		subjective = is_sexp_true(n);
		n = CDR(n);
	}

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(speed);
	Current_sexp_network_packet.send_int(axis);
	Current_sexp_network_packet.send_bool(subjective);

	switch (oswpt.type)
	{
		case OSWPT_TYPE_SHIP:
			sexp_set_object_speed(oswpt.objp(), speed, axis, subjective);
			Current_sexp_network_packet.send_object(oswpt.objp());
			break;

		case OSWPT_TYPE_WING:
		{
			for (int i = 0; i < oswpt.wingp()->current_count; ++i)
			{
				auto shipp = &Ships[oswpt.wingp()->ship_index[i]];

				sexp_set_object_speed(&Objects[shipp->objnum], speed, axis, subjective);
				Current_sexp_network_packet.send_object(&Objects[shipp->objnum]);
			}
			break;
		}

		default:
			break;
	}

	Current_sexp_network_packet.end_callback();
}

//CommanderDJ
void multi_sexp_set_object_speed()
{
	object *objp;
	int speed = 0, axis = 0;
	bool subjective = false;

	Current_sexp_network_packet.get_int(speed);
	Current_sexp_network_packet.get_int(axis);
	Current_sexp_network_packet.get_bool(subjective);

	while (Current_sexp_network_packet.get_object(objp))
		sexp_set_object_speed(objp, speed, axis, subjective);
}

int sexp_get_object_speed(object *objp, int axis, bool subjective)
{
	Assertion(((axis >= 0) && (axis <= 2)), "Axis is out of range (%d)", axis);
	int speed;

	if (subjective)
	{
		// return the speed based on the orentation of the object
		vec3d subjective_vel;
		vm_vec_rotate(&subjective_vel, &objp->phys_info.vel, &objp->orient);
		speed = fl2i(subjective_vel.a1d[axis]);
		vm_vec_unrotate(&objp->phys_info.vel, &subjective_vel, &objp->orient);
	}
	else
	{
		// return the speed according to the grid
		speed = fl2i(objp->phys_info.vel.a1d[axis]);
	}
	return speed;
}

int sexp_get_object_speed(int n, int axis)
{
	Assert(n >= 0);

	int speed;
	bool subjective = false;
	object_ship_wing_point_team oswpt;

	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	if (n >= 0)
	{
		subjective = is_sexp_true(n);
		n = CDR(n);
	}

	switch (oswpt.type)
	{
		case OSWPT_TYPE_EXITED:
			return SEXP_NAN_FOREVER;

		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WING:
			speed = sexp_get_object_speed(oswpt.objp(), axis, subjective);
			break;

		default:
			return SEXP_NAN;
	}
	return speed;
}

// Goober5000
int sexp_calculate_coordinate(vec3d *origin, matrix *orient, vec3d *relative_location, int axis)
{
	Assert(origin != nullptr);
	Assert(orient != nullptr);
	Assert(axis >= 0 && axis <= 2);

	if (relative_location == nullptr)
	{
		return fl2i(origin->a1d[axis]);
	}
	else
	{
		vec3d new_world_pos;

		vm_vec_unrotate(&new_world_pos, relative_location, orient);
		vm_vec_add2(&new_world_pos, origin);

		return fl2i(new_world_pos.a1d[axis]);
	}
}

// Goober5000
int sexp_calculate_angle(matrix *orient, int axis)
{
	Assert(orient != nullptr);
	Assert(axis >= 0 && axis <= 2);

	angles a;
	vm_extract_angles_matrix_alternate(&a, orient);

	// blugh
	float rad;
	switch (axis)
	{
		case 0:	rad = a.p; break;
		case 1:	rad = a.b; break;
		case 2:	rad = a.h; break;
		default: rad = 0.0f; break;
	}

	float deg = fl_degrees(rad);

	int deg2 = static_cast<int>(deg < 0.0f ? deg - 0.5f : deg + 0.5f);
	if (deg2 < 0)
		deg2 += 360;

	return deg2;
}

// Goober5000
int sexp_get_object_coordinate(int n, int axis) 
{
	Assert(n >= 0);

	const char *subsystem_name = nullptr;
	vec3d *pos = nullptr, *relative_location = nullptr, relative_location_buf, subsys_pos_buf;
	object_ship_wing_point_team oswpt;

	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	if (n >= 0)
	{
		subsystem_name = CTEXT(n);
		n = CDR(n);

		if (n >= 0)
		{
			relative_location = &relative_location_buf;

			bool is_nan, is_nan_forever;
			eval_vec3d(relative_location, n, is_nan, is_nan_forever);
			if (is_nan)
				return SEXP_NAN;
			if (is_nan_forever)
				return SEXP_NAN_FOREVER;
		}
	}

	switch (oswpt.type)
	{
		case OSWPT_TYPE_EXITED:
			return SEXP_NAN_FOREVER;

		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WING:
		case OSWPT_TYPE_WAYPOINT:
			pos = &oswpt.objp()->pos;
			break;

		default:
			return SEXP_NAN;
	}

	// see if we have a subsys
	if (oswpt.objp()->type == OBJ_SHIP)
	{
		if ((subsystem_name != nullptr) && stricmp(subsystem_name, SEXP_NONE_STRING) != 0 && stricmp(subsystem_name, SEXP_HULL_STRING) != 0)
		{
			pos = &subsys_pos_buf;
			// get the world pos but bail if we can't get one
			if (!sexp_get_subsystem_world_pos(pos, oswpt.ship_entry(), subsystem_name))
				return SEXP_NAN;
		}
	}

	return sexp_calculate_coordinate(pos, &oswpt.objp()->orient, relative_location, axis);
}

// Goober5000
int sexp_get_object_angle(int n, int axis) 
{
	Assert(n >= 0);

	object_ship_wing_point_team oswpt;
	eval_object_ship_wing_point_team(&oswpt, n);

	switch (oswpt.type)
	{
		case OSWPT_TYPE_EXITED:
			return SEXP_NAN_FOREVER;

		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WING:
			return sexp_calculate_angle(&oswpt.objp()->orient, axis);

		default:
			return SEXP_NAN;
	}
}

int sexp_angle_vectors(int node) {
	vec3d v1, v2;
	bool is_nan1, is_nan_forever1, is_nan2, is_nan_forever2;

	eval_vec3d(&v1, node, is_nan1, is_nan_forever1);
	eval_vec3d(&v2, node, is_nan2, is_nan_forever2);

	if (is_nan_forever1 || is_nan_forever2)
		return SEXP_NAN_FOREVER;

	if (is_nan1 || is_nan2)
		return SEXP_NAN;

	// coverity[uninit_use_in_call:FALSE] - v1 and v2 are always populated by eval_vec3d
	if (IS_VEC_NULL(&v1) || IS_VEC_NULL(&v2))
		return SEXP_NAN;

	float angle = fl_degrees(vm_vec_dot(&v1, &v2) / (vm_vec_mag(&v1) * vm_vec_mag(&v2)));

	return fl2ir(angle);
}

int sexp_angle_fvec_target(int node)
{
	auto* ship_entry = eval_ship(node);

	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	else if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	const vec3d& pos1 = ship_entry->objp()->pos;
	const vec3d& v1 = ship_entry->objp()->orient.vec.fvec;

	object_ship_wing_point_team oswpt;
	eval_object_ship_wing_point_team(&oswpt, CDR(node));

	switch (oswpt.type)
	{
	case OSWPT_TYPE_SHIP:
	case OSWPT_TYPE_WING:
	case OSWPT_TYPE_WAYPOINT: {
		const vec3d& pos2 = oswpt.objp()->pos;

		if (vm_vec_equal(pos1, pos2))
			return SEXP_NAN;

		vec3d v2;
		vm_vec_normalized_dir(&v2, &pos2, &pos1);

		// No need to divide by product of magnitudes, both fvec and v2 are normalized
		float angle = fl_degrees(acosf_safe(vm_vec_dot(&v1, &v2)));

		return fl2ir(angle);
	}

	case OSWPT_TYPE_NONE:
	case OSWPT_TYPE_EXITED:
		return SEXP_NAN_FOREVER;

	default:
		return SEXP_NAN;
	}
}

void set_object_for_clients(object *objp)
{
	if (!(Game_mode & GM_MULTIPLAYER)) {
		return;
	}

	// Tell the player (if this is a client) that they've moved.
	if ((objp->flags[Object::Object_Flags::Player_ship]) && (objp != Player_obj) ){
		multi_oo_send_changed_object(objp);
	}
}

void sexp_set_object_position(int n) 
{
	vec3d target_vec, orig_leader_vec;
	object_ship_wing_point_team oswpt;

	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	bool is_nan, is_nan_forever;
	eval_vec3d(&target_vec, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	switch (oswpt.type)
	{
		case OSWPT_TYPE_SHIP:
		{
			oswpt.objp()->pos = target_vec;
			set_object_for_clients(oswpt.objp());

			if (oswpt.objp()->flags[Object::Object_Flags::Collides])
				obj_collide_obj_cache_stale(oswpt.objp());

			break;
		}

		case OSWPT_TYPE_PARSE_OBJECT:
		{
			oswpt.p_objp()->pos = target_vec;
			break;
		}

		case OSWPT_TYPE_WAYPOINT:
		{
			oswpt.waypointp()->set_pos(&target_vec);
			Current_sexp_network_packet.start_callback();
			Current_sexp_network_packet.send_ushort(oswpt.objp()->net_signature);
			Current_sexp_network_packet.send_float(target_vec.xyz.x);
			Current_sexp_network_packet.send_float(target_vec.xyz.y);
			Current_sexp_network_packet.send_float(target_vec.xyz.z);
			Current_sexp_network_packet.end_callback();
			break;
		}

		case OSWPT_TYPE_WING:
		{
			// move the wing leader first
			orig_leader_vec = oswpt.objp()->pos;
			oswpt.objp()->pos = target_vec;
			set_object_for_clients(oswpt.objp());

			// move everything in the wing
			for (int i = 0; i < oswpt.wingp()->current_count; i++)
			{
				int objnum = Ships[oswpt.wingp()->ship_index[i]].objnum;
				object *objp = &Objects[objnum];

				if (objnum != oswpt.objnum)
				{
					vm_vec_sub2(&objp->pos, &orig_leader_vec);
					vm_vec_add2(&objp->pos, &target_vec);
					set_object_for_clients(objp);
				}

				if (objp->flags[Object::Object_Flags::Collides])
					obj_collide_obj_cache_stale(objp);
			}

			break;
		}

		case OSWPT_TYPE_WING_NOT_PRESENT:
		{
			// search the arrival list for the wing leader and move him first
			bool found = false;
			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				if (p_objp->wingnum == oswpt.wingnum && p_objp->pos_in_wing == 0)
				{
					orig_leader_vec = p_objp->pos;
					p_objp->pos = target_vec;

					found = true;
					break;
				}
			}

			// if we didn't find him, the wing will never arrive, so bail
			if (!found)
				break;

			// move everything in the wing
			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				if (p_objp->wingnum == oswpt.wingnum && p_objp->pos_in_wing != 0)
				{
					vm_vec_sub2(&p_objp->pos, &orig_leader_vec);
					vm_vec_add2(&p_objp->pos, &target_vec);
				}
			}

			break;
		}

		default:
			break;
	}
}

// only for waypoints cause they don't get transferred the normal way
void multi_sexp_set_object_position()
{
	object *objp;
	vec3d wp_vec;
	ushort obj_sig;
	Current_sexp_network_packet.get_ushort(obj_sig);
	Current_sexp_network_packet.get_float(wp_vec.xyz.x);
	Current_sexp_network_packet.get_float(wp_vec.xyz.y);
	Current_sexp_network_packet.get_float(wp_vec.xyz.z);
	objp = multi_get_network_object(obj_sig);
	if (objp->type == OBJ_WAYPOINT) {
		waypoint *wpt = find_waypoint_with_instance(objp->instance);
		wpt->set_pos(&wp_vec);
	}
}

// Goober5000
void sexp_set_object_orientation(int n) 
{
	angles a;
	matrix target_orient;
	object_ship_wing_point_team oswpt;

	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	bool is_nan, is_nan_forever;
	eval_angles(&a, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	vm_angles_2_matrix(&target_orient, &a);

	switch (oswpt.type)
	{
		case OSWPT_TYPE_SHIP:
		{
			oswpt.objp()->orient = target_orient;
			set_object_for_clients(oswpt.objp());

			if (oswpt.objp()->flags[Object::Object_Flags::Collides])
				obj_collide_obj_cache_stale(oswpt.objp());

			break;
		}

		case OSWPT_TYPE_PARSE_OBJECT:
		{
			oswpt.p_objp()->orient = target_orient;
			break;
		}

		case OSWPT_TYPE_WING:
		{
			// move everything in the wing
			for (int i = 0; i < oswpt.wingp()->current_count; i++)
			{
				auto objp = &Objects[Ships[oswpt.wingp()->ship_index[i]].objnum];
				objp->orient = target_orient;
				set_object_for_clients(objp);

				if (objp->flags[Object::Object_Flags::Collides])
					obj_collide_obj_cache_stale(objp);
			}

			break;
		}

		case OSWPT_TYPE_WING_NOT_PRESENT:
		{
			// move everything in the wing
			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				if (p_objp->wingnum == oswpt.wingnum)
					p_objp->orient = target_orient;
			}

			break;
		}

		default:
			break;
	}
}

// Goober5000
// this is different from sexp_set_object_orientation
// and now can be used for arbitrary orients (i.e. for parse objects), not just objects
void sexp_set_orient_sub(matrix *orient_to_set, const vec3d *pos, const vec3d *location, int turn_time = 0, int bank = 0, object *objp = nullptr)
{
	Assert(orient_to_set && pos && location);

	vec3d v_orient;
	matrix m_orient;


	// are we doing this via ai? -------------------
	if (objp && turn_time)
	{
		// set flag
		int bankflag = 0;
		if (!bank) 
		{
			bankflag = AITTV_IGNORE_BANK;
		}

		// slow down or speed up the ship's turnrate to match the given turn time
		vec3d turnrate_mod;
		float turnrate_adjust = Ship_info[Ships[objp->instance].ship_info_index].srotation_time / (turn_time / 1000.f);
		vm_vec_make(&turnrate_mod, turnrate_adjust, turnrate_adjust, turnrate_adjust);
		// turn
		ai_turn_towards_vector(location, objp, nullptr, nullptr, 0.0f, (AITTV_VIA_SEXP | bankflag), nullptr, &turnrate_mod);

		// return
		return;
	}


	// calculate orientation matrix ----------------

	vm_vec_sub(&v_orient, location, pos);

	if (IS_VEC_NULL_SQ_SAFE(&v_orient))
	{
		Warning(LOCATION, "error in sexp setting ship orientation: can't point to self; quitting...\n");
		return;
	}

	vm_vector_2_matrix(&m_orient, &v_orient, nullptr, nullptr);


	// set orientation -----------------------------
	*orient_to_set = m_orient;

	// Tell the player (assuming it's a client) that they've moved.
	if (objp)
		set_object_for_clients(objp);
}

// Goober5000
void sexp_stuff_oswpt_location(vec3d **location, object_ship_wing_point_team *oswpt)
{
	switch (oswpt->type)
	{
		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WING:
		case OSWPT_TYPE_WAYPOINT:
			*location = &oswpt->objp()->pos;
			break;

		case OSWPT_TYPE_PARSE_OBJECT:
			*location = &oswpt->p_objp()->pos;
			break;

		case OSWPT_TYPE_WING_NOT_PRESENT:
		{
			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				// use the wing leader's position, same as if the wing were present
				if (p_objp->wingnum == oswpt->wingnum && p_objp->pos_in_wing == 0)
				{
					*location = &p_objp->pos;
					break;
				}
			}
			break;
		}

		default:
			break;
	}
}

// Goober5000
void sexp_set_oswpt_facing(object_ship_wing_point_team *oswpt, const vec3d *location, int turn_time = 0, int bank = 0)
{
	Assert(oswpt && location);

	switch (oswpt->type)
	{
		case OSWPT_TYPE_SHIP:
			sexp_set_orient_sub(&oswpt->objp()->orient, &oswpt->objp()->pos, location, turn_time, bank, oswpt->objp());
			break;

		case OSWPT_TYPE_PARSE_OBJECT:
			sexp_set_orient_sub(&oswpt->p_objp()->orient, &oswpt->p_objp()->pos, location);
			break;

		case OSWPT_TYPE_WING:
		{
			for (int i = 0; i < oswpt->wingp()->current_count; i++)
			{
				auto objp = &Objects[Ships[oswpt->wingp()->ship_index[i]].objnum];
				sexp_set_orient_sub(&objp->orient, &objp->pos, location, turn_time, bank, objp);
			}
			break;
		}

		case OSWPT_TYPE_WING_NOT_PRESENT:
		{
			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				if (p_objp->wingnum == oswpt->wingnum)
					sexp_set_orient_sub(&p_objp->orient, &p_objp->pos, location);
			}
			break;
		}

		default:
			break;
	}
}

// Goober5000
void sexp_set_object_facing(int n, bool facing_object)
{
	bool is_nan, is_nan_forever;
	vec3d *location = nullptr;
	vec3d location_buf;
	int turn_time, bank;
	object_ship_wing_point_team oswpt1, oswpt2;

	// get ship or wing
	eval_object_ship_wing_point_team(&oswpt1, n);
	n = CDR(n);

	// get location
	if (facing_object)
	{
		eval_object_ship_wing_point_team(&oswpt2, n);
		n = CDR(n);

		sexp_stuff_oswpt_location(&location, &oswpt2);

		// ensure it's valid
		if (location == nullptr)
			return;
	}
	else
	{
		location = &location_buf;

		eval_vec3d(location, n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
	}

	// get optional turn time and bank
	eval_nums(n, is_nan, is_nan_forever, turn_time, bank);
	if (is_nan || is_nan_forever)
		return;

	sexp_set_oswpt_facing(&oswpt1, location, turn_time, bank);
}

void sexp_set_ship_man(ship *shipp, int duration, int heading, int pitch, int bank, bool apply_all_rotate, int up, int sideways, int forward, bool apply_all_lat, int maneuver_flags)
{
	ai_info	*aip = &Ai_info[shipp->ai_index];
	
	if (!(maneuver_flags & CIF_DONT_OVERRIDE_OLD_MANEUVERS)) {
		aip->ai_override_flags.reset();
	}
	bool applied_rot, applied_lat;
	applied_rot = applied_lat = false;

	if (apply_all_rotate) {
		aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Full_rot);
		aip->ai_override_ci.bank = bank / 100.0f;
		aip->ai_override_ci.pitch = pitch / 100.0f;
		aip->ai_override_ci.heading = heading / 100.0f;
		applied_rot = true;
	} else {
		if (bank != 0) {
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Roll);
			aip->ai_override_ci.bank = bank / 100.0f;
			applied_rot = true;
		}
		if (pitch != 0) {
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Pitch);
			aip->ai_override_ci.pitch = pitch / 100.0f;
			applied_rot = true;
		}
		if (heading != 0) {
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Heading);
			aip->ai_override_ci.heading = heading / 100.0f;
			applied_rot = true;
		}
	}

	if (apply_all_lat) {
		aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Full_lat);
		aip->ai_override_ci.vertical = up / 100.0f;
		aip->ai_override_ci.sideways = sideways / 100.0f;
		aip->ai_override_ci.forward = forward / 100.0f;
		applied_lat = true;
	} else {
		if (up != 0) {
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Up);
			aip->ai_override_ci.vertical = up / 100.0f;
			applied_lat = true;
		}
		if (sideways != 0) {
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Sideways);
			aip->ai_override_ci.sideways = sideways / 100.0f;
			applied_lat = true;
		}
		if (forward != 0) {
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Forward);
			aip->ai_override_ci.forward = forward / 100.0f;
			applied_lat = true;
		}
	}

	// handle infinite timestamps
	if (duration >= 2) {
		if (applied_rot)
			aip->ai_override_rot_timestamp = timestamp(duration);
		if (applied_lat)
			aip->ai_override_lat_timestamp = timestamp(duration);
	}
	else {
		if (applied_rot) {
			aip->ai_override_rot_timestamp = timestamp(10);
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Rotational_never_expire);
		}
		if (applied_lat) {
			aip->ai_override_lat_timestamp = timestamp(10);
			aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Lateral_never_expire);
		}
	}

	if (maneuver_flags & CIF_DONT_BANK_WHEN_TURNING) {
		aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Dont_bank_when_turning);
	}
	if (maneuver_flags & CIF_DONT_CLAMP_MAX_VELOCITY) {
		aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Dont_clamp_max_velocity);
	}
	if (maneuver_flags & CIF_INSTANTANEOUS_ACCELERATION) {
		aip->ai_override_flags.set(AI::Maneuver_Override_Flags::Instantaneous_acceleration);
	}
}

void sexp_set_oswpt_maneuver(object_ship_wing_point_team *oswpt, int duration, int heading, int pitch, int bank, bool apply_all_rotate, int up, int sideways, int forward, bool apply_all_lat, int maneuver_flags)
{
	Assert(oswpt);

	switch (oswpt->type)
	{
		case OSWPT_TYPE_SHIP:
			sexp_set_ship_man(oswpt->shipp(), duration, heading, pitch, bank, apply_all_rotate, up, sideways, forward, apply_all_lat, maneuver_flags);
			break;

		case OSWPT_TYPE_WING:
		{
			for (int i = 0; i < oswpt->wingp()->current_count; i++)
			{
				auto shipp = &Ships[oswpt->wingp()->ship_index[i]];
				sexp_set_ship_man(shipp, duration, heading, pitch, bank, apply_all_rotate, up, sideways, forward, apply_all_lat, maneuver_flags);
			}

			break;
		}

		default:
			break;
	}
}

void sexp_set_ship_maneuver(int n, int op_num)
{
	int bank = 0, heading = 0, pitch = 0;
	int up = 0, sideways = 0, forward = 0;
	int duration, i, temp, maneuver_flags = 0;
	bool apply_all_rotate = false, apply_all_lat = false, is_nan, is_nan_forever;
	object_ship_wing_point_team oswpt;

	eval_object_ship_wing_point_team(&oswpt, n);

	n = CDR(n);
	duration = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	if (op_num == OP_SHIP_ROT_MANEUVER || op_num == OP_SHIP_MANEUVER) {
		for(i=0;i<3;i++) {
			n = CDR(n);

			temp = eval_num(n, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;

			if (i == 0)
				heading = temp;
			else if (i == 1)
				pitch = temp;
			else
				bank = temp;
		}

		n = CDR(n);
		apply_all_rotate = is_sexp_true(n);
	}

	if (op_num == OP_SHIP_LAT_MANEUVER || op_num == OP_SHIP_MANEUVER) {
		for(i=0;i<3;i++) {
			n = CDR(n);

			temp = eval_num(n, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;

			if (i == 0)
				up = temp;
			else if (i == 1)
				sideways = temp;
			else
				forward = temp;
		}

		n = CDR(n);
		apply_all_lat = is_sexp_true(n);
	}

	if ((bank == 0) && (pitch == 0) && (heading == 0) && !apply_all_rotate && (up == 0) && (sideways == 0) && (forward == 0) && !apply_all_lat)
		return;

	n = CDR(n);
	if (n >= 0) {
		maneuver_flags = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
	}

	sexp_set_oswpt_maneuver(&oswpt, duration, heading, pitch, bank, apply_all_rotate, up, sideways, forward, apply_all_lat, maneuver_flags);
}

/**
 * Determine when the last meaningful order was given to one or more ships.
 * 
 * @return true or false depending on whether or not a meaningful order was received
 */
int sexp_last_order_time(int n)
{
	int i;
	fix time;
	ai_goal *aigp;
	bool is_nan, is_nan_forever;

	time = i2f(eval_num(n, is_nan, is_nan_forever));
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	Assert ( time >= 0 );

	n = CDR(n);
	while ( n != -1 ) {
		aigp = nullptr;

		auto ship_entry = eval_ship(n);
		if (ship_entry) {
			if (ship_entry->has_shipp()) {
				aigp = Ai_info[ship_entry->shipp()->ai_index].goals;
			}
		} else {
			auto wingp = eval_wing(n);
			if (wingp && !wing_has_yet_to_arrive(wingp) && !wingp->flags[Ship::Wing_Flags::Gone]) {
				aigp = wingp->ai_goals;
			}
		}

		// if we cannot find ship or wing, return SEXP_FALSE
		if (!aigp) {
			return SEXP_FALSE;
		}

		// check the ai_goals structure and determine if there are any
		// orders which are < time seconds since current mission time
		for ( i = 0; i < MAX_AI_GOALS; i++ ) {
			int mode;

			mode = aigp->ai_mode;
			if ( (mode  != AI_GOAL_NONE) && (mode != AI_GOAL_WARP) )
				if ( (aigp->time + time) > Missiontime )
					break;
			aigp++;
		}
		if ( i == MAX_AI_GOALS )
			return SEXP_TRUE;

		n = CDR(n);
	}

	return SEXP_FALSE;
}

/**
 * Return the number of players in the mission
 */
int sexp_num_players()
{
	int count = 0;

	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->flags[Object::Object_Flags::Player_ship])
			count++;
	}

	return count;
}

/**
 * Determine if the current skill level of the game is at least the skill level given in the SEXP
 */
int sexp_skill_level_at_least(int n)
{
	auto level_name = CTEXT(n);
	if (!level_name)
		return SEXP_KNOWN_FALSE;

	for (int i = 0; i < NUM_SKILL_LEVELS; ++i) {
		if ( !stricmp(level_name, Skill_level_names(i, 0)) ) {
			if ( Game_skill_level >= i ){
				return SEXP_TRUE;
			} else {
				return SEXP_FALSE;
			}
		}
	}

	// return SEXP_FALSE if not found!!!
	return SEXP_FALSE;
}

int sexp_was_promotion_granted(int  /*n*/)
{
	if (Player->flags & PLAYER_FLAGS_PROMOTED)
		return SEXP_TRUE;

	return SEXP_FALSE;
}

int sexp_was_medal_granted(int n)
{
	if (n < 0) {
		if (Player->stats.m_medal_earned >= 0)
			return SEXP_TRUE;

		return SEXP_FALSE;
	}

	auto medal_name = CTEXT(n);

	for (int i = 0; i < (int)Medals.size(); ++i) {
		if (!stricmp(medal_name, Medals[i].name)) {
			if (Player->stats.m_medal_earned == i) {
				return SEXP_TRUE;
			} else {
				break;
			}
		}
	}
	
	return SEXP_FALSE;
}

float get_damage_caused(const ship_registry_entry *ship_entry, int attacker_sig)
{
	int idx;
	float damage_total = 0.0f;

	// is the ship that took damage on the exit list?
	if (ship_entry->exited_index >= 0) {
		for (idx = 0; idx < MAX_DAMAGE_SLOTS; idx++) {
			if (Ships_exited[ship_entry->exited_index].damage_ship_id[idx] == attacker_sig) {
				damage_total += Ships_exited[ship_entry->exited_index].damage_ship[idx];
				break;
			}
		}
	}
	// is it referenceable?
	else if (ship_entry->has_shipp()) {
		for (idx = 0; idx < MAX_DAMAGE_SLOTS; idx++) {
			if (ship_entry->shipp()->damage_ship_id[idx] == attacker_sig) {
				damage_total += ship_entry->shipp()->damage_ship[idx];
				break;
			}
		}
	}

	return damage_total;
}

// Karajorma
int sexp_get_damage_caused(int node) 
{
	int ship_class, attacker_sig;
	float damage_caused = 0.0f;

	auto ship_entry = eval_ship(node);
	if (!ship_entry)
		return SEXP_NAN;

	// a ship which hasn't arrived can't have taken any damage yet
	if (ship_entry->status == ShipStatus::NOT_YET_PRESENT) {
		return 0;
	}

	// this ship may have exited already.
	if (ship_entry->exited_index >= 0) {
		ship_class = Ships_exited[ship_entry->exited_index].ship_class;
	} else if (ship_entry->has_shipp()) {
		ship_class = ship_entry->shipp()->ship_info_index;
	} else {
		// it probably vanished
		return SEXP_NAN_FOREVER;
	}

	node = CDR(node);
	Assert (node != -1);

	// go through the list of ships who we think may have attacked the ship
	for ( ; node != -1; node = CDR(node) ) {
		auto attacker = eval_ship(node);

		if (!attacker || attacker->status == ShipStatus::NOT_YET_PRESENT) {
			continue;
		}

		// this ship may have exited already.
		if (attacker->exited_index >= 0) {
			attacker_sig = Ships_exited[attacker->exited_index].obj_signature;
		} else if (attacker->has_objp()) {
			attacker_sig = attacker->objp()->signature;
		} else {
			// it probably vanished
			continue;
		}

		damage_caused += get_damage_caused (ship_entry, attacker_sig);
	}
	
	Assertion((ship_class > -1) && (ship_class < ship_info_size()), "Invalid ship class '%d' passed to sexp_get_damage_caused() (should be >= 0 and < %d); get a coder!\n", ship_class, ship_info_size());
	return (int) ((damage_caused/Ship_info[ship_class].max_hull_strength) * 100.0f);
}

/**
 * Returns true if the percentage of ships (and ships in wings) that have met the given objective is at least the percentage given.
 * 
 * "what" determines if we should check destroyed or departed status
 * Goober5000 - added disarm, disable, and scan
 */
int sexp_percent_ships_arrive_depart_destroy_disarm_disable_scan(int n, int what)
{
	int percent;
	int total, count, impossible_count;
	bool is_nan, is_nan_forever;

	percent = eval_num(n, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	total = 0;
	count = 0;
	impossible_count = 0;
	// iterate through the rest of the ships/wings in the list and tally the departures and the
	// total
	for ( n = CDR(n); n != -1; n = CDR(n) ) {
		// this might be a wing
		auto wingp = eval_wing(n);
		if (wingp) {
			// for wings, we can increment the total by the total number of ships that we expect for
			// this wing, and the departures by the number of departures stored for this wing
			total += (wingp->wave_count * wingp->num_waves);

			if ( what == OP_PERCENT_SHIPS_DEPARTED ) {
				count += wingp->total_departed;
				impossible_count += (wingp->total_destroyed + wingp->total_vanished);
			} else if ( what == OP_PERCENT_SHIPS_DESTROYED ) {
				count += wingp->total_destroyed;
				impossible_count += (wingp->total_departed + wingp->total_vanished);
			} else if ( what == OP_PERCENT_SHIPS_ARRIVED ) {
				count += wingp->total_arrived_count;
			} else
				Warning(LOCATION, "Invalid status check '%d' for wing '%s' in sexp_percent_ships_arrive_depart_destroy_disarm_disable_scan", what, wingp->name);
		} else {
			auto ship_entry = eval_ship(n);

			// must be a ship, so increment the total by 1, then determine if this ship has departed
			total++;

			// no need to check invalid ships, or ones that haven't arrived
			if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT) {
				continue;
			}
			auto name = ship_entry->name;
			int old_count = count;

			if ( what == OP_PERCENT_SHIPS_DEPARTED ) {
				if ( mission_log_get_time(LOG_SHIP_DEPARTED, name, nullptr, nullptr) )
					count++;
			} else if ( what == OP_PERCENT_SHIPS_DESTROYED ) {
				if ( mission_log_get_time(LOG_SHIP_DESTROYED, name, nullptr, nullptr) || mission_log_get_time(LOG_SELF_DESTRUCTED, name, nullptr, nullptr) )
					count++;
			} else if ( what == OP_PERCENT_SHIPS_DISABLED ) {
				if ( mission_log_get_time(LOG_SHIP_DISABLED, name, nullptr, nullptr) )
					count++;
			} else if ( what == OP_PERCENT_SHIPS_DISARMED ) {
				if ( mission_log_get_time(LOG_SHIP_DISARMED, name, nullptr, nullptr) )
					count++;
			} else if ( what == OP_PERCENT_SHIPS_ARRIVED ) {
				if ( mission_log_get_time(LOG_SHIP_ARRIVED, name, nullptr, nullptr) )
					count++;
			} else if ( what == OP_PERCENT_SHIPS_SCANNED ) {
				if ( mission_log_get_time(LOG_CARGO_REVEALED, name, nullptr, nullptr) )
					count++;
			} else
				Warning(LOCATION, "Invalid status check '%d' for ship '%s' in sexp_percent_ships_arrive_depart_destroy_disarm_disable_scan", what, name);

			if (old_count == count && ship_entry->status == ShipStatus::EXITED)
				impossible_count++;
		}
	}

	// now, look at the percentage
	if ( ((count * 100) / total) >= percent )
		return SEXP_KNOWN_TRUE;
	// now see if the percentage can't be satisfied
	else if ( ((impossible_count * 100) / total) > (100 - percent) )
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

/**
 * Determine if a list of ships has departed from within a radius of a given jump node.
 * @return true N seconds after the list of ships have departed
 */
int sexp_depart_node_delay(int n)
{
	int count, num_departed;
	fix delay, latest_time, this_time;
	bool is_nan, is_nan_forever;

	// get the delay
	delay = i2f(eval_num(n, is_nan, is_nan_forever));
	n = CDR(n);

	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	auto jump_node_name = CTEXT(n);
	n = CDR(n);

	// iterate through the list of ships
	latest_time = 0;
	count = 0;
	num_departed = 0;
	while ( n != -1 ) {
		count++;

		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			return SEXP_FALSE;
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_CANT_EVAL;

		if (ship_entry->status == ShipStatus::EXITED)
		{
			if (mission_log_get_time(LOG_SHIP_DEPARTED, ship_entry->name, jump_node_name, &this_time)) {
				num_departed++;
				if (this_time > latest_time)
					latest_time = this_time;
			}
			else
				return SEXP_KNOWN_FALSE;	// exited but not departed (or not through the node)
		}

		n = CDR(n);
	}

	if ( (count == num_departed) && ((Missiontime - latest_time) >= delay) )
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

/**
 * Returns true when the listed ships/wings have all been destroyed or have departed.
 */
int sexp_destroyed_departed_delay(int n)
{
	int count, total;
	fix delay, latest_time;
	bool is_nan, is_nan_forever;

	Assert( n >= 0 );

	// get the delay
	delay = i2f(eval_num(n, is_nan, is_nan_forever));
	n = CDR(n);

	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	count = 0;					// number destroyed or departed
	total = 0;					// total number of ships/wings to check
	latest_time = 0;
	while ( n != -1 ) {
		fix time_gone = 0;
		total++;

		auto wingp = eval_wing(n);
		if (wingp) {
			if (wing_has_yet_to_arrive(wingp)) {
				return SEXP_CANT_EVAL;
			}

			// for wings, check the WF_GONE flag to see if there are no more ships in this wing to arrive.
			if (wingp->flags[Ship::Wing_Flags::Gone]) {
				// be sure to get the latest time of one of these 
				if (wingp->time_gone > latest_time) {
					time_gone = wingp->time_gone;
				}
				count++;
			}
		} else {
			auto ship_entry = eval_ship(n);
			if (ship_entry) {
				if (ship_entry->status == ShipStatus::NOT_YET_PRESENT) {
					return SEXP_CANT_EVAL;
				}

				if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED) {
					if (mission_log_get_time(LOG_SHIP_DEPARTED, ship_entry->name, nullptr, &time_gone)) {
						count++;
					} else if (mission_log_get_time(LOG_SHIP_DESTROYED, ship_entry->name, nullptr, &time_gone)) {
						count++;
					} else if (mission_log_get_time(LOG_SELF_DESTRUCTED, ship_entry->name, nullptr, &time_gone)) {
						count++;
					}
					// apparently we don't count vanished ships
					// (and we can't, because the mission log didn't record the time they left)
					else {
						return SEXP_KNOWN_FALSE;
					}
				}
			}
		}

		// check our latest time
		if ( time_gone > latest_time ){
			latest_time = time_gone;
		}

		n = CDR(n);
	}

	if ( (count == total) && (Missiontime > (latest_time + delay)) )
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

void sexp_special_warpout_name( int node )
{
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	auto knossos_entry = eval_ship(CDR(node));
	if (!knossos_entry || !knossos_entry->has_shipp())
		return;

	// set special warpout objnum
	ship_entry->shipp()->special_warpout_objnum = knossos_entry->objnum;
}

/**
 * Determines if N seconds have elapsed since all discovery of all cargo of given ships
 *
 * Goober5000 - I reworked this function to allow for the set-scanned and set-unscanned sexps
 * to work multiple times in a row and also to fix the potential bug where exited ships are
 * checked against their departure time, not against their cargo known time
 */
int sexp_is_cargo_known( int n, bool check_delay )
{
	bool is_nan, is_nan_forever;
	int count, num_known, delay;

	Assert ( n >= 0 );

	count = 0;
	num_known = 0;

	// get the delay value (if there is one)
	delay = 0;
	if ( check_delay )
	{
		delay = eval_num(n, is_nan, is_nan_forever);
		n = CDR(n);

		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;
	}

	while ( n != -1 )
	{
		fix time_known;
		bool is_known = false;
		count++;

		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			return SEXP_FALSE;
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_CANT_EVAL;

		// see if the ship has already exited the mission (either through departure or destruction)
		if (ship_entry->exited_index >= 0)
		{
			// if not known, the whole thing is known false
			if ( !(Ships_exited[ship_entry->exited_index].flags[Ship::Exit_Flags::Cargo_known]) )
				return SEXP_KNOWN_FALSE;

			// check the delay of when we found out
			time_known = Missiontime - Ships_exited[ship_entry->exited_index].time_cargo_revealed;
			if ( f2i(time_known) >= delay )
				is_known = true;
		}
		// ship is in mission
		else if (ship_entry->has_shipp())
		{
			if ( ship_entry->shipp()->flags[Ship::Ship_Flags::Cargo_revealed] )
			{
				time_known = Missiontime - ship_entry->shipp()->time_cargo_revealed;
				if ( f2i(time_known) >= delay )
					is_known = true;
			}
		}
		// ship probably vanished
		else
			return SEXP_NAN_FOREVER;

		// if cargo is known, mark our variable
		if ( is_known )
			num_known++;

		n = CDR(n);
	}

	Directive_count += count - num_known;
	if ( count == num_known )
		return SEXP_TRUE;
	else
		return SEXP_FALSE;
}

void get_cap_subsys_cargo_flags(const ship *shipp, const char *subsys_name, int *known, fix *time_revealed)
{
	// find the ship subsystem
	ship_subsys *ss = ship_get_subsys(shipp, subsys_name);
	if (ss)
	{
		// set the flags
		*known = (ss->flags[Ship::Subsystem_Flags::Cargo_revealed]);
		*time_revealed = ss->time_subsys_cargo_revealed;
	}
	// if we didn't find the subsystem, the ship hasn't arrived yet
	else
	{
		*known = -1;
		*time_revealed = 0;
	}
}

// reworked by Goober5000 to allow for set-scanned and set-unscanned to be used more than once
int sexp_cap_subsys_cargo_known_delay(int n)
{
	bool is_nan, is_nan_forever;
	int delay, count, num_known;

	num_known = 0;
	count = 0;

	Assert( n >= 0 );

	// get delay
	delay = eval_num(n, is_nan, is_nan_forever);
	n = CDR(n);

	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	// get ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry)
		return SEXP_FALSE;
	if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_CANT_EVAL;
	n = CDR(n);

	while ( n != -1 )
	{
		fix time_known;
		bool is_known = false;
		count++;

		// get subsys name
		auto subsys_name = CTEXT(n);

		// see if the ship has already exited the mission (either through departure or destruction)
		if (ship_entry->status == ShipStatus::EXITED)
		{
			// check the delay of when we found out...
			// Since there is no way to keep track of subsystem status once a ship has departed
			// or has been destroyed, check the mission log.  This will work in 99.9999999% of
			// all cases; however, if the mission designer repeatedly sets and resets the scanned
			// status of the subsystem, the mission log will only return the first occurrence of the
			// subsystem cargo being revealed (regardless of whether it was first hidden using
			// set-unscanned).  Normally, ships keep track of cargo data in the subsystem struct,
			// but once the ship has left the mission, the subsystem linked list is purged,
			// causing the loss of this information.  I judged the significant rework of the
			// subsystem code not worth the rare instance that this sexp may be required to work
			// in this way, especially since this problem only occurs after the ship departs.  If
			// the mission designer really needs this functionality, he or she can achieve the
			// same result with creative combinations of event chaining and is-event-true.
			if (!mission_log_get_time(LOG_CAP_SUBSYS_CARGO_REVEALED, ship_entry->name, subsys_name, &time_known))
			{
				// if not known, the whole thing is known false
				return SEXP_KNOWN_FALSE;
			}

			if (f2i(Missiontime - time_known) >= delay)
				is_known = true;
		}
		// ship is in mission
		else
		{
			int cargo_revealed(0);
			fix time_revealed(0);

			// get flags
			get_cap_subsys_cargo_flags(ship_entry->shipp(), subsys_name, &cargo_revealed, &time_revealed);

			if (cargo_revealed)
			{
				time_known = Missiontime - time_revealed;
				if ( f2i(time_known) >= delay )
					is_known = true;
			}
		}

		// if cargo is known, mark our variable
		if ( is_known )
			num_known++;

		n = CDR(n);
	}

	Directive_count += count - num_known;
	if ( count == num_known )
		return SEXP_TRUE;
	else
		return SEXP_FALSE;
}

// Goober5000
void sexp_set_scanned_unscanned(int n, int flag)
{
	// get ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	// check for possible next optional argument: subsystem
	n = CDR(n);

	// if no subsystem specified, just do it for the ship and exit
	if (n == -1)
	{
		if (flag)
			ship_do_cargo_revealed(ship_entry->shipp());
		else
			ship_do_cargo_hidden(ship_entry->shipp());

		return;
	}

	// iterate through all subsystems
	while (n != -1)
	{
		auto subsys_name = CTEXT(n);

		// find the ship subsystem
		auto ss = ship_get_subsys(ship_entry->shipp(), subsys_name);
		if (ss)
		{
			// do it for the subsystem
			if (flag)
				ship_do_cap_subsys_cargo_revealed(ship_entry->shipp(), ss);
			else
				ship_do_cap_subsys_cargo_hidden(ship_entry->shipp(), ss);
		}

		// if we didn't find the subsystem -- bad
		if (!ss && ship_class_unchanged(ship_entry)) {
			Warning(LOCATION, "Couldn't find subsystem '%s' on ship '%s' in sexp_set_scanned_unscanned", subsys_name, ship_entry->name);
		}

		// but if it did, loop again
		n = CDR(n);
	}
}

int sexp_has_been_tagged_delay(int n)
{
	bool is_nan, is_nan_forever;
	int count, num_known, delay;

	Assert ( n >= 0 );

	count = 0;
	num_known = 0;

	// get the delay value
	delay = eval_num(n, is_nan, is_nan_forever);
	n = CDR(n);

	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	while ( n != -1 )
	{
		fix time_known;
		bool is_known = false;
		count++;

		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			return SEXP_FALSE;
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_CANT_EVAL;

		// see if the ship has already exited the mission (either through departure or destruction).  If so,
		// grab the status of the tag from this list
		if (ship_entry->exited_index >= 0)
		{
			if ( !(Ships_exited[ship_entry->exited_index].flags[Ship::Exit_Flags::Been_tagged]) )
				return SEXP_KNOWN_FALSE;

			// check the delay of when we found out.  We use the ship died time which isn't entirely accurate
			// but won't cause huge delays.
			time_known = Missiontime - Ships_exited[ship_entry->exited_index].time;
			if ( f2i(time_known) >= delay )
				is_known = true;
		}
		// ship is in mission
		else if (ship_entry->has_shipp())
		{
			if ( ship_entry->shipp()->time_first_tagged != 0 )
			{
				time_known = Missiontime - ship_entry->shipp()->time_first_tagged;
				if ( f2i(time_known) >= delay )
					is_known = true;
			}
		}
		// ship probably vanished
		else
			return SEXP_NAN_FOREVER;

		// if ship is tagged, mark our variable
		if ( is_known )
			num_known++;

		n = CDR(n);
	}

	Directive_count += count - num_known;
	if ( count == num_known )
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_FALSE;
}

// Karajorma
void eval_when_for_each_special_argument( int cur_node )
{
	arg_item *ptr;

	// loop through all the supplied arguments
	ptr = Sexp_applicable_argument_list.get_next();
	while (ptr != nullptr)
	{
		// acquire argument to be used
		Sexp_replacement_arguments.emplace_back(ptr->text, ptr->node);

		Sexp_current_argument_nesting_level++;
		Sexp_applicable_argument_list.add_data(ptr->text, ptr->node);

		// execute sexp... CTEXT will insert the argument as necessary
		eval_sexp(cur_node);
		
		// clean up any special sexp stuff
		Sexp_applicable_argument_list.clear_nesting_level();
		Sexp_current_argument_nesting_level--;

		// remove the argument 
		Sexp_replacement_arguments.pop_back(); 

		// continue along argument list
		ptr = ptr->get_next();
	}
}


// Goober5000
void do_action_for_each_special_argument( int cur_node )
{
	arg_item *ptr;

	// loop through all the supplied arguments
	ptr = Sexp_applicable_argument_list.get_next();
	while (ptr != nullptr)
	{
		// acquire argument to be used
		Sexp_replacement_arguments.emplace_back(ptr->text, ptr->node);

		// execute sexp... CTEXT will insert the argument as necessary
		// (since these are all actions, they don't return any meaningful values)
		eval_sexp(cur_node);

		// remove the argument 
		Sexp_replacement_arguments.pop_back(); 
		// continue along argument list
		ptr = ptr->get_next();
	}
}

bool is_descendant_of_when_argument_op(int node)
{
	// empty tree
	if (node < 0)
		return false;

	// cached?
	if (Sexp_nodes[node].flags & SNF_DESCENDANT_OF_WHEN_ARG_OP)
		return true;
	if (Sexp_nodes[node].flags & SNF_NOT_DESCENDANT_OF_WHEN_ARG_OP)
		return false;

	// see whether this node or any parent node qualifies
	// (note that find_parent_operator() is expensive, so we want to cache any and all results)
	bool result;
	if (Sexp_nodes[node].type == SEXP_ATOM && Sexp_nodes[node].subtype == SEXP_ATOM_OPERATOR && is_when_argument_op(get_operator_const(node)))
		result = true;
	else
		result = is_descendant_of_when_argument_op(find_parent_operator(node));

	// cache this result
	if (result)
		Sexp_nodes[node].flags |= SNF_DESCENDANT_OF_WHEN_ARG_OP;
	else
		Sexp_nodes[node].flags |= SNF_NOT_DESCENDANT_OF_WHEN_ARG_OP;

	return result;
}

// Goober5000
bool special_argument_appears_in_sexp_tree(int node)
{
	// empty tree
	if (node < 0)
		return false;

	// cached?
	if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_TREE)
		return true;
	if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_NOT_IN_TREE)
		return false;

	// special argument?
	if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)
		return true;

	// we don't want to include special arguments if they are nested in a new argument SEXP
	if (Sexp_nodes[node].type == SEXP_ATOM && Sexp_nodes[node].subtype == SEXP_ATOM_OPERATOR) {
		if (is_when_argument_op(get_operator_const(node))) {
			return false; 
		}
	}

	bool result = special_argument_appears_in_sexp_tree(CAR(node))
				|| special_argument_appears_in_sexp_tree(CDR(node));

	// cache this result
	if (result)
		Sexp_nodes[node].flags |= SNF_SPECIAL_ARG_IN_TREE;
	else
		Sexp_nodes[node].flags |= SNF_SPECIAL_ARG_NOT_IN_TREE;

	return result;
}

// Goober5000
bool special_argument_appears_in_sexp_list(int node)
{
	// look through list
	while (node != -1)
	{
		// special argument?
		if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE)
			return true;

		node = CDR(node);
	}

	return false;
}

// conditional sexpressions follow

// Goober5000
void eval_when_do_one_exp(int exp)
{	
	arg_item *ptr;
	int do_node;

	switch (get_operator_const(exp))
	{
		// if the op is a conditional then we just evaluate it
		case OP_WHEN:
		case OP_EVERY_TIME:
		case OP_IF_THEN_ELSE:
			// need to account for the possibility this call uses <arguments>
			if (special_argument_appears_in_sexp_tree(exp)) { 
				ptr = Sexp_applicable_argument_list.get_next();
				if (ptr != nullptr) {
					eval_when_for_each_special_argument(exp);
				}
				else {
					eval_sexp(exp);
				}
			}
			else {
				eval_sexp(exp);
			}
			break;

		case OP_DO_FOR_VALID_ARGUMENTS:
			if (special_argument_appears_in_sexp_tree(exp)) { 
				Warning(LOCATION, "<Argument> used within do-for-valid-arguments SEXP. Skipping entire SEXP");
				break; 
			}

			do_node = CDR(exp); 
			while (do_node != -1) {
				do_action_for_each_special_argument(do_node); 
				do_node = CDR(do_node); 
			}
			break;

		case OP_WHEN_ARGUMENT:
		case OP_EVERY_TIME_ARGUMENT:
			eval_sexp(exp);
			break; 

		// otherwise we need to check if arguments are used
		default: 
			// if we're using the special argument in this action
			if (special_argument_appears_in_sexp_tree(exp))
			{
				do_action_for_each_special_argument(exp);			// these sexps eval'd only for side effects
			}
			// if not, just evaluate it once as-is
			else
			{
				// Goober5000 - possible bug? (see when val is used below)
				/*val = */eval_sexp(exp);							// these sexps eval'd only for side effects
			}
	}
}


// Karajorma
void eval_when_do_all_exp(int all_actions, int when_op_num)
{
	arg_item *ptr;
	int exp;
	int actions; 
	int op_num;

	bool first_loop = true;

	// loop through all the supplied arguments
	ptr = Sexp_applicable_argument_list.get_next();

	while (ptr != nullptr)
	{	
		// acquire argument to be used
		Sexp_replacement_arguments.emplace_back(ptr->text, ptr->node);
		actions = all_actions; 

		while (actions != -1)
		{	
			exp = CAR(actions);	

			op_num = get_operator_const(exp);

			if (op_num == OP_DO_FOR_VALID_ARGUMENTS) {
				int do_node = CDR(exp); 
				while (do_node != -1) {
					eval_sexp(do_node); 
					do_node = CDR(do_node); 
				}
			}
			else if ( first_loop || special_argument_appears_in_sexp_tree(exp) ) {
				switch (op_num)
				{
					// if the op is a conditional we have to make sure that it can access arguments
					case OP_WHEN:
					case OP_EVERY_TIME:
					case OP_IF_THEN_ELSE:
						Sexp_current_argument_nesting_level++;
						Sexp_applicable_argument_list.add_data(ptr->text, ptr->node);
						eval_sexp(exp);
						Sexp_applicable_argument_list.clear_nesting_level();
						Sexp_current_argument_nesting_level--;
						break;

					default:
						eval_sexp(exp);
				}
			}
			
			// iterate
			actions = CDR(actions);

			// if-then-else only has one "if" action
			if (when_op_num == OP_IF_THEN_ELSE)
				break;
		}
		
		first_loop = false;

		// remove the argument 
		Sexp_replacement_arguments.pop_back(); 
		// continue along argument list
		ptr = ptr->get_next();
	}
}

int sexp_functional_when_eval_type_lookup(const char *text)
{
	for (int i = 0; i < Num_functional_when_eval_types; i++)
		if (!stricmp(text, Functional_when_eval_type[i]))
			return i;
	return -1;
}

/**
 * This is like using when, but it takes a lot of shortcuts.  It's clearer just to separate it out into its own function, especially since it's not supposed to start
 * a new level of special argument handling, like eval_when would do.  It's a lot like the original retail version of eval_when!
 */
int eval_perform_actions(int n, int op_num)
{
	int return_cond, actions_cond, val = SEXP_NAN, actions;
	bool retval_first;
	Assert( n >= 0 );

	if (op_num == OP_PERFORM_ACTIONS_BOOL_FIRST)
	{
		return_cond = CAR(n);
		retval_first = true;
		actions_cond = Locked_sexp_true;
		actions = CDR(n);
	}
	else if (op_num == OP_PERFORM_ACTIONS_BOOL_LAST)
	{
		return_cond = CAR(n);
		retval_first = false;
		actions_cond = Locked_sexp_true;
		actions = CDR(n);
	}
	else if (op_num == OP_FUNCTIONAL_WHEN)
	{
		return_cond = CAR(n);
		n = CDR(n);

		int eval_type = sexp_functional_when_eval_type_lookup(CTEXT(n));
		if (eval_type == FWET_RETVAL_FIRST)
			retval_first = true;
		else if (eval_type == FWET_RETVAL_LAST)
			retval_first = false;
		else
			return SEXP_NAN;
		n = CDR(n);

		actions_cond = CAR(n);
		n = CDR(n);

		actions = n;
	}
	else if (op_num == OP_ON_MISSION_SKIP)
	{
		retval_first = true;
		actions = n;

		// if the player is skipping the mission, we'll unconditionally perform the actions
		if ((Game_mode & GM_CAMPAIGN_MODE) && (Campaign.current_mission >= 0) && (Campaign.missions[Campaign.current_mission].flags & CMISSION_FLAG_SKIPPED))
		{
			return_cond = Locked_sexp_true;
			actions_cond = Locked_sexp_true;
		}
		// if the player isn't skipping the mission (which is most of the time), we'll return false
		else
		{
			return_cond = Locked_sexp_false;
			actions_cond = Locked_sexp_false;
		}
	}
	else
	{
		UNREACHABLE("Unsupported SEXP %d!", op_num);
		return SEXP_NAN_FOREVER;
	}

	if (retval_first)
	{
		// evaluate the conditional to see what value we eventually return
		val = eval_sexp(return_cond);
	}

	if (is_sexp_true(actions_cond))
	{
		// perform all the actions in the rest of the sexp
		// (Since we are technically inside a boolean condition already, no special argument handling is needed.  The special argument, if any,
		// will have been provided by a higher level of nesting.)
		while (actions != -1)
		{
			// get the operator
			int exp = CAR(actions);
			if (exp != -1)
				eval_sexp(exp);

			// iterate
			actions = CDR(actions);
		}
	}

	if (!retval_first)
	{
		// evaluate the conditional to see what value we now return
		val = eval_sexp(return_cond);
	}

	// return whatever val was, but don't return known-*
	// note: SEXP_KNOWN_TRUE/SEXP_KNOWN_FALSE are never returned from eval_sexp
	return val;
}

void eval_switch(int n)
{
	bool is_nan, is_nan_forever;

	int choice = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever || choice < 0)
		return;
	n = CDR(n);

	// iterate until we land on the action we want to perform
	while (choice > 0 && n >= 0)
	{
		--choice;
		n = CDR(n);
	}

	// out of range?
	if (n < 0)
		return;

	// do it
	int exp = CAR(n);
	if (exp >= 0)
		eval_sexp(exp);
}

/**
 * Evaluates the when conditional
 *
 * @note Goober5000 - added capability for arguments
 * @note Goober5000 - and also if-then-else and perform-actions
 */
int eval_when(int n, int when_op_num)
{
	int arg_handler = -1, cond, val, actions;
	Assert( n >= 0 );
	arg_item *ptr;

	// get the parts of the sexp and evaluate the conditional
	if (is_when_argument_op(when_op_num))
	{
		arg_handler = CAR(n);
		cond = CADR(n);
		actions = CDDR(n);

		Sexp_current_argument_nesting_level++;
		sexp_container_set_special_arg_status(arg_handler, true);
		// evaluate for custom arguments
		val = eval_sexp(arg_handler, cond);
	}
	// normal evaluation
	else
	{
		cond = CAR(n);
		actions = CDR(n);

		// evaluate just as-is
		val = eval_sexp(cond);
	}


	// if value is true, perform the actions in the 'then' part
	if (val == SEXP_TRUE) // note: SEXP_KNOWN_TRUE is never returned from eval_sexp
	{
		// get the operator
		int exp = CAR(actions);

		// if the mod.tbl setting is in effect we want to each evaluate all the SEXPs for 
		// each argument	
		if (True_loop_argument_sexps && special_argument_appears_in_sexp_tree(actions)) {	
			if (exp != -1) {
				eval_when_do_all_exp(actions, when_op_num);
			}
		}
		// without the mod.tbl setting (or if there are no arguments in this SEXP) we loop 
		// through every action performing them for all arguments
		else {
			while (actions != -1)
			{
				// get the operator
				exp = CAR(actions);
				if (exp != -1)
					eval_when_do_one_exp(exp);

				// iterate
				actions = CDR(actions);

				// if-then-else only has one "if" action
				if (when_op_num == OP_IF_THEN_ELSE)
					break;
			}
		}
	}
	// if-then-else has actions to perform under "else"
	else if (val == SEXP_FALSE && when_op_num == OP_IF_THEN_ELSE) // note: SEXP_KNOWN_FALSE is never returned from eval_sexp
	{
		// skip past the "if" action
		actions = CDR(actions);

		// loop through every action
		while (actions != -1)
		{
			// get the operator
			int exp = CAR(actions);
			if (exp != -1)
				eval_when_do_one_exp(exp);

			// iterate
			actions = CDR(actions);
		}

		// invert val so that we behave like a when with opposite results
		// note: SEXP_KNOWN_FALSE is never returned from eval_sexp
		val = SEXP_TRUE;
	}

	if (is_when_argument_op(when_op_num))
	{
		if (Log_event) {	
			ptr = Sexp_applicable_argument_list.get_next();		
			while(ptr != nullptr) {
				// See if we have an argument. 
				Current_event_log_argument_buffer->push_back(ptr->text); 
				ptr = ptr->get_next();
			}
		}	

		// clean up any special sexp stuff
		Sexp_applicable_argument_list.clear_nesting_level();
		Sexp_current_argument_nesting_level--;
		sexp_container_set_special_arg_status(arg_handler, false);
	}

	// thanks to MageKing17 for noticing that we need to short-circuit on the correct node!
	int short_circuit_node = (arg_handler >= 0) ? arg_handler : cond;

	if (Sexp_nodes[short_circuit_node].value == SEXP_KNOWN_FALSE || Sexp_nodes[short_circuit_node].value == SEXP_NAN_FOREVER)
		return SEXP_KNOWN_FALSE;  // no need to waste time on this anymore

	// note: val can't be SEXP_KNOWN_FALSE at this point

	return val;
}

/**
 * Evaluate the conditional
 */
int eval_cond(int n)
{
	int cond = 0, node, val = SEXP_FALSE;

	Assert (n >= 0);
	while (n >= 0)
	{
		node = CAR(n);
		cond = CAR(node);
		val = eval_sexp(cond);

		// if the conditional evaluated to true, then we must evaluate the rest of the expression returning
		// the value of this evaluation
		if (val == SEXP_TRUE) // note: any SEXP_KNOWN_TRUE result is returned as SEXP_TRUE
		{
			int actions, exp;

			val = SEXP_FALSE;
			actions = CDR(node);
			while (actions >= 0)
			{
				exp = CAR(actions);
				if (exp >= -1)
					val = eval_sexp(exp);								// these sexp evaled only for side effects

				actions = CDR(actions);
			}

			break;
		}

		// move onto the next cond clause
		n = CDR(n);
	}

	return val;
}

// Goober5000
// NOTE: if you change this function, check to see if the following function should also be changed!
int test_argument_nodes_for_condition(int n, int condition_node, int *num_true, int *num_false, int *num_known_true, int *num_known_false, int threshold = -1)
{
	int val, num_valid_arguments;
	SCP_vector<std::pair<const char*, int>> Applicable_arguments_temp;
	Assert(n != -1 && condition_node != -1);
	Assert((num_true != nullptr) && (num_false != nullptr) && (num_known_true != nullptr) && (num_known_false != nullptr));

	// ensure special argument list is empty
	Sexp_applicable_argument_list.clear_nesting_level();
	Applicable_arguments_temp.clear();

	// ditto for counters
	num_valid_arguments = 0;
	*num_true = 0;
	*num_false = 0;
	*num_known_true = 0;
	*num_known_false = 0;

	// loop through all arguments
	while (n != -1)
	{
		// only eval this argument if it's valid
		if (Sexp_nodes[n].flags & SNF_ARGUMENT_VALID)
		{
			const int num_args = copy_node_to_replacement_args(n);

			for (int i = 0; i < num_args; ++i) {
				// evaluate conditional for current argument
				val = eval_sexp(condition_node);
				if (Sexp_nodes[condition_node].value == SEXP_KNOWN_TRUE ||
					Sexp_nodes[condition_node].value == SEXP_KNOWN_FALSE) {
					val = Sexp_nodes[condition_node].value;
				} else if (Sexp_nodes[condition_node].value == SEXP_NAN_FOREVER) {
					// In accordance with SEXP_NAN/SEXP_NAN_FOREVER becoming SEXP_FALSE in eval_sexp()
					val = SEXP_KNOWN_FALSE;
				}

				const auto &argument = Sexp_replacement_arguments.back();

				switch (val)
				{
					case SEXP_TRUE:
						(*num_true)++;
						Applicable_arguments_temp.emplace_back(argument.first, argument.second);
						break;

					case SEXP_FALSE:
						(*num_false)++;
						break;

					case SEXP_KNOWN_TRUE:
						(*num_known_true)++;
						Applicable_arguments_temp.emplace_back(argument.first, argument.second);
						break;

					case SEXP_KNOWN_FALSE:
						(*num_known_false)++;
						break;
				}

				// clear argument, but not list, as we'll need it later
				Sexp_replacement_arguments.pop_back();

				// increment
				num_valid_arguments++;
			}
		}

		// continue along argument list
		n = CDR(n);
	}

	// we may only want a subset of the argument list
	if (threshold >= 0 && Applicable_arguments_temp.size() > (size_t)threshold)
		Applicable_arguments_temp.resize(threshold);

	// now we write from the temporary store into the real one, reversing the order. We do this because 
	// Sexp_applicable_argument_list is a stack and we want the first argument in the list to be the first one out
	while (!Applicable_arguments_temp.empty())
	{
		Sexp_applicable_argument_list.add_data(Applicable_arguments_temp.back());
		Applicable_arguments_temp.pop_back();
	}

	return num_valid_arguments;
}

/**
 * Internal parameter type, only used with test_argument_vector_for_condition
 */
enum class STRDUP_STATUS { ALREADY_DUPPED, DUP_NEEDED, DUP_NOT_NEEDED };

// Goober5000
// NOTE: if you change this function, check to see if the previous function should also be changed!
int test_argument_vector_for_condition(const SCP_vector<std::pair<const char*, int>> &argument_vector, STRDUP_STATUS strdup_status, int condition_node, int *num_true, int *num_false, int *num_known_true, int *num_known_false)
{
	int val, num_valid_arguments;
	SCP_vector<std::pair<const char*, int>> Applicable_arguments_temp;
	Assert(condition_node != -1);
	Assert((num_true != nullptr) && (num_false != nullptr) && (num_known_true != nullptr) && (num_known_false != nullptr));

	// ensure special argument list is empty
	Sexp_applicable_argument_list.clear_nesting_level();
	Applicable_arguments_temp.clear();

	// ditto for counters
	num_valid_arguments = 0;
	*num_true = 0;
	*num_false = 0;
	*num_known_true = 0;
	*num_known_false = 0;

	// loop through all arguments
	for (const auto &argument : argument_vector)
	{
		// since we can't see or modify the validity, assume all are valid
		{
			// evaluate conditional for current argument
			Sexp_replacement_arguments.push_back(argument);
			val = eval_sexp(condition_node);
			if ( Sexp_nodes[condition_node].value == SEXP_KNOWN_TRUE ||
					Sexp_nodes[condition_node].value == SEXP_KNOWN_FALSE) {
				val = Sexp_nodes[condition_node].value;
			} else if ( Sexp_nodes[condition_node].value == SEXP_NAN_FOREVER ) {
				// In accordance with SEXP_NAN/SEXP_NAN_FOREVER becoming SEXP_FALSE in eval_sexp()
				val = SEXP_KNOWN_FALSE;
			}

			switch (val)
			{
				case SEXP_TRUE:
					(*num_true)++;
					Applicable_arguments_temp.push_back(argument);
					break;

				case SEXP_FALSE:
					(*num_false)++;
					break;

				case SEXP_KNOWN_TRUE:
					(*num_known_true)++;
					Applicable_arguments_temp.push_back(argument);
					break;

				case SEXP_KNOWN_FALSE:
					(*num_known_false)++;
					break;
			}

			// if the argument was already dup'd, but not added as an applicable argument,
			// we need to free it here before we cause a memory leak
			if ((val == SEXP_FALSE || val == SEXP_KNOWN_FALSE) && (strdup_status == STRDUP_STATUS::ALREADY_DUPPED))
				vm_free(const_cast<char*>(argument.first));

			// clear argument, but not list, as we'll need it later
			Sexp_replacement_arguments.pop_back();

			// increment
			num_valid_arguments++;
		}
	}

	// now we write from the temporary store into the real one, reversing the order. We do this because 
	// Sexp_applicable_argument_list is a stack and we want the first argument in the list to be the first one out
	while (!Applicable_arguments_temp.empty())
	{
		// if we're using a temporary buffer for the string (as opposed to a permanent buffer like shipp->ship_name or Sexp_nodes[n].text)
		// then we need to dup the strings, but we need to know whether the calling function dup'd them, or whether we should dup them here
		if (strdup_status == STRDUP_STATUS::ALREADY_DUPPED)
			Sexp_applicable_argument_list.add_data_set_dup(Applicable_arguments_temp.back());
		else if (strdup_status == STRDUP_STATUS::DUP_NEEDED)
			Sexp_applicable_argument_list.add_data_dup(Applicable_arguments_temp.back());
		else
			Sexp_applicable_argument_list.add_data(Applicable_arguments_temp.back());

		Applicable_arguments_temp.pop_back(); 
	}

	return num_valid_arguments;
}

// Goober5000
int eval_any_of(int arg_handler_node, int condition_node)
{
	int n, num_valid_arguments, num_true, num_false, num_known_true, num_known_false;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	// the arguments should just be data, not operators, so we can skip the CAR
	n = CDR(arg_handler_node);

	// test the whole argument list
	num_valid_arguments = test_argument_nodes_for_condition(n, condition_node, &num_true, &num_false, &num_known_true, &num_known_false);

	// use the sexp_or algorithm
	if (num_known_true || num_true)
		return SEXP_TRUE;
	else if (num_known_false == num_valid_arguments)
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

// Goober5000
int eval_every_of(int arg_handler_node, int condition_node)
{
	int n, num_valid_arguments, num_true, num_false, num_known_true, num_known_false;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	// the arguments should just be data, not operators, so we can skip the CAR
	n = CDR(arg_handler_node);

	// test the whole argument list
	num_valid_arguments = test_argument_nodes_for_condition(n, condition_node, &num_true, &num_false, &num_known_true, &num_known_false);

	// use the sexp_and algorithm
	if ((!num_valid_arguments) || num_known_false)
		return SEXP_KNOWN_FALSE;
	else if (num_false)
		return SEXP_FALSE;
	else
		return SEXP_TRUE;
}

// Goober5000
int eval_number_of(int arg_handler_node, int condition_node)
{
	bool is_nan, is_nan_forever;
	int n, num_valid_arguments, num_true, num_false, num_known_true, num_known_false, threshold;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	// the arguments should just be data, not operators, so we can skip the CAR
	n = CDR(arg_handler_node);

	// the first argument is the number threshold
	threshold = eval_num(n, is_nan, is_nan_forever);
	n = CDR(n);

	// test the whole argument list
	num_valid_arguments = test_argument_nodes_for_condition(n, condition_node, &num_true, &num_false, &num_known_true, &num_known_false);

	// we check for NaN after the conditions are evaluated, just as the logical operators evaluate all their conditions
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	// use the sexp_or algorithm, modified
	// (true if at least threshold arguments are true)
	if (num_true + num_known_true >= threshold)
		return SEXP_TRUE;
	else if (num_valid_arguments - num_known_false < threshold)
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

// Goober5000
// this works a little differently... we randomly pick one argument to use
// for our condition, but this argument must be saved among sexp calls...
// so we select an argument and set its flag
int eval_random_of(int arg_handler_node, int condition_node)
{
	int n = -1, i, val, num_valid_args, random_argument, num_known_false = 0;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	// get the number of valid arguments
	num_valid_args = query_sexp_args_count(arg_handler_node, true);
	Assert(num_valid_args >= 0);

	if (num_valid_args == 0)
	{
		return SEXP_KNOWN_FALSE;	// Not much point in trying to evaluate it again.
	}

	// find which argument we picked, if we picked one
	n = CDR(arg_handler_node);

	// iterate to the argument we previously selected
	for ( ; n != -1; n = CDR(n))
	{
		if (Sexp_nodes[n].flags & SNF_ARGUMENT_SELECT)
			break;
	}

	// if argument not found (or never specified in the first place), we have to pick one
	if (n == -1)
	{
		n = CDR(arg_handler_node);
		int temp_node = n;

		// pick an argument and iterate to it
		random_argument = rand_internal(1, num_valid_args);
		i = 0;
		for (int j = 0; j < num_valid_args; temp_node = CDR(temp_node))
		{
			Assert(n >= 0);

			// count only valid arguments
			if (Sexp_nodes[temp_node].flags & SNF_ARGUMENT_VALID) {
				j++;
				if (i < random_argument && (++i == random_argument)) {
					// Found the node we want, store it for use
					n = temp_node;
				}

				if ((Sexp_nodes[temp_node].value == SEXP_KNOWN_FALSE) || (Sexp_nodes[temp_node].value == SEXP_NAN_FOREVER))
					num_known_false++;
			}
		}

		if (num_known_false == num_valid_args) {
			return SEXP_KNOWN_FALSE;	// We're going nowhere fast.
		}

		// save it
		Sexp_nodes[n].flags |= SNF_ARGUMENT_SELECT;
	}

	// only eval this argument if it's valid
	val = SEXP_FALSE;
	if (Sexp_nodes[n].flags & SNF_ARGUMENT_VALID)
	{
		// ensure special argument list is empty
		Sexp_applicable_argument_list.clear_nesting_level();

		// evaluate conditional for current argument
		Sexp_replacement_arguments.emplace_back(Sexp_nodes[n].text, n);
		val = eval_sexp(condition_node);

		// true?
		if (val == SEXP_TRUE)
		{
			Sexp_applicable_argument_list.add_data(Sexp_nodes[n].text, n);
		}
		else if (Sexp_nodes[condition_node].value == SEXP_KNOWN_FALSE || Sexp_nodes[condition_node].value == SEXP_NAN_FOREVER)
		{
			val = SEXP_KNOWN_FALSE; // If we can't randomly pick another one and this one is guaranteed never to be true, then give up now.
		}

		// clear argument, but not list, as we'll need it later
		Sexp_replacement_arguments.pop_back();
	}

	// true if our selected argument is true
	return val;
}

// originally part of eval_random_of() but pulled out as the code paths diverged
int eval_random_multiple_of(int arg_handler_node, int condition_node)
{
	Assertion(arg_handler_node != -1, "No argument handler provided to random-multiple-of. Please report!");
	Assertion(condition_node != -1, "No condition provided to random-multiple-of. Please report!");

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	// get the number of valid arguments
	SCP_vector<int> cumulative_arg_counts;

	const int num_valid_arg_nodes =
		sexp_container_query_sexp_args_count(arg_handler_node, cumulative_arg_counts, true);
	Assertion(num_valid_arg_nodes >= 0,
		"random-multiple-of found invalid number of valid arguments (%d). please report!",
		num_valid_arg_nodes);

	if (num_valid_arg_nodes == 0)
	{
		return SEXP_KNOWN_FALSE;	// Not much point in trying to evaluate it again.
	}

	Assertion(!cumulative_arg_counts.empty(),
		"Attempt to count arguments for random-multiple-of SEXP failed. Please report!");
	const int num_arg_values = cumulative_arg_counts.back();
	Assertion(num_arg_values >= 0,
		"random-multiple-of found invalid number of valid argument values (%d). please report!",
		num_arg_values);
	if (num_arg_values == 0)
	{
		// valid args are one or more empty containers, whose data could change later
		return SEXP_FALSE;
	}

	int n = CDR(arg_handler_node);
	Assertion(n >= 0, "No arguments provided to random-multiple-of. Please report!");
	int temp_node = n;

	// pick an argument and iterate to it
	const int random_arg_value = rand_internal(1, num_arg_values);
	int random_argument = 0;
	// skip the first entry, which is the initial zero value
	for (int k = 1, size = (int)cumulative_arg_counts.size(); k < size; ++k) {
		if (random_arg_value <= cumulative_arg_counts[k]) {
			random_argument = k;
			break;
		}
	}
	Assertion(random_argument > 0,
		"Attempt to find randomly selected argument in random-multiple-of failed. Please report!");

	int i = 0;
	int num_known_false = 0;
	bool valid_container_arg_found = false;
	for (int j = 0; j < num_valid_arg_nodes; temp_node = CDR(temp_node))
	{
		// count only valid arguments
		if (Sexp_nodes[temp_node].flags & SNF_ARGUMENT_VALID) {
			j++;

			if (Sexp_nodes[temp_node].subtype == SEXP_ATOM_CONTAINER_NAME) {
				valid_container_arg_found = true;
			}
			if (i < random_argument && (++i == random_argument)) {
				// Found the node we want, store it for use
				n = temp_node;
			}

			if ((Sexp_nodes[temp_node].value == SEXP_KNOWN_FALSE) || (Sexp_nodes[temp_node].value == SEXP_NAN_FOREVER))
				num_known_false++;
		}
	}

	if (num_known_false == num_valid_arg_nodes) {
		return SEXP_KNOWN_FALSE;	// We're going nowhere fast.
	}

	// only eval this argument if it's valid
	int val = SEXP_FALSE;
	if (Sexp_nodes[n].flags & SNF_ARGUMENT_VALID)
	{
		// ensure special argument list is empty
		Sexp_applicable_argument_list.clear_nesting_level();

		const int arg_value_index = (Sexp_nodes[n].subtype == SEXP_ATOM_CONTAINER_NAME)
										? random_arg_value - cumulative_arg_counts[random_argument - 1] - 1
										: -1;
		copy_node_to_replacement_args(n, arg_value_index);

		// evaluate conditional for current argument
		val = eval_sexp(condition_node);

		// true?
		if (val == SEXP_TRUE)
		{
			const char *arg_text = Sexp_replacement_arguments.back().first;
			Sexp_applicable_argument_list.add_data(arg_text, n);
		} else if ((num_valid_arg_nodes == 1) && !valid_container_arg_found &&
				   (Sexp_nodes[condition_node].value == SEXP_KNOWN_FALSE ||
					   Sexp_nodes[condition_node].value == SEXP_NAN_FOREVER))
		{
			val = SEXP_KNOWN_FALSE; // If the only valid arg is guaranteed never to be true, then give up now.
		}

		// clear argument, but not list, as we'll need it later
		Sexp_replacement_arguments.pop_back();
	}

	// true if our selected argument is true
	return val;
}

// Karajorma - this conditional returns the first valid option on its list. 
int eval_in_sequence(int arg_handler_node, int condition_node)
{
	int val = SEXP_FALSE;
	int n = -1 ;
	
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	// get the first argument
	n = CDR(arg_handler_node);
	Assert (n != -1);

	// loop through the nodes until we find one that is holds a valid argument or run out of nodes
	for (int i=1 ; i<query_sexp_args_count(arg_handler_node) ; i++)
	{
		if (!(Sexp_nodes[n].flags & SNF_ARGUMENT_VALID)) {
			n = CDR(n) ;
		}
		// if we've found a valid node there is no need to continue
		else {
			break; 
		}
	}

	// Only execute if the argument is valid (if all nodes were invalid we would still reach this point)
	if (Sexp_nodes[n].flags & SNF_ARGUMENT_VALID)
	{
		// ensure special argument list is empty
		Sexp_applicable_argument_list.clear_nesting_level();

		// evaluate conditional for current argument
		Sexp_replacement_arguments.emplace_back(Sexp_nodes[n].text, n);
		val = eval_sexp(condition_node);

		// true?
		if (val == SEXP_TRUE)
		{
			Sexp_applicable_argument_list.add_data(Sexp_nodes[n].text, n);
		}
		else if ((Sexp_nodes[condition_node].value == SEXP_KNOWN_FALSE) || (Sexp_nodes[condition_node].value == SEXP_NAN_FOREVER))
		{
			val = SEXP_KNOWN_FALSE;	// If we're wasting our time evaluating this ever again, just go ahead and short-circuit.
		}

		// clear argument, but not list, as we'll need it later
		Sexp_replacement_arguments.pop_back();
	}

	// return the value of the conditional
	return val;
}

// Goober5000
int eval_for_counter(int arg_handler_node, int condition_node, bool just_count = false)
{
	bool is_nan, is_nan_forever;
	int n, num_valid_arguments = 0, num_true, num_false, num_known_true, num_known_false;
	int i, count, counter_start, counter_stop, counter_step;
	SCP_vector<std::pair<const char*, int>> argument_vector;
	char buf[NAME_LENGTH];
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	n = CDR(arg_handler_node);

	// determine the counter parameters
	count = eval_nums(n, is_nan, is_nan_forever, counter_start, counter_stop, counter_step);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	if (count < 3)
		counter_step = 1;

	// a bunch of error checking
	if (counter_step == 0)
	{
		Warning(LOCATION, "A counter increment of 0 is illegal!  (start=%d, stop=%d, increment=%d)", counter_start, counter_stop, counter_step);
		return SEXP_KNOWN_FALSE;
	}
	else if (counter_start == counter_stop)
	{
		Warning(LOCATION, "The counter start and stop values are identical!  (start=%d, stop=%d, increment=%d)", counter_start, counter_stop, counter_step);
		return SEXP_KNOWN_FALSE;
	}
	else if (sign(counter_stop - counter_start) != sign(counter_step))
	{
		Warning(LOCATION, "The counter cannot complete with the given values!  (start=%d, stop=%d, increment=%d)", counter_start, counter_stop, counter_step);
		return SEXP_KNOWN_FALSE;
	}

	// build a vector of counter values
	for (i = counter_start; ((counter_step > 0) ? i <= counter_stop : i >= counter_stop); i += counter_step)
	{
		// if we're just counting we should just avoid the memory management problem mentioned below,
		// since we return before calling test_argument_vector_for_condition
		if (just_count)
			num_valid_arguments++;
		else
		{
			sprintf(buf, "%d", i);
			argument_vector.emplace_back(vm_strdup(buf), -1);
			// Note: we do not call vm_free() on the contents of argument_vector, and we don't for the very good
			// reason that those pointers are then passed along by test_argument_vector_for_condition(), below.
			// The strings will then be freed by arg_item::expunge() or arg_item::clear_nesting_level(), or even
			// inside test_argument_vector_for_condition() (if the argument doesn't satisfy the condition). So
			// under no circumstances try to free these strings inside this function! It will cause a double-free
			// situation, resulting in a crash at best. -MageKing17
		}
	}

	if (just_count)
		return num_valid_arguments;

	// test the whole argument vector
	num_valid_arguments = test_argument_vector_for_condition(argument_vector, STRDUP_STATUS::ALREADY_DUPPED, condition_node, &num_true, &num_false, &num_known_true, &num_known_false);

	// use the sexp_or algorithm
	if (num_known_true || num_true)
		return SEXP_TRUE;
	else if (num_known_false == num_valid_arguments)
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

// Goober5000
int eval_for_ship_collection(int arg_handler_node, int condition_node, int op_const, bool just_count = false)
{
	int n, num_valid_arguments = 0, num_true, num_false, num_known_true, num_known_false;
	SCP_vector<std::pair<const char*, int>> argument_vector;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	n = CDR(arg_handler_node);

	// handle each parameter
	for (; n >= 0; n = CDR(n))
	{
		auto constraint = CTEXT(n);
		int constraint_index = -1;

		switch (op_const)
		{
			case OP_FOR_SHIP_CLASS:
				constraint_index = ship_info_lookup(constraint);
				break;
			case OP_FOR_SHIP_TYPE:
				constraint_index = ship_type_name_lookup(constraint);
				if (constraint_index < 0 && Fighter_bomber_valid && !stricmp(constraint, Fighter_bomber_type_name))
					constraint_index = Ship_type_fighter_bomber;
				break;
			case OP_FOR_SHIP_TEAM:
				constraint_index = iff_lookup(constraint);
				break;
			case OP_FOR_SHIP_SPECIES:
				constraint_index = species_info_lookup(constraint);
				break;
		}

		if (constraint_index < 0)
			continue;

		// add all ships of this constraint which are present in the mission
		for (auto so: list_range(&Ship_obj_list))
		{
			auto objp = &Objects[so->objnum];
			if (objp->flags[Object::Object_Flags::Should_be_dead])
				continue;

			auto shipp = &Ships[objp->instance];
			int ship_index = -1;

			switch (op_const)
			{
				case OP_FOR_SHIP_CLASS:
					ship_index = shipp->ship_info_index;
					break;
				case OP_FOR_SHIP_TYPE:
					ship_index = ship_class_query_general_type(shipp->ship_info_index);
					break;
				case OP_FOR_SHIP_TEAM:
					ship_index = shipp->team;
					break;
				case OP_FOR_SHIP_SPECIES:
					ship_index = Ship_info[shipp->ship_info_index].species;
					break;
			}

			bool constraint_matches;
			if (op_const == OP_FOR_SHIP_TYPE && constraint_index == Ship_type_fighter_bomber)
				constraint_matches = (ship_index == Ship_type_fighter || ship_index == Ship_type_bomber);
			else
				constraint_matches = (ship_index == constraint_index);

			if (constraint_matches)
			{
				if (just_count)
					num_valid_arguments++;
				else
					argument_vector.emplace_back(shipp->ship_name, -1);
			}
		}
	}

	if (just_count)
		return num_valid_arguments;

	// test the whole argument vector
	num_valid_arguments = test_argument_vector_for_condition(argument_vector, STRDUP_STATUS::DUP_NOT_NEEDED, condition_node, &num_true, &num_false, &num_known_true, &num_known_false);
	SCP_UNUSED(num_valid_arguments);

	// use the sexp_or algorithm
	if (num_known_true || num_true)
		return SEXP_TRUE;
// Don't short-circuit because ships can arrive later on, and this sexp's ship collection is intrinsically unknowable.
//	else if (num_known_false == num_valid_arguments)
//		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

// Goober5000
int eval_for_subsystems(int arg_handler_node, int condition_node, bool just_count = false)
{
	int n, num_valid_arguments = 0, num_true, num_false, num_known_true, num_known_false;
	SCP_vector<std::pair<const char*, int>> argument_vector;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	n = CDR(arg_handler_node);

	// get the ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_FALSE;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_KNOWN_FALSE;
	n = CDR(n);

	bool subsystem_type_specified = (n >= 0);
	SCP_unordered_set<int> subsystem_types;

	// get all the types we need to search for
	for (; n >= 0; n = CDR(n))
	{
		auto type_index = string_lookup(CTEXT(n), Subsystem_types, SUBSYSTEM_MAX);
		if (type_index >= 0)
			subsystem_types.insert(type_index);
	}

	// go through all subsystems and add the relevant ones
	for (auto ss : list_range(&ship_entry->shipp()->subsys_list))
	{
		if (!subsystem_type_specified || subsystem_types.find(ss->system_info->type) != subsystem_types.end())
		{
			if (just_count)
				num_valid_arguments++;
			else
				argument_vector.emplace_back(ss->system_info->subobj_name, -1);
		}
	}

	if (just_count)
		return num_valid_arguments;

	// test the whole argument vector
	num_valid_arguments = test_argument_vector_for_condition(argument_vector, STRDUP_STATUS::DUP_NOT_NEEDED, condition_node, &num_true, &num_false, &num_known_true, &num_known_false);
	SCP_UNUSED(num_valid_arguments);

	// use the sexp_or algorithm
	if (num_known_true || num_true)
		return SEXP_TRUE;
	// ok to short-circuit here because a ship's subsystem collection is known and fixed
	else if (num_known_false == num_valid_arguments)
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

// jg18
int eval_for_container(int arg_handler_node, int condition_node, int op_const, bool just_count = false)
{
	Assertion(arg_handler_node != -1,
		"Attempt to use invalid argument handler with a for-container SEXP (%d). Please report!",
		op_const);
	Assertion(condition_node != -1,
		"Attempt to use invalid condition with a for-container SEXP (%d). Please report!",
		op_const);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	int num_arguments = 0;
	SCP_vector<std::pair<const char*, int>> argument_vector;

	const int arg_node = CDR(arg_handler_node);

	switch (op_const)
	{
		case OP_FOR_CONTAINER_DATA:
			num_arguments = sexp_container_collect_data_arguments(arg_node, argument_vector, just_count);
			break;

		case OP_FOR_MAP_CONTAINER_KEYS:
			num_arguments = sexp_container_collect_map_key_arguments(arg_node, argument_vector, just_count);
			break;

		default:
			UNREACHABLE("Unhandled for-container SEXP (%d). Please report!", op_const);
			break;
	}

	if (just_count) {
		// cleaning up argument_vector isn't needed
		// because the "collect arguments" functions didn't touch it
		return num_arguments;
	}

	// test the whole argument vector
	int num_true, num_false, num_known_true, num_known_false;
	const int num_valid_arguments = test_argument_vector_for_condition(argument_vector,
		STRDUP_STATUS::ALREADY_DUPPED,
		condition_node,
		&num_true,
		&num_false,
		&num_known_true,
		&num_known_false);
	SCP_UNUSED(num_valid_arguments);

	// use the sexp_or algorithm
	if (num_known_true || num_true) {
		return SEXP_TRUE;
	// Don't short-circuit because container data/keys can change later on,
	// and this sexp's container is intrinsically unknowable.
	//} else if (num_valid_arguments > 0 && num_known_false == num_valid_arguments) {
	//	return SEXP_KNOWN_FALSE;
	} else {
		return SEXP_FALSE;
	}
}

// Goober5000
int eval_for_players(int arg_handler_node, int condition_node, bool just_count = false)
{
	int num_valid_arguments = 0, num_true, num_false, num_known_true, num_known_false;
	SCP_vector<std::pair<const char*, int>> argument_vector;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	if (Game_mode & GM_MULTIPLAYER)
	{
		// this operator has no parameters, so just generate a vector of players
		for (int i = 0; i < MAX_PLAYERS; ++i)
		{
			int shipnum = multi_get_player_ship(i);
			if (shipnum >= 0)
			{
				if (just_count)
					num_valid_arguments++;
				else
					argument_vector.emplace_back(vm_strdup(Ships[shipnum].ship_name), -1);
			}
		}
	}
	else
	{
		// in single-player it's just one ship
		if (Player_ship)
		{
			if (just_count)
				num_valid_arguments++;
			else
				argument_vector.emplace_back(Player_ship->ship_name, -1);
		}
	}

	if (just_count)
		return num_valid_arguments;

	// test the whole argument vector
	num_valid_arguments = test_argument_vector_for_condition(argument_vector, STRDUP_STATUS::DUP_NOT_NEEDED, condition_node, &num_true, &num_false, &num_known_true, &num_known_false);

	// use the sexp_or algorithm
	if (num_known_true || num_true)
		return SEXP_TRUE;
	else if (num_valid_arguments > 0 && num_known_false == num_valid_arguments)
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

// MageKing17 - this is sort of a cross between any-of, number-of, and in-sequence
int eval_first_of(int arg_handler_node, int condition_node)
{
	bool is_nan, is_nan_forever;
	int n, num_valid_arguments, num_true, num_false, num_known_true, num_known_false, threshold;
	Assert(arg_handler_node > -1 && condition_node > -1);

	// Don't use invalid indexes - Coverity 1523820
	if(arg_handler_node < 0 || condition_node < 0)
		return SEXP_FALSE;

	// the arguments should just be data, not operators, so we can skip the CAR
	n = CDR(arg_handler_node);

	// the first argument is the number threshold
	threshold = eval_num(n, is_nan, is_nan_forever);
	n = CDR(n);

	// test the whole argument list
	num_valid_arguments = test_argument_nodes_for_condition(n, condition_node, &num_true, &num_false, &num_known_true, &num_known_false, threshold);

	// we check for NaN after the conditions are evaluated, just as the logical operators evaluate all their conditions
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	// use the sexp_or algorithm
	if (num_known_true || num_true)
		return SEXP_TRUE;
	else if (num_known_false == num_valid_arguments)
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_FALSE;
}

void sexp_change_all_argument_validity(int n, bool invalidate)
{
	int arg_handler, arg_n;

	arg_handler = find_argument_provider(n);

	// prevent a crash if the SEXP is used somewhere it's not supposed to be
	if (arg_handler < 0)
		return;

	// can't change validity of for-* sexps
	auto op_const = get_operator_const(arg_handler);
	if (is_implicit_argument_provider_op(op_const))
		return;
		
	while (n != -1)
	{
		arg_n = CDR(arg_handler);
		while (arg_n != -1) {
			if (invalidate) {
				Sexp_nodes[arg_n].flags &= ~SNF_ARGUMENT_VALID;
			}
			else {
				Sexp_nodes[arg_n].flags |= SNF_ARGUMENT_VALID;
			}
			// iterate
			arg_n = CDR(arg_n);
		}
		
		// iterate
		n = CDR(n);
	}
}

int sexp_num_valid_arguments( int n )
{
	int arg_handler, arg_n;
	int matches = 0;

	arg_handler = find_argument_provider(n);

	// prevent a crash if the SEXP is used somewhere it's not supposed to be
	if (arg_handler < 0)
		return 0;

	// the for-* sexps require special handling: they don't list their arguments explicitly but rather generate them on-the-fly
	auto op_const = get_operator_const(arg_handler);
	switch (op_const)
	{
		case OP_FOR_COUNTER:
			return eval_for_counter(arg_handler, Locked_sexp_true, true);

		case OP_FOR_PLAYERS:
			return eval_for_players(arg_handler, Locked_sexp_true, true);

		case OP_FOR_SHIP_CLASS:
		case OP_FOR_SHIP_TYPE:
		case OP_FOR_SHIP_TEAM:
		case OP_FOR_SHIP_SPECIES:
			return eval_for_ship_collection(arg_handler, Locked_sexp_true, op_const, true);

		case OP_FOR_SUBSYSTEMS:
			return eval_for_subsystems(arg_handler, Locked_sexp_true, true);

		case OP_FOR_CONTAINER_DATA:
		case OP_FOR_MAP_CONTAINER_KEYS:
			return eval_for_container(arg_handler, Locked_sexp_true, op_const, true);
	}

	// loop through arguments
	arg_n = CDR(arg_handler);
	while (arg_n != -1) {
		if (Sexp_nodes[arg_n].flags & SNF_ARGUMENT_VALID) {
			matches++;
		}

		// iterate
		arg_n = CDR(arg_n);
	}

	return matches;
}

// Goober5000
void sexp_change_argument_validity(int n, bool invalidate)
{
	int arg_handler, arg_n;
	bool toggled;

	arg_handler = find_argument_provider(n);

	// prevent a crash if the SEXP is used somewhere it's not supposed to be
	// (thanks to woutersmits for finding this bug)
	if (arg_handler < 0)
		return;

	// can't change validity of for-* sexps
	auto op_const = get_operator_const(arg_handler);
	if (is_implicit_argument_provider_op(op_const))
		return;
		
	// loop through arguments
	while (n != -1)
	{
		toggled = false; 

		// first we must check if the arg_handler marks a selection. At the moment random-of is the only one that does this
		arg_n = CDR(arg_handler);
		while (invalidate && (arg_n != -1)) {
			Assertion(Sexp_nodes[arg_n].subtype != SEXP_ATOM_CONTAINER_NAME,
				"Attempt to use invalidate-argument with container %s. Please report!",
				Sexp_nodes[arg_n].text);
			Assertion(Sexp_nodes[arg_n].subtype != SEXP_ATOM_CONTAINER_DATA,
				"Attempt to use invalidate-argument with data from container %s. Please report!",
				Sexp_nodes[arg_n].text);

			if (Sexp_nodes[arg_n].flags & SNF_ARGUMENT_SELECT) {
				// now check if the selected argument matches the one we want to invalidate
				if (!strcmp(CTEXT(n), CTEXT(arg_n))) {
					// set it as invalid
					Sexp_nodes[arg_n].flags &= ~SNF_ARGUMENT_VALID;
					toggled = true; 
				}
			}

			// iterate
			arg_n = CDR(arg_n);
		}
		
		if (!toggled) {
			// search for argument in arg_handler list
			arg_n = CDR(arg_handler);
			while (arg_n != -1)
			{
				Assertion(Sexp_nodes[arg_n].subtype != SEXP_ATOM_CONTAINER_NAME,
					"Attempt to change argument validity of container %s. Please report!",
					Sexp_nodes[arg_n].text);
				Assertion(Sexp_nodes[arg_n].subtype != SEXP_ATOM_CONTAINER_DATA,
					"Attempt to change argument validity of data from container %s. Please report!",
					Sexp_nodes[arg_n].text);

				// match?
				if (!strcmp(CTEXT(n), CTEXT(arg_n)))
				{
					if (invalidate) {
						// we need to check if the argument is already invalid as some argument lists may contain duplicates
						if (Sexp_nodes[arg_n].flags & SNF_ARGUMENT_VALID) {
							// set it as invalid
							Sexp_nodes[arg_n].flags &= ~SNF_ARGUMENT_VALID;

							// exit inner loop
							break;
						}
					}
					else {
						if (!(Sexp_nodes[arg_n].flags & SNF_ARGUMENT_VALID)) {
							// set it as valid
							Sexp_nodes[arg_n].flags |= SNF_ARGUMENT_VALID;

							// exit inner loop
							break;
						}
					}
				}

				// iterate
				arg_n = CDR(arg_n);
			}
		}

		// iterate
		n = CDR(n);
	}
}

/**
 * Given any SEXP node, return the node of the argument provider operator used in the enclosing when-argument-type SEXP, or -1 if not found.
 */
int find_argument_provider(int node)
{
	int conditional, arg_handler;

	if (node < 0) {
		return -1;
	}

	conditional = node;
	do {
		// find the conditional sexp
		conditional = find_parent_operator(conditional);
		if (conditional == -1) {
			return -1;
		}
	}
	while (!is_when_argument_op(get_operator_const(conditional)));

	// get the first op of the parent, which should be a *_of operator
	arg_handler = CADR(conditional);
	if (arg_handler < 0 || !is_argument_provider_op(get_operator_const(arg_handler))) {
		return -1;
	}

	return arg_handler;
}

/**
 * Checks whether this operator is when-argument or every-time-argument.  (Any future similar operators should be added here too.)
 */
bool is_when_argument_op(const int op_const)
{
	switch (op_const)
	{
		case OP_WHEN_ARGUMENT:
		case OP_EVERY_TIME_ARGUMENT:
			return true;

		default:
			return false;
	}
}

/**
 * Checks whether this operator can provide values (whether explicitly or implicitly) to a when-argument-type operator.
 */
bool is_argument_provider_op(const int op_const)
{
	// implicit operators are covered by their own function
	if (is_implicit_argument_provider_op(op_const)) {
		return true;
	}

	// these operators provide values by explicitly listing them
	switch (op_const)
	{
		case OP_ANY_OF:
		case OP_EVERY_OF:
		case OP_NUMBER_OF:
		case OP_RANDOM_OF:
		case OP_RANDOM_MULTIPLE_OF:
		case OP_IN_SEQUENCE:
		case OP_FIRST_OF:
			return true;

		default:
			return false;
	}
}

/**
 * Checks whether this operator provides values implicitly, that is, by generating them rather than listing them in the SEXP.
 */
bool is_implicit_argument_provider_op(const int op_const)
{
	switch (op_const)
	{
		case OP_FOR_COUNTER:
		case OP_FOR_SHIP_CLASS:
		case OP_FOR_SHIP_TYPE:
		case OP_FOR_SHIP_TEAM:
		case OP_FOR_SHIP_SPECIES:
		case OP_FOR_PLAYERS:
		case OP_FOR_SUBSYSTEMS:
		case OP_FOR_CONTAINER_DATA:
		case OP_FOR_MAP_CONTAINER_KEYS:
			return true;

		default:
			return false;
	}
}

// Goober5000
int sexp_functional_if_then_else(int node)
{
	int num1, num2, n = node;
	bool is_nan1, is_nan_forever1, is_nan2, is_nan_forever2;

	Assertion(CAR(n) >= 0, "The condition in functional-if-then-else must be an operator!");

	// decision time
	int condition = eval_sexp(CAR(n));
	n = CDR(n);

	// we need to evaluate both numbers regardless of which one we pick
	num1 = eval_num(n, is_nan1, is_nan_forever1);
	n = CDR(n);
	num2 = eval_num(n, is_nan2, is_nan_forever2);

	// pick one
	if (condition == SEXP_TRUE || condition == SEXP_KNOWN_TRUE)
	{
		if (is_nan1)
			return SEXP_NAN;
		else if (is_nan_forever1)
			return SEXP_NAN_FOREVER;
		else
			return num1;
	}
	else
	{
		if (is_nan2)
			return SEXP_NAN;
		else if (is_nan_forever2)
			return SEXP_NAN_FOREVER;
		else
			return num2;
	}
}

// Goober5000
int sexp_functional_switch(int node)
{
	bool is_nan, is_nan_forever;
	int n = node, result = 0;

	int choice = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever || choice < 0)
		return 0;
	n = CDR(n);

	// evaluate all the numbers, but only keep the one we want
	while (n >= 0)
	{
		int temp = eval_num(n, is_nan, is_nan_forever);

		if (choice == 0)
		{
			if (is_nan)
				result = SEXP_NAN;
			else if (is_nan_forever)
				result = SEXP_NAN_FOREVER;
			else
				result = temp;
		}

		--choice;
		n = CDR(n);
	}

	return result;
}

// Goober500
int sexp_is_true_for_duration(int op_node, int node)
{
	// find our start time using the duration slot, creating one if necessary
	if (Sexp_nodes[op_node].duration_index < 0)
	{
		Sexp_nodes[op_node].duration_index = static_cast<int>(Sexp_is_true_for_duration_times.size());
		Sexp_is_true_for_duration_times.push_back(Missiontime);
	}
	fix& start_time = Sexp_is_true_for_duration_times[Sexp_nodes[op_node].duration_index];

	// get the duration we want
	bool is_nan, is_nan_forever;
	int duration = eval_num(node, is_nan, is_nan_forever);
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	if (is_nan || duration < 0)
		return SEXP_FALSE;

	// see if all the conditions are true
	bool all_true = true;
	for (int n = CDR(node); n >= 0; n = CDR(n))
	{
		if (!is_sexp_true(n))
			all_true = false;
		// don't break because we want to evaluate all the conditions -- see sexp_and()
	}

	// reset the clock?
	if (!all_true)
	{
		start_time = Missiontime;
		return SEXP_FALSE;
	}

	// figure out the elapsed delta in milliseconds
	int elapsed_delta = fl2i(f2fl(Missiontime - start_time) * MILLISECONDS_PER_SECOND);

	// compare to the duration
	if (elapsed_delta >= duration)
		return SEXP_TRUE;
	else
		return SEXP_FALSE;
}

// Goober5000 - added wing capability
int sexp_is_iff_or_species(int n, bool iff)
{
	int i, value;

	// iff/species value is the first parameter, second is a list of one or more ships/wings to check to see if the value matches
	if (iff)
		value = iff_lookup(CTEXT(n));
	else
		value = species_info_lookup(CTEXT(n));
	n = CDR(n);

	for ( ; n != -1; n = CDR(n) )
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		switch (oswpt.type)
		{
			case OSWPT_TYPE_SHIP:
			{
				// if the field doesn't match the value specified, return false immediately
				if (iff)
				{
					if (oswpt.shipp()->team != value)
						return SEXP_FALSE;
				}
				else
				{
					if (Ship_info[oswpt.shipp()->ship_info_index].species != value)
						return SEXP_FALSE;
				}

				break;
			}

			case OSWPT_TYPE_PARSE_OBJECT:
			{
				// if the field doesn't match the value specified, return false immediately
				if (iff)
				{
					if (oswpt.p_objp()->team != value)
						return SEXP_FALSE;
				}
				else
				{
					if (Ship_info[oswpt.p_objp()->ship_class].species != value)
						return SEXP_FALSE;
				}

				break;
			}

			case OSWPT_TYPE_WING:
			{
				for (i = 0; i < oswpt.wingp()->current_count; i++)
				{
					// if the field doesn't match the value specified, return false immediately
					if (iff)
					{
						if (Ships[oswpt.wingp()->ship_index[i]].team != value)
							return SEXP_FALSE;
					}
					else
					{
						if (Ship_info[Ships[oswpt.wingp()->ship_index[i]].ship_info_index].species != value)
							return SEXP_FALSE;
					}
				}

				break;
			}

			case OSWPT_TYPE_WING_NOT_PRESENT:
			{
				bool at_least_one = false;
				for (const auto& pobj : Parse_objects)
				{
					if (pobj.wingnum == oswpt.wingnum)
					{
						at_least_one = true;

						// if the field doesn't match the value specified, return false immediately
						if (iff)
						{
							if (pobj.team != value)
								return SEXP_FALSE;
						}
						else
						{
							if (Ship_info[pobj.ship_class].species != value)
								return SEXP_FALSE;
						}
					}
				}
				if (!at_least_one)
					return SEXP_FALSE;

				break;
			}

			case OSWPT_TYPE_EXITED:
			{
				// see if we can find information about the exited ship (if it is a ship)
				if (oswpt.has_ship_entry())
				{
					// ship is properly exited
					if (oswpt.ship_entry()->exited_index >= 0)
					{
						// if the field doesn't match the value specified, return false immediately
						if (iff)
						{
							if (Ships_exited[oswpt.ship_entry()->exited_index].team != value)
								return SEXP_KNOWN_FALSE;
						}
						else
						{
							if (Ship_info[Ships_exited[oswpt.ship_entry()->exited_index].ship_class].species != value)
								return SEXP_KNOWN_FALSE;
						}
					}
					// ship is in the EXITED state but probably in the process of exploding
					else if (oswpt.has_shipp())
					{
						UNREACHABLE("With the addition of the ShipStatus::DEATH_ROLL state, this shouldn't happen");
						return SEXP_KNOWN_FALSE;
					}
					// ship has vanished
					else
						return SEXP_NAN_FOREVER;
				}
				// it's probably an exited wing, which we don't store information about
				else
				{
					return SEXP_NAN_FOREVER;
				}

				break;
			}

			// we don't handle the other cases
			default:
				return SEXP_NAN;
		}
	}

	// got this far: we must be okay for all ships/wings
	return SEXP_TRUE;
}

// Goober5000
void sexp_ingame_ship_change_iff(ship *shipp, int new_team)
{
	Assert(shipp != nullptr);

	shipp->team = new_team;
}

// Goober5000
void sexp_parse_ship_change_iff(p_object *parse_obj, int new_team)
{
	Assert(parse_obj);

	parse_obj->team = new_team;
}

void sexp_change_iff_helper(object_ship_wing_point_team &oswpt, int new_team)
{
	switch (oswpt.type)
	{
		// change ingame ship
		case OSWPT_TYPE_SHIP:
		{
			sexp_ingame_ship_change_iff(oswpt.shipp(), new_team);

			break;
		}

		// change ship yet to arrive
		case OSWPT_TYPE_PARSE_OBJECT:
		{
			sexp_parse_ship_change_iff(oswpt.p_objp(), new_team);

			break;
		}

		// change wing (we must set the flags for all ships present as well as all ships yet to arrive)
		case OSWPT_TYPE_WING:
		case OSWPT_TYPE_WING_NOT_PRESENT:
		{
			// current ships
			for (int i = 0; i < oswpt.wingp()->current_count; i++)
				sexp_ingame_ship_change_iff(&Ships[oswpt.wingp()->ship_index[i]], new_team);

			// ships yet to arrive
			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				if (p_objp->wingnum == oswpt.wingnum)
					sexp_parse_ship_change_iff(p_objp, new_team);
			}

			break;
		}

		case OSWPT_TYPE_WHOLE_TEAM:
		{
			for (auto so: list_range(&Ship_obj_list)) {
				if (Objects[so->objnum].flags[Object::Object_Flags::Should_be_dead])
					continue;

				ship* shipp = &Ships[Objects[so->objnum].instance];
				if (shipp->team == oswpt.team)
					sexp_ingame_ship_change_iff(shipp, new_team);
			}

			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				if (p_objp->team == oswpt.team)
					sexp_parse_ship_change_iff(p_objp, new_team);
			}

			break;
		}

		default:
			break;
	}
}

// Goober5000 - added wing capability
void sexp_change_iff(int n)
{
	int new_team;

	new_team = iff_lookup(CTEXT(n));
	n = CDR(n);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(new_team);

	for ( ; n != -1; n = CDR(n) )
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		Current_sexp_network_packet.send_string(CTEXT(n));

		sexp_change_iff_helper(oswpt, new_team);
	}

	Current_sexp_network_packet.end_callback();
}

void multi_sexp_change_iff()
{
	int new_team;
	char name[TOKEN_LENGTH];

	Current_sexp_network_packet.get_int(new_team);
	while (Current_sexp_network_packet.get_string(name)) {

		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, -1, name);

		sexp_change_iff_helper(oswpt, new_team);
	}	
}

void sexp_ingame_ship_change_iff_color(ship *shipp, int observer_team, int observed_team, int alternate_iff_color)
{
	Assert(shipp != nullptr);

	shipp->ship_iff_color[{observer_team, observed_team}] = alternate_iff_color;
}

void sexp_parse_ship_change_iff_color(p_object *parse_obj, int observer_team, int observed_team, int alternate_iff_color)
{
	Assert(parse_obj);

	parse_obj->alt_iff_color[{observer_team, observed_team}] = alternate_iff_color;
}

void sexp_change_iff_color_helper(object_ship_wing_point_team &oswpt, int observer_team, int observed_team, int alternate_iff_color)
{
	int i;

	switch (oswpt.type)
	{
		// change ingame ship
		case OSWPT_TYPE_SHIP:
		{
			sexp_ingame_ship_change_iff_color(oswpt.shipp(), observer_team, observed_team, alternate_iff_color);

			break;
		}

		// change ship yet to arrive
		case OSWPT_TYPE_PARSE_OBJECT:
		{
			sexp_parse_ship_change_iff_color(oswpt.p_objp(), observer_team, observed_team, alternate_iff_color);

			break;
		}

		// change wing (we must set the flags for all ships present as well as all ships yet to arrive)
		case OSWPT_TYPE_WING:
		case OSWPT_TYPE_WING_NOT_PRESENT:
		{
			// current ships
			for (i = 0; i < oswpt.wingp()->current_count; i++)
				sexp_ingame_ship_change_iff_color(&Ships[oswpt.wingp()->ship_index[i]], observer_team, observed_team, alternate_iff_color);

			// ships yet to arrive
			for (auto p_objp: list_range(&Ship_arrival_list))
			{
				if (p_objp->wingnum == oswpt.wingnum)
					sexp_parse_ship_change_iff_color(p_objp, observer_team, observed_team, alternate_iff_color);
			}

			break;
		}

		default:
			break;
	}
}

 // Wanderer
void sexp_change_iff_color(int n)
{
	int observer_team, observed_team, alternate_iff_color;
	std::array<int, 3> rgb;
	bool is_nan, is_nan_forever;

	// First node
	if (n == -1) {
		Warning(LOCATION, "Detected missing observer team parameter in sexp-change_iff_color");
		return;
	}
	observer_team = iff_lookup(CTEXT(n));
	n = CDR(n);

	// Second node
	if (n == -1) {
		Warning(LOCATION, "Detected missing observed team parameter in sexp-change_iff_color");
		return;
	}
	observed_team = iff_lookup(CTEXT(n));
	n = CDR(n);

	// Three following nodes
	int count = eval_array<int>(rgb, n, is_nan, is_nan_forever, [](int num)->int {
		if (num < 0 || num > 255) {
			Warning(LOCATION, "Invalid argument for iff color in sexp-change-iff-color. Valid range is 0 to 255.\n");
			num = 255;
		}
		return num;
	});
	if (count < 3) {
		Warning(LOCATION, "Detected incomplete color parameter list in sexp-change_iff_color\n");
		return;
	}
	if (is_nan || is_nan_forever) {
		return;
	}
	alternate_iff_color = iff_init_color(rgb[0], rgb[1], rgb[2]);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(observer_team);
	Current_sexp_network_packet.send_int(observed_team);
	Current_sexp_network_packet.send_int(alternate_iff_color);

	// Rest of the nodes
	for (; n >= 0; n = CDR(n))
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		Current_sexp_network_packet.send_string(CTEXT(n));

		sexp_change_iff_color_helper(oswpt, observer_team, observed_team, alternate_iff_color);
	}

	Current_sexp_network_packet.end_callback(); 
}

void multi_sexp_change_iff_color()
{
	int observer_team, observed_team, alternate_iff_color;
	char name[TOKEN_LENGTH];

	Current_sexp_network_packet.get_int(observer_team);
	Current_sexp_network_packet.get_int(observed_team);
	Current_sexp_network_packet.get_int(alternate_iff_color);

	while (Current_sexp_network_packet.get_string(name))
	{ 
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, -1, name);

		sexp_change_iff_color_helper(oswpt, observer_team, observed_team, alternate_iff_color);
	}
}

// Goober5000
int sexp_is_ship_class_or_type(int n, bool ship_class)
{
	Assert( n >= 0 );

	// get class or type
	auto name = CTEXT(n);
	int constraint_index;
	if (ship_class)
		constraint_index = ship_info_lookup(name);
	else
	{
		constraint_index = ship_type_name_lookup(name);
		if (constraint_index < 0 && Fighter_bomber_valid && !stricmp(name, Fighter_bomber_type_name))
			constraint_index = Ship_type_fighter_bomber;
	}
	n = CDR(n);

	// not valid; no need to check
	if (constraint_index < 0)
		return SEXP_FALSE;

	// eval ships
	while (n != -1)
	{
		// get ship
		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			return SEXP_NAN;

		int ship_index;
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		{
			ship_index = ship_entry->p_objp()->ship_class;
		}
		else if (ship_entry->exited_index >= 0)
		{
			ship_index = Ships_exited[ship_entry->exited_index].ship_class;
		}
		else if (ship_entry->has_shipp())
		{
			ship_index = ship_entry->shipp()->ship_info_index;
		}
		else
			return SEXP_NAN_FOREVER;

		// maybe convert from class to type
		if (!ship_class)
			ship_index = ship_class_query_general_type(ship_index);

		// if it doesn't match, return false
		if (!ship_class && constraint_index == Ship_type_fighter_bomber)
		{
			if (ship_index != Ship_type_fighter && ship_index != Ship_type_bomber)
				return SEXP_FALSE;
		}
		else
		{
			if (ship_index != constraint_index)
				return SEXP_FALSE;
		}

		// increment
		n = CDR(n);
	}

	// we're this far; it must be true
	return SEXP_TRUE;
}

// Goober5000
// ai class value is the first parameter, second is a ship, rest are subsystems to check
int sexp_is_ai_class(int n)
{
	Assert ( n >= 0 );

	auto ai_class_name = CTEXT(n);
	n = CDR(n);

	// find ai class
	int ai_class_to_check = -1;
	for (int i=0; i<Num_ai_classes; ++i)
	{
		if (!stricmp(Ai_class_names[i], ai_class_name))
			ai_class_to_check = i;
	}

	if (ai_class_to_check < 0) {
		Warning(LOCATION, "is-ai-class called with nonexistent AI class [%s].", ai_class_name);
		return SEXP_FALSE;
	}

	// find ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;
	n = CDR(n);

	// subsys?
	if (n >= 0)
	{
		// loopity-loop
		for ( ; n != -1; n = CDR(n) )
		{
			auto subsystem = CTEXT(n);

			// find the ship subsystem
			// if it doesn't match, or subsys isn't found, return false immediately
			auto ss = ship_get_subsys(ship_entry->shipp(), subsystem);
			if (ss)
			{
				if (ss->weapons.ai_class != ai_class_to_check)
					return SEXP_FALSE;
			}
			else
				return SEXP_FALSE;
		}

		// we've come this far; it must all be true
		return SEXP_TRUE;
	}
	// just the ship
	else
	{
		if (Ai_info[ship_entry->shipp()->ai_index].ai_class == ai_class_to_check)
			return SEXP_TRUE;
		else
			return SEXP_FALSE;
	}
}

// Goober5000
void sexp_change_ai_class(int n)
{
	Assert ( n >= 0 );

	auto ai_class_name = CTEXT(n);
	n = CDR(n);

	// find ai class
	int new_ai_class = -1;
	for (int i=0; i<Num_ai_classes; ++i)
	{
		if (!stricmp(Ai_class_names[i], ai_class_name))
			new_ai_class = i;
	}

	if (new_ai_class < 0) {
		Warning(LOCATION, "change-ai-class called with nonexistent AI class [%s].", ai_class_name);
		return;
	}

	// find ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_ship(ship_entry->shipp());
	Current_sexp_network_packet.send_int(new_ai_class);

	// subsys?
	if (n >= 0)
	{
		// loopity-loop
		process_ship_subsystems(ship_entry, true, n, true, "change-ai-class", [&](ProcessSubsystemType type, ship_subsys *ss)
		{
			if (type == ProcessSubsystemType::SUBSYSTEM)
			{
				ship_subsystem_set_new_ai_class(ss, new_ai_class);
				Current_sexp_network_packet.send_string(ss->system_info->subobj_name);
			}
			else
				Warning(LOCATION, "change-ai-class does not support %s!", special_subsystem_string(type));
		});
	}
	// just the one ship
	else
	{
		ship_set_new_ai_class(ship_entry->shipp(), new_ai_class);
	}

	Current_sexp_network_packet.end_callback();
}

void multi_sexp_change_ai_class()
{
	int new_ai_class;
	ship *shipp;
	ship_subsys *ss;
	char subsystem[TOKEN_LENGTH];

	Current_sexp_network_packet.get_ship(shipp); 
	Current_sexp_network_packet.get_int(new_ai_class);

	// subsystem?
	if (Current_sexp_network_packet.get_string(subsystem)) {
		ss = ship_get_subsys(shipp, subsystem);
		ship_subsystem_set_new_ai_class(ss, new_ai_class);

		// deal with any other subsystems
		while (Current_sexp_network_packet.get_string(subsystem)) {
			ss = ship_get_subsys(shipp, subsystem);
			ship_subsystem_set_new_ai_class(ss, new_ai_class);
		}		 
	}
	else {
		ship_set_new_ai_class(shipp, new_ai_class);
	}
}

// following routine adds an ai goal to a ship structure.  The sexpression index
// passed in should be an ai-goal of the proper form.  The code in MissionGoal should
// check the syntax.

/**
 * Adds a goal to the specified entry (ships and wings have unique names between the two sets).
 */
void sexp_add_goal(int n)
{
	Assert ( n >= 0 );

	int goal_node = CDR(n);

	auto ship_entry = eval_ship(n);
	if (ship_entry)
	{
		if (!ship_entry->has_shipp())
			return;										// ship not around anymore???? then forget it!

		ai_add_ship_goal_sexp(goal_node, ai_goal_type::EVENT_SHIP, &(Ai_info[ship_entry->shipp()->ai_index]));
		return;
	}

	auto wingp = eval_wing(n);
	if (wingp)
	{
		if (wingp->flags[Ship::Wing_Flags::Gone])
			return;										// wing not around anymore???? then forget it!

		ai_add_wing_goal_sexp(goal_node, ai_goal_type::EVENT_WING, wingp);
	}
}

// Goober5000
void sexp_remove_goal(int n)
{
	Assert( n >= 0 );

	int goal_node = CDR(n);

	auto ship_entry = eval_ship(n);
	if (ship_entry)
	{
		if (!ship_entry->has_shipp())
			return;										// ship not around anymore???? then forget it!

		bool remove_more = false;
		auto aip = &Ai_info[ship_entry->shipp()->ai_index];
		do {
			int goalindex = ai_remove_goal_sexp_sub(goal_node, aip->goals, remove_more);
			if (goalindex >= 0)
			{
				if (aip->active_goal == goalindex)
					aip->active_goal = AI_ACTIVE_GOAL_NONE;
			}
		} while (remove_more);
		return;
	}

	auto wingp = eval_wing(n);
	if (wingp)
	{
		if (wingp->flags[Ship::Wing_Flags::Gone])
			return;										// wing not around anymore???? then forget it!

		ai_remove_wing_goal_sexp(goal_node, wingp);
	}
}

/**
 * Clear all AI goals for the given ship or wing
 */
void sexp_clear_goals(int n)
{
	for (; n >= 0; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (ship_entry)
		{
			if (!ship_entry->has_shipp())
				continue;										// ship not around anymore???? then forget it!

			ai_clear_ship_goals(&(Ai_info[ship_entry->shipp()->ai_index]));
			continue;
		}

		auto wingp = eval_wing(n);
		if (wingp)
		{
			if (wingp->flags[Ship::Wing_Flags::Gone])
				continue;										// wing not around anymore???? then forget it!

			ai_clear_wing_goals(wingp);
		}
	}
}

// Goober5000
void sexp_hud_disable(int n)
{
	bool is_nan, is_nan_forever;
	int disable_hud = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	hud_set_draw(!disable_hud);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(disable_hud);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_hud_disable()
{
	int disable_hud;

	if (Current_sexp_network_packet.get_int(disable_hud)) {
		hud_set_draw(!disable_hud);
	}
}

// Goober5000
void sexp_hud_disable_except_messages(int n)
{
	bool is_nan, is_nan_forever;
	int disable_hud = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	hud_disable_except_messages(disable_hud);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(disable_hud);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_hud_disable_except_messages()
{
	int disable_hud;

	if (Current_sexp_network_packet.get_int(disable_hud)) {
		hud_disable_except_messages(disable_hud);
	}
}

void sexp_hud_set_text_num(int n)
{
	bool is_nan, is_nan_forever;
	auto gaugename = CTEXT(n);
	char tmp[16] = "";

	HudGauge* cg = hud_get_custom_gauge(gaugename);
	if (cg) {
		int num = eval_num(CDR(n), is_nan, is_nan_forever);
		if (is_nan || is_nan_forever) {
			strcpy_s(tmp, "NaN");
		}
		else {
			sprintf(tmp, "%d", num);
		}
		cg->updateCustomGaugeText(tmp);
	} else {
		WarningEx(LOCATION, "Could not find a custom hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_set_text(int n)
{
	auto gaugename = CTEXT(n);
	auto text = CTEXT(CDR(n));

	HudGauge* cg = hud_get_custom_gauge(gaugename);
	if (cg) {
		cg->updateCustomGaugeText(text);
	} else {
		WarningEx(LOCATION, "Could not find a custom hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_set_xstr(int n)
{
	bool is_nan, is_nan_forever;

	auto gaugename = CTEXT(n);
	auto text = CTEXT(CDR(n));
	auto id = eval_num(CDDR(n), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever) {
		id = -1;
	}

	// Translate the string using tstrings
	SCP_string xstr;
	sprintf(xstr, "XSTR(\"%s\", %d)", text, id);
	SCP_string translated_string;
	lcl_ext_localize(xstr, translated_string);

	// Now replace tokens and variables
	sexp_replace_variable_names_with_values(translated_string);
	sexp_container_replace_refs_with_values(translated_string);
	message_translate_tokens(translated_string);

	HudGauge* cg = hud_get_custom_gauge(gaugename);
	if (cg) {
		cg->updateCustomGaugeText(translated_string.c_str());
	} else {
		WarningEx(LOCATION, "Could not find a custom hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_set_message(int n)
{
	auto gaugename = CTEXT(n);
	auto text = CTEXT(CDR(n));
	SCP_string message;

	for (int i = 0; i < Num_messages; i++) {
		if ( !stricmp(text, Messages[i].name) ) {
			message = Messages[i].message;

			sexp_replace_variable_names_with_values(message);
			sexp_container_replace_refs_with_values(message);
			message_translate_tokens(message);

			HudGauge* cg = hud_get_custom_gauge(gaugename);
			if (cg) {
				cg->updateCustomGaugeText(message);
			} else {
				WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
			}
			return;
		}
	}

	WarningEx(LOCATION, "sexp_hud_set_message couldn't find a message by the name of %s in the mission\n", text);
}

void sexp_hud_force_static(int n)
{
	Sensor_static_forced = is_sexp_true(n);
}

void sexp_hud_force_emp(int n)
{
	bool is_nan, is_nan_forever;

	int intensity = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	n = CDR(n);

	int duration = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	n = CDR(n);

	SCP_string text = "";

	if (n >= 0) {

		// assume just text
		text = CTEXT(n);

		// but use an actual message if one exists
		for (int i = 0; i < Num_messages; i++) {
			if (!stricmp(Messages[i].name, text.c_str())) {
				text = Messages[i].message;
				break;
			}
		}

		text = message_translate_tokens(text.c_str());
	}

	emp_start_local(i2fl(intensity), i2fl(duration) / 1000, text);
}

void sexp_hud_set_directive(int n)
{
	auto gaugename = CTEXT(n);
	auto text = CTEXT(CDR(n));
	SCP_string message = message_translate_tokens(text);

	if (message.size() >= MESSAGE_LENGTH) {
		WarningEx(LOCATION, "Message %s is too long for use in a HUD gauge. Please shorten it to %d characters or less.", message.c_str(), MESSAGE_LENGTH - 1);
		return;
	}

	HudGauge* cg = hud_get_custom_gauge(gaugename);
	if (cg) {
		cg->updateCustomGaugeText(message);
	} else {
		WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_clear_messages()
{
	if(!Ship_info[Player_ship->ship_info_index].hud_gauges.empty()) {
		size_t num_gauges = Ship_info[Player_ship->ship_info_index].hud_gauges.size();

		for(size_t i = 0; i < num_gauges; i++) {
			if (Ship_info[Player_ship->ship_info_index].hud_gauges[i]->getObjectType() == HUD_OBJECT_MESSAGES) {
				auto* gauge = static_cast<HudGaugeMessages*>(Ship_info[Player_ship->ship_info_index].hud_gauges[i].get());
				gauge->clearMessages();
			}
		}
	} else {
		size_t num_gauges = default_hud_gauges.size();

		for(size_t i = 0; i < num_gauges; i++) {
			if (default_hud_gauges[i]->getObjectType() == HUD_OBJECT_MESSAGES) {
				auto* gauge = static_cast<HudGaugeMessages*>(default_hud_gauges[i].get());
				gauge->clearMessages();
			}
		}
	}
}

void sexp_hud_set_coords(int n)
{
	int coord_x, coord_y;
	bool is_nan, is_nan_forever;

	auto gaugename = CTEXT(n);
	n = CDR(n);

	eval_nums(n, is_nan, is_nan_forever, coord_x, coord_y);
	if (is_nan || is_nan_forever)
		return;

	HudGauge* hg = hud_get_gauge(gaugename);
	if (hg) {
		// we might need to adjust the coordinates
		if (HUD_set_coords_screen_base_res[0] > 0 && HUD_set_coords_screen_base_res[1] > 0) {
			coord_x = fl2i(i2fl(coord_x) * gr_screen.center_w / HUD_set_coords_screen_base_res[0]);
			coord_y = fl2i(i2fl(coord_y) * gr_screen.center_h / HUD_set_coords_screen_base_res[1]);
		}

		hg->setGaugeCoords(coord_x, coord_y);
	} else {
		WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_set_frame(int n)
{
	bool is_nan, is_nan_forever;
	auto gaugename = CTEXT(n);

	int frame_num = eval_num(CDR(n), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	HudGauge* hg = hud_get_gauge(gaugename);
	if (hg) {
		hg->setGaugeFrame(frame_num);
	} else {
		WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_set_color(int n)
{
	auto gaugename = CTEXT(n);
	n = CDR(n);

	std::array<int, 3> rgb;
	bool is_nan, is_nan_forever;
	eval_array<int>(rgb, n, is_nan, is_nan_forever, [](int num)->int {
		CLAMP(num, 0, 255);
		return num;
	});
	if (is_nan || is_nan_forever)
		return;

	HudGauge* hg = hud_get_gauge(gaugename);
	if (hg) {
		hg->sexpLockConfigColor(false);
		hg->updateColor(rgb[0], rgb[1], rgb[2], (HUD_color_alpha + 1) * 16);
		hg->sexpLockConfigColor(true);
	} else {
		WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_reset_color(int n)
{
	for (; n >= 0; n = CDR(n))
	{
		auto gaugename = CTEXT(n);
		HudGauge* hg = hud_get_gauge(gaugename);
		if (hg) {
			// only a subset of default HUD gauges have a configured color
			int type = hg->getConfigType();
			auto ii = std::find_if(std::begin(Legacy_HUD_gauges), std::end(Legacy_HUD_gauges), [type](const Legacy_HUD_gauge_pair& pair) { return pair.hud_gauge_type == type; });

			if (ii == std::end(Legacy_HUD_gauges)) {
				WarningEx(LOCATION, "HUD gauge %s does not have a built-in configured color!", gaugename);
			} else {
				// use the color as specified in the HUD config
				auto id = hg->getConfigId();
				auto& c = HUD_config.gauge_colors[id];

				hg->sexpLockConfigColor(false);
				hg->updateColor(c.red, c.green, c.blue, (HUD_color_alpha + 1) * 16);
				hg->sexpLockConfigColor(true);
			}
		} else {
			WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
		}
	}
}

// Goober5000
void sexp_hud_set_max_targeting_range(int n)
{
	int val;
	bool is_nan, is_nan_forever;

	val = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	if (val < 0) {
		val = 0;
	}

	Hud_max_targeting_range = val;

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(Hud_max_targeting_range);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_hud_set_max_targeting_range()
{
	Current_sexp_network_packet.get_int(Hud_max_targeting_range);
}

/* Make sure that the Sexp_hud_display_* get added to the game_state
transitions in freespace.cpp (game_enter_state()). */
int Sexp_hud_display_warpout = 0;

void sexp_hud_display_gauge(int n)
{
	bool is_nan, is_nan_forever;

	int show_for = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	auto gauge = CTEXT(CDR(n));

	if ( stricmp(SEXP_HUD_GAUGE_WARPOUT, gauge) == 0 ) {
		Sexp_hud_display_warpout = (show_for > 1)? timestamp(show_for) : (show_for);
	} 
}

void sexp_hud_gauge_set_active(int n)
{
	HudGauge* hg;
	auto gaugename = CTEXT(n);
	bool active = is_sexp_true(CDR(n));

	hg = hud_get_gauge(gaugename);
	if (hg) {
		hg->updateActive(active);
	} else {
		WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
	}
}

void sexp_hud_set_custom_gauge_active(int node)
{
	HudGauge* hg;
	bool activate = is_sexp_true(node);
	node = CDR(node);
	for(; node >= 0; node = CDR(node)) {

		auto gaugename = CTEXT(node);
		hg = hud_get_custom_gauge(gaugename);

		if (hg) {
			hg->updateActive(activate);
		} else {
			WarningEx(LOCATION, "Could not find a hud gauge named %s\n", gaugename);
		}
	}
}

int hud_gauge_type_lookup(const char* name)
{
	for(int i = 0; i < Num_hud_gauge_types; i++) {
		if(!stricmp(name, Hud_gauge_types[i].name))
			return Hud_gauge_types[i].def;
	}
	return -1;
}

void sexp_hud_activate_gauge_type(int n)
{
	int config_type = hud_gauge_type_lookup(CTEXT(n));
	bool active = is_sexp_true(CDR(n));
	
	if (config_type != -1) { 
		if(!Ship_info[Player_ship->ship_info_index].hud_gauges.empty()) {
			size_t num_gauges = Ship_info[Player_ship->ship_info_index].hud_gauges.size();

			for(size_t i = 0; i < num_gauges; i++) {
				if (Ship_info[Player_ship->ship_info_index].hud_gauges[i]->getObjectType() == config_type)
					Ship_info[Player_ship->ship_info_index].hud_gauges[i]->updateSexpOverride(!active);
			}
		} else {
			size_t num_gauges = default_hud_gauges.size();

			for(size_t i = 0; i < num_gauges; i++) {
				if (default_hud_gauges[i]->getObjectType() == config_type)
					default_hud_gauges[i]->updateSexpOverride(!active);
			}
		}
	}
}

void sexp_hud_set_builtin_gauge_active(int node)
{
	bool activate = is_sexp_true(node);
	node = CDR(node);

	for(; node >= 0; node = CDR(node)) {

		int config_type = hud_gauge_type_lookup(CTEXT(node));

		if (config_type != -1) {
			if(!Ship_info[Player_ship->ship_info_index].hud_gauges.empty()) {
			size_t num_gauges = Ship_info[Player_ship->ship_info_index].hud_gauges.size();

			for(size_t i = 0; i < num_gauges; i++) {
				if (Ship_info[Player_ship->ship_info_index].hud_gauges[i]->getObjectType() == config_type)
					Ship_info[Player_ship->ship_info_index].hud_gauges[i]->updateSexpOverride(!activate);
				}
			} else {
			size_t num_gauges = default_hud_gauges.size();

			for(size_t i = 0; i < num_gauges; i++) {
				if (default_hud_gauges[i]->getObjectType() == config_type)
					default_hud_gauges[i]->updateSexpOverride(!activate);
				}
			}
		}
	}
}

void multi_sexp_hud_display_gauge()
{
	int show_for; 

	if (Current_sexp_network_packet.get_int(show_for)) {
		Sexp_hud_display_warpout = (show_for > 1)? timestamp(show_for) : (show_for);
	}
}

void sexp_set_squadron_wings(int node)
{
	std::array<int, MAX_SQUADRON_WINGS> new_squad_wingnums;

	// get the new set of squadron wings
	for (int i = 0, n = node; i < MAX_SQUADRON_WINGS; i++)
	{
		int wingnum = -1;

		if (n >= 0)
		{
			auto wingp = eval_wing(n);
			if (wingp)
				wingnum = WING_INDEX(wingp);

			n = CDR(n);
		}

		new_squad_wingnums[i] = wingnum;
	}

	hud_set_new_squadron_wings(new_squad_wingnums);
}

// Goober5000
/**
 * Trigger whether player uses the game AI for stuff
 */
void sexp_player_use_ai(bool use_ai)
{
	// if turning off player AI, clean up the AI to prevent any stale state from causing trouble later
	if (Player_use_ai && !use_ai)
	{
		ai_clear_ship_goals(Player_ai);
		Player_ai->mode = AIM_NONE;
		set_target_objnum(Player_ai, -1);
	}

	Player_use_ai = use_ai;

	if (!use_ai)
	{
		Player_ai->ai_override_flags.reset();
		Player_obj->phys_info.flags &= ~PF_MANEUVER_NO_DAMP;
	}
}

void sexp_set_friendly_damage_caps(int n) {
	bool is_nan;
	bool is_nan_forever;
	ai_profile_t& aip = *The_mission.ai_profile;

	float beam_friendly_cap = i2fl(eval_num(n, is_nan, is_nan_forever));
	if (!is_nan && !is_nan_forever) {
		aip.beam_friendly_damage_cap[Game_skill_level] = beam_friendly_cap;
	}

	n = CDR(n);
	if (n < 0) {
		return;
	}

	float weapon_friendly_cap = i2fl(eval_num(n, is_nan, is_nan_forever));
	if (!is_nan && !is_nan_forever) {
		aip.weapon_friendly_damage_cap[Game_skill_level] = weapon_friendly_cap;
	}

	n = CDR(n);
	if (n < 0) {
		return;
	}
	
	float weapon_self_cap = i2fl(eval_num(n, is_nan, is_nan_forever));
	if (!is_nan && !is_nan_forever) {
		aip.weapon_self_damage_cap[Game_skill_level] = weapon_self_cap;
	}
}

// Luytenky
/*
 * Sets the player's target to the specified, either ship, and or the subsystem on said ship.
 */
void sexp_set_player_target(int node)
{	
	const ship_registry_entry *ship_entry = eval_ship(node);
	if (ship_entry == nullptr)
		return;

	int shipnum = ship_entry->shipnum;
	if (shipnum < 0)
		return;

	ship* shipp = &Ships[shipnum];
	int objnum = shipp->objnum;
	int n = CDR(node);
	ship_subsys * new_subsys = nullptr;
	if (n >= 0) {
		const char* subsys_name = CTEXT(n);
		if (stricmp(subsys_name, SEXP_NONE_STRING) != 0) {
			new_subsys = ship_get_subsys(shipp, subsys_name);
		}
	}
    set_target_objnum(Player_ai, objnum);
    set_targeted_subsys(Player_ai, new_subsys, objnum);
}

// Luytenky
/*
 * Clears the player's targeting.
 */
void sexp_clear_player_target()
{
	set_target_objnum(Player_ai, -1);
	set_targeted_subsys(Player_ai, nullptr, -1);
}

// Karajorma
void sexp_allow_treason (int n) 
{
	if (n != -1) {
		The_mission.flags.set(Mission::Mission_Flags::No_traitor, is_sexp_true(n));
	}
}

void sexp_set_player_orders(int n) 
{
	bool allow_order;
	SCP_set<size_t> orders;

	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	auto shipp = ship_entry->shipp();

	// we need to know which orders this ship class can accept.
	const SCP_set<size_t> &default_orders = ship_get_default_orders_accepted(&Ship_info[shipp->ship_info_index]);
	n = CDR(n);
	allow_order = is_sexp_true(n);
	n = CDR(n);
	do {
		for( size_t order : default_orders){
			// Once we exceed the number of valid orders, break and warn
			if (order >= Player_orders.size()) {
				Warning(LOCATION, "Invalid order name %s found in sexp!", CTEXT(n));
				break;
			}
			// OPF_AI_ORDER returns hud_name, so we must compare to that instead of parse_name
			if (!stricmp(CTEXT(n), Player_orders[order].hud_name.c_str())) {
				orders.insert(order);
				break;
			}
		}

		n = CDR(n);
	}while (n >= 0);
		
	// set or unset the orders
	if (allow_order) {
		shipp->orders_accepted.insert(orders.begin(), orders.end());
	}
	else {
		SCP_set<size_t> diff;
		std::set_difference(shipp->orders_accepted.begin(), shipp->orders_accepted.end(), orders.begin(), orders.end(),
							std::inserter(diff, diff.end()));
		shipp->orders_accepted = std::move(diff);
	}
}

void sexp_set_order_allowed_for_target(int n)
{
	auto ship_entry = eval_ship(n);

	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	auto shipp = ship_entry->shipp();

	const auto default_orders = ship_get_default_orders_against();
	n = CDR(n);
	bool allow_order = is_sexp_true(n);
	n = CDR(n);
	SCP_set<size_t> orders;
	do {
		for( size_t order : default_orders){
			//Once we exceed the number of valid orders, break and warn
			if (order >= Player_orders.size()) {
				Warning(LOCATION, "Invalid order name %s found in sexp!", CTEXT(n));
				break;
			}

			// OPF_AI_ORDER returns hud_name, so we must compare to that instead of parse_name
			if (!stricmp(CTEXT(n), Player_orders[order].hud_name.c_str())) {
				orders.insert(order);
				break;
			}
		}
		n = CDR(n);
	}while (n >= 0);
		
	// set or unset the orders
	if (allow_order) {
		shipp->orders_allowed_against.insert(orders.begin(), orders.end());
	}
	else {
		SCP_set<size_t> diff;
		std::set_difference(shipp->orders_allowed_against.begin(), shipp->orders_allowed_against.end(), orders.begin(), orders.end(),
							std::inserter(diff, diff.end()));
		shipp->orders_allowed_against = std::move(diff);
	}
}

void sexp_enable_or_validate_general_orders(int n, bool enable)
{
	bool choice = is_sexp_true(n);
	n = CDR(n);

	do {
		int order_num = ai_lua_find_general_order_id(CTEXT(n));

		// if we got an invalid order number then skip
		if (order_num < 0) {
			Warning(LOCATION, "Invalid order name %s found in sexp!", CTEXT(n));
			continue;
		}

		if (enable) {
			ai_lua_enable_general_order(order_num, choice);
		} else {
			ai_lua_validate_general_order(order_num, choice);
		}

		n = CDR(n);
	} while (n >= 0);
}

// Goober5000
void sexp_change_soundtrack(int n)
{
	event_sexp_change_soundtrack(CTEXT(n));

	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback(); 
		Current_sexp_network_packet.send_string(CTEXT(n));
		Current_sexp_network_packet.end_callback(); 
	}
}

void multi_sexp_change_soundtrack()
{
	char new_track[NAME_LENGTH]; 

	if (Current_sexp_network_packet.get_string(new_track)) {
		event_sexp_change_soundtrack(new_track);
	}
}

// Goober5000
int sexp_find_music_handle_index(int sexp_var)
{
	// check the default case
	if (sexp_var < 0)
	{
		// we might not have any music at this point
		if (Sexp_music_handles.empty())
			return -1;

		return 0;
	}

	// get the handle from the variable
	int music_handle = -1;
	try
	{
		music_handle = std::stoi(Sexp_variables[sexp_var].text);
	}
	catch (const std::exception&)
	{
		// not a number
		return -1;
	}

	// find it in our collection of handles (skip index 0)
	for (size_t i = 1; i < Sexp_music_handles.size(); ++i)
	{
		if (Sexp_music_handles[i] == music_handle)
			return (int)i;
	}

	return -1;
}

// Goober5000
void sexp_pause_unpause_music(bool pause, int sexp_var)
{
	if (Cmdline_freespace_no_music)
		return;

	int index = sexp_find_music_handle_index(sexp_var);
	if (index < 0)
	{
		// bad variable, maybe
		if (sexp_var >= 0)
			Warning(LOCATION, "In sexp_pause_unpause_music, variable %s did not store a valid music handle!", Sexp_variables[sexp_var].variable_name);

		return;
	}

	int music_handle = Sexp_music_handles[index];

	// actually pause the music
	if (music_handle >= 0)
	{
		if (pause && !audiostream_is_paused(music_handle))
			audiostream_pause(music_handle, true);
		else if (!pause && audiostream_is_paused(music_handle))
			audiostream_unpause(music_handle, true);
	}
}

// Goober5000
void sexp_stop_music(bool fade, int sexp_var)
{
	if (Cmdline_freespace_no_music)
		return;

	int index = sexp_find_music_handle_index(sexp_var);
	if (index < 0)
	{
		// bad variable, maybe
		if (sexp_var >= 0)
			Warning(LOCATION, "In sexp_stop_music, variable %s did not store a valid music handle!", Sexp_variables[sexp_var].variable_name);

		return;
	}

	int music_handle = Sexp_music_handles[index];

	// actually stop the music and free the handle
	if (music_handle >= 0)
	{
		audiostream_close_file(music_handle, fade);
		Sexp_music_handles[index] = -1;
	}

	// also clear the variable
	if (sexp_var >= 0)
		sexp_modify_variable("-1", sexp_var);
}

// Goober5000
void sexp_music_close()
{
	if (Cmdline_freespace_no_music)
		return;

	// close all music used by this mission
	for (auto music_handle : Sexp_music_handles)
		audiostream_close_file(music_handle);
	Sexp_music_handles.clear();
}

// Goober5000
void sexp_load_music(const char *filename, int type = -1, int sexp_var = -1)
{
	if (Cmdline_freespace_no_music || filename == nullptr)
		return;

	if (type < 0)
		type = ASF_MENUMUSIC;

	// the default music handle is always index 0, which at this point may or may not exist
	if (Sexp_music_handles.empty())
		Sexp_music_handles.push_back(-1);

	// if a variable is supplied, we'll be creating a new handle to be stored in the variable
	int index;
	if (sexp_var >= 0)
	{
		// if any handle has expired, it can be closed; and any closed or expired index can be reused
		// (note that handles are not unique across an entire mission; any variables pointing to handles
		// that have been closed will see those handles reused for new sounds)
		int found_i = -1, n = sz2i(Sexp_music_handles.size());
		for (int i = 1; i < n; ++i)	// skip the default handle
		{
			int this_handle = Sexp_music_handles[i];

			if ((this_handle < 0) || (!audiostream_is_playing(this_handle) && !audiostream_is_paused(this_handle)))
			{
				audiostream_close_file(this_handle, false);
				found_i = i;
				break;
			}
		}

		// either reuse an index or choose a new index
		if (found_i >= 0)
			index = found_i;
		else
		{
			index = static_cast<int>(Sexp_music_handles.size());
			Sexp_music_handles.push_back(-1);
		}
	}
	// otherwise we'll be reusing the default handle, so close anything that's already playing
	else
	{
		index = 0;
		audiostream_close_file(Sexp_music_handles[index]);
	}

	// open the stream and save the handle in our list
	Sexp_music_handles[index] = audiostream_open(filename, type);
	if (Sexp_music_handles[index] < 0)
		Warning(LOCATION, "In sexp_load_music, could not create audio handle for '%s'!  You might be trying to play too many sounds at once.", filename);

	// if we have a variable, save it there too
	if (sexp_var >= 0)
	{
		char number_as_str[TOKEN_LENGTH];
		sprintf(number_as_str, "%d", Sexp_music_handles[index]);
		sexp_modify_variable(number_as_str, sexp_var);
	}
}

// Goober5000
void sexp_start_music(int loop, int sexp_var)
{
	int index = sexp_find_music_handle_index(sexp_var);

	// shouldn't happen
	if (index < 0)
	{
		// really shouldn't happen
		if (sexp_var < 0)
			Warning(LOCATION, "In sexp_start_music, unable to find a default music index?!?!");
		else
			Warning(LOCATION, "In sexp_start_music, no music handle index available for variable %s!", Sexp_variables[sexp_var].variable_name);

		return;
	}

	int music_handle = Sexp_music_handles[index];

	// also shouldn't happen
	if (music_handle < 0)
	{
		Warning(LOCATION, "In sexp_start_music, music handle was unavailable!");
		return;
	}

	// start playing
	if (!audiostream_is_playing(music_handle))
		audiostream_play(music_handle, (Master_event_music_volume * aav_music_volume), loop);
}

gamesnd_id sexp_get_sound_index(int node)
{
	Assert(node >= 0);
	bool is_nan, is_nan_forever;
	gamesnd_id sound_index;

	// this node is another SEXP operator or a plain number
	if (CAR(node) != -1 || Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER)
	{
		int index = eval_num(node, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			index = -1;

		sound_index = gamesnd_get_by_tbl_index(index);
	}
	// it's gotta be a name
	else
	{
		const char *sound_name = CTEXT(node);

		// if it's not <none>, try looking it up
		if (stricmp(sound_name, SEXP_NONE_STRING) != 0)
		{
			sound_index = gamesnd_get_by_name(sound_name);

			if (!sound_index.isValid())
				Warning(LOCATION, "unrecognized sound name \"%s\"!", sound_name);
		}
	}

	return sound_index;
}

// Goober5000
void sexp_play_sound_from_table(int n)
{
	bool is_nan, is_nan_forever;
	vec3d origin;

	Assert( n >= 0 );

	// read in data --------------------------------
	eval_vec3d(&origin, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	auto sound_index = sexp_get_sound_index(n);


	// play sound effect ---------------------------
	if (sound_index.isValid()) {
		game_snd *snd = gamesnd_get_game_sound(sound_index);
		if (snd->min == 0 && snd->max == 0) {
			// if sound doesn't specify 3d range, don't play in 3d
			snd_play( snd, 0.0f, 1.0f, SND_PRIORITY_MUST_PLAY );
		} else {
			snd_play_3d( snd, &origin, &View_position, 0.0f, nullptr, 0, 1.0f, SND_PRIORITY_MUST_PLAY );
		}
	}

	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_float(origin.xyz.x);
		Current_sexp_network_packet.send_float(origin.xyz.y);
		Current_sexp_network_packet.send_float(origin.xyz.z);
		Current_sexp_network_packet.send_int(sound_index.value());
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_play_sound_from_table()
{
	vec3d origin;
	int sound_index = -1;

	Current_sexp_network_packet.get_float(origin.xyz.x);
	Current_sexp_network_packet.get_float(origin.xyz.y);
	Current_sexp_network_packet.get_float(origin.xyz.z);
	Current_sexp_network_packet.get_int(sound_index);

	auto sound = gamesnd_id(sound_index);

	// play sound effect ---------------------------
	if (sound.isValid()) {
		game_snd *snd = gamesnd_get_game_sound(sound);
		if (snd->min == 0 && snd->max == 0) {
			// if sound doesn't specify 3d range, don't play in 3d
			snd_play( snd, 0.0f, 1.0f, SND_PRIORITY_MUST_PLAY );
		} else {
			snd_play_3d( snd, &origin, &View_position, 0.0f, nullptr, 0, 1.0f, SND_PRIORITY_MUST_PLAY );
		}
	}
}

// Goober5000
void sexp_close_sound_from_file(int n)
{
	// fade out?
	bool fade;
	if (n < 0)
		fade = true;
	else
	{
		fade = is_sexp_true(n);
		n = CDR(n);
	}

	// we might have a variable
	int sexp_var = -1;
	if (n >= 0)
	{
		sexp_var = sexp_get_variable_index(n);
		if (sexp_var < 0)
		{
			Warning(LOCATION, "close-sound-from-file: Variable %s does not exist!", Sexp_nodes[n].text);
			return;
		}

		if (!(Sexp_variables[sexp_var].type & SEXP_VARIABLE_NUMBER))
		{
			Warning(LOCATION, "close-sound-from-file: Variable %s must be a number variable!", Sexp_variables[sexp_var].variable_name);
			return;
		}
	}

	// stop sound file
	sexp_stop_music(fade, sexp_var);

	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_bool(fade);
		Current_sexp_network_packet.send_int(sexp_var);
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_close_sound_from_file()
{
	bool fade = true;
	int sexp_var = -1;

	Current_sexp_network_packet.get_bool(fade);
	Current_sexp_network_packet.get_int(sexp_var);

	sexp_stop_music(fade, sexp_var);
}

// Goober5000
void sexp_play_sound_from_file(int n)
{
	// get the music track
	const char *filename = CTEXT(n);
	n = CDR(n);

	// we might loop it
	bool loop = false;
	if (n >= 0)
	{
		loop = (eval_sexp(n) != 0);
		n = CDR(n);
	}

	// we might use environmental effects
	int type = ASF_MENUMUSIC;
	if (n >= 0)
	{
		if (eval_sexp(n) != 0)
			type = ASF_SOUNDFX;
		n = CDR(n);
	}

	// we might have a variable
	int sexp_var = -1;
	if (n >= 0)
	{
		sexp_var = sexp_get_variable_index(n);
		if (sexp_var < 0)
		{
			Warning(LOCATION, "play-sound-from-file: Variable %s does not exist!", Sexp_nodes[n].text);
			return;
		}

		if (!(Sexp_variables[sexp_var].type & SEXP_VARIABLE_NUMBER))
		{
			Warning(LOCATION, "play-sound-from-file: Variable %s must be a number variable!", Sexp_variables[sexp_var].variable_name);
			return;
		}
	}

	// load sound file
	sexp_load_music(filename, type, sexp_var);
	
	// play sound file
	sexp_start_music(loop, sexp_var);

	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_string(filename);
		Current_sexp_network_packet.send_bool(loop);
		Current_sexp_network_packet.send_int(type);
		Current_sexp_network_packet.send_int(sexp_var);
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_play_sound_from_file()
{
	char filename[NAME_LENGTH];
	bool loop = false;
	int type = -1;
	int sexp_var = -1;

	if (!Current_sexp_network_packet.get_string(filename))
		return;
	Current_sexp_network_packet.get_bool(loop);
	Current_sexp_network_packet.get_int(type);
	Current_sexp_network_packet.get_int(sexp_var);

	sexp_load_music(filename, type, sexp_var);
	sexp_start_music(loop, sexp_var);
}

// Goober5000
void sexp_pause_sound_from_file(int n)
{
	bool pause = is_sexp_true(n);
	n = CDR(n);

	// we might have a variable
	int sexp_var = -1;
	if (n >= 0)
	{
		sexp_var = sexp_get_variable_index(n);
		if (sexp_var < 0)
		{
			Warning(LOCATION, "pause-sound-from-file: Variable %s does not exist!", Sexp_nodes[n].text);
			return;
		}

		if (!(Sexp_variables[sexp_var].type & SEXP_VARIABLE_NUMBER))
		{
			Warning(LOCATION, "pause-sound-from-file: Variable %s must be a number variable!", Sexp_variables[sexp_var].variable_name);
			return;
		}
	}

	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_bool(pause);
		Current_sexp_network_packet.send_int(sexp_var);
		Current_sexp_network_packet.end_callback();
	}

	sexp_pause_unpause_music(pause, sexp_var);
}

void multi_sexp_pause_sound_from_file()
{
	bool pause; 
	int sexp_var = -1;

	if (!Current_sexp_network_packet.get_bool(pause))
		return;
	Current_sexp_network_packet.get_int(sexp_var);

	sexp_pause_unpause_music(pause, sexp_var);
}

int sexp_sound_environment_option_lookup(const char *text)
{
	int i;

	Assert(text != nullptr);
	if (text == nullptr) {
		return -1;
	}
	
	for (i = 0; i < Num_sound_environment_options; i++) {
		if (!strcmp(text, Sound_environment_option[i])) {
			return i;
		}
	}

	return -1;
}

// Taylor
void sexp_set_sound_environment(int node)
{
	int n = node;
	sound_env env;
	int preset_id = -1;
	bool is_nan, is_nan_forever;

	auto preset = CTEXT(n);
	n = CDR(n);

	if ( preset && !stricmp(preset, SEXP_NONE_STRING) ) {
		sound_env_disable();
		return;
	}

	preset_id = ds_eax_get_preset_id( preset );
	if (preset_id < 0) {
		return;
	}

	// fill in defaults for this preset, in case we don't set everything
	if ( sound_env_get(&env, preset_id) ) {
		return;
	}

	while (n >= 0) {
		int option = sexp_sound_environment_option_lookup(CTEXT(n));
		n = CDR(n);

		// watch out for bogus options
		if (n < 0) {
			break;
		}

		float val = (float)eval_num(n, is_nan, is_nan_forever) / 1000.0f;
		n = CDR(n);
		if (is_nan || is_nan_forever)
			continue;

		if ( option == SEO_VOLUME ) {
			env.volume = val;
		} else if ( option == SEO_DECAY_TIME ) {
			env.decay = val;
		} else if ( option == SEO_DAMPING ) {
			env.damping = val;
		}
	}
	
	sound_env_set(&env);
}

// Taylor
void sexp_update_sound_environment(int node)
{
	int n = node;
	bool is_nan, is_nan_forever;

	while (n >= 0) {
		int option = sexp_sound_environment_option_lookup(CTEXT(n));
		n = CDR(n);

		// watch out for bogus options
		if (n < 0) {
			break;
		}

		float val = (float)eval_num(n, is_nan, is_nan_forever) / 1000.0f;
		n = CDR(n);
		if (is_nan || is_nan_forever)
			continue;

		if ( option == SEO_VOLUME ) {
			ds_eax_set_volume(val);
		} else if ( option == SEO_DECAY_TIME ) {
			ds_eax_set_decay_time(val);
		} else if ( option == SEO_DAMPING ) {
			ds_eax_set_damping(val);
		}
	}
}

//The E
	//From sexp help:
	//{ OP_ADJUST_AUDIO_VOLUME, "adjust-audio-volume\r\n"
	//	"Adjusts the relative volume of one sound type. Takes 2 or 3 arguments....\r\n"
	//	"\t1:\tSound Type to adjust, either Music, Voice or Effects\r\n"
	//	"\t2:\tPercentage of the users' settings to adjust to, 0 will be silence, 100 means the maximum volume as set by the user\r\n"
	//	"\t3:\tFade time (optional), time in milliseconds to adjust the volume"},

int audio_volume_option_lookup(const char *text)
{
	int i;

	Assert(text != nullptr);
	if (text == nullptr) {
		return -1;
	}
	
	for (i = 0; i < Num_adjust_audio_options; i++) {
		if (!strcmp(text, Adjust_audio_options[i])) {
			return i;
		}
	}

	return -1;
}

void sexp_adjust_audio_volume(int node)
{
	int n = node;
	bool is_nan, is_nan_forever;

	if (n > 0) {
		int option = audio_volume_option_lookup(CTEXT(n));
		if (option >= 0) {
			n = CDR(n);

			float target_volume = 1.0f;
			if (n >= 0) {
				target_volume = (float)eval_num(n, is_nan, is_nan_forever) / 100.0f;
				CLAMP(target_volume, 0.0f, 1.0f);
				n = CDR(n);

				if (is_nan || is_nan_forever)
					return;
			}

			int time = 0;
			if (n >= 0) {
				time = eval_num(n, is_nan, is_nan_forever);
				n = CDR(n);

				if (is_nan || is_nan_forever)
					return;
			}

			snd_adjust_audio_volume(option, target_volume, time);
		}
	}
}

int sexp_explosion_option_lookup(const char *text)
{
	int i;

	Assert(text != nullptr);
	if (text == nullptr) {
		return -1;
	}
	
	for (i = 0; i < Num_explosion_options; i++) {
		if (!strcmp(text, Explosion_option[i])) {
			return i;
		}
	}

	return -1;
}

// Goober5000
void sexp_set_explosion_option(int node)
{
	int n = node;
	ship_info *sip;
	shockwave_create_info *sci;
	bool is_nan, is_nan_forever;

	// get ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	auto shipp = ship_entry->shipp();
	sip = &Ship_info[shipp->ship_info_index];
	sci = &sip->shockwave;


	// if we haven't changed anything yet, create a new special-exp with the same values as a standard exp
	if (!shipp->use_special_explosion)
	{
		shipp->special_exp_damage = fl2i(sci->damage);
		shipp->special_exp_blast = fl2i(sci->blast);
		shipp->special_exp_inner = fl2i(sci->inner_rad);
		shipp->special_exp_outer = fl2i(sci->outer_rad);
		shipp->special_exp_shockwave_speed = fl2i(sci->speed);
		shipp->special_exp_deathroll_time = 0;

		shipp->use_special_explosion = true;
		shipp->use_shockwave = (sci->speed > 0);
	}

	// process all options
	while (n >= 0)
	{
		int option = sexp_explosion_option_lookup(CTEXT(n));
		n = CDR(n);

		// watch out for bogus options
		if (n < 0)
			break;

		int val = eval_num(n, is_nan, is_nan_forever);
		Assert(val >= 0);	// should be true due to OPF_POSITIVE
		n = CDR(n);

		if (is_nan || is_nan_forever) {
			continue;
		} else if (option == EO_DAMAGE) {
			shipp->special_exp_damage = val;
		} else if (option == EO_BLAST) {
			shipp->special_exp_blast = val;
		} else if (option == EO_INNER_RADIUS) {
			shipp->special_exp_inner = val;
		} else if (option == EO_OUTER_RADIUS) {
			shipp->special_exp_outer = val;
		} else if (option == EO_SHOCKWAVE_SPEED) {
			shipp->special_exp_shockwave_speed = val;
			shipp->use_shockwave = (val > 0);
		} else if (option == EO_DEATH_ROLL_TIME) {
			shipp->special_exp_deathroll_time = val;

			// hmm, it would be cool to modify the explosion in progress
			if (shipp->flags[Ship::Ship_Flags::Dying] && val >= 2) {
				shipp->final_death_time = timestamp(val);
			}
		}
	}

	// if all our values are the same as a standard exp, turn off the special exp
	if ((shipp->special_exp_damage == sci->damage) && (shipp->special_exp_blast == sci->blast) && (shipp->special_exp_inner == sci->inner_rad)
		&& (shipp->special_exp_outer == sci->outer_rad) && (shipp->special_exp_shockwave_speed == sci->speed) && (shipp->special_exp_deathroll_time == 0))
	{
		shipp->use_special_explosion = false;
		shipp->use_shockwave = false;

		shipp->special_exp_damage = -1;
		shipp->special_exp_blast = -1;
		shipp->special_exp_inner = -1;
		shipp->special_exp_outer = -1;
		shipp->special_exp_shockwave_speed = -1;
		shipp->special_exp_deathroll_time = 0;
	}
}

void sexp_create_bolt(int n)
{
	int bolt = get_bolt_type_by_name(CTEXT(n));

	if (bolt < 0)
		return;

	float coords[6];
	bool is_nan, is_nan_forever;
	for (int i = 0; i <= 5; i++) {
		n = CDR(n);
		int buf = eval_num(n, is_nan, is_nan_forever);

		if (is_nan || is_nan_forever)
			return;

		coords[i] = i2fl(buf);
	}
	n = CDR(n);

	bool playSound = true;
	
	if (n >= 0)
		playSound = eval_sexp(CDR(n));

	vec3d origin = vm_vec_new(coords[0], coords[1], coords[2]);
	vec3d dest = vm_vec_new(coords[3], coords[4], coords[5]);
	
	nebl_bolt(bolt, &origin, &dest, playSound);
}

// Goober5000
// Basically, this function pretends that there's a ship at the origin that's blowing up, and
// it does stuff accordingly.  In some places, it has to tiptoe around a little because the
// code often expects a parent object when in fact there is none. <.<  >.>
void sexp_explosion_effect(int n)
{
	vec3d origin;
	int max_damage, max_blast, explosion_size, inner_radius, outer_radius, shockwave_speed, num, fireball_type;
	int emp_intensity, emp_duration;
	bool use_emp_time_for_capship_turrets, is_nan, is_nan_forever;

	Assert( n >= 0 );

	// read in data --------------------------------
	std::array<int, 9> numbers;
	eval_array(numbers, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	origin.xyz.x = (float)numbers[0];
	origin.xyz.y = (float)numbers[1];
	origin.xyz.z = (float)numbers[2];
	max_damage = numbers[3];
	max_blast = numbers[4];
	explosion_size = numbers[5];
	inner_radius = numbers[6];
	outer_radius = numbers[7];
	shockwave_speed = numbers[8];

	// fireball type
	// -------------

	// this node is another SEXP operator or a plain number
	num = -1;
	if (CAR(n) != -1 || Sexp_nodes[n].subtype == SEXP_ATOM_NUMBER)
	{
		num = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
	}
	else if (sexp_can_construe_as_integer(n))
		num = sexp_atoi(n);

	// is it a number?
	if (num >= 0)
	{
		if (num == 0)
		{
			fireball_type = FIREBALL_EXPLOSION_MEDIUM;
		}
		else if (num == 1)
		{
			fireball_type = FIREBALL_EXPLOSION_LARGE1;
		}
		else if (num == 2)
		{
			fireball_type = FIREBALL_EXPLOSION_LARGE2;
		}
		else if (!SCP_vector_inbounds(Fireball_info, num))
		{
			Warning(LOCATION, "explosion-effect fireball type is out of range; quitting the explosion...\n");
			return;
		}
		else
		{
			fireball_type = num;
		}
	}
	// it's gotta be a name
	else
	{
		const char *unique_id = CTEXT(n);
		fireball_type = fireball_info_lookup(unique_id);

		if (fireball_type < 0)
		{
			Warning(LOCATION, "unrecognized fireball entry \'%s\'; quitting the explosion...\n", unique_id);
			return;
		}
	}
	n = CDR(n);

	// -------------

	auto sound_index = sexp_get_sound_index(n);
	n = CDR(n);

	// optional EMP
	eval_nums(n, is_nan, is_nan_forever, emp_intensity, emp_duration);
	if (is_nan || is_nan_forever)
		return;
	use_emp_time_for_capship_turrets = false;
	if (n != -1)
	{
		use_emp_time_for_capship_turrets = is_sexp_true(n);
		n = CDR(n);
	}


	// play sound effect ---------------------------
	if (sound_index.isValid())
	{
		snd_play_3d( gamesnd_get_game_sound(sound_index), &origin, &View_position, 0.0f, nullptr, 0, 1.0f, SND_PRIORITY_MUST_PLAY  );
	}


	// create the fireball -------------------------
	if (explosion_size && inner_radius && outer_radius)
	{
		if(fireball_type == FIREBALL_EXPLOSION_MEDIUM)
			fireball_create( &origin, fireball_type, FIREBALL_MEDIUM_EXPLOSION, -1, (float)explosion_size );
		else
			fireball_create( &origin, fireball_type, FIREBALL_LARGE_EXPLOSION, -1, (float)explosion_size );
	}

	// apply area affect damage --------------------
	if (max_damage || max_blast)
	{
		if ( shockwave_speed > 0 )
		{
			shockwave_create_info sci;
			shockwave_create_info_init(&sci);

			sci.inner_rad = (float)inner_radius;
			sci.outer_rad = (float)outer_radius;
			sci.blast = (float)max_blast;
			sci.damage = (float)max_damage;
			sci.speed = (float)shockwave_speed;
			sci.rot_angles.p = frand_range(0.0f, 1.99f*PI);
			sci.rot_angles.b = frand_range(0.0f, 1.99f*PI);
			sci.rot_angles.h = frand_range(0.0f, 1.99f*PI);
			shockwave_create(-1, &origin, &sci, SW_SHIP_DEATH);
		}
		else
		{
			float t_blast = 0.0f;
			float t_damage = 0.0f;
			for (auto objp: list_range(&obj_used_list))
			{
				if (objp->flags[Object::Object_Flags::Should_be_dead])
					continue;

				if ( (objp->type != OBJ_SHIP) && (objp->type != OBJ_ASTEROID) )
				{
					continue;
				}
		
				// don't blast no-collide or navbuoys
				if ( !objp->flags[Object::Object_Flags::Collides] || (objp->type == OBJ_SHIP && ship_get_SIF(objp->instance)[Ship::Info_Flags::Navbuoy]) )
				{
					continue;
				}

				if ( ship_explode_area_calc_damage( &origin, &objp->pos, (float)inner_radius, (float)outer_radius, (float)max_damage, (float)max_blast, &t_damage, &t_blast ) == -1 )
				{
					continue;
				}

				vec3d force = vmd_zero_vector;
				vec3d vec_ship_to_impact = objp->pos - origin;	
				switch ( objp->type )
				{
					case OBJ_SHIP:
						ship_apply_global_damage( objp, nullptr, &origin, t_damage, -1 );
						vec_ship_to_impact = objp->pos - origin;
						if (!IS_VEC_NULL_SQ_SAFE( &vec_ship_to_impact )) {
							vm_vec_copy_normalize( &force, &vec_ship_to_impact );
							force *= (float)max_blast;
							ship_apply_whack( &force, &origin, objp );
						}
						break;

					case OBJ_ASTEROID:
						if (!IS_VEC_NULL_SQ_SAFE(&vec_ship_to_impact)) {
							vm_vec_copy_normalize(&force, &vec_ship_to_impact);
							force *= (float)max_blast;
						}
						asteroid_hit(objp, nullptr, nullptr, t_damage, &force);
						break;
	
					default:
						Assertion(false, "Object magically changed type after exploding!");
						break;
				}
			}	// end for
		}
	}


	// apply emp damage if applicable --------------
	if (emp_intensity && emp_duration)
	{
		emp_apply(&origin, (float)inner_radius, (float)outer_radius, (float)emp_intensity, (float)emp_duration, use_emp_time_for_capship_turrets);
	}
}

// Goober5000
void sexp_warp_effect(int n)
{
	vec3d origin, location, v_orient;
	matrix m_orient;
	int num, shape, fireball_type, extra_flags = FBF_WARP_VIA_SEXP;
	float radius, duration, warp_open_duration, warp_close_duration;
	bool is_nan, is_nan_forever;

	// read in data --------------------------------
	std::array<float, 8> numbers;
	eval_array(numbers, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	origin.xyz.x = numbers[0];
	origin.xyz.y = numbers[1];
	origin.xyz.z = numbers[2];

	location.xyz.x = numbers[3];
	location.xyz.y = numbers[4];
	location.xyz.z = numbers[5];

	radius = numbers[6];
	duration = numbers[7];

	if (duration < 4.0f)
		duration = 4.0f;

	auto warp_open_sound_index = sexp_get_sound_index(n);
	n = CDR(n);
	auto warp_close_sound_index = sexp_get_sound_index(n);
	n = CDR(n);

	// fireball type
	// -------------

	// this node is another SEXP operator or a plain number
	num = -1;
	if (CAR(n) != -1 || Sexp_nodes[n].subtype == SEXP_ATOM_NUMBER)
	{
		num = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
	}
	else if (sexp_can_construe_as_integer(n))
		num = sexp_atoi(n);

	// is it a number?
	if (num >= 0)
	{
		if (num == 0)
		{
			fireball_type = FIREBALL_WARP;
		}
		else if (num == 1)
		{
			fireball_type = FIREBALL_KNOSSOS;
		}
		else if (!SCP_vector_inbounds(Fireball_info, num))
		{
			Warning(LOCATION, "warp-effect fireball type is out of range; quitting the warp...\n");
			return;
		}
		else
		{
			fireball_type = num;
		}
	}
	// it's gotta be a name
	else
	{
		const char *unique_id = CTEXT(n);
		fireball_type = fireball_info_lookup(unique_id);

		if (fireball_type < 0)
		{
			Warning(LOCATION, "unrecognized fireball entry \'%s\'; quitting the warp...\n", unique_id);
			return;
		}
	}
	n = CDR(n);

	// -------------

	// shape
	shape = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	if (shape == 0)
	{
		// do nothing; this is standard
	}
	else if (shape == 1)
	{
		extra_flags |= FBF_WARP_3D;
	}
	else
	{
		Warning(LOCATION, "warp-effect shape is out of range; quitting the warp...\n");
		return;
	}
	n = CDR(n);

	// opening and closing durations
	warp_open_duration = warp_close_duration = -1.0f;
	if (n >= 0)
	{
		warp_open_duration = warp_close_duration = ((float)eval_num(n, is_nan, is_nan_forever)) / 1000.0f;
		n = CDR(n);

		if (is_nan || is_nan_forever)
			return;
	}
	if (n >= 0)
	{
		warp_close_duration = ((float)eval_num(n, is_nan, is_nan_forever)) / 1000.0f;
		n = CDR(n);

		if (is_nan || is_nan_forever)
			return;
	}

	// sanity check, if these were specified
	if (duration < warp_open_duration + warp_close_duration)
	{
		Warning(LOCATION, "Both warp opening and warp closing must occur within the duration of the warp effect.  Adjusting opening and closing durations to fit.");
		warp_open_duration = warp_close_duration = duration / 2.0f;
	}


	// calculate orientation matrix ----------------

	vm_vec_sub(&v_orient, &location, &origin);

	if (IS_VEC_NULL_SQ_SAFE(&v_orient))
	{
		Warning(LOCATION, "error in warp-effect: warp can't point to itself; quitting the warp...\n");
		return;
	}

	vm_vector_2_matrix(&m_orient, &v_orient, nullptr, nullptr);

	// create fireball -----------------------------

	fireball_create(&origin, fireball_type, FIREBALL_WARP_EFFECT, -1, radius, false, nullptr, duration, -1, &m_orient, 0, extra_flags, warp_open_sound_index, warp_close_sound_index, warp_open_duration, warp_close_duration);
}

void sexp_set_gravity_accel(int node)
{
	Assert(node >= 0);

	bool is_nan, is_nan_forever;
	int accel = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	float fl_accel = (float)(-accel) / 100.0f;
	vec3d old_gravity = The_mission.gravity;
	The_mission.gravity = vm_vec_new(0.0f, fl_accel, 0.0f);

	if ((IS_VEC_NULL(&old_gravity) && !IS_VEC_NULL(&The_mission.gravity)) ||
		(!IS_VEC_NULL(&old_gravity) && IS_VEC_NULL(&The_mission.gravity))) {
		// gravity was turned on or turned off
		collide_apply_gravity_flags_weapons();
	}

	// do the multiplayer callback
	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_float(fl_accel);
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_set_gravity_accel()
{
	float accel = 0.0f;

	Current_sexp_network_packet.get_float(accel);

	vec3d old_gravity = The_mission.gravity;
	The_mission.gravity = vm_vec_new(0.0f, accel, 0.0f);
	if ((IS_VEC_NULL(&old_gravity) && !IS_VEC_NULL(&The_mission.gravity)) ||
		(!IS_VEC_NULL(&old_gravity) && IS_VEC_NULL(&The_mission.gravity))) {
		// gravity was turned on or turned off
		collide_apply_gravity_flags_weapons();
	}
}

void sexp_force_rearm(int node)
{
	// This is kiiiiinda hacky. All the related repair methods expect a docker and a dockee, but
	// nothing really special happens between them. It checks that they are same team and tells the docker
	// to undock when repair is completed. So with minimal changes we can skip the dock stuff and just tell
	// the code that the repairer is the repairee and all works as expected. Basically we simply created a
	// way for a ship to 'repair itself'.

	for (int n = node; n >= 0; n = CDR(n)) {

		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_objp())
			return;
		auto objp = ship_entry->objp();

		// Set flags
		ai_info* aip = &Ai_info[Ships[objp->instance].ai_index];
		aip->support_ship_signature = objp->signature;
		aip->support_ship_objnum = ship_entry->objnum;
		aip->submode_start_time = Missiontime;

		// Start the HUD stuff
		if (aip == Player_ai) {
			hud_support_view_start();
		}

		// Begin the repair
		ai_do_objects_repairing_stuff(objp, objp, REPAIR_INFO_BEGIN);
	}
}

void sexp_abort_rearm(int node)
{
	for (int n = node; n >= 0; n = CDR(n)) {

		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_objp())
			return;
		auto objp = ship_entry->objp();

		ai_info* aip = &Ai_info[Ships[objp->instance].ai_index];

		// Begin the repair
		ai_do_objects_repairing_stuff(objp, &Objects[aip->support_ship_objnum], REPAIR_INFO_ABORT);
	}
}

// this function get called by send-message or send-message-random with the name of the message, sender,
// and priority.
void sexp_send_one_message( const char *name, const char *who_from, const char *priority, int group, int delay, int event_num = -1, bool do_hash_fallback = false )
{
	int ipriority, source;
	int ship_index = -1;
	int wingnum = -1;
	ship *shipp;

	if(physics_paused){
		return;
	}

	Assert( (name != nullptr) && (who_from != nullptr) && (priority != nullptr) );

	// determine the priority of the message
	if ( !stricmp(priority, "low") )
		ipriority = MESSAGE_PRIORITY_LOW;
	else if ( !stricmp(priority, "normal") )
		ipriority = MESSAGE_PRIORITY_NORMAL;
	else if ( !stricmp(priority, "high") )
		ipriority = MESSAGE_PRIORITY_HIGH;
	else {
		Warning(LOCATION, "Encountered invalid priority \"%s\" in send-message", priority);
		ipriority = MESSAGE_PRIORITY_NORMAL;
	}

	// do some pre-checks
	auto ship_entry = ship_registry_get(who_from);
	if (!ship_entry)
		wingnum = wing_lookup(who_from);

	// check to see if the 'who_from' string is a ship that had been destroyed or departed.  If so,
	// then don't send the message.  We must look at 'who_from' to determine what to look for.  who_from
	// may be any allied person, any wingman, a wingman from a specific wing, or a specific ship
	shipp = nullptr;
	source = MESSAGE_SOURCE_COMMAND;
	if (is_special_sender(who_from)) {
		message_send_unique( name, &(who_from[1]), MESSAGE_SOURCE_SPECIAL, ipriority, group, delay, event_num );
		return;
	} else if ( !ship_entry && wingnum < 0 && do_hash_fallback ) {
		// ship/wing not found, so act as if this who_from has a hash prepended to it
		message_send_unique( name, who_from, MESSAGE_SOURCE_SPECIAL, ipriority, group, delay, event_num );
		return;
	} else if (!stricmp(who_from, "<any allied>")) {
		return;
	} else if ( wingnum >= 0 && Wings[wingnum].current_count > 0 ) {
		// message from a wing
		// choose wing leader to speak for wing (hence "1" at end of ship_get_random_ship_in_wing)
		ship_index = ship_get_random_ship_in_wing( wingnum, SHIP_GET_UNSILENCED, 1 );
		if ( ship_index == -1 ) {
			if ( ipriority != MESSAGE_PRIORITY_HIGH )
				return;
			source = MESSAGE_SOURCE_COMMAND;
		}
	} else if ( ship_entry && ship_entry->shipnum >= 0 ) {
		// Message from a specific ship
		ship_index = ship_entry->shipnum;

	} else if ( ship_entry || wingnum >= 0  ) {
		// getting into this if statement means that the ship or wing (sender) is no longer (or not yet) in the mission
		// if message is high priority, make it come from Terran Command
		if ( ipriority != MESSAGE_PRIORITY_HIGH )
			return;
		source = MESSAGE_SOURCE_COMMAND;

	} else if ( !stricmp(who_from, "<any wingman>") || (wing_name_lookup(who_from) != -1) ) {
		source = MESSAGE_SOURCE_WINGMAN;
	} else if ( !stricmp(who_from, "<none>") ) {
		source = MESSAGE_SOURCE_NONE;
	} else {
		Warning(LOCATION, "Invalid message source '%s'.  Unable to send message.", who_from);
		return;
	}

	if ( ship_index == -1 ){
		shipp = nullptr;
	} else {
		shipp = &Ships[ship_index];
		source = MESSAGE_SOURCE_SHIP;
	}

	message_send_unique( name, shipp, source, ipriority, group, delay, event_num );
}

void sexp_send_message(int n)
{
	if(physics_paused){
		return;
	}

	Assert ( n != -1 );
	auto who_from = CTEXT(n);
	auto priority = CTEXT(CDR(n));
	auto name = CTEXT(CDR(CDR(n)));

	// a temporary check to see if the name field matched a priority since I am in the process
	// of reordering the arguments
	if ( !stricmp(name, "low") || !stricmp(name, "normal") || !stricmp(name, "high") ) {
		auto tmp = name;
		name = priority;
		priority = tmp;
	}

	// we might override the sender
	bool do_hash_fallback = false;
	if (The_mission.flags[Mission::Mission_Flags::Override_hashcommand] && !stricmp(who_from, DEFAULT_HASHCOMMAND)) {
		who_from = The_mission.command_sender;
		do_hash_fallback = true;
	}

	sexp_send_one_message( name, who_from, priority, 0, 0, -1, do_hash_fallback );
}

ship* get_builtin_message_sender(const char* name) {
	auto ship_entry = ship_registry_get(name);
	if (ship_entry) {
		return ship_entry->shipp_or_null();
	}
	
	auto wing_index = wing_lookup(name);
	if (wing_index >= 0) {
		auto ship_index = ship_get_random_ship_in_wing(wing_index, SHIP_GET_UNSILENCED);
		return ship_index < 0 ? nullptr : &Ships[ship_index];
	}

	if (!stricmp(name, "<any wingman>")) {
		auto ship_index = ship_get_random_player_wing_ship(SHIP_GET_UNSILENCED);
		return ship_index < 0 ? nullptr : &Ships[ship_index];
	}

	return nullptr;
}

void sexp_send_builtin_message(int n) {
	// Argument 1: Message type
	auto type = get_builtin_message_type(CTEXT(n));
	n = CDR(n);
	// Argument 2: Subject (or <none>)
	auto subject_name = CTEXT(n);
	auto need_subject = stricmp(subject_name, SEXP_NONE_STRING) != 0;
	auto subject_entry = ship_registry_get(subject_name);
	auto have_subject = subject_entry && subject_entry->has_shipp();
	n = CDR(n);
	// Argument 3: Shuffle the senders?
	auto shuffle = is_sexp_true(n);
	n = CDR(n);
	// Argument 4+: Possible senders
	SCP_vector<const char*> sender_names;
	while (n >= 0) {
		sender_names.push_back(CTEXT(n));
		n = CDR(n);
	}

	// Done consuming arguments; maybe short-circuit?
	if (type == MESSAGE_NONE) {
		return;
	}
	if (need_subject && !have_subject) {
		return;
	}

	if (shuffle) {
		std::random_device rd;
		std::shuffle(sender_names.begin(), sender_names.end(), std::mt19937(rd()));
	}

	auto subject = have_subject ? subject_entry->shipp() : nullptr;
	for (auto sender_name : sender_names) {
		if (is_special_sender(sender_name)) {
			message_send_builtin(type, nullptr, subject);
			return;
		} else {
			auto sender = get_builtin_message_sender(sender_name);
			if (sender != nullptr) {
				message_send_builtin(type, sender, subject);
				return;
			}
		}
	}
}

void sexp_send_message_list(int n, bool send_message_chain)
{
	int delay = 0, event_num = -1;
	bool is_nan, is_nan_forever;

	if(physics_paused){
		return;
	}

	// find the event that will cancel the message chain
	if (send_message_chain) {
		auto name = CTEXT(n);
		n = CDR(n);

		for (int i = 0; i < (int)Mission_events.size(); ++i) {
			if (!stricmp(Mission_events[i].name.c_str(), name)) {
				event_num = i;
				break;
			}
		}
		if (event_num < 0) {
			return;
		}
	}

	// send a bunch of messages
	while(n != -1){
		auto who_from = CTEXT(n);
		n = CDR(n);

		// next node
		if(n == -1){
			Warning(LOCATION, "Detected incomplete parameter list in sexp-send-message-list");
			return;
		}
		auto priority = CTEXT(n);
		n = CDR(n);

		// next node
		if(n == -1){
			Warning(LOCATION, "Detected incomplete parameter list in sexp-send-message-list");
			return;
		}
		auto name = CTEXT(n);
		n = CDR(n);

		// next node
		if(n == -1){
			Warning(LOCATION, "Detected incomplete parameter list in sexp-send-message-list");
			return;
		}
		delay += eval_num(n, is_nan, is_nan_forever);
		n = CDR(n);

		if (is_nan || is_nan_forever) {
			Warning(LOCATION, "Encountered a NaN in sexp-send-message-list");
			return;
		}

		// we might override the sender
		bool do_hash_fallback = false;
		if (The_mission.flags[Mission::Mission_Flags::Override_hashcommand] && !stricmp(who_from, DEFAULT_HASHCOMMAND)) {
			who_from = The_mission.command_sender;
			do_hash_fallback = true;
		}

		// send the message
		sexp_send_one_message( name, who_from, priority, 1, delay, event_num, do_hash_fallback );
	}
}

void sexp_send_random_message(int n)
{
	int temp, num_messages, message_num;

	Assert ( n != -1 );
	auto who_from = CTEXT(n);
	auto priority = CTEXT(CDR(n));

	if(physics_paused){
		return;
	}

	// count the number of messages that we have
	n = CDR(CDR(n));
	temp = n;
	num_messages = 0;
	while ( n != -1 ) {
		n = CDR(n);
		num_messages++;
	}
	Assert ( num_messages >= 1 );
	
	// get a random message, and pass the parameters to send_one_message
	message_num = Random::next(num_messages);
	n = temp;
	while ( n != -1 ) {
		if ( message_num == 0 )
			break;
		message_num--;
		n = CDR(n);
	}
	Assert (n != -1);		// should have found the message!!!
	auto name = CTEXT(n);

	// we might override the sender
	bool do_hash_fallback = false;
	if (The_mission.flags[Mission::Mission_Flags::Override_hashcommand] && !stricmp(who_from, DEFAULT_HASHCOMMAND)) {
		who_from = The_mission.command_sender;
		do_hash_fallback = true;
	}

	sexp_send_one_message( name, who_from, priority, 0, 0, -1, do_hash_fallback );
}

void sexp_self_destruct(int node)
{
	for (int n = node; n != -1; n = CDR(n))	{
		// get the ship
		auto ship_entry = eval_ship(n);

		// if it still exists, destroy it
		if (ship_entry && ship_entry->status == ShipStatus::PRESENT) {
			ship_self_destruct(ship_entry->objp());
		}
	}
}

void sexp_cancel_future_waves(int node)
{
	for (int n = node; n != -1; n = CDR(n))
	{
		auto wingp = eval_wing(n);
		if (wingp)
		{
			// prevent any more waves from arriving by marking this as the last wave
			wingp->num_waves = wingp->current_wave;

			// we might need to clean up this wing if there are no ships currently in the mission
			wing_maybe_cleanup(wingp);
		}
	}
}

void sexp_next_mission(int n)
{
	auto mission_name = CTEXT(n);

	if (mission_name == nullptr) {
		Error( LOCATION, "Mission name is NULL in campaign file for next-mission command!");
	}

	for (int i = 0; i < Campaign.num_missions; ++i) {
		if ( !stricmp(Campaign.missions[i].name, mission_name) ) {
			Campaign.next_mission = i;
			return;
		}
	}
	Error(LOCATION, "Mission name %s not found in campaign file for next-mission command", mission_name);
}

/**
 * Deal with the end-of-campaign sexpression. 
 */
void sexp_end_of_campaign(int  /*n*/)
{
	// this is really a do-nothing sexpression.  It is pretty much a placeholder to allow
	// campaigns to have repeat-mission branches at the end of the campaign.  By not setting
	// anything in this function, the higher level campaign code will see this as end-of-campaign
	// since next_mission isn't set to anything.  (To be safe, we'll set to -1).
	Campaign.next_mission = -1;	
}

// sexpression to end everything.  One parameter is the movie to play when this is over.
// Goober5000 - edited to only to the FS2-specific code when actually ending the FS2 main
// campaign, and otherwise to do the conventional code
void sexp_end_campaign(int n)
{
	bool ignore_player_mortality = true;

	if (!(Game_mode & GM_CAMPAIGN_MODE)) {
		return;
	}

	if (n != -1) {
		ignore_player_mortality = is_sexp_true(n);
	}

	// if the player is dead we may want to let the death screen handle things
	if (!ignore_player_mortality && (Player_ship->flags[Ship::Ship_Flags::Dying])) {
		return;
	}

	// in FS2 our ending is a bit wacky. we'll just flag the mission as having ended the campaign	
	//
	// changed this to check for an active supernova rather than a special campaign since the supernova
	// code needs special time to execute and will post GS_EVENT_END_CAMPAIGN with Game_mode check
	// or show death-popup when it's done - taylor
	if (supernova_active() /*&& !stricmp(Campaign.filename, "freespace2")*/) {
		Campaign_ending_via_supernova = 1;
	} else {
		// post and event to move us to the end-of-campaign state
		gameseq_post_event(GS_EVENT_END_CAMPAIGN);
	}
}

void set_subsys_strength_and_maybe_ancestors(ship *shipp, ship_subsys *ss, polymodel *pm, const int *assign_percent, const int *repair_percent, const int *sabotage_percent, bool do_submodel_repair, bool repair_ancestors)
{
	Assertion(shipp != nullptr && ss != nullptr, "Ship and subsys must not be null!");
	Assertion(assign_percent != nullptr || repair_percent != nullptr || sabotage_percent != nullptr, "Either assign_percent or repair_percent or sabotage_percent must not be null!");

	bool originally_zero = (ss->current_hits <= 0);
	if (ss->submodel_instance_1 && ss->submodel_instance_1->blown_off)
		originally_zero = true;
	if (ss->submodel_instance_2 && ss->submodel_instance_2->blown_off)
		originally_zero = true;

	if (assign_percent != nullptr)
	{
		int percentage = *assign_percent;
		Assertion(percentage >= 0 && percentage <= 100, "Percentage must be in range [0, 100]");

		// assign the hitpoints
		ss->current_hits = ss->max_hits * ((float)percentage / 100.0f);

		// maybe blow up subsys
		if (ss->current_hits <= 0 && !originally_zero)
			do_subobj_destroyed_stuff(shipp, ss, nullptr);
	}
	else if (repair_percent != nullptr)
	{
		int percentage = *repair_percent;
		Assertion(percentage > 0, "Repair percentage must be more than zero!");

		// repair the hitpoints
		float repair_hits = ss->max_hits * ((float)percentage / 100.0f);
		ss->current_hits += repair_hits;
		if (ss->current_hits > ss->max_hits)
			ss->current_hits = ss->max_hits;
	}
	else if (sabotage_percent != nullptr)
	{
		int percentage = *sabotage_percent;
		Assertion(percentage > 0, "Sabotage percentage must be more than zero!");

		// sabotage the hitpoints
		float sabotage_hits = ss->max_hits * ((float)percentage / 100.0f);
		ss->current_hits -= sabotage_hits;
		if (ss->current_hits < 0.0f)
			ss->current_hits = 0.0f;

		// maybe blow up subsys
		if (ss->current_hits <= 0 && !originally_zero)
			do_subobj_destroyed_stuff(shipp, ss, nullptr);
	}
	else
		return;

	// and now see if we are repairing from zero
	if (originally_zero && ss->current_hits > 0 && do_submodel_repair)
	{
		if (ss->submodel_instance_1)
			ss->submodel_instance_1->blown_off = false;
		if (ss->submodel_instance_2)
			ss->submodel_instance_2->blown_off = false;

		// special case for subsystems that don't correspond to a submodel
		check_subsystem_submodel_link(shipp, ss, false);

		// see if we are handling ancestors and if this subsystem has a submodel
		int subobj = ss->system_info->subobj_num;
		if (repair_ancestors && subobj >= 0)
		{
			if (pm == nullptr)
				pm = model_get(Ship_info[shipp->ship_info_index].model_num);

			// do we have a parent?
			int parent_subobj = pm->submodel[subobj].parent;
			if (parent_subobj >= 0)
			{
				// search for the subsystem
				for (ship_subsys *parent_ss = GET_FIRST(&shipp->subsys_list); parent_ss != END_OF_LIST(&shipp->subsys_list); parent_ss = GET_NEXT(parent_ss))
				{
					// found parent?
					if (parent_ss->system_info->subobj_num == parent_subobj)
					{
						bool parent_destroyed = (parent_ss->current_hits <= 0);
						if (parent_ss->submodel_instance_1 && parent_ss->submodel_instance_1->blown_off)
							parent_destroyed = true;
						if (parent_ss->submodel_instance_2 && parent_ss->submodel_instance_2->blown_off)
							parent_destroyed = true;

						// if the parent subobject was destroyed...
						if (parent_destroyed)
						{
							// ...repair the parent in the same way
							set_subsys_strength_and_maybe_ancestors(shipp, parent_ss, pm, assign_percent, repair_percent, sabotage_percent, do_submodel_repair, repair_ancestors);
						}

						// only one parent
						break;
					}
				}
			}
		}
	}
}

/**
 * Reduces the strength of a subsystem by the given percentage.
 *
 * If it is reduced to below 0%, then the hits of the subsystem are set to 0
 */
void sexp_sabotage_subsystem(int n)
{
	int	percentage, subsystem_node;
	float sabotage_hits;
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	auto shipp = ship_entry->shipp();
	n = CDR(n);

	subsystem_node = n;
	n = CDR(n);

	percentage = eval_num(n, is_nan, is_nan_forever);
	n = CDR(n);

	if (is_nan || is_nan_forever)
		return;

	// abort if we're not even sabotaging anything
	if (percentage <= 0) {
		return;
	}

	bool any_subsystem = false;

	// process the single subsystem, which could be hull, sim hull, or a generic type like <all engines>
	process_ship_subsystems(ship_entry, true, subsystem_node, false, "sabotage-subsystem", [&](ProcessSubsystemType type, ship_subsys *ss) {
		if (type == ProcessSubsystemType::HULL) {
			float ihs;
			auto objp = ship_entry->objp();

			ihs = shipp->ship_max_hull_strength;
			sabotage_hits = ihs * (i2fl(percentage) / 100.0f);
			objp->hull_strength -= sabotage_hits;

			// self destruct the ship if <= 0.
			if (objp->hull_strength <= 0.0f)
				ship_self_destruct(objp);
		} else if (type == ProcessSubsystemType::SIM_HULL) {
			float ihs;
			auto objp = ship_entry->objp();

			ihs = shipp->ship_max_hull_strength;
			sabotage_hits = ihs * (i2fl(percentage) / 100.0f);
			objp->sim_hull_strength -= sabotage_hits;
		} else if (type == ProcessSubsystemType::SHIELDS) {
			Warning(LOCATION, "sabotage-subsystem does not support " SEXP_SHIELD_STRING);
		} else {
			// get the pointer to the subsystem.  Check its current hits against its max hits, and
			// set the strength to the given percentage if current strength is > given percentage
			set_subsys_strength_and_maybe_ancestors(shipp, ss, nullptr, nullptr, nullptr, &percentage, false, false);
			any_subsystem = true;
		}
	});

	// recalculate when done
	if (any_subsystem)
		ship_recalc_subsys_strength(shipp);
}

/**
 * Adds some percentage of hits to a subsystem.
 * 
 * Anything repaired about 100% is set to max hits
 */
void sexp_repair_subsystem(int n)
{
	int	percentage, subsystem_node;
	bool do_submodel_repair = true, do_ancestor_repair = true;
	float repair_hits;
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	auto shipp = ship_entry->shipp();
	n = CDR(n);

	subsystem_node = n;
	n = CDR(n);

	percentage = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	// abort if we're not even repairing anything
	if (percentage <= 0) {
		return;
	}

	if (n >= 0)
	{
		do_submodel_repair = is_sexp_true(n);
		n = CDR(n);

		if (n >= 0)
			do_ancestor_repair = is_sexp_true(n);
	}
	
	bool any_subsystem = false;

	// process the single subsystem, which could be hull, sim hull, or a generic type like <all engines>
	process_ship_subsystems(ship_entry, true, subsystem_node, false, "repair-subsystem", [&](ProcessSubsystemType type, ship_subsys *ss) {
		if (type == ProcessSubsystemType::HULL) {
			float ihs;
			auto objp = ship_entry->objp();

			ihs = shipp->ship_max_hull_strength;
			repair_hits = ihs * (i2fl(percentage) / 100.0f);
			objp->hull_strength += repair_hits;

			if (objp->hull_strength > ihs)
				objp->hull_strength = ihs;
		} else if (type == ProcessSubsystemType::SIM_HULL) {
			float ihs;
			auto objp = ship_entry->objp();

			ihs = shipp->ship_max_hull_strength;
			repair_hits = ihs * (i2fl(percentage) / 100.0f);
			objp->sim_hull_strength += repair_hits;

			if (objp->sim_hull_strength > ihs)
				objp->sim_hull_strength = ihs;
		} else if (type == ProcessSubsystemType::SHIELDS) {
			Warning(LOCATION, "repair-subsystem does not support " SEXP_SHIELD_STRING);
		} else {
			// get the pointer to the subsystem.  Check its current hits against its max hits, and
			// set the strength to the given percentage if current strength is < given percentage
			set_subsys_strength_and_maybe_ancestors(shipp, ss, nullptr, nullptr, &percentage, nullptr, do_submodel_repair, do_ancestor_repair);
			any_subsystem = true;
		}
	});

	// recalculate when done
	if (any_subsystem)
		ship_recalc_subsys_strength(shipp);
}

/**
 * Set a subsystem of a ship at a specific percentage
 */
void sexp_set_subsystem_strength(int n)
{
	int	percentage, subsystem_node;
	bool do_submodel_repair = true, do_ancestor_repair = true;
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	auto shipp = ship_entry->shipp();
	n = CDR(n);

	subsystem_node = n;
	n = CDR(n);

	percentage = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	if (n >= 0)
	{
		do_submodel_repair = is_sexp_true(n);
		n = CDR(n);

		if (n >= 0)
			do_ancestor_repair = is_sexp_true(n);
	}

	if ( percentage > 100 ) {
		mprintf(("Percentage for set-subsystem-strength > 100 on ship %s for subsystem '%s' -- setting to 100\n", ship_entry->name, CTEXT(subsystem_node)));
		percentage = 100;
	} else if ( percentage < 0 ) {
		mprintf(("Percentage for set-subsystem-strength < 0 on ship %s for subsystem '%s' -- setting to 0\n", ship_entry->name, CTEXT(subsystem_node)));
		percentage = 0;
	}

	bool any_subsystem = false;

	// process the single subsystem, which could be hull, sim hull, or a generic type like <all engines>
	process_ship_subsystems(ship_entry, true, subsystem_node, false, "set-subsystem-strength", [&](ProcessSubsystemType type, ship_subsys *ss) {
		if (type == ProcessSubsystemType::HULL) {
			float ihs;
			auto objp = ship_entry->objp();

			// destroy the ship if percentage is 0
			if (percentage == 0) {
				ship_self_destruct(objp);
			} else {
				ihs = shipp->ship_max_hull_strength;
				objp->hull_strength = ihs * (i2fl(percentage) / 100.0f);
			}
		} else if (type == ProcessSubsystemType::SIM_HULL) {
			float ihs;
			auto objp = ship_entry->objp();

			ihs = shipp->ship_max_hull_strength;
			objp->sim_hull_strength = ihs * (i2fl(percentage) / 100.0f);
		} else if (type == ProcessSubsystemType::SHIELDS) {
			Warning(LOCATION, "set-subsystem-strength does not support " SEXP_SHIELD_STRING);
		} else {
			// get the pointer to the subsystem.  Check its current hits against its max hits, and
			// set the strength to the given percentage
			set_subsys_strength_and_maybe_ancestors(shipp, ss, nullptr, &percentage, nullptr, nullptr, do_submodel_repair, do_ancestor_repair);
			any_subsystem = true;
		}
	});

	// recalculate when done
	if (any_subsystem)
		ship_recalc_subsys_strength(shipp);
}

// destroys a subsystem without explosions
void sexp_destroy_subsys_instantly(int n)
{
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	auto shipp = ship_entry->shipp();
	n = CDR(n);

	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_ship(shipp);
	}

	bool any_subsystem = false;

	// process the subsystems, which could be hull, sim hull, or a generic type like <all engines>
	process_ship_subsystems(ship_entry, true, n, true, "destroy-subsys-instantly", [&](ProcessSubsystemType type, ship_subsys *ss)
	{
		// this sexp is only for subsystems
		if (type == ProcessSubsystemType::SUBSYSTEM)
		{
			// do destruction stuff
			ss->current_hits = 0;
			do_subobj_destroyed_stuff(shipp, ss, nullptr, true);

			if (MULTIPLAYER_MASTER)
			{
				int subsys_index = ship_get_subsys_index(ss);
				Assert(subsys_index >= 0);
				Current_sexp_network_packet.send_int(subsys_index);
			}

			any_subsystem = true;
		}
	});

	// recalculate when done
	if (any_subsystem)
		ship_recalc_subsys_strength(shipp);

	if (MULTIPLAYER_MASTER)
		Current_sexp_network_packet.end_callback();
}

void multi_sexp_destroy_subsys_instantly()
{
	ship *shipp;
	int subsys_index;
	ship_subsys *ss;

	Current_sexp_network_packet.get_ship(shipp);

	// destroy subsystems
	while (Current_sexp_network_packet.get_int(subsys_index))
	{
		// find subsystem
		Assert(subsys_index >= 0);
		ss = ship_get_indexed_subsys(shipp, subsys_index);

		// do destruction stuff
		ss->current_hits = 0;
		do_subobj_destroyed_stuff(shipp, ss, nullptr, true);
	}

	// recalculate when done
	ship_recalc_subsys_strength(shipp);
}

/**
 * Changes the validity of a goal.
 * 
 * The flag paramater tells us whether to mark the goals as valid or invalid
 */
void sexp_change_goal_validity( int n, bool flag )
{
	while ( n != -1 ) {
		auto name = CTEXT(n);
		mission_goal_mark_valid( name, flag );

		n = CDR(n);
	}
}

// Goober5000
// yeesh - be careful of the cargo-no-deplete flag :p
int sexp_is_cargo(int n)
{
	auto cargo = CTEXT(n);
	n = CDR(n);

	auto ship_entry = eval_ship(n);
	if (!ship_entry)
		return SEXP_NAN;
	n = CDR(n);

	auto subsystem = (n >= 0) ? CTEXT(n) : nullptr;

	int cargo_index = -1;

	// exited (on the exited_ships list)?
	if (ship_entry->exited_index >= 0)
	{
		// can't check subsys of ships not in mission
		if (subsystem)
			return SEXP_NAN_FOREVER;

		cargo_index = Ships_exited[ship_entry->exited_index].cargo1;
	}
	// not arrived yet?
	else if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
	{
		// can't check subsys of ships not in mission
		if (subsystem)
			return SEXP_NAN;

		cargo_index = ship_entry->p_objp()->cargo1;
	}
	// in-mission?
	else if (ship_entry->has_shipp())
	{
		if (subsystem)
		{
			// find the ship subsystem
			ship_subsys *ss = ship_get_subsys(ship_entry->shipp(), subsystem);
			if (ss)
			{
				// set cargo
				cargo_index = ss->subsys_cargo_name;
			}
		}
		else
		{
			cargo_index = ship_entry->shipp()->cargo1;
		}
	}
	// probably vanished
	else
		return SEXP_NAN_FOREVER;

	// did we get any cargo
	if (cargo_index < 0)
		return SEXP_FALSE;

	// check cargo
	if (!stricmp(Cargo_names[cargo_index & CARGO_INDEX_MASK], cargo))
		return SEXP_TRUE;
	else
		return SEXP_FALSE;
}

// Goober5000
// yeesh - be careful of the cargo-no-deplete flag :p
void sexp_set_cargo(int n)
{
	auto cargo = CTEXT(n);
	n = CDR(n);

	auto ship_entry = eval_ship(n);
	if (!ship_entry)
		return;
	n = CDR(n);

	auto subsystem = (n >= 0) ? CTEXT(n) : nullptr;

	int cargo_index = -1;

	// find this cargo in the cargo list
	for (int i = 0; i < Num_cargo; ++i)
	{
		// found it?
		if (!stricmp(cargo, Cargo_names[i]))
		{
			cargo_index = i;
			break;
		}
	}

	// not found
	if (cargo_index == -1)
	{
		// make new entry if possible
		if (Num_cargo + 1 >= MAX_CARGO)
		{
			Warning(LOCATION, "set-cargo: Maximum number of cargo names (%d) reached.  Ignoring new name.\n", MAX_CARGO);
			return;
		}

		Assert(strlen(cargo) <= NAME_LENGTH - 1);

		cargo_index = Num_cargo;
		Num_cargo++;

		strcpy(Cargo_names[cargo_index], cargo);
	}

	// exited (on the exited_ships list)?
	if (ship_entry->exited_index >= 0)
	{
		return;
	}
	// not arrived yet?
	else if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
	{
		// can't check subsys of ships not in mission
		if (subsystem)
			return;

		ship_entry->p_objp()->cargo1 = char(cargo_index | (ship_entry->p_objp()->cargo1 & CARGO_NO_DEPLETE));
	}
	// in-mission?
	else if (ship_entry->has_shipp())
	{
		if (subsystem)
		{
			ship_subsys *ss = ship_get_subsys(ship_entry->shipp(), subsystem);
			if (ss)
			{
				// set cargo
				ss->subsys_cargo_name = cargo_index | (ss->subsys_cargo_name & CARGO_NO_DEPLETE);
			}
		}
		else
		{
			// simply set the ship cargo
			ship_entry->shipp()->cargo1 = char(cargo_index | (ship_entry->shipp()->cargo1 & CARGO_NO_DEPLETE));
		}
	}
}

/**
 * Transfer cargo from one ship to another
 */
void sexp_transfer_cargo(int n)
{
	// find the ships -- if neither in the mission, abort
	auto ship1 = eval_ship(n);
	if (!ship1 || !ship1->has_shipp())
		return;
	auto ship2 = eval_ship(CDR(n));
	if (!ship2 || !ship2->has_shipp())
		return;

	if ( !stricmp(Cargo_names[ship1->shipp()->cargo1 & CARGO_INDEX_MASK], "nothing") ) {
		return;
	}

	// transfer cargo from ship1 to ship2
#ifndef NDEBUG
	// Don't give warning for large ships (cruiser on up) 
	if (! (Ship_info[ship2->shipp()->ship_info_index].is_big_or_huge()) ) {
		if ( stricmp(Cargo_names[ship2->shipp()->cargo1 & CARGO_INDEX_MASK], "nothing") != 0 ) {
			Warning(LOCATION, "Transferring cargo to %s which already\nhas cargo %s.\nCargo will be replaced", ship2->name, Cargo_names[ship2->shipp()->cargo1 & CARGO_INDEX_MASK] );
		}
	}
#endif
	ship2->shipp()->cargo1 = char((ship1->shipp()->cargo1 & CARGO_INDEX_MASK) | (ship2->shipp()->cargo1 & CARGO_NO_DEPLETE));

	if ( !(ship1->shipp()->cargo1 & CARGO_NO_DEPLETE) ) {
		int i = 0;

		// need to set ship1's cargo to nothing.  scan the cargo_names array looking for the string nothing.
		for (; i < Num_cargo; ++i) {
			if ( !stricmp(Cargo_names[i], NOX("nothing")) ) {
				ship1->shipp()->cargo1 = char(i);
				return;
			}
		}

		// add it if not found
		strcpy(Cargo_names[i], NOX("Nothing"));
		Num_cargo++;
	}
}

/**
 * Exchanges cargo between two ships
 */
void sexp_exchange_cargo(int n)
{
	// find the ships -- if neither in the mission, abort
	auto ship1 = eval_ship(n);
	if (!ship1 || !ship1->has_shipp())
		return;
	auto ship2 = eval_ship(CDR(n));
	if (!ship2 || !ship2->has_shipp())
		return;

	// we must be sure that these two objects are indeed docked
	if (!dock_check_find_direct_docked_object(ship1->objp(), ship2->objp())) {
		Warning(LOCATION, "Tried to exchange cargo between %s and %s although they aren't docked!", ship1->name, ship2->name);
		return;
	}

	int temp = (ship1->shipp()->cargo1 & CARGO_INDEX_MASK);
	ship1->shipp()->cargo1 = char(ship2->shipp()->cargo1 & CARGO_INDEX_MASK);
	ship2->shipp()->cargo1 = char(temp);
}

void sexp_cap_waypoint_speed(int n)
{
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	int speed = eval_num(CDR(n), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	// cap speed to range (-1, 32767) to store within int
	if (speed < 0) {
		speed = -1;
	}

	if (speed > 32767) {
		speed = 32767;
	}

	Ai_info[ship_entry->shipp()->ai_index].waypoint_speed_cap = speed;
}

void sexp_set_wing_formation(int node)
{
	bool is_nan, is_nan_forever;

	auto formation = wing_formation_lookup(CTEXT(node));
	if (formation < 0)
	{
		if (stricmp(CTEXT(node), "Default") != 0)
			return;
	}

	int multiplier = eval_num(CDR(node), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	if (multiplier == 0)
	{
		Warning(LOCATION, "Formation percent cannot be 0!  Defaulting to 100.");
		multiplier = 100;
	}

	float factor = i2fl(multiplier) / 100;

	for (int n = CDDR(node); n >= 0; n = CDR(n))
	{
		auto wingp = eval_wing(n);
		if (wingp)
		{
			wingp->formation = formation;
			wingp->formation_scale = factor;
		}
	}
}

/**
 * Causes a ship to jettison its cargo
 */
void sexp_jettison_cargo(int n, bool jettison_new)
{
	float jettison_speed;
	bool is_nan, is_nan_forever;

	auto parent = eval_ship(n);
	if (!parent || !parent->has_shipp())
		return;
	n = CDR(n);

	// in jettison-cargo-delay, this is the delay (which is unimplemented)
	// in jettison-cargo, this is the jettison speed, which is optional
	if (n >= 0)
	{
		jettison_speed = static_cast<float>(eval_num(n, is_nan, is_nan_forever));
		n = CDR(n);

		// it would be fun to break the physics engine, but let's not
		if (is_nan || is_nan_forever)
			return;
	}
	// per sexp help, if unspecified, default to 25
	// (see also OP_JETTISON_CARGO_NEW in sexp_tree.cpp)
	else
		jettison_speed = 25.0f;

	// no arguments - jettison all docked objects
	if (n < 0)
	{
		// Goober5000 - as with ai_deathroll_start, we can't simply iterate through the dock list while we're
		// undocking things.  So just repeatedly jettison the first object.
		while (object_is_docked(parent->objp()))
		{
			object_jettison_cargo(parent->objp(), dock_get_first_docked_object(parent->objp()), jettison_speed, jettison_new);
		}
	}
	// arguments - jettison only those objects
	else
	{
		for (; n != -1; n = CDR(n))
		{
			// make sure ship exists
			auto child = eval_ship(n);
			if (!child || !child->has_objp())
				continue;

			// make sure we are docked to it
			if (!dock_check_find_direct_docked_object(parent->objp(), child->objp()))
				continue;

			object_jettison_cargo(parent->objp(), child->objp(), jettison_speed, jettison_new);
		}
	}
}

void sexp_set_docked(int n)
{
	// get some data
	auto docker = eval_ship(n);
	if (!docker || !docker->has_shipp())
		return;
	n = CDR(n);

	auto docker_point_name = CTEXT(n);
	n = CDR(n);

	auto dockee = eval_ship(n);
	if (!dockee || !dockee->has_shipp())
		return;
	n = CDR(n);

	auto dockee_point_name = CTEXT(n);

	//Get dockpoints by name
	int docker_point_index = model_find_dock_name_index(Ship_info[docker->shipp()->ship_info_index].model_num, docker_point_name);
	int dockee_point_index = model_find_dock_name_index(Ship_info[dockee->shipp()->ship_info_index].model_num, dockee_point_name);

	Assertion(docker_point_index >= 0, "Docker point '%s' not found on docker ship '%s'", docker_point_name, docker->name);
	Assertion(dockee_point_index >= 0, "Dockee point '%s' not found on dockee ship '%s'", dockee_point_name, dockee->name);

	//Make sure that the specified dockpoints are all free (if not, do nothing)
	if (dock_find_object_at_dockpoint(docker->objp(), docker_point_index) != nullptr ||
		dock_find_object_at_dockpoint(dockee->objp(), dockee_point_index) != nullptr)
	{
		return;
	}

	//Set docked
	dock_orient_and_approach(docker->objp(), docker_point_index, dockee->objp(), dockee_point_index, DOA_DOCK_STAY);
	ai_do_objects_docked_stuff(docker->objp(), docker_point_index, dockee->objp(), dockee_point_index, true);
}

void sexp_cargo_no_deplete(int n)
{
	int no_deplete = 1;
	bool is_nan, is_nan_forever;

	// get some data
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	if ( !(Ship_info[ship_entry->shipp()->ship_info_index].is_big_or_huge()) ) {
		Warning(LOCATION, "Trying to make non BIG or HUGE ship %s with non-depletable cargo.\n", ship_entry->name);
		return;
	}

	if (n != -1) {
		no_deplete = eval_num(n, is_nan, is_nan_forever);
		Assert((no_deplete == 0) || (no_deplete == 1));
		if (is_nan || is_nan_forever) {
			no_deplete = 0;
		}
		else if ( (no_deplete != 0) && (no_deplete != 1) ) {
			no_deplete = 1;
		}
	}

	if (no_deplete) {
		ship_entry->shipp()->cargo1 |= CARGO_NO_DEPLETE;
	} else {
		ship_entry->shipp()->cargo1 &= (~CARGO_NO_DEPLETE);
	}
}

// Goober5000
void sexp_force_jump()
{
	// Shouldn't be gliding now....
	Player_obj->phys_info.flags &= ~PF_GLIDING;
	Player_obj->phys_info.flags &= ~PF_FORCE_GLIDE;

	if (Game_mode & GM_MULTIPLAYER) {
		multi_handle_end_mission_request(); 
	}
	else {
		// forced warp, taken from training failure code
		gameseq_post_event( GS_EVENT_PLAYER_WARPOUT_START_FORCED );	//	Force player to warp out.
	}

}

void sexp_mission_set_nebula(int n)
{
	bool is_nan, is_nan_forever;
	int set_it, range;

	// range is optional, so if it isn't found, it will be set to 0,
	// which will in turn be set to a default in the next function
	// (this means the sexp cannot set the nebula range to 0,
	// but the same is true in the background editor)
	eval_nums(n, is_nan, is_nan_forever, set_it, range);
	if (is_nan || is_nan_forever)
		return;

	stars_set_nebula(set_it > 0, static_cast<float>(range));
}

/* freespace.cpp does not have these availiable externally, and we must call
them so that the main simulation loop does not have to constantly check for
Game_subspace_effect so that it could turn on the subspace sounds.

Because these are in freespace.cpp there are also stubs of these functions
in fred.cpp as it does not deal with the game loop (obviously) but still
links against code.lib. */
extern void game_start_subspace_ambient_sound();
extern void game_stop_subspace_ambient_sound();

void sexp_mission_set_subspace(int n)
{
	bool is_nan, is_nan_forever;
	int set_it = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	if (set_it > 0)
	{
		if (Game_subspace_effect)
			return;
		Game_subspace_effect = 1;
		game_start_subspace_ambient_sound();
	}
	else
	{
		if (!Game_subspace_effect)
			return;
		Game_subspace_effect = 0;
		game_stop_subspace_ambient_sound();
	}

	stars_set_dynamic_environment(Game_subspace_effect != 0);
	The_mission.flags.set(Mission::Mission_Flags::Subspace, Game_subspace_effect != 0);
}

void sexp_change_background(int node)
{
	bool is_nan, is_nan_forever;
	int background_idx = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	// human/computer offset
	background_idx--;

	// range check
	if (background_idx < 0 || background_idx >= (int)Backgrounds.size())
		return;

	stars_load_background(background_idx);
}

void sexp_add_background_bitmap(int n, bool is_sun, bool uses_correct_angles)
{
	int sexp_var, new_number, sanity;
	bool is_nan, is_nan_forever;
	char number_as_str[TOKEN_LENGTH];
	starfield_list_entry sle;

	// filename
	strcpy_s(sle.filename, CTEXT(n));
	n = CDR(n);

	// sanity checking
	sanity = is_sun ? stars_find_sun(sle.filename) : stars_find_bitmap(sle.filename);
	if (sanity < 0)
	{
		Warning(LOCATION, "sexp-add-%s-bitmap: '%s' not found!", is_sun ? "sun" : "background", sle.filename);
		return;
	}

	// angles
	eval_angles(&sle.ang, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	if (!uses_correct_angles) {
		if (is_sun)
			stars_correct_background_sun_angles(&sle.ang);
		else
			stars_correct_background_bitmap_angles(&sle.ang);
	}

	if (is_sun)
	{
		int num = eval_num(n, is_nan, is_nan_forever);
		n = CDR(n);
		if (is_nan || is_nan_forever)
			return;

		// scale
		sle.scale_x = num / 100.0f;
		sle.scale_y = sle.scale_x;

		// div
		sle.div_x = 1;
		sle.div_y = 1;

		// restrict parameters
		if (sle.scale_x > 50) sle.scale_x = 50;
		if (sle.scale_x < 0.1f) sle.scale_x = 0.1f;
		if (sle.scale_y > 50) sle.scale_y = 50;
		if (sle.scale_y < 0.1f) sle.scale_y = 0.1f;
	}
	else
	{
		// next 4
		std::array<int, 4> numbers;
		eval_array(numbers, n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;

		// scale
		sle.scale_x = numbers[0] / 100.0f;
		sle.scale_y = numbers[1] / 100.0f;

		// div
		sle.div_x = numbers[2];
		sle.div_y = numbers[3];

		// restrict parameters
		if (sle.scale_x > 18) sle.scale_x = 18;
		if (sle.scale_x < 0.1f) sle.scale_x = 0.1f;
		if (sle.scale_y > 18) sle.scale_y = 18;
		if (sle.scale_y < 0.1f) sle.scale_y = 0.1f;
		if (sle.div_x > 5) sle.div_x = 5;
		if (sle.div_x < 1) sle.div_x = 1;
		if (sle.div_y > 5) sle.div_y = 5;
		if (sle.div_y < 1) sle.div_y = 1;
	}

	if (n == -1) {
		if (is_sun) {
			stars_add_sun_entry(&sle);
		} else {
			stars_add_bitmap_entry(&sle);
		}
	} else {
		Assert((n >= 0) && (n < Num_sexp_nodes));

		// ripped from sexp_modify_variable()
		sexp_var = sexp_get_variable_index(n);
		if (sexp_var < 0)
		{
			Warning(LOCATION, "sexp-add-%s-bitmap: Variable %s does not exist!", is_sun ? "sun" : "background", Sexp_nodes[n].text);
			return;
		}

		if (Sexp_variables[sexp_var].type & SEXP_VARIABLE_NUMBER)
		{
			// get new numerical value
			new_number = is_sun ? stars_add_sun_entry(&sle) : stars_add_bitmap_entry(&sle);
			if (new_number < 0)
			{
				Warning(LOCATION, "Unable to add %s: '%s'!", is_sun ? "sun" : "starfield bitmap", sle.filename);
				new_number = 0;
			}

			sprintf(number_as_str, "%d", new_number);

			// assign to variable
			sexp_modify_variable(number_as_str, sexp_var);
		}
		else
		{
			Warning(LOCATION, "sexp-add-%s-bitmap: Variable %s must be a number variable!", is_sun ? "sun" : "background", Sexp_variables[sexp_var].variable_name);
			return;
		}
	}
}

void sexp_remove_background_bitmap(int n, bool is_sun)
{
	bool is_nan, is_nan_forever;
	int slot = eval_num(n, is_nan, is_nan_forever);

	if (slot >= 0 && !is_nan && !is_nan_forever) {
		int instances = is_sun ? stars_get_num_suns() : stars_get_num_bitmaps();
		if (instances > slot) {
			if (is_sun) {
				stars_mark_sun_unused(slot);
			} else {
				stars_mark_bitmap_unused(slot);
			}
		} else {
			Warning(LOCATION, "remove-%s-bitmap: slot %d does not exist. Slot must be less than %d.", is_sun ? "sun" : "background", slot, instances);
		}
	}
}

void sexp_nebula_change_storm(int n)
{
	if (!(The_mission.flags[Mission::Mission_Flags::Fullneb]))
		return;
	
	nebl_set_storm(CTEXT(n));
}

void sexp_nebula_set_poofs(int node, bool legacy)
{
	bool result;
	int n = node;

	// new and legacy sexps have different argument orders
	if (legacy)
		result = is_sexp_true(CDR(n));
	else
	{
		result = is_sexp_true(n);
		n = CDR(n);
	}

	// toggle all the poofs
	if (n < 0)
	{
		int count = static_cast<int>(Poof_info.size());
		for (int i = 0; i < count; ++i)
			neb2_toggle_poof(i, result);
	}
	// toggle the selected poofs
	else
	{
		for (; n >= 0; n = CDR(n))
		{
			auto name = CTEXT(n);

			int index = find_item_with_string(Poof_info, &poof_info::name, name);
			if (index >= 0)
				neb2_toggle_poof(index, result);

			if (legacy)
				break;	// the legacy sexp affects only one poof
		}
	}

	// do this once at the end of all the toggling
	neb2_toggle_poof_finalize();
}

void sexp_nebula_fade_poofs(int node, bool legacy)
{
	bool result, is_nan, is_nan_forever;
	int duration;
	int n = node;

	// new and legacy sexps have different argument orders
	if (legacy)
	{
		duration = eval_num(CDR(n), is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
		result = is_sexp_true(CDDR(n));
	}
	else
	{
		result = is_sexp_true(n);
		n = CDR(n);

		duration = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
		n = CDR(n);
	}

	// fade all the poofs
	if (n < 0)
	{
		int count = static_cast<int>(Poof_info.size());
		for (int i = 0; i < count; ++i)
			neb2_fade_poof(i, duration, result);
	}
	// fade the specified poofs
	else
	{
		for (; n >= 0; n = CDR(n))
		{
			auto name = CTEXT(n);

			int index = find_item_with_string(Poof_info, &poof_info::name, name);
			if (index >= 0)
				neb2_fade_poof(index, duration, result);

			if (legacy)
				break;	// the legacy sexp affects only one poof
		}
	}
}

void sexp_nebula_change_pattern(int n)
{
	if (!(The_mission.flags[Mission::Mission_Flags::Fullneb]))
		return;
	
	strcpy_s(Neb2_texture_name,(CTEXT(n)));
	The_mission.flags.remove(Mission::Mission_Flags::Neb2_fog_color_override);

	neb2_post_level_init(The_mission.flags[Mission::Mission_Flags::Neb2_fog_color_override]);
}

void sexp_nebula_change_fog_color(int node)
{
	if (!(The_mission.flags[Mission::Mission_Flags::Fullneb]))
		return;

	int red, green, blue = 0;
	bool is_nan, is_nan_forever;

	Assert(node >= 0);

	eval_nums(node, is_nan, is_nan_forever, red, green, blue);
	if (is_nan || is_nan_forever)
		return;

	CLAMP(red, 0, 255);
	CLAMP(green, 0, 255);
	CLAMP(blue, 0, 255);

	Neb2_fog_color[0] = (ubyte)red;
	Neb2_fog_color[1] = (ubyte)green;
	Neb2_fog_color[2] = (ubyte)blue;

	The_mission.flags |= Mission::Mission_Flags::Neb2_fog_color_override;
}

void sexp_nebula_set_range(int node)
{
	bool is_nan, is_nan_forever;
	int range = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever || range < 1)
		return;

	Neb2_awacs = i2fl(range);
}

void sexp_volumetrics_toggle(int n)
{
	if (!The_mission.volumetrics)
		return;

	The_mission.volumetrics->set_enabled(is_sexp_true(n));
}

void sexp_toggle_asteroid_field(int n)
{
	Asteroids_enabled = is_sexp_true(n);
}

void sexp_set_asteroid_field(int n, bool new_sexp)
{
	bool is_nan, is_nan_forever;

	int field_type = 0, num_asteroids = 0, asteroid_speed = 0;
	for (int i = 0; i < 3; i++) {

		if (n >= 0) {
			int temp = eval_num(n, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;

			if (i == 0)
				num_asteroids = temp;
			else if (i == 1)
				field_type = temp;
			else
				asteroid_speed = temp;
			n = CDR(n);
		}
	}

	SCP_vector<SCP_string> asteroid_types;

	if (!new_sexp) {
		if (n >= 0) {
			if (is_sexp_true(n)) {
				asteroid_types.push_back("Brown");
			}
			n = CDR(n);
		}

		if (n >= 0) {
			if (is_sexp_true(n)) {
				asteroid_types.push_back("Blue");
			}
			n = CDR(n);
		}

		if (n >= 0) {
			if (is_sexp_true(n)) {
				asteroid_types.push_back("Orange");
			}
			n = CDR(n);
		}
	}

	int o_minx = -1000, o_miny = -1000, o_minz = -1000;
	int o_maxx = 1000, o_maxy = 1000, o_maxz = 1000;

	for (int i = 0; i < 6; i++) {

		if (n >= 0) {
			int temp = eval_num(n, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;

			if (i == 0)
				o_minx = temp;
			else if (i == 1)
				o_maxx = temp;
			else if (i == 2)
				o_miny = temp;
			else if (i == 3)
				o_maxy = temp;
			else if (i == 4)
				o_minz = temp;
			else
				o_maxz = temp;
			n = CDR(n);
		}
	}

	bool inner_box = false;
	if (n >= 0) {
		inner_box = is_sexp_true(n);
		n = CDR(n);
	}

	int i_minx = -500, i_miny = -500, i_minz = -500;
	int i_maxx = 500, i_maxy = 500, i_maxz = 500;
	
	for (int i = 0; i < 6; i++) {

		if (n >= 0) {
			int temp = eval_num(n, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;

			if (i == 0)
				i_minx = temp;
			else if (i == 1)
				i_maxx = temp;
			else if (i == 2)
				i_miny = temp;
			else if (i == 3)
				i_maxy = temp;
			else if (i == 4)
				i_minz = temp;
			else
				i_maxz = temp;
			n = CDR(n);
		}
	}

	SCP_vector<SCP_string> targets;
	if (!new_sexp) {
		if (n >= 0) {
			for (; n >= 0;n = CDR(n)) {
				auto ship_entry = eval_ship(n);
				if (!ship_entry)
					continue;

				targets.push_back(ship_entry->name);
			}
		}
	} else {
		if (n >= 0) {
			auto list = get_list_valid_asteroid_subtypes();
			for (; n >= 0;n = CDR(n)) {

				// Verify that it's a valid asteroid type
				for (const auto& item : list) {
					if (stricmp(CTEXT(n), item.c_str())) {
						continue;
					}
				}

				asteroid_types.emplace_back(CTEXT(n));
			}
		}
	}

	if (num_asteroids > MAX_ASTEROIDS) {
		num_asteroids = MAX_ASTEROIDS;
	}

	vec3d o_min = vm_vec_new((float)o_minx, (float)o_miny, (float)o_minz);
	vec3d o_max = vm_vec_new((float)o_maxx, (float)o_maxy, (float)o_maxz);

	vec3d i_min = vm_vec_new((float)i_minx, (float)i_miny, (float)i_minz);
	vec3d i_max = vm_vec_new((float)i_maxx, (float)i_maxy, (float)i_maxz);

	asteroid_create_asteroid_field(
		num_asteroids,
		field_type,
		asteroid_speed,
		o_min,
		o_max,
		inner_box,
		i_min,
		i_max,
		std::move(asteroid_types));

	if (!new_sexp) {
		Asteroid_field.target_names = std::move(targets);
	}

}

void sexp_set_debris_field(int n, bool new_sexp)
{
	bool is_nan, is_nan_forever;

	int num_asteroids = 0, asteroid_speed = 0;
	for (int i = 0; i < 2; i++) {
		
		if (n >= 0) {
			int temp = eval_num(n, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;

			if (i == 0)
				num_asteroids = temp;
			else
				asteroid_speed = temp;
			n = CDR(n);
		}
	}

	SCP_vector<int> debris_types;
	if (!new_sexp) {
		if (n >= 0) {
			int debris1 = get_asteroid_index(CTEXT(n));
			if (debris1 >= 0) {
				debris_types.push_back(debris1);
			}
			n = CDR(n);
		}

		if (n >= 0) {
			int debris2 = get_asteroid_index(CTEXT(n));
			if (debris2 >= 0) {
				debris_types.push_back(debris2);
			}
			n = CDR(n);
		}

		if (n >= 0) {
			int debris3 = get_asteroid_index(CTEXT(n));
			if (debris3 >= 0) {
				debris_types.push_back(debris3);
			}
			n = CDR(n);
		}
	}

	int o_minx = -1000, o_miny = -1000, o_minz = -1000;
	int o_maxx = 1000, o_maxy = 1000, o_maxz = 1000;
	for (int i = 0; i < 6; i++) {

		if (n >= 0) {
			int temp = eval_num(n, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;

			if (i == 0)
				o_minx = temp;
			else if (i == 1)
				o_maxx = temp;
			else if (i == 2)
				o_miny = temp;
			else if (i == 3)
				o_maxy = temp;
			else if (i == 4)
				o_minz = temp;
			else
				o_maxz = temp;
			n = CDR(n);
		}
	}

	bool enhanced = false;
	if (n >= 0) {
		enhanced = is_sexp_true(n);
		n = CDR(n);
	}

	if (num_asteroids > MAX_ASTEROIDS) {
		num_asteroids = MAX_ASTEROIDS;
	}

	vec3d o_min = vm_vec_new((float)o_minx, (float)o_miny, (float)o_minz);
	vec3d o_max = vm_vec_new((float)o_maxx, (float)o_maxy, (float)o_maxz);

	if (new_sexp) {
		if (n >= 0) {
			for (; n >= 0; n = CDR(n)) {

				// Verify that it's a valid debris type
				auto idx = get_asteroid_index(CTEXT(n));
				if (idx < 0)
					continue;

				debris_types.push_back(idx);
			}
		}
	}

	asteroid_create_debris_field(
		num_asteroids,
		asteroid_speed,
		std::move(debris_types),
		o_min,
		o_max,
		enhanced);
}

void sexp_config_field_targets(int n)
{
	// Do nothing for disabled fields
	if (Asteroid_field.num_initial_asteroids == 0) {
		return;
	}
	
	// Do nothing for passive fields
	if (Asteroid_field.field_type == FT_PASSIVE) {
		return;
	}
	
	bool toggle = false;
	if (n >= 0) {
		toggle = is_sexp_true(n);
		n = CDR(n);
	}

	SCP_vector<SCP_string> targets;
	if (n >= 0) {
		for (; n >= 0; n = CDR(n)) {
			auto ship_entry = eval_ship(n);
			if (!ship_entry)
				continue;

			targets.push_back(ship_entry->name);
		}
	}

	if (targets.size() == 0) {
		if (toggle) {
			// No ships provided to add so abort
			return;
		} else {
			Asteroid_field.target_names.clear();
			return;
		}
	} else {
		if (toggle) {
			// Add the targets
			for (const auto& ship : targets) {
				// Check if the ship is already in the target_names
				if (std::find(Asteroid_field.target_names.begin(), Asteroid_field.target_names.end(), ship) == Asteroid_field.target_names.end()) {
					Asteroid_field.target_names.push_back(ship);
				}
			}
		} else {
			// Remove the targets
			for (const auto& ship : targets) {
				Asteroid_field.target_names.erase(std::remove(Asteroid_field.target_names.begin(), Asteroid_field.target_names.end(), ship), Asteroid_field.target_names.end());
			}
		}
	}
}

void sexp_set_motion_debris_type(int n)
{
	SCP_string name = CTEXT(n);

	if (!stricmp(name.c_str(), SEXP_NONE_STRING)) {
		Motion_debris_override = true;
	} else {
		Motion_debris_override = false;
		stars_load_debris(false, name);
	}
}

/**
 * End the mission.
 *
 * Implemented by Sesquipedalian; fixed by EdrickV; enhanced by others
 */
void sexp_end_mission(int n)
{
	bool ignore_player_mortality = true;
	bool boot_to_main_hall = false;
	bool from_debrief_to_main_hall = false;

	if (n != -1) {
		ignore_player_mortality = is_sexp_true(n);
		n = CDR(n);
	}
	if (n != -1) {
		boot_to_main_hall = is_sexp_true(n);
		n = CDR(n);
	}
	if (n != -1) {
		from_debrief_to_main_hall = is_sexp_true(n);
		n = CDR(n);
	}

	// if the player is dead we may want to let the death screen handle things
	if (!ignore_player_mortality && (Player_ship->flags[Ship::Ship_Flags::Dying])) {
		return;
	}

	// ending via debrief and then going to mainhall could maybe work with multiplayer?
	The_mission.flags.set(Mission::Mission_Flags::End_to_mainhall, from_debrief_to_main_hall != 0);
	
	// if we go straight to the main hall we have to clean up the mission without entering the debriefing
	if (boot_to_main_hall && !(Game_mode & GM_MULTIPLAYER)) {
		gameseq_post_event(GS_EVENT_END_GAME);
	} else {
		send_debrief_event();
	}

	Current_sexp_network_packet.do_callback();
}

void multi_sexp_end_mission()
{
	// Signal process_endgame_packet() to skip the warp-out sequence by entering
	// debrief immediately when the MISSION_END packet arrives.  We cannot call
	// send_debrief_event() here directly: on standalone servers the client has not
	// yet received mission stats (m_okKills etc.) at this point, so
	// scoring_level_close() would run with zeroed kill counts and
	// Pilot.update_stats() would create no kill entries.  debrief_close() would
	// then try to back out entries that were never created and hit an UNREACHABLE.
	//
	// send_endgame_packet() broadcasts stats to clients *before* sending MISSION_END,
	// so by the time process_endgame_packet() fires, the stats are already present
	// and it is safe to enter debrief there.
	Multi_sexp_end_mission_pending = true;
	multi_handle_end_mission_request();
}

// Goober5000
void sexp_set_debriefing_toggled(int node)
{
	The_mission.flags.set(Mission::Mission_Flags::Toggle_debriefing, is_sexp_true(node));
}

// Goober5000
void sexp_set_debriefing_persona(int node)
{
	bool is_nan, is_nan_forever;
	int persona = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever || persona < 0)
		return;
	The_mission.debriefing_persona = persona;
}

// MjnMixael
void sexp_set_traitor_override(int node)
{
	SCP_string override = CTEXT(node);

	if (!stricmp(override.c_str(), SEXP_NONE_STRING)) {
		The_mission.traitor_override = nullptr;
	}else{
		The_mission.traitor_override = get_traitor_override_pointer(override);
	}
}

/**
 * Toggle the status for the AI code which tells the AI if it is a good or bad time to rearm.
 */
void sexp_good_bad_time_to_rearm(int n, bool good)
{
	int team, time;
	bool is_nan, is_nan_forever;

	team = iff_lookup(CTEXT(n));
	time = eval_num(CDR(n), is_nan, is_nan_forever);			// this is the time for how long a good rearm is active -- in seconds

	if (is_nan || is_nan_forever)
		return;

	if (good)
		ai_set_good_rearm_time(team, time);
	else
		ai_set_bad_rearm_time(team, time);
}

/**
 * Grants promotion to the player
 */
void sexp_grant_promotion()
{
	// short circuit multiplayer for now until we figure out what to do.
	if ( Game_mode & GM_MULTIPLAYER )
		return;

	// set a bit to tell player should get promoted at the end of the mission.  I suppose the other
	// thing that we could do would be to set the players score to at least the amount of
	// points for the next level, but this way is better I think.
	if ( Game_mode & GM_CAMPAIGN_MODE ) {
		Player->flags |= PLAYER_FLAGS_PROMOTED;
	}
}

/**
 * Gives the named medal to the players in the mission
 */
void sexp_grant_medal(int n)
{
	int i;

	// don't give medals in normal gameplay when not in campaign mode
	if ( (Game_mode & GM_NORMAL) && !(Game_mode & GM_CAMPAIGN_MODE) )
		return;

	auto medal_name = CTEXT(n);

	if (Player->stats.m_medal_earned >= 0) {
		Warning(LOCATION, "Cannot grant more than one medal per mission!  New medal '%s' will replace old medal '%s'!", medal_name, Medals[Player->stats.m_medal_earned].name);
	}

	for (i = 0; i < (int)Medals.size(); i++) {
		if ( !stricmp(medal_name, Medals[i].name) )
			break;
	}

	if (i < (int)Medals.size()) {
		Player->stats.m_medal_earned = i;

		if ( Game_mode & GM_MULTIPLAYER ) {
			for ( int j = 0; j < MAX_PLAYERS; j++ ) {
				if ( MULTI_CONNECTED(Net_players[j]) ) {
					Net_players[j].m_player->stats.m_medal_earned = i;
				}
			}
		}
	}
}

void sexp_change_player_score(int node)
{
	int score;
	bool is_nan, is_nan_forever;

	score = eval_num(node, is_nan, is_nan_forever); 
	node = CDR(node);

	if (is_nan || is_nan_forever)
		return;

	while (node >= 0) {
		auto player = get_player_from_ship_node(node, true);
		if (player) {
			player->stats.m_score += score;
				
			if (player->stats.m_score < 0) {
				player->stats.m_score = 0;
			}
		} else {
			if (!(Game_mode & GM_MULTIPLAYER)) {
				Warning(LOCATION, "Can not award points to '%s'. Ship is not a player!", CTEXT(node));
			}
		}

		node = CDR(node);
	}
}

void sexp_change_team_score(int node)
{
	int i, score, team;
	bool is_nan, is_nan_forever;

	// since we only have a team score in TvT
	if ( !(MULTI_TEAM) ) {
		return;
	}

	eval_nums(node, is_nan, is_nan_forever, score, team);
	if (is_nan || is_nan_forever)
		return;

	if (team == 0) {
		for (i = 0; i < MAX_TVT_TEAMS; i++) {
			Multi_team_score[i] += score;  
		}
	}
	else if (team > 0 && team <= MAX_TVT_TEAMS) {
		Multi_team_score[team - 1] += score;  
	}
	else {
		Warning(LOCATION, "Invalid team number. Team %d does not exist", team);
	}
}

void sexp_red_alert()
{
	// in the case of a red_alert mission, simply call the red alert function to close
	// the current campaign's mission and move forward to the next mission
	red_alert_start_mission();

	Current_sexp_network_packet.do_callback();
}

void multi_sexp_red_alert()
{
	red_alert_start_mission();
}

void sexp_tech_add_ship(int node)
{
	// this function doesn't mean anything when not in campaign mode
	if ( !(Game_mode & GM_CAMPAIGN_MODE) )
		return;

	while (node >= 0) {
		auto name = CTEXT(node);
		int i = ship_info_lookup(name);
		if (i >= 0)
		{
			if (Player && (Player->flags & PLAYER_FLAGS_IS_MULTI))
				Ship_info[i].flags.set(Ship::Info_Flags::In_tech_database_m);
			else
				Ship_info[i].flags.set(Ship::Info_Flags::In_tech_database);
		}
		else
			Warning(LOCATION, "In tech-add-ship, ship class \"%s\" invalid", name);

		node = CDR(node);
	}
}

void sexp_tech_add_weapon(int node)
{
	// this function doesn't mean anything when not in campaign mode
	if ( !(Game_mode & GM_CAMPAIGN_MODE) )
		return;

	while (node >= 0) {
		auto name = CTEXT(node);
		int i = weapon_info_lookup(name);
		if (i >= 0)
			Weapon_info[i].wi_flags.set(Weapon::Info_Flags::In_tech_database);
		else
			Warning(LOCATION, "In tech-add-weapon, weapon class \"%s\" invalid", name);

		node = CDR(node);
	}
}

// Goober5000
// expanded with remove parameter by wookieejedi
void sexp_tech_toggle_intel(int node, bool add, bool xstr)
{
	int id, n = node;

	// this function doesn't mean anything when not in campaign mode
	if ( !(Game_mode & GM_CAMPAIGN_MODE) )
		return;

	while (n >= 0)
	{
		// don't use things like CTEXT or eval_num, since we didn't in the preloader
		auto name = Sexp_nodes[n].text;
		n = CDR(n);

		// the xstr variant must have an id
		if (xstr)
		{
			if (n < 0)
				break;
			id = atoi(Sexp_nodes[n].text);
			n = CDR(n);
		}
		else
			id = -1;

		// if we have an xstr, we already translated this node in the preloader, so just look it up
		int i = intel_info_lookup(name);
		if (i >= 0) {
			if (add) {
				Intel_info[i].flags |= IIF_IN_TECH_DATABASE;
			} else {
				Intel_info[i].flags &= ~ IIF_IN_TECH_DATABASE;
			}
		}
		else if (xstr)
			Warning(LOCATION, "In tech-%s-intel-xstr, entry XSTR(\"%s\", %d) invalid", (add ? "add" : "remove"), name, id);
		else
			Warning(LOCATION, "In tech-%s-intel, entry \"%s\" invalid", (add ? "add" : "remove"), name);
	}
}

// Goober5000 - reset all the tech entries to their default states
void sexp_tech_reset_to_default()
{
	// this function doesn't mean anything when not in campaign mode
	if ( !(Game_mode & GM_CAMPAIGN_MODE) )
		return;

	tech_reset_to_default();
}

/**
 * Set variables needed to grant a new ship/weapon to the player during the course
 * of a mission
 */
void sexp_allow_ship(int n)
{
	// this function doesn't mean anything when not in campaign mode
	if ( !(Game_mode & GM_CAMPAIGN_MODE) )
		return;

	// get the name of the ship and lookup up the ship_info index for it
	auto name = CTEXT(n);
	int sindex = ship_info_lookup( name );
	if ( sindex == -1 )
		return;

	// now we have a valid index --
	mission_campaign_save_persistent( CAMPAIGN_PERSISTENT_SHIP, sindex );
}

void sexp_allow_weapon(int n)
{
	// this function doesn't mean anything when not in campaign mode
	if ( !(Game_mode & GM_CAMPAIGN_MODE) )
		return;

	// get the name of the weapon and lookup up the weapon_info index for it
	auto name = CTEXT(n);
	int sindex = weapon_info_lookup( name );
	if ( sindex == -1 )
		return;

	// now we have a valid index --
	mission_campaign_save_persistent( CAMPAIGN_PERSISTENT_WEAPON, sindex );
}

/**
 * generic function for all those sexps that set flags
 * For all flag type parameters: If a particular flag should not be set, use the ::NUM_VALUES member of that enum
 *
 * @note this function has a similar purpose to sexp_alter_ship_flag_helper; make sure you check/update both
 */
void sexp_deal_with_ship_flag(int node, bool process_subsequent_nodes, Object::Object_Flags object_flag, Ship::Ship_Flags ship_flag, Mission::Parse_Object_Flags p_object_flag, bool set_it, bool send_multiplayer = false)
{
	int n = node;

	if (send_multiplayer && MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback(); 
		Current_sexp_network_packet.send_int((int)object_flag); 
		Current_sexp_network_packet.send_int((int)ship_flag); 
		Current_sexp_network_packet.send_int((int)p_object_flag); 
		Current_sexp_network_packet.send_bool(set_it); 
	}

	// loop for all ships in the sexp
	// NB: if the flag is set, we will continue acting on nodes until we run out of them;
	//     if not, we will only act on the first one
	for (; n >= 0; process_subsequent_nodes ? n = CDR(n) : n = -1)
	{
		// get ship
		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			continue;

		// check to see if ship destroyed or departed.  In either case, do nothing.
		if (ship_entry->status == ShipStatus::EXITED)
			continue;

		// if ship is in-mission
		if (ship_entry->has_shipp())
		{
			// save flags for state change comparisons
			auto object_flag_orig = ship_entry->objp()->flags;

			// see if we have an object flag to set
			if (object_flag != Object::Object_Flags::NUM_VALUES)
			{
				// set or clear?
				ship_entry->objp()->flags.set((Object::Object_Flags)object_flag, set_it);
			}

			// handle ETS when modifying shields
			if (object_flag == Object::Object_Flags::No_shields) {
				if (set_it) {
					zero_one_ets(&ship_entry->shipp()->shield_recharge_index, &ship_entry->shipp()->weapon_recharge_index, &ship_entry->shipp()->engine_recharge_index);

					ets_update_max_speed(ship_entry->objp());
				} else if (object_flag_orig[Object::Object_Flags::No_shields]) {
					set_default_recharge_rates(ship_entry->objp());
				}
			}

			// see if we have a ship flag to set
			if (ship_flag != Ship::Ship_Flags::NUM_VALUES)
			{
				// set or clear?
				ship_entry->shipp()->flags.set((Ship::Ship_Flags)ship_flag, set_it);
			}

			// the lock afterburner SEXP also needs to set a physics flag
			if (ship_flag == Ship::Ship_Flags::Afterburner_locked) {
				if (set_it) {
					afterburners_stop(ship_entry->objp(), 1);
				}
			}

			if (send_multiplayer && MULTIPLAYER_MASTER) {
				Current_sexp_network_packet.send_bool(true); 
				Current_sexp_network_packet.send_ship(ship_entry->shipp());
			}
		}
		// if it hasn't arrived yet
		else if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		{
			// see if we have a p_object flag to set
			if (p_object_flag != Mission::Parse_Object_Flags::NUM_VALUES)
			{
				// set or clear?
				ship_entry->p_objp()->flags.set((Mission::Parse_Object_Flags)p_object_flag, set_it);
			}

			if (send_multiplayer && MULTIPLAYER_MASTER) {
				Current_sexp_network_packet.send_bool(false);
				Current_sexp_network_packet.send_parse_object(ship_entry->p_objp());
			}
		}
	}

	if (send_multiplayer && MULTIPLAYER_MASTER) {
		 Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_deal_with_ship_flag() 
{
	int object_flag = (int)Object::Object_Flags::NUM_VALUES;
	int ship_flag = (int)Ship::Ship_Flags::NUM_VALUES; 
	int p_object_flag = (int)Mission::Parse_Object_Flags::NUM_VALUES;
	bool set_it = false;
	bool ship_arrived = true; 
	ship *shipp = nullptr;
	p_object *pobjp = nullptr;

	Current_sexp_network_packet.get_int(object_flag); 
	Current_sexp_network_packet.get_int(ship_flag); 
	Current_sexp_network_packet.get_int(p_object_flag); 
	Current_sexp_network_packet.get_bool(set_it);

	// if any of the above failed so will this loop
	while (Current_sexp_network_packet.get_bool(ship_arrived)) 
	{
		if (ship_arrived) {
			Current_sexp_network_packet.get_ship(shipp);
			if (shipp == nullptr) {
				WarningEx(LOCATION, "Null ship pointer in multi_sexp_deal_with_ship_flag(), tell a coder.\n");
				return;
			}

			// save flags for state change comparisons
			auto object_flag_orig = Objects[shipp->objnum].flags;

			if (object_flag != (int)Object::Object_Flags::NUM_VALUES) {
				Objects[shipp->objnum].flags.set((Object::Object_Flags)object_flag, set_it);
			}
			if (ship_flag != (int)Ship::Ship_Flags::NUM_VALUES) {
				shipp->flags.set((Ship::Ship_Flags)ship_flag, set_it);
			}

			// deal with side effects of these flags
			if (object_flag == (int)Object::Object_Flags::No_shields) {
				if (set_it) {
					zero_one_ets(&shipp->shield_recharge_index, &shipp->weapon_recharge_index, &shipp->engine_recharge_index);

					ets_update_max_speed(&Objects[shipp->objnum]);
				} else if (object_flag_orig[Object::Object_Flags::No_shields]) {
					set_default_recharge_rates(&Objects[shipp->objnum]);
				}
			}

			if (ship_flag == (int)Ship::Ship_Flags::Afterburner_locked) {
				if (set_it) {
					afterburners_stop(&Objects[shipp->objnum], 1);
				}
			}

			if ((ship_flag == (int)Ship::Ship_Flags::Stealth) && !set_it) {
				if (shipp->flags[Ship::Ship_Flags::Escort]) {
					hud_add_ship_to_escort(shipp->objnum, 1);
				}			
			}
			if ((ship_flag == (int)Ship::Ship_Flags::Friendly_stealth_invis) && !set_it && (shipp->flags[Ship::Ship_Flags::Stealth]) && (shipp->team == Player_ship->team)) {
				if (shipp->flags[Ship::Ship_Flags::Escort]) {
					hud_add_ship_to_escort(shipp->objnum, 1);
				}			
			}
			if (ship_flag == (int)Ship::Ship_Flags::Hidden_from_sensors) {
				if (set_it) {
					if (Player_ai->target_objnum == shipp->objnum) {
						hud_cease_targeting(); 
					}
				}
				else {
					if (shipp->flags[Ship::Ship_Flags::Escort]) {
						hud_add_ship_to_escort(shipp->objnum, 1);
					}		
				}
			}
		}
		else {
			Current_sexp_network_packet.get_parse_object(pobjp); 
			if ((pobjp != nullptr) && (p_object_flag != (int)Mission::Parse_Object_Flags::NUM_VALUES)) {
				pobjp->flags.set((Mission::Parse_Object_Flags)p_object_flag, set_it);
			}
		}
	}
}

/**
 * sets flags on objects from alter-ship-flag
 *
 * @note this function has a similar purpose to sexp_deal_with_ship_flag; make sure you check/update both
 */
void sexp_alter_ship_flag_helper(object_ship_wing_point_team &oswpt, bool future_ships, Object::Object_Flags object_flag, Ship::Ship_Flags ship_flag, Mission::Parse_Object_Flags parse_obj_flag, AI::AI_Flags ai_flag, bool set_flag)
{
	int i;
	flagset<Object::Object_Flags> object_flag_orig;

	switch (oswpt.type)
	{			
		case OSWPT_TYPE_NONE:
		case OSWPT_TYPE_EXITED:
			return;

		case OSWPT_TYPE_WHOLE_TEAM:
			Assert (oswpt.team >= 0);
			for (auto so: list_range(&Ship_obj_list)) {
				if (Objects[so->objnum].flags[Object::Object_Flags::Should_be_dead])
					continue;

				if (Ships[Objects[so->objnum].instance].team == oswpt.team) {
					// recurse
					object_ship_wing_point_team oswpt2(so);
					sexp_alter_ship_flag_helper(oswpt2, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
				}
			}

			if (future_ships) {
				for (auto p_objp: list_range(&Ship_arrival_list)) {
					if (p_objp->team == oswpt.team) {
						// recurse
						object_ship_wing_point_team oswpt2(p_objp);
						sexp_alter_ship_flag_helper(oswpt2, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
					}
				}
			}
			break;

		case OSWPT_TYPE_WING:
		case OSWPT_TYPE_WING_NOT_PRESENT:
			// if the wing isn't here, and we're only dealing with ships which are, we're done. 
			if  (!future_ships){
				if (oswpt.type == OSWPT_TYPE_WING_NOT_PRESENT) {
					return; 
				}
			}
			else {
				for (auto p_objp: list_range(&Ship_arrival_list)) {
					if (p_objp->wingnum == oswpt.wingnum) {
						// recurse
						object_ship_wing_point_team oswpt2(p_objp);
						sexp_alter_ship_flag_helper(oswpt2, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
					}
				}
			}

			for (i = 0; i < oswpt.wingp()->current_count; i++) {
				// recurse
				object_ship_wing_point_team oswpt2(&Ships[oswpt.wingp()->ship_index[i]]);
				sexp_alter_ship_flag_helper(oswpt2, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
			}

			break;

		// finally! If we actually have a ship, we can set its flags!
		case OSWPT_TYPE_SHIP:
			// save flags for state change comparisons
			object_flag_orig = oswpt.objp()->flags;

			// see if we have an object flag to set
			if (object_flag != Object::Object_Flags::NUM_VALUES)
			{
				auto tmp_flagset = oswpt.objp()->flags;
				// set or clear?
				tmp_flagset.set(object_flag, set_flag);
					
				obj_set_flags(oswpt.objp(), tmp_flagset);
			}

			// handle ETS when modifying shields
			if (object_flag == Object::Object_Flags::No_shields) {
				if (set_flag) {
					zero_one_ets(&oswpt.shipp()->shield_recharge_index, &oswpt.shipp()->weapon_recharge_index, &oswpt.shipp()->engine_recharge_index);

					ets_update_max_speed(oswpt.objp());
				} else if (object_flag_orig[Object::Object_Flags::No_shields]) {
					set_default_recharge_rates(oswpt.objp());
				}
			}

			// see if we have a ship flag to set
			if (ship_flag != Ship::Ship_Flags::NUM_VALUES)
			{
				// set or clear?
					oswpt.shipp()->flags.set((Ship::Ship_Flags)ship_flag, set_flag);
			}

			// the lock afterburner SEXP also needs to set a physics flag
			if (ship_flag == Ship::Ship_Flags::Afterburner_locked) {
				if (set_flag) {
					afterburners_stop(oswpt.objp(), 1);
				}
			}

			// special case: the "no_collide" parse object flag is the same, but opposite, as the "collides" object flag
			if (parse_obj_flag == Mission::Parse_Object_Flags::OF_No_collide)
			{
				auto tmp_flagset = oswpt.objp()->flags;
				tmp_flagset.set(Object::Object_Flags::Collides, !set_flag);
				obj_set_flags(oswpt.objp(), tmp_flagset);
			}

			// see if we have an ai flag to set
			if (ai_flag != AI::AI_Flags::NUM_VALUES)
			{
				// set or clear?
				Ai_info[oswpt.shipp()->ai_index].ai_flags.set(ai_flag, set_flag);
			}

			// no break statement. We want to fall through.
			FALLTHROUGH;
			
		case OSWPT_TYPE_PARSE_OBJECT:
			// only apply the flag to future ships if we want to and we are able to
			if (!future_ships || !oswpt.has_p_objp()) {
				return;
			}

			// special case: the "collides" object flag is the same, but opposite, as the "no_collide" parse object flag
			if (object_flag == Object::Object_Flags::Collides)
			{
				oswpt.p_objp()->flags.set(Mission::Parse_Object_Flags::OF_No_collide, !set_flag);
			}

			// see if we have a p_object flag to set
			if (parse_obj_flag != Mission::Parse_Object_Flags::NUM_VALUES)
			{
				oswpt.p_objp()->flags.set(parse_obj_flag, set_flag);
			}
			break;

		default:
			break;
	}
}

void alter_flag_for_all_ships(bool future_ships, Object::Object_Flags object_flag, Ship::Ship_Flags ship_flag, Mission::Parse_Object_Flags parse_obj_flag, AI::AI_Flags ai_flag, bool set_flag)
{
	// set all the ships present in the mission
	for (auto so: list_range(&Ship_obj_list)) {
		if (Objects[so->objnum].flags[Object::Object_Flags::Should_be_dead])
			continue;

		object_ship_wing_point_team oswpt(so);
		sexp_alter_ship_flag_helper(oswpt, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
	}

	// set up all the ships which have yet to arrive
	if (future_ships) {
		for (auto p_objp: list_range(&Ship_arrival_list)) {
			object_ship_wing_point_team oswpt(p_objp);
			sexp_alter_ship_flag_helper(oswpt, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
		}
	}
}

bool sexp_check_flag_arrays(const char *flag_name, Object::Object_Flags &object_flag, Ship::Ship_Flags &ship_flag, Mission::Parse_Object_Flags &parse_obj_flag, AI::AI_Flags &ai_flag)
{
	size_t i;
	bool send_multi = false;

	for ( i = 0; i < (size_t)Num_object_flag_names; i++) {
		if (!stricmp(Object_flag_names[i].flag_name, flag_name)) {
			object_flag = Object_flag_names[i].flag;
			break;
		}
	}

	for ( i = 0; i < Num_ship_flag_names; i++) {
		if (!stricmp(Ship_flag_names[i].flag_name, flag_name)) {
			ship_flag = Ship_flag_names[i].flag;
			send_multi = true;
			break;
		}
	}

	// parse files already have a list of names in the same order as the flags, so we can do something slightly different here.
	for (i = 0; i < Num_parse_object_flags; i++) {
		if (!stricmp(Parse_object_flags[i].name, flag_name)) {
			parse_obj_flag = Parse_object_flags[i].def;
			break;
		}
	}

	for ( i = 0; i < (size_t)Num_ai_flag_names; i++) {
		if (!stricmp(Ai_flag_names[i].flag_name, flag_name)) {
			ai_flag = Ai_flag_names[i].flag;
			break;
		}
	}

	// check for deprecated flags
	if (!stricmp("cannot-perform-scan", flag_name)) {
		ship_flag = Ship::Ship_Flags::Cannot_perform_scan_hide_cargo;
		parse_obj_flag = Mission::Parse_Object_Flags::SF_Cannot_perform_scan_hide_cargo;
		send_multi = true;
	}

	return send_multi;
}

bool sexp_check_flag_array(const char *flag_name, Ship::Wing_Flags &wing_flag)
{
	size_t i;
	bool send_multi = false;

	for ( i = 0; i < Num_wing_flag_names; i++) {
		if (!stricmp(Wing_flag_names[i].flag_name, flag_name)) {
			wing_flag = Wing_flag_names[i].flag;
			send_multi = true;
			break;
		}
	}

	return send_multi;
}

int sexp_are_ship_flags_set(int node)
{
	Object::Object_Flags object_flag = Object::Object_Flags::NUM_VALUES;
	Ship::Ship_Flags ship_flag = Ship::Ship_Flags::NUM_VALUES;
	Mission::Parse_Object_Flags parse_obj_flag = Mission::Parse_Object_Flags::NUM_VALUES;
	AI::AI_Flags ai_flag = AI::AI_Flags::NUM_VALUES;

	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;
	node = CDR(node);

	auto shipp = ship_entry->shipp();
	auto objp = ship_entry->objp();
	auto aip = &Ai_info[shipp->ai_index];

	while (node != -1) {
		auto flag_name = CTEXT(node); 
		sexp_check_flag_arrays(flag_name, object_flag, ship_flag, parse_obj_flag, ai_flag);

		// now check the flags
		if (object_flag != Object::Object_Flags::NUM_VALUES) {
			if (!(objp->flags[object_flag]))
				return SEXP_FALSE;
		}

		if (ship_flag != Ship::Ship_Flags::NUM_VALUES) {
			if (!(shipp->flags[ship_flag]))
				return SEXP_FALSE;
		}

		// we don't check parse flags, except for one that can be an object flag in reverse
		if (parse_obj_flag == Mission::Parse_Object_Flags::OF_No_collide) {
			if (objp->flags[Object::Object_Flags::Collides])
				return SEXP_FALSE;
		}

		if (ai_flag != AI::AI_Flags::NUM_VALUES) {
			if (!(aip->ai_flags[ai_flag]))
				return SEXP_FALSE;
		}

		node = CDR(node); 
	}

	// if we're still here, all the flags we were looking for were present
	return SEXP_TRUE; 
}

int sexp_are_wing_flags_set(int node)
{
	Ship::Wing_Flags wing_flag = Ship::Wing_Flags::NUM_VALUES;

	auto wingp = eval_wing(node);
	if (!wingp)
		return SEXP_NAN_FOREVER;
	node = CDR(node);

	while (node != -1)
	{
		auto flag_name = CTEXT(node); 
		sexp_check_flag_array(flag_name, wing_flag);

		// now check the flags
		if (wing_flag != Ship::Wing_Flags::NUM_VALUES)
		{
			if (!(wingp->flags[wing_flag]))
				return SEXP_FALSE;
		}

		node = CDR(node); 
	}

	// if we're still here, all the flags we were looking for were present
	return SEXP_TRUE; 
}

int sexp_is_ship_emp_active(int n)
{
	Assert(n >= 0);

	while (n != -1) {

		auto ship_entry = eval_ship(n);
		if (!ship_entry)
			return SEXP_FALSE;
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_CANT_EVAL;

		// see if the ship has already exited the mission (either through departure or destruction)
		if (ship_entry->exited_index >= 0) {
			return SEXP_KNOWN_FALSE;

		}
		// ship is in mission
		else if (ship_entry->has_shipp()) {
			if (ship_entry->shipp()->emp_intensity <= 0.0f) {
				return SEXP_FALSE;
			}
		}
		// ship probably vanished
		else
			return SEXP_NAN_FOREVER;

		n = CDR(n);
	}

	return SEXP_TRUE;
}

void sexp_alter_ship_flag(int node)
{
	Object::Object_Flags object_flag = Object::Object_Flags::NUM_VALUES;
	Ship::Ship_Flags ship_flag = Ship::Ship_Flags::NUM_VALUES;
	Mission::Parse_Object_Flags parse_obj_flag = Mission::Parse_Object_Flags::NUM_VALUES;
	AI::AI_Flags ai_flag = AI::AI_Flags::NUM_VALUES;
	bool set_flag = false; 
	bool future_ships = false; 
	object_ship_wing_point_team oswpt;

	auto flag_name = CTEXT(node); 

	sexp_check_flag_arrays(flag_name, object_flag, ship_flag, parse_obj_flag, ai_flag);

	node = CDR(node); 
	if (is_sexp_true(node)) {
		set_flag = true;
	}

	node = CDR(node);
	if (is_sexp_true(node)) {
		future_ships = true;
	}

	// start the multiplayer packet
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_flag(object_flag);
	Current_sexp_network_packet.send_flag(ship_flag);
	Current_sexp_network_packet.send_flag(parse_obj_flag);
	Current_sexp_network_packet.send_flag(ai_flag);
	Current_sexp_network_packet.send_bool(set_flag);
	Current_sexp_network_packet.send_bool(future_ships);

	node = CDR(node);

	// no 4th argument means do this to every ship in the mission (and if the flag is set, every ship that will be too).
	if (node == -1) {
		// send a message to the clients saying there were no more arguments
		Current_sexp_network_packet.send_bool(false);

		alter_flag_for_all_ships(future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
	}
	else {
		// send a message to the clients saying there are more arguments
		Current_sexp_network_packet.send_bool(true);

		for (; node != -1; node = CDR(node)) {
			eval_object_ship_wing_point_team(&oswpt, node);

			// no point in setting these flags at all
			if (oswpt.type == OSWPT_TYPE_NONE || oswpt.type == OSWPT_TYPE_EXITED ) {
				continue; 
			}

			sexp_alter_ship_flag_helper(oswpt, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);

			Current_sexp_network_packet.send_int((int)oswpt.type);

			switch (oswpt.type) {
				case OSWPT_TYPE_SHIP:
					Current_sexp_network_packet.send_ship(oswpt.shipp());
					break;

				case OSWPT_TYPE_PARSE_OBJECT:
					Current_sexp_network_packet.send_parse_object(oswpt.p_objp());
					break;

				case OSWPT_TYPE_WING_NOT_PRESENT:
				case OSWPT_TYPE_WING:
					Current_sexp_network_packet.send_ushort(oswpt.wingp()->net_signature);
					break;

				case OSWPT_TYPE_WHOLE_TEAM:
					Current_sexp_network_packet.send_int(oswpt.team);
					break;

				default:
					break;
			}
		}
	}
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_alter_ship_flag() 
{
	Object::Object_Flags object_flag = Object::Object_Flags::NUM_VALUES;
	Ship::Ship_Flags ship_flag = Ship::Ship_Flags::NUM_VALUES; 
	Mission::Parse_Object_Flags parse_obj_flag = Mission::Parse_Object_Flags::NUM_VALUES;
	AI::AI_Flags ai_flag = AI::AI_Flags::NUM_VALUES;
	bool set_flag = false;
	bool future_ships = true; 
	bool process_data = false;

	Current_sexp_network_packet.get_flag(object_flag); 
	Current_sexp_network_packet.get_flag(ship_flag);
	Current_sexp_network_packet.get_flag(parse_obj_flag);
	Current_sexp_network_packet.get_flag(ai_flag); 
	Current_sexp_network_packet.get_bool(set_flag);
	Current_sexp_network_packet.get_bool(future_ships);
 
	// if any of the above failed so will this loop
	if (!Current_sexp_network_packet.get_bool(process_data)) 
	{
		return;
	}

	// no more data means do this to every ship in the mission
	if (!process_data)
	{
		alter_flag_for_all_ships(future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
	}
	else
	{
		int type;
		while (Current_sexp_network_packet.get_int(type))
		{
			std::unique_ptr<object_ship_wing_point_team> oswptp;

			switch ((oswpt_type)type)
			{
				case OSWPT_TYPE_SHIP:
				{
					ship *shipp;
					if (Current_sexp_network_packet.get_ship(shipp))
						oswptp.reset(new object_ship_wing_point_team(shipp));
					else
						Warning(LOCATION, "OSWPT had an invalid ship in multi_sexp_alter_ship_flag(), skipping");
					break;
				}

				case OSWPT_TYPE_PARSE_OBJECT:
				{
					p_object *p_objp;
					if (Current_sexp_network_packet.get_parse_object(p_objp))
						oswptp.reset(new object_ship_wing_point_team(p_objp));
					else
						Warning(LOCATION, "OSWPT had an invalid parse object in multi_sexp_alter_ship_flag(), skipping");
					break;
				}

				case OSWPT_TYPE_WING_NOT_PRESENT:
				case OSWPT_TYPE_WING:
				{
					wing *wingp;
					if (Current_sexp_network_packet.get_wing(wingp))
						oswptp.reset(new object_ship_wing_point_team(wingp));
					else
						Warning(LOCATION, "OSWPT had an invalid wing in multi_sexp_alter_ship_flag(), skipping");
					break;
				}

				case OSWPT_TYPE_WHOLE_TEAM:
				{
					int team;
					if (Current_sexp_network_packet.get_int(team))
					{
						oswptp.reset(new object_ship_wing_point_team());
						oswptp->type = OSWPT_TYPE_WHOLE_TEAM;
						oswptp->team = team;
					}
					else
						Warning(LOCATION, "OSWPT had an invalid team in multi_sexp_alter_ship_flag(), skipping");
					break;
				}

				default:
					break;
			}

			if (oswptp)
				sexp_alter_ship_flag_helper(*oswptp, future_ships, object_flag, ship_flag, parse_obj_flag, ai_flag, set_flag);
		}
	}
}

void sexp_alter_wing_flag(int node)
{
	Ship::Wing_Flags wing_flag = Ship::Wing_Flags::NUM_VALUES;
	bool set_flag = false; 

	auto flag_name = CTEXT(node); 
	node = CDR(node);

	sexp_check_flag_array(flag_name, wing_flag);

	if (is_sexp_true(node))
		set_flag = true;
	node = CDR(node);

	// start the multiplayer packet
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_flag(wing_flag);
	Current_sexp_network_packet.send_bool(set_flag);

	// no 3rd argument means do this to every wing in the mission
	if (node < 0)
	{
		// send a message to the clients saying there were no more arguments
		Current_sexp_network_packet.send_bool(false);

		for (int i = 0; i < Num_wings; i++)
			Wings[i].flags.set(wing_flag, set_flag);
	}
	else
	{
		// send a message to the clients saying there are more arguments
		Current_sexp_network_packet.send_bool(true);

		for (; node != -1; node = CDR(node))
		{
			auto wingp = eval_wing(node);
			if (!wingp)
				continue;

			wingp->flags.set(wing_flag, set_flag);

			Current_sexp_network_packet.send_wing(wingp);
		}
	}
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_alter_wing_flag() 
{
	Ship::Wing_Flags wing_flag = Ship::Wing_Flags::NUM_VALUES; 
	bool set_flag = false;
	bool process_data = false;

	Current_sexp_network_packet.get_flag(wing_flag);
	Current_sexp_network_packet.get_bool(set_flag);
 
	// if any of the above failed so will this loop
	if (!Current_sexp_network_packet.get_bool(process_data)) 
	{
		return;
	}

	// no more data means do this to every wing in the mission
	if (!process_data)
	{
		for (int i = 0; i < Num_wings; i++)
			Wings[i].flags.set(wing_flag, set_flag);
	}
	else
	{
		wing *wingp;
		while (Current_sexp_network_packet.get_wing(wingp))
		{
			if (!wingp)
				continue;

			wingp->flags.set(wing_flag, set_flag);
		}
	}
}

// modified by Goober5000; now it should work properly
// function to deal with breaking/fixing the warp engines on ships/wings.
// --repairable is true when we are breaking the warp drive (can be repaired)
// --damage_it is true when we are sabotaging it, one way or the other; false when fixing it
void sexp_deal_with_warp(int n, bool repairable, bool damage_it)
{
	Ship::Ship_Flags ship_flag;
	Mission::Parse_Object_Flags p_object_flag;

	if (repairable)
	{
		ship_flag = Ship::Ship_Flags::Warp_broken;
		p_object_flag = Mission::Parse_Object_Flags::SF_Warp_broken;
	}
	else
	{
		ship_flag = Ship::Ship_Flags::Warp_never;
		p_object_flag = Mission::Parse_Object_Flags::SF_Warp_never;
	}

	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::NUM_VALUES, ship_flag, p_object_flag, damage_it);
}

// Goober5000
void sexp_set_subspace_drive(int node)
{
	bool set_flag = !is_sexp_true(node);

	sexp_deal_with_ship_flag(CDR(node), true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::No_subspace_drive, Mission::Parse_Object_Flags::NUM_VALUES, set_flag);
}

//forward declaration
extern void ai_set_or_clear_preferred_primary_weapon(bool set_it, const object_ship_wing_point_team *subject, const object_ship_wing_point_team *target, int weapon_idx);

void sexp_good_primary_time(int n)
{
	object_ship_wing_point_team subject;
	eval_object_ship_wing_point_team(&subject, n);
	//if we don't get a ship, wing, or team, bail
	if (subject.type != oswpt_type::SHIP && subject.type != oswpt_type::WING && subject.type != oswpt_type::WHOLE_TEAM) {
		return;
	}
	n = CDR(n);

	auto weapon_name = CTEXT(n);
	int weapon_index = weapon_info_lookup(weapon_name);
	if (weapon_index < 0) {
		nprintf(("Warning", "couldn't find weapon %s for good-secondary-time\n", weapon_name));
		return;
	}
	n = CDR(n);

	object_ship_wing_point_team target;
	eval_object_ship_wing_point_team(&target, n);
	if (target.type != oswpt_type::SHIP && target.type != oswpt_type::WING && target.type != oswpt_type::WHOLE_TEAM) {
		return;
	}
	n = CDR(n);

	auto activate = is_sexp_true(n);
	ai_set_or_clear_preferred_primary_weapon(activate, &subject, &target, weapon_index);
}

/**
 * Tell the AI when it is okay to fire certain secondary weapons at other ships.
 */
void sexp_good_secondary_time(int n)
{
	bool is_nan, is_nan_forever;

	auto team_name = CTEXT(n);
	int team = iff_lookup(team_name);
	if (team < 0) {
		nprintf(("Warning", "couldn't find team %s for good-secondary-time\n", team_name));
		return;
	}
	n = CDR(n);

	int num_weapons = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	auto weapon_name = CTEXT(n);
	int weapon_index = weapon_info_lookup(weapon_name);
	if (weapon_index < 0) {
		nprintf(("Warning", "couldn't find weapon %s for good-secondary-time\n", weapon_name));
		return;
	}
	n = CDR(n);

	auto ship_entry = eval_ship(n);
	if (!ship_entry)
		return;

	// see if the ship has exited.  If so, then we don't need to set up the AI stuff
	if (ship_entry->status == ShipStatus::EXITED)
		return;

	ai_good_secondary_time( team, weapon_index, num_weapons, ship_entry->name );
}

// Karajorma - Turns the built in messages for pilots and command on or off
void sexp_toggle_builtin_messages (int node, bool enable_messages)
{
	// If no arguments were supplied then turn off all messages then bail
	if (node < 0)
	{
		The_mission.flags.set(Mission::Mission_Flags::No_builtin_msgs, !enable_messages);
		return;
	}

	// iterate through all the nodes supplied
	while (node >= 0)
	{		
		auto ship_name = CTEXT(node);

		// check that this isn't a request to silence command. 
		if ( ((*ship_name == '#') && !stricmp(&ship_name[1], The_mission.command_sender))
			|| (The_mission.flags[Mission::Mission_Flags::Override_hashcommand] && !stricmp(ship_name, DEFAULT_HASHCOMMAND)) )
		{
			// Either disable or enable messages from command
			The_mission.flags.set(Mission::Mission_Flags::No_builtin_command, !enable_messages);
		}
		else if (!stricmp(ship_name, "<Any Wingman>"))
		{
			// Since trying to determine whose wingman in a stand alone multiplayer game opens a can of worms
			// Any Wingman silences all ships in wings regardless of whose side they're on. 
			for (int wingnum = 0; wingnum < Num_wings; ++wingnum) {
				for (int shipnum = 0; shipnum < Wings[wingnum].current_count; ++shipnum) {
					int ship_index = Wings[wingnum].ship_index[shipnum];
					Assert( ship_index != -1 );
					Ships[ship_index].flags.set(Ship::Ship_Flags::No_builtin_messages, !enable_messages);
				}
			}
		}
		// If it isn't command then assume that we're dealing with a ship 
		else 
		{
			sexp_deal_with_ship_flag(node, false, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::No_builtin_messages, Mission::Parse_Object_Flags::SF_No_builtin_messages, !enable_messages);
		}

		node = CDR(node);
	}
}

void sexp_set_persona (int node) 
{
	int persona_index = -1;
	auto persona_name = CTEXT(node);

	for (int i = 0; i < (int)Personas.size(); ++i) {
		if (!strcmp(persona_name, Personas[i].name) && (Personas[i].flags & PERSONA_FLAG_WINGMAN)) {
			persona_index = i;
			break;
		}
	}

	if (persona_index == -1) {
		Warning(LOCATION, "Unable to change to persona type: '%s'. Persona is not a wingman!", persona_name);
		return; 
	}

	node = CDR(node); 
	Assert (node >=0);

	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_int(persona_index); 
	}

	// now loop through the list of ships
	for ( ; node >= 0; node = CDR(node) ) {
		auto ship_entry = eval_ship(node);
		if (!ship_entry || !ship_entry->has_shipp()) {
			continue;
		}

		ship_entry->shipp()->persona_index = persona_index;

		if (MULTIPLAYER_MASTER) {
			Current_sexp_network_packet.send_ship(ship_entry->shipp());
		}
	}

	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_set_persona()
{
	ship *shipp = nullptr; 
	int persona_index = -1; 

	if (! Current_sexp_network_packet.get_int(persona_index) ) {
		return; 
	}

	while (Current_sexp_network_packet.get_ship(shipp)) {
		Assert(persona_index != -1);
		if (shipp != nullptr) {
			shipp->persona_index = persona_index;
		}
	}
}

void sexp_set_mission_mood (int node)
{
	auto mood = CTEXT(node);
	for (SCP_vector<SCP_string>::iterator iter = Builtin_moods.begin(); iter != Builtin_moods.end(); ++iter) {
		if (!strcmp(iter->c_str(), mood)) {
			Current_mission_mood = (int)std::distance(Builtin_moods.begin(), iter);
			return;
		}
	}

	Warning(LOCATION, "Sexp-mission-mood attempted to set mood %s which does not exist in messages.tbl", mood);
}

int sexp_turret_fired_delay(int node)
{
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(node);
	if (!ship_entry) {
		return SEXP_FALSE;
	}
	if (ship_entry->status == ShipStatus::EXITED) {
		return SEXP_KNOWN_FALSE;
	}
	if (ship_entry->status == ShipStatus::NOT_YET_PRESENT) {
		return SEXP_CANT_EVAL;
	}
	auto shipp = ship_entry->shipp();

	// get the subsystem
	node = CDR(node);
	auto turret = ship_get_subsys(shipp, CTEXT(node));
	if (!turret || turret->system_info->type != SUBSYSTEM_TURRET)
		return SEXP_KNOWN_FALSE;

	// get the delay
	node = CDR(node);
	int delay = eval_num(node, is_nan, is_nan_forever);
	if (is_nan_forever) {
		return SEXP_KNOWN_FALSE;
	}
	else if (is_nan || delay <= 0) {
		return SEXP_FALSE;
	}

	if (turret->turret_last_fired.isNever()) {
		// weapon was never fired
		return SEXP_FALSE;
	}
		
	if (timestamp_since(turret->turret_last_fired) < delay) {
		return SEXP_TRUE;
	}

	return SEXP_FALSE;
}

int sexp_weapon_fired_delay(int node, int op_num)
{
	int requested_bank, delay, last_fired = -1;
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(node);
	if (!ship_entry) {
		return SEXP_FALSE;
	}
	if (ship_entry->status == ShipStatus::EXITED) {
		return SEXP_KNOWN_FALSE;
	}
	if (ship_entry->status == ShipStatus::NOT_YET_PRESENT) {
		return SEXP_CANT_EVAL;
	}
	auto shipp = ship_entry->shipp();

	// Get the bank to check
	node = CDR(node);
	requested_bank = eval_num(node, is_nan, is_nan_forever);
	if (is_nan_forever) {
		return SEXP_KNOWN_FALSE;
	} else if (is_nan || requested_bank < 0) {
		return SEXP_FALSE;
	}

	// get the delay
	node = CDR(node);
	delay = eval_num(node, is_nan, is_nan_forever);
	if (is_nan_forever) {
		return SEXP_KNOWN_FALSE;
	} else if (is_nan || delay <= 0) {
		return SEXP_FALSE; 
	}

	switch (op_num) {
		case OP_PRIMARY_FIRED_SINCE: 
			if (requested_bank >= shipp->weapons.num_primary_banks) {
				return SEXP_FALSE;
			}
			last_fired = shipp->weapons.last_primary_fire_stamp[requested_bank];
			break; 

		case OP_SECONDARY_FIRED_SINCE: 
			if (requested_bank >= shipp->weapons.num_secondary_banks) {
				return SEXP_FALSE;
			}
			last_fired = shipp->weapons.last_secondary_fire_stamp[requested_bank];
			break; 
	}

	if (last_fired < 0) {
		// weapon was never fired
		return SEXP_FALSE;
	}

	if (timestamp() - delay < last_fired) {
		return SEXP_TRUE; 
	}

	return SEXP_FALSE;
}

int sexp_has_weapon(int node, int op_num)
{
	// Get the ship to check
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;
	node = CDR(node);

	auto shipp = ship_entry->shipp();
	ship_subsys *turret = nullptr;

	switch (op_num){
		case OP_TURRET_HAS_PRIMARY_WEAPON:
		case OP_TURRET_HAS_SECONDARY_WEAPON:
			// get the subsystem
			turret = ship_get_subsys(shipp, CTEXT(node));
			if (!turret || turret->system_info->type != SUBSYSTEM_TURRET)
				return SEXP_NAN_FOREVER;

			node = CDR(node);
			break;

		default:
			break;
	}


	// Get the bank to check
	int requested_bank;
	bool is_nan, is_nan_forever;

	if (!strcmp(CTEXT(node), SEXP_ALL_BANKS_STRING)) {
		requested_bank = -1;
	}
	else {
		requested_bank = eval_num(node, is_nan, is_nan_forever);
		if (is_nan) {
			return SEXP_FALSE;
		}
		if (is_nan_forever) {
			return SEXP_KNOWN_FALSE;
		}
	}
	node = CDR(node);
 
	// Get the banks of the ship or turret
	int num_weapon_banks = 0;
	int* weapon_banks = nullptr;

	switch (op_num) {
		case OP_HAS_PRIMARY_WEAPON:
			weapon_banks = shipp->weapons.primary_bank_weapons;
			num_weapon_banks = shipp->weapons.num_primary_banks;
			break;

		case OP_HAS_SECONDARY_WEAPON:
			weapon_banks = shipp->weapons.secondary_bank_weapons;
			num_weapon_banks = shipp->weapons.num_secondary_banks;
			break;

		case OP_TURRET_HAS_PRIMARY_WEAPON:
			weapon_banks = turret->weapons.primary_bank_weapons;
			num_weapon_banks = turret->weapons.num_primary_banks;
			break;

		case OP_TURRET_HAS_SECONDARY_WEAPON:
			weapon_banks = turret->weapons.secondary_bank_weapons;
			num_weapon_banks = turret->weapons.num_secondary_banks;
			break;

		default:
			Warning(LOCATION, "Unrecognised bank type used in has-x-weapon. Returning false");
			return SEXP_FALSE;
	}
	

	//loop through the weapons and test them
	int weapon_index;

	while (node > -1) {
		weapon_index = weapon_info_lookup(CTEXT(node));
		Assertion (weapon_index >= 0, "Weapon name %s is unknown.", CTEXT(node));

		// if we're checking every bank
		if (requested_bank == -1) {
			for (int i = 0; i < num_weapon_banks; i++) {
				if (weapon_index == weapon_banks[i]) {
					return SEXP_TRUE;
				}
			}
		}

		// if we're only checking one bank
		else {
			if (weapon_index == weapon_banks[requested_bank]) {
				return SEXP_TRUE;
			}
		}
	
		node = CDR(node);
	}

	return SEXP_FALSE;
}

/**
 * Gets status of goals for previous missions (in the current campaign).
 *
 * @param n Sexp node number
 * @param status tell this function if we are looking for a goal_satisfied, goal_failed, or goal incomplete event
 */
int sexp_previous_goal_status( int n, int status )
{
	int rval = 0;
	int i, mission_num;
	bool default_value = false, use_defaults = true;

	auto mission_name = CTEXT(n);
	auto goal_name = CTEXT(CDR(n));

	// check for possible next optional argument
	n = CDR(CDR(n));
	if ( n != -1 ) {
		default_value = is_sexp_true(n);
	}

	// try to find the given mission name in the current list of missions in the campaign.
	if ( Game_mode & GM_CAMPAIGN_MODE ) {
		i = mission_campaign_find_mission( mission_name );

		if ( i == -1 ) {
			// if mission not found, assume that goal was false (so previous-goal-false returns true)
			nprintf(("General", "Couldn't find mission name \"%s\" in current campaign's list of missions.\nReturning %s for goal-status function.\n", mission_name, (status==GOAL_COMPLETE)?"false":"true"));
			if ( status == GOAL_COMPLETE )
				rval = SEXP_KNOWN_FALSE;
			else
				rval = SEXP_KNOWN_TRUE;

			use_defaults = false;
		} else if (Campaign.missions[i].flags & CMISSION_FLAG_SKIPPED) {
			use_defaults = true;
		} else {
			// now try and find the goal this mission
			mission_num = i;
			for (i = 0; i < (int)Campaign.missions[mission_num].goals.size(); i++) {
				if ( !stricmp(Campaign.missions[mission_num].goals[i].name, goal_name) )
					break;
			}

			if ( i == (int)Campaign.missions[mission_num].goals.size() ) {
				Warning(LOCATION, "Couldn't find goal name \"%s\" in mission %s.\nReturning %s for goal-true function.", goal_name, mission_name, (status==GOAL_COMPLETE)?"false":"true");
				if ( status == GOAL_COMPLETE )
					rval = SEXP_KNOWN_FALSE;
				else
					rval = SEXP_KNOWN_TRUE;

			} else {
				// now return KNOWN_TRUE or KNOWN_FALSE based on the status field in the goal structure
				if ( Campaign.missions[mission_num].goals[i].status == status )
					rval = SEXP_KNOWN_TRUE;
				else
					rval = SEXP_KNOWN_FALSE;
			}

			use_defaults = false;
		}
	}

	if (use_defaults) {
		// when not in campaign mode, always return KNOWN_TRUE when looking for goal complete, and KNOWN_FALSE
		// otherwise
		if ( n != -1 ) {
			if ( default_value )
				rval = SEXP_KNOWN_TRUE;
			else
				rval = SEXP_KNOWN_FALSE;
		} else {
			if ( status == GOAL_COMPLETE )
				rval = SEXP_KNOWN_TRUE;
			else
				rval = SEXP_KNOWN_FALSE;
		}
	}

	return rval;
}

// sexpression which gets the status of an event from a previous mission.  Like the above function but
// dealing with events instead of goals.  Again, the status parameter tells the code if we are looking
// for an event_true, event_false, or event_incomplete status
int sexp_previous_event_status( int n, EventStatus status )
{
	int rval = 0;
	int i, mission_num;
	bool default_value = false, use_defaults = true;

	auto mission_name = CTEXT(n);
	auto name = CTEXT(CDR(n));

	// check for possible optional parameter
	n = CDR(CDR(n));
	if ( n != -1 ){
		default_value = is_sexp_true(n);
	}

	if ( Game_mode & GM_CAMPAIGN_MODE ) {
		// following function returns -1 when mission isn't found.
		i = mission_campaign_find_mission( mission_name );

		// if the mission name wasn't found -- make this return FALSE for the event status.
		if ( i == -1 ) {
			nprintf(("General", "Couldn't find mission name \"%s\" in current campaign's list of missions.\nReturning %s for event-status function.\n", mission_name, (status==EventStatus::SATISFIED)?"false":"true"));
			if ( status == EventStatus::SATISFIED ) {
				rval = SEXP_KNOWN_FALSE;
			} else {
				rval = SEXP_KNOWN_TRUE;
			}

			use_defaults = false;
		} else if (Campaign.missions[i].flags & CMISSION_FLAG_SKIPPED) {
			use_defaults = true;
		} else {
			// now try and find the goal this mission
			mission_num = i;
			for (i = 0; i < (int)Campaign.missions[mission_num].events.size(); i++) {
				if ( !stricmp(Campaign.missions[mission_num].events[i].name, name) )
					break;
			}

			if ( i == (int)Campaign.missions[mission_num].events.size() ) {
				Warning(LOCATION, "Couldn't find event name \"%s\" in mission %s.\nReturning %s for event_status function.", name, mission_name, (status==EventStatus::SATISFIED)?"false":"true");
				if ( status == EventStatus::SATISFIED )
					rval = SEXP_KNOWN_FALSE;
				else
					rval = SEXP_KNOWN_TRUE;

			} else {
				// now return KNOWN_TRUE or KNOWN_FALSE based on the status field in the goal structure
				if ( Campaign.missions[mission_num].events[i].status == static_cast<int>(status) )
					rval = SEXP_KNOWN_TRUE;
				else
					rval = SEXP_KNOWN_FALSE;
			}

			use_defaults = false;
		}
	} 

	if (use_defaults) {
		if ( n != -1 ) {
			if ( default_value )
				rval = SEXP_KNOWN_TRUE;
			else
				rval = SEXP_KNOWN_FALSE;
		} else {
			if ( status == EventStatus::SATISFIED )
				rval = SEXP_KNOWN_TRUE;
			else
				rval = SEXP_KNOWN_FALSE;
		}
	}

	return rval;
}

/**
 * Return the status of an event in the current mission.  
 *
 * @param n Sexp node number
 * @param want_true indicates if we are checking whether the event is true or the event is false.
 */
int sexp_event_status( int n, int want_true )
{
	int rval = SEXP_FALSE;
	auto name = CTEXT(n);

	for (int i = 0; i < (int)Mission_events.size(); ++i) {
		// look for the event name; check its status.  If formula is gone, we know the state won't ever change.
		if ( !stricmp(Mission_events[i].name.c_str(), name) ) {
			int result = Mission_events[i].result;
			if (Mission_events[i].flags & MEF_EVENT_IS_DONE) {
				if ( (want_true && result) || (!want_true && !result) )
					rval = SEXP_KNOWN_TRUE;
				else
					rval = SEXP_KNOWN_FALSE;

			} else {
				if ( (want_true && result) || (!want_true && !result) )
					rval = SEXP_TRUE;
				else
					rval = SEXP_FALSE;
			}
			break;
		}
	}

	// don't make the enclosing event current if this operator doesn't return true
	if ((rval != SEXP_TRUE) && (rval != SEXP_KNOWN_TRUE))
		Assume_event_is_current = false;  // indicate sexp isn't current yet

	return rval;
}

/**
 * Return the status of an event N seconds after the event is true or false.
 *
 * Similar to above function but waits N seconds before returning true
 */
int sexp_event_delay_status( int n, int want_true, bool use_msecs = false)
{
	bool is_nan, is_nan_forever;
	int delay;
	int rval = SEXP_FALSE;
	bool use_as_directive = false;

	auto name = CTEXT(n);

	delay = eval_num(CDR(n), is_nan, is_nan_forever);
	if (is_nan) {
		Assume_event_is_current = false;  // indicate sexp isn't current yet
		return SEXP_FALSE;
	}
	else if (is_nan_forever) {
		Assume_event_is_current = false;  // indicate sexp isn't current yet
		return SEXP_KNOWN_FALSE;
	}
	if (!use_msecs) {
		delay *= MILLISECONDS_PER_SECOND;
	}

	for (int i = 0; i < (int)Mission_events.size(); ++i) {
		// look for the event name; check its status.  If formula is gone, we know the state won't ever change.
		if ( !stricmp(Mission_events[i].name.c_str(), name) ) {
			// deduct the interval using the same logic as in mission_process_event()
			if (Mission_events[i].flags & MEF_TIMESTAMP_HAS_INTERVAL) {
				if (Mission_events[i].flags & MEF_USE_MSECS) {
					delay -= Mission_events[i].interval;
				} else {
					delay -= Mission_events[i].interval * MILLISECONDS_PER_SECOND;
				}
			}

			// Events that have never fired are not subject to the delay check. This matches the behavior of the
			// original public source code release and also allows checks for these events to work in debriefings.
			// Addendum: Also do not check the delay if we're not in-mission.  This allows debriefings to check
			// events that have fired on the same timestamp that the mission ends.  These events would otherwise
			// not be checked due to the "one frame must elapse" rule.
			if (!Mission_events[i].timestamp.isValid() || !(Game_mode & GM_IN_MISSION)) {
				/* do not set rval */;
			}
			// Check that the event delay has elapsed, again using the same logic as in mission_process_event()
			else if (!Fixed_chaining_to_repeat && Mission_events[i].flags & MEF_TIMESTAMP_HAS_INTERVAL) {
				/* do not set rval */;
			}
			// Note that if the event and the timestamp happen simultaneously, at least one frame must elapse first;
			// this matches the delay check in the original public source code release
			else if (!timestamp_elapsed_last_frame(timestamp_delta(Mission_events[i].timestamp, delay))) {
				rval = SEXP_FALSE;
				break;
			}

			int result = Mission_events[i].result;
			if (Mission_events[i].flags & MEF_EVENT_IS_DONE) {
				if ( (want_true && result) || (!want_true && !result) ) {
					rval = SEXP_KNOWN_TRUE;
					break;
				} else {
					rval = SEXP_KNOWN_FALSE;
					break;
				}
			} else {
				if ( want_true && result ) {  //) || (!want_true && !result) )
					rval = SEXP_TRUE;
					break;
				} else {
					rval = SEXP_FALSE;
					break;
				}
			}
		}
	}

	// check for possible optional parameter
	n = CDDR(n);
	if (n != -1)
		use_as_directive = is_sexp_true(n);

	// don't make the enclosing event current if this operator doesn't return true and we don't want this for specific directive use
	if ( !use_as_directive && (rval != SEXP_TRUE) && (rval != SEXP_KNOWN_TRUE) )
		Assume_event_is_current = false;  // indicate sexp isn't current yet

	return rval;
}

/**
 * Returns true if the given event is still incomplete
 */
int sexp_event_incomplete(int n)
{
	int rval = SEXP_FALSE;
	auto name = CTEXT(n);
	
	for (int i = 0; i < (int)Mission_events.size(); ++i) {
		if ( !stricmp(Mission_events[i].name.c_str(), name ) ) {
			// if the formula is still >= 0 (meaning it is still getting eval'ed), then
			// the event is incomplete
			// Goober5000 - check the flag instead
			if (!(Mission_events[i].flags & MEF_EVENT_IS_DONE))
				rval = SEXP_TRUE;
			else
				rval = SEXP_KNOWN_FALSE;
			break;
		}
	}

	// don't make the enclosing event current if this operator doesn't return true
	if ((rval != SEXP_TRUE) && (rval != SEXP_KNOWN_TRUE))
		Assume_event_is_current = false;  // indicate sexp isn't current yet

	return rval;
}

/**
 * Return the status of an goal N seconds after the goal is true or false.
 *
 * Similar to above function but operates on goals instead of events
 */
int sexp_goal_delay_status( int n, int want_true )
{
	fix delay, time;
	bool is_nan, is_nan_forever;

	auto name = CTEXT(n);
	delay = i2f(eval_num(CDR(n), is_nan, is_nan_forever));
	if (is_nan) {
		return SEXP_FALSE;
	}
	else if (is_nan_forever) {
		return SEXP_KNOWN_FALSE;
	}
	
	if ( want_true ) {
		// if we are looking for a goal true entry and we find a false, then return known false here
		if ( mission_log_get_time(LOG_GOAL_FAILED, name, nullptr, nullptr) )
			return SEXP_KNOWN_FALSE;
		else if ( mission_log_get_time(LOG_GOAL_SATISFIED, name, nullptr, &time) ) {
			if ( (Missiontime - time) >= delay )
				return SEXP_KNOWN_TRUE;
		}
	} else {
		// if we are looking for a goal false entry and we find a true, then return known false here
		if ( mission_log_get_time(LOG_GOAL_SATISFIED, name, nullptr, nullptr) )
			return SEXP_KNOWN_FALSE;
		else if ( mission_log_get_time(LOG_GOAL_FAILED, name, nullptr, &time) ) {
			if ( (Missiontime - time) >= delay )
				return SEXP_KNOWN_TRUE;
		}
	}

	return SEXP_FALSE;
}

/**
 * Returns true if the given goal is still incomplete
 */
int sexp_goal_incomplete(int n)
{
	auto name = CTEXT(n);

	if ( mission_log_get_time( LOG_GOAL_SATISFIED, name, nullptr, nullptr) || mission_log_get_time( LOG_GOAL_FAILED, name, nullptr, nullptr) )
		return SEXP_KNOWN_FALSE;
	else
		return SEXP_TRUE;
}

/**
 * Resets an event, its status, and its nodes so that it is as if the event had never been evaluated.
 */
void sexp_reset_event(int node)
{
	for (int n = node; n >= 0; n = CDR(n))
	{
		auto name = CTEXT(n);
		int event_num = mission_event_lookup(name);
		if (event_num >= 0)
		{
			auto eventp = &Mission_events[event_num];

			eventp->result = 0;
			eventp->timestamp = TIMESTAMP::invalid();

			// clear the flags that are changed over the course of the event's lifetime,
			// leaving alone the flags that define how the event is configured
			eventp->flags &= ~MEF_CURRENT;
			eventp->flags &= ~MEF_DIRECTIVE_SPECIAL;
			eventp->flags &= ~MEF_DIRECTIVE_TEMP_TRUE;
			eventp->flags &= ~MEF_TIMESTAMP_HAS_INTERVAL;
			eventp->flags &= ~MEF_EVENT_IS_DONE;

			eventp->count = 0;
			eventp->satisfied_time = TIMESTAMP::invalid();
			eventp->born_on_date = TIMESTAMP::invalid();
			eventp->previous_result = 0;

			flush_sexp_tree(eventp->formula);
		}
		else
			Warning(LOCATION, "Could not find event '%s'", name);
	}
}

/**
 * Resets a goal, its status, and its nodes so that it is as if the goal had never been evaluated.
 */
void sexp_reset_goal(int node)
{
	for (int n = node; n >= 0; n = CDR(n))
	{
		auto name = CTEXT(n);
		int goal_num = mission_goal_lookup(name);
		if (goal_num >= 0)
		{
			auto goalp = &Mission_goals[goal_num];

			goalp->satisfied = GOAL_INCOMPLETE;
			flush_sexp_tree(goalp->formula);
		}
		else
			Warning(LOCATION, "Could not find goal '%s'", name);
	}
}

/**
 * Protects/unprotects a ship.
 *
 * @param n Sexp node number
 * @param flag Whether or not the protect bit should be set (flag==true) or cleared (flag==false)
 */
void sexp_protect_ships(int n, bool flag)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Protected, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Protected, flag);
}

/**
 * Protects/unprotects a ship from beams.
 *
 * @param n Sexp node number
 * @param flag Whether or not the protect bit should be set (flag==true) or cleared (flag==false)
 */
void sexp_beam_protect_ships(int n, bool flag)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Beam_protected, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Beam_protected, flag);
}

/**
 * Protects/unprotects a ship from various turrets.
 *
 * @param n Sexp node number
 * @param flag Whether or not the protect bit should be set (flag==true) or cleared (flag==false)
 */
void sexp_turret_protect_ships(int n, bool flag)
{
	auto turret_type = CTEXT(n);
	n = CDR(n);

	if (!stricmp(turret_type, "beam"))
		sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Beam_protected, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Beam_protected, flag);
	else if (!stricmp(turret_type, "flak"))
		sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Flak_protected, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Flak_protected, flag);
	else if (!stricmp(turret_type, "laser"))
		sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Laser_protected, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Laser_protected, flag);
	else if (!stricmp(turret_type, "missile"))
		sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Missile_protected, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Missile_protected, flag);
	else
		Warning(LOCATION, "Invalid turret type '%s'!", turret_type);
}

// Goober5000 - sets the "don't collide invisible" flag on a list of ships
void sexp_dont_collide_invisible(int n, bool dont_collide)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Dont_collide_invis, Mission::Parse_Object_Flags::SF_Dont_collide_invis, dont_collide);
}

// Goober5000 - sets the "immobile" flag on a list of ships; also always clears "don't-change-position" and "don't-change-orientation" to avoid conflicts
void sexp_set_immobile(int n, bool immobile)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Immobile, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Immobile, immobile);
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Dont_change_position, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Dont_change_position, false);
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Dont_change_orientation, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Dont_change_orientation, false);
}

// Goober5000 - sets the "no-ets" flag on a list of ships
void sexp_disable_ets(int n, bool disable)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::No_ets, Mission::Parse_Object_Flags::SF_No_ets, disable);
}

// Goober5000 - sets the vaporize flag on a list of ships
void sexp_ships_vaporize(int n, bool vaporize)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Vaporize, Mission::Parse_Object_Flags::SF_Vaporize, vaporize);
}

/**
 * Make ships "visible" and "invisible" to sensors.
 *
 * @param n Sexp node number
 * @param visible Is true when making ships visible, false otherwise
 */
void sexp_ships_visible(int n, bool visible)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Hidden_from_sensors, Mission::Parse_Object_Flags::SF_Hidden_from_sensors, !visible, true);

	// we also have to add any escort ships that were made visible
	for (; n >= 0; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp())
			continue;

		if (!visible && Player_ai->target_objnum == ship_entry->objnum) {
			hud_cease_targeting(); 
		}
		else if (visible && (ship_entry->shipp()->flags[Ship::Ship_Flags::Escort])) {
			hud_add_ship_to_escort(ship_entry->objnum, 1);
		}		
	}
}

// Goober5000
void sexp_ships_stealthy(int n, bool stealthy)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Stealth, Mission::Parse_Object_Flags::SF_Stealth, stealthy, true);

	// we also have to add any escort ships that were made visible
	if (!stealthy)
	{
		for (; n >= 0; n = CDR(n))
		{
			auto ship_entry = eval_ship(n);
			if (!ship_entry || !ship_entry->has_shipp())
				continue;

			if (ship_entry->shipp()->flags[Ship::Ship_Flags::Escort])
				hud_add_ship_to_escort(ship_entry->objnum, 1);
		}
	}
}

// Goober5000
void sexp_friendly_stealth_invisible(int n, bool invisible)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Friendly_stealth_invis, Mission::Parse_Object_Flags::SF_Friendly_stealth_invis, invisible, true);

	// we also have to add any escort ships that were made visible
	if (!invisible)
	{
		for (; n >= 0; n = CDR(n))
		{
			auto ship_entry = eval_ship(n);
			if (!ship_entry || !ship_entry->has_shipp())
				continue;

			if (ship_entry->shipp()->flags[Ship::Ship_Flags::Stealth] && Player_ship->team == ship_entry->shipp()->team)
			{
				if (ship_entry->shipp()->flags[Ship::Ship_Flags::Escort])
					hud_add_ship_to_escort(ship_entry->objnum, 1);
			}
		}
	}
}

//FUBAR
//generic function to deal with subsystem flag sexps.
//setit only passed for backward compatibility with older sexps.
void sexp_ship_deal_with_subsystem_flag(int op_node, int node, Ship::Subsystem_Flags ss_flag, bool sendit = false, bool setit = false)
{	
	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	auto shipp = ship_entry->shipp();
	node = CDR(node);

	//replace or not
	// OP_SHIP_SUBSYS_TARGETABLE/UNTARGETABLE, OP_SHIP_SUBSYS_TARGETABLE and OP_TURRET_SUBSYS_TARGET_ENABLE/DISABLE 
	// will have already passed us this data we don't need to set it for them. 
	// backward compatibility hack for older sexps
	if (!((ss_flag == Ship::Subsystem_Flags::Untargetable) || (ss_flag == Ship::Subsystem_Flags::No_SS_targeting)))
	{
		setit = is_sexp_true(node);
		node = CDR(node);
	}
	
	//multiplayer packet start
	if (sendit)
	{
		Current_sexp_network_packet.start_callback(); 
		Current_sexp_network_packet.send_ship(shipp);
		Current_sexp_network_packet.send_bool(setit);
	}

	//Process subsystems
	const char *op_name = Sexp_nodes[op_node].text;
	process_ship_subsystems(ship_entry, true, node, true, op_name, [&](ProcessSubsystemType type, ship_subsys *ss)
	{
		if (type == ProcessSubsystemType::SUBSYSTEM)
		{
			ss->flags.set(ss_flag, setit);

			// multiplayer send subsystem name
			if (sendit)
				Current_sexp_network_packet.send_string(ss->system_info->subobj_name);
		}
		else
			Warning(LOCATION, "%s does not support %s!", op_name, special_subsystem_string(type));
	});

	// multiplayer end of packet
	if (sendit)
		Current_sexp_network_packet.end_callback();
}

void multi_sexp_deal_with_subsys_flag(Ship::Subsystem_Flags ss_flag)
{
	bool setit = false;
	ship *shipp = nullptr;
	char ss_name[MAX_NAME_LEN];

	Current_sexp_network_packet.get_ship(shipp);
	Current_sexp_network_packet.get_bool(setit);
 
	while (Current_sexp_network_packet.get_string(ss_name)) 
	{
		// deal with generic subsystem names
		// (sexp_ship_deal_with_subsystem_flag will have resolved this already, but this keeps compatibility with earlier builds)
		int generic_type = get_generic_subsys(ss_name);
		if (generic_type) {
			for (auto ss: list_range(&shipp->subsys_list)) {
				if (generic_type == SUBSYSTEM_ALL || generic_type == ss->system_info->type) {
					ss->flags.set(ss_flag, setit);
				}
			}
		}
		else
		{
			auto ss = ship_get_subsys(shipp, ss_name);
			if (ss)
			{	
				// set the flag
				ss->flags.set(ss_flag, setit);
			}
		}
	}
}

// Goober5000
void sexp_ship_tag( int n, int tag )
{
	int tag_level, tag_time, ssm_index(0), ssm_team(0);
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	// if untag, then unset everything and leave
	if (!tag)
	{
		ship_entry->shipp()->tag_left = -1.0f;
		ship_entry->shipp()->level2_tag_left = -1.0f;
		return;
	}

	// get the tag level and time
	eval_nums(n, is_nan, is_nan_forever, tag_level, tag_time);
	if (is_nan || is_nan_forever)
		return;

	// get SSM info if needed
	vec3d start;
	if (tag_level == 3)
	{
		if (n < 0)
			return;
		ssm_index = ssm_info_lookup(CTEXT(n));
		if (ssm_index < 0)
			return;
		n = CDR(n);

		eval_vec3d(&start, n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;

		if (n >= 0)
			ssm_team = iff_lookup(CTEXT(n));
	}

	ship_apply_tag(ship_entry->shipp(), tag_level, (float)tag_time, ship_entry->objp(), &start, ssm_index, ssm_team);
}

// sexpression to toggle invulnerability flag of ships.
void sexp_ships_invulnerable( int n, bool invulnerable )
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Invulnerable, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Invulnerable, invulnerable);
}

void sexp_ships_bomb_targetable(int n, bool targetable)
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::Targetable_as_bomb, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_Targetable_as_bomb, targetable, true);
}

// Goober5000
void sexp_ship_guardian_threshold(int node)
{
	int threshold, n = node;
	bool is_nan, is_nan_forever;

	threshold = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	// for all ships
	for ( ; n != -1; n = CDR(n) ) {
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp()) {
			continue;
		}

		ship_entry->shipp()->ship_guardian_threshold = threshold;
	}
}

// MjnMixael
void sexp_set_guard_range(int node)
{
	int range, n = node;
	bool is_nan, is_nan_forever;

	range = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	for (; n != -1; n = CDR(n)) {
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp()) {
			continue;
		}

		// Intentionally no lower bound validation beyond disabling at <= 0.
		// Mission authors may choose very small positive values for highly restrictive escort behavior.
		ship_entry->shipp()->max_guard_radius = (range > 0) ? static_cast<float>(range) : -1.0f;
	}
}

// Goober5000
void sexp_ship_subsys_guardian_threshold(int node)
{
	int threshold, n = node;
	bool is_nan, is_nan_forever;

	threshold = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	process_ship_subsystems(ship_entry, true, n, true, "ship-subsys-guardian-threshold", [&](ProcessSubsystemType type, ship_subsys *ss)
	{
		if (type == ProcessSubsystemType::HULL)
			ship_entry->shipp()->ship_guardian_threshold = threshold;
		else if (type == ProcessSubsystemType::SUBSYSTEM)
			ss->subsys_guardian_threshold = threshold;
		else
			Warning(LOCATION, "ship-subsys-guardian-threshold does not support %s!", special_subsystem_string(type));
	});
}

// sexpression to toggle KEEP ALIVE flag of ship object
void sexp_ships_guardian( int n, bool guardian )
{
	for ( ; n != -1; n = CDR(n) )
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || ship_entry->status == ShipStatus::EXITED)
			continue;

		if (ship_entry->has_shipp()) {
			ship_entry->shipp()->ship_guardian_threshold = guardian ? SHIP_GUARDIAN_THRESHOLD_DEFAULT : 0;
		} else if (ship_entry->has_p_objp()) {
			ship_entry->p_objp()->flags.set(Mission::Parse_Object_Flags::SF_Guardian, guardian);
		}
	}
}

void sexp_ship_create(int n)
{
	int new_ship_class, angle_count, team = -1;
	vec3d new_ship_pos;
	angles new_ship_ang;
	matrix new_ship_ori;
	bool is_nan, is_nan_forever, show_in_mission_log = true;

	Assert( n >= 0 );

	// get ship name
	auto new_ship_name = CTEXT(n);
	n = CDR(n);

	// none means don't specify it
	// if ship with this name already exists, ship_create will respond appropriately
	if (!stricmp(new_ship_name, SEXP_NONE_STRING))
		new_ship_name = nullptr;
	
	//Get ship class
	new_ship_class = ship_info_lookup(CTEXT(n));
	if (new_ship_class < 0)
	{
		Warning(LOCATION, "Invalid ship class passed to ship-create; ship type '%s' does not exist", CTEXT(n));
		return;
	}
	n = CDR(n);

	eval_vec3d(&new_ship_pos, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	angle_count = eval_angles(&new_ship_ang, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	//This is a costly function, so only do it if needed
	if (angle_count > 0)
		vm_angles_2_matrix(&new_ship_ori, &new_ship_ang);
	else
		new_ship_ori = vmd_identity_matrix;

	if (n >= 0)
	{
		team = iff_lookup(CTEXT(n));
		n = CDR(n);
	}

	if (n >= 0)
	{
		show_in_mission_log = is_sexp_true(n);
		n = CDR(n);
	}

	int objnum = ship_create(&new_ship_ori, &new_ship_pos, new_ship_class, new_ship_name);
	Assert(objnum != -1);

	// do some initialization that is usually handled by parse_create_object_sub
	int shipnum = Objects[objnum].instance;
	ship *shipp = &Ships[shipnum];
	ship_info *sip = &Ship_info[shipp->ship_info_index];

	if (team >= 0)
		shipp->team = team;

	// note: model_page_in_textures was called via the sexp preloader

	ship_set_warp_effects(&Objects[objnum]);

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

	if (sip->flags[Ship::Info_Flags::Intrinsic_no_shields])
		Objects[objnum].flags.set(Object::Object_Flags::No_shields);

	mission_log_add_entry(LOG_SHIP_ARRIVED, shipp->ship_name, nullptr, -1, show_in_mission_log ? 0 : MLF_HIDDEN);

	if (scripting::hooks::OnShipArrive->isActive()) {
		scripting::hooks::OnShipArrive->run(scripting::hooks::ShipArriveConditions{ shipp, ArrivalLocation::AT_LOCATION, nullptr },
			scripting::hook_param_list(
				scripting::hook_param("Ship", 'o', &Objects[objnum])
			));
	}
}

// MjnMixael
void sexp_prop_create(int n)
{
	int new_prop_class, angle_count;
	vec3d new_prop_pos;
	angles new_prop_ang;
	matrix new_prop_ori;
	bool is_nan, is_nan_forever;

	Assert( n >= 0 );

	// get ship name
	auto new_ship_name = CTEXT(n);
	n = CDR(n);

	// none means don't specify it
	// if ship with this name already exists, ship_create will respond appropriately
	if (!stricmp(new_ship_name, SEXP_NONE_STRING))
		new_ship_name = nullptr;
	
	//Get ship class
	new_prop_class = prop_info_lookup(CTEXT(n));
	if (new_prop_class < 0)
	{
		Warning(LOCATION, "Invalid prop class passed to prop-create; prop type '%s' does not exist", CTEXT(n));
		return;
	}
	n = CDR(n);

	eval_vec3d(&new_prop_pos, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	angle_count = eval_angles(&new_prop_ang, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	//This is a costly function, so only do it if needed
	if (angle_count > 0)
		vm_angles_2_matrix(&new_prop_ori, &new_prop_ang);
	else
		new_prop_ori = vmd_identity_matrix;

	int objnum = prop_create(&new_prop_ori, &new_prop_pos, new_prop_class, new_ship_name);
	Assert(objnum != -1);

	// note: model_page_in_textures was called via the sexp preloader
}

// Goober5000
void sexp_weapon_create(int n)
{
	int weapon_class, parent_objnum, target_objnum, weapon_objnum, angle_count, is_locked;
	ship_subsys *targeted_ss;
	vec3d weapon_pos;
	angles weapon_angles;
	matrix weapon_orient;
	bool is_nan, is_nan_forever;

	parent_objnum = -1;
	auto parent = eval_ship(n);
	if (parent)
	{
		if (parent->objnum >= 0)
			parent_objnum = parent->objnum;
		else
			return;		// parent ship isn't present
	}
	else if (stricmp(CTEXT(n), SEXP_NONE_STRING) != 0)
	{
		Warning(LOCATION, "A nonexistent ship %s was specified as a parent for weapon-create!", CTEXT(n));
		return;
	}
	n = CDR(n);
	
	weapon_class = weapon_info_lookup(CTEXT(n));
	if (weapon_class < 0)
	{
		Warning(LOCATION, "Invalid weapon class passed to weapon-create; weapon type '%s' does not exist", CTEXT(n));
		return;
	}
	n = CDR(n);

	eval_vec3d(&weapon_pos, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	angle_count = eval_angles(&weapon_angles, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	// This is a costly function, so only do it if needed
	if (angle_count > 0)
		vm_angles_2_matrix(&weapon_orient, &weapon_angles);
	else
		weapon_orient = vmd_identity_matrix;

	target_objnum = -1;
	if (n >= 0)
	{
		auto ship_entry = eval_ship(n);
		if (ship_entry && ship_entry->objnum >= 0)
			target_objnum = ship_entry->objnum;

		n = CDR(n);
	}

	targeted_ss = nullptr;
	if (n >= 0)
	{
		if (target_objnum >= 0)
			targeted_ss = ship_get_subsys(&Ships[Objects[target_objnum].instance], CTEXT(n));

		n = CDR(n);
	}

	is_locked = (target_objnum >= 0) ? 1 : 0;	// assume full lock; this lets lasers track if people want them to

	// create the weapon
	// coverity[uninit_use_in_call:FALSE] - weapon_pos is always populated by eval_vec3d
	weapon_objnum = weapon_create(&weapon_pos, &weapon_orient, weapon_class, parent_objnum, -1, is_locked);

	// maybe make the weapon track its target
	if (is_locked)
		weapon_set_tracking_info(weapon_objnum, parent_objnum, target_objnum, is_locked, targeted_ss);
}

// make ship vanish without a trace (and what it's docked to)
void sexp_ship_vanish(int n)
{
	for ( ; n != -1; n = CDR(n) )
	{
		auto ship_entry = eval_ship(n);
		if (ship_entry && ship_entry->status == ShipStatus::PRESENT)
			ship_actually_depart(ship_entry->shipnum, SHIP_VANISHED);
	}
}

// make prop vanish without a trace
void sexp_prop_vanish(int n)
{
	for (; n != -1; n = CDR(n)) {
		auto prop_entry = eval_prop(n);
		if (prop_entry != nullptr) {
			Objects[prop_entry->objnum].flags.set(Object::Object_Flags::Should_be_dead);
		}
	}
}

void sexp_destroy_instantly(int n, bool with_debris)
{
	if (MULTIPLAYER_MASTER)
		Current_sexp_network_packet.start_callback();

	for ( ; n >= 0; n = CDR(n) )
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || ship_entry->status != ShipStatus::PRESENT)
			continue;

		// if it's the player don't destroy
		if (!get_player_from_ship_node(n))
		{
			// multiplayer callback
			if (MULTIPLAYER_MASTER)
				Current_sexp_network_packet.send_ship(ship_entry->shipp());

			ship_destroy_instantly(ship_entry->objp(), with_debris);
		}
	}

	if (MULTIPLAYER_MASTER)
		Current_sexp_network_packet.end_callback();
}

void multi_sexp_destroy_instantly()
{
	ship *shipp;

	// destroy ships
	while (Current_sexp_network_packet.get_ship(shipp))
		ship_destroy_instantly(&Objects[shipp->objnum]);
}

void sexp_shields_off(int n, bool shields_off ) //-Sesquipedalian
{
	sexp_deal_with_ship_flag(n, true, Object::Object_Flags::No_shields, Ship::Ship_Flags::NUM_VALUES, Mission::Parse_Object_Flags::OF_No_shields, shields_off, true);
}

// Goober5000
void sexp_ingame_ship_kamikaze(ship *shipp, int kdamage)
{
	Assertion(shipp, "Invalid ship pointer passed to sexp_ingame_ship_kamikaze.\n");
	Assertion(kdamage >= 0, "Invalid value passed to sexp_ingame_ship_kamikaze. Kamikaze damage must be >= 0, is %i.\n", kdamage);

	ai_info *aip = &Ai_info[shipp->ai_index];

	aip->ai_flags.set(AI::AI_Flags::Kamikaze, kdamage > 0);
	aip->kamikaze_damage = kdamage;

}

// Goober5000
void sexp_parse_ship_kamikaze(p_object *parse_obj, int kdamage)
{
	Assert(parse_obj);

	parse_obj->flags.set(Mission::Parse_Object_Flags::AIF_Kamikaze, kdamage > 0);
	if (kdamage > 0)
	{
		parse_obj->kamikaze_damage = kdamage;
	}
	else
	{
		parse_obj->kamikaze_damage = 0;
	}
}

// Goober5000 - redone, added wing stuff
void sexp_kamikaze(int n, int kamikaze)
{
	int kdamage;
	bool is_nan, is_nan_forever;

	kdamage = 0;
	if (kamikaze)
	{
		kdamage = eval_num(n, is_nan, is_nan_forever);
		n = CDR(n);

		if (is_nan || is_nan_forever)
			return;
	}

	for ( ; n != -1; n = CDR(n) )
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		switch (oswpt.type)
		{
			// change ingame ship
			case OSWPT_TYPE_SHIP:
			{
				sexp_ingame_ship_kamikaze(oswpt.shipp(), kdamage);
				break;
			}

			// change ship yet to arrive
			case OSWPT_TYPE_PARSE_OBJECT:
			{
				sexp_parse_ship_kamikaze(oswpt.p_objp(), kdamage);
				break;
			}

			// change wing (we must set the flags for all ships present as well as all ships yet to arrive)
			case OSWPT_TYPE_WING:
			case OSWPT_TYPE_WING_NOT_PRESENT:
			{
				// current ships
				for (int i = 0; i < oswpt.wingp()->current_count; i++)
					sexp_ingame_ship_kamikaze(&Ships[oswpt.wingp()->ship_index[i]], kdamage);

				// ships yet to arrive
				for (auto p_objp: list_range(&Ship_arrival_list))
				{
					if (p_objp->wingnum == oswpt.wingnum)
						sexp_parse_ship_kamikaze(p_objp, kdamage);
				}
				break;
			}

			default:
				break;
		}
	}
}

// Goober5000
void sexp_ingame_ship_alt_name_or_display_name_or_callsign(ship *shipp, bool clear_it, int alt_index, const char *display_name, int op_num)
{
	// we might be clearing it
	if (clear_it)
	{
		if (op_num == OP_SHIP_CHANGE_ALT_NAME)
			shipp->alt_type_index = -1;
		else if (op_num == OP_SHIP_CHANGE_CALLSIGN)
			shipp->callsign_index = -1;
		else if (op_num == OP_SHIP_CHANGE_DISPLAY_NAME)
		{
			shipp->display_name = "";
			shipp->flags.remove(Ship::Ship_Flags::Has_display_name);
		}
		else
			UNREACHABLE("Unhandled op_num %d!", op_num);

		return;
	}

	if (op_num == OP_SHIP_CHANGE_ALT_NAME)
	{
		// see if this is actually the ship class
		if (!stricmp(Ship_info[shipp->ship_info_index].name, Mission_alt_types[alt_index]))
		{
			shipp->alt_type_index = -1;
			return;
		}
	}

	if (op_num == OP_SHIP_CHANGE_ALT_NAME)
		shipp->alt_type_index = alt_index;
	else if (op_num == OP_SHIP_CHANGE_CALLSIGN)
		shipp->callsign_index = alt_index;
	else if (op_num == OP_SHIP_CHANGE_DISPLAY_NAME)
	{
		shipp->display_name = display_name;
		shipp->flags.set(Ship::Ship_Flags::Has_display_name);
	}
	else
		UNREACHABLE("Unhandled op_num %d!", op_num);
}

// Goober5000
void sexp_parse_ship_alt_name_or_display_name_or_callsign(p_object *parse_obj, bool clear_it, int alt_index, const char *display_name, int op_num)
{
	// we might be clearing it
	if (clear_it)
	{
		if (op_num == OP_SHIP_CHANGE_ALT_NAME)
			parse_obj->alt_type_index = -1;
		else if (op_num == OP_SHIP_CHANGE_CALLSIGN)
			parse_obj->callsign_index = -1;
		else if (op_num == OP_SHIP_CHANGE_DISPLAY_NAME)
		{
			parse_obj->display_name = "";
			parse_obj->flags.remove(Mission::Parse_Object_Flags::SF_Has_display_name);
		}
		else
			UNREACHABLE("Unhandled op_num %d!", op_num);

		return;
	}

	if (op_num == OP_SHIP_CHANGE_ALT_NAME)
	{
		// see if this is actually the ship class
		if (!stricmp(Ship_class_names[parse_obj->ship_class], Mission_alt_types[alt_index]))
		{
			parse_obj->alt_type_index = -1;
			return;
		}
	}

	if (op_num == OP_SHIP_CHANGE_ALT_NAME)
		parse_obj->alt_type_index = alt_index;
	else if (op_num == OP_SHIP_CHANGE_CALLSIGN)
		parse_obj->callsign_index = alt_index;
	else if (op_num == OP_SHIP_CHANGE_DISPLAY_NAME)
	{
		parse_obj->display_name = display_name;
		parse_obj->flags.set(Mission::Parse_Object_Flags::SF_Has_display_name);
	}
	else
		UNREACHABLE("Unhandled op_num %d!", op_num);
}

// Goober5000
void sexp_ship_change_alt_name_or_display_name_or_callsign(int node, int op_num)
{
	char new_string[TOKEN_LENGTH];
	int n = node, new_index = -1;
	bool string_was_empty = false, clear_it = false;

	// get the string
	strcpy_s(new_string, CTEXT(n));
	n = CDR(n);

	// packets for multi
	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_string(new_string);
	}

	// if the hash is at the beginning, the string might be truncated to empty, so check this first
	string_was_empty = (*new_string == '\0');

	if (op_num == OP_SHIP_CHANGE_ALT_NAME || op_num == OP_SHIP_CHANGE_DISPLAY_NAME)
	{
		// truncate at a single hash
		end_string_at_first_hash_symbol(new_string, true);

		// ## -> #
		consolidate_double_characters(new_string, '#');
	}

	// get the string's index
	if (string_was_empty || !stricmp(new_string, SEXP_ANY_STRING))
	{
		clear_it = true;
	}
	else
	{
		if (op_num == OP_SHIP_CHANGE_ALT_NAME)
		{
			new_index = mission_parse_lookup_alt(new_string);
			if (new_index < 0)
				new_index = mission_parse_add_alt(new_string);
		}
		else if (op_num == OP_SHIP_CHANGE_CALLSIGN)
		{
			new_index = mission_parse_lookup_callsign(new_string);
			if (new_index < 0)
				new_index = mission_parse_add_callsign(new_string);
		}
	}

	for ( ; n != -1; n = CDR(n) )
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		if (MULTIPLAYER_MASTER)
			Current_sexp_network_packet.send_int((int)oswpt.type);

		switch (oswpt.type)
		{
			// change ingame ship
			case OSWPT_TYPE_SHIP:
			{
				sexp_ingame_ship_alt_name_or_display_name_or_callsign(oswpt.shipp(), clear_it, new_index, new_string, op_num);
				if (MULTIPLAYER_MASTER)
					Current_sexp_network_packet.send_ship(oswpt.shipp());
				break;
			}

			// change ship yet to arrive
			case OSWPT_TYPE_PARSE_OBJECT:
			{
				sexp_parse_ship_alt_name_or_display_name_or_callsign(oswpt.p_objp(), clear_it, new_index, new_string, op_num);
				if (MULTIPLAYER_MASTER)
					Current_sexp_network_packet.send_parse_object(oswpt.p_objp());
				break;
			}

			// change wing (we must set the flags for all ships present as well as all ships yet to arrive)
			case OSWPT_TYPE_WING:
			case OSWPT_TYPE_WING_NOT_PRESENT:
			{
				// current ships
				for (int i = 0; i < oswpt.wingp()->current_count; i++)
					sexp_ingame_ship_alt_name_or_display_name_or_callsign(&Ships[oswpt.wingp()->ship_index[i]], clear_it, new_index, new_string, op_num);
	
				// ships yet to arrive
				for (auto p_objp: list_range(&Ship_arrival_list))
				{
					if (p_objp->wingnum == oswpt.wingnum)
						sexp_parse_ship_alt_name_or_display_name_or_callsign(p_objp, clear_it, new_index, new_string, op_num);
				}

				if (MULTIPLAYER_MASTER)
					Current_sexp_network_packet.send_wing(oswpt.wingp());
				break;
			}

			default:
				break;
		}
	}

	if (MULTIPLAYER_MASTER)
		Current_sexp_network_packet.end_callback();
}

void multi_sexp_ship_change_alt_name_or_display_name_or_callsign(int op_num)
{
	char new_string[TOKEN_LENGTH];
	int type, new_index = -1;
	bool string_was_empty = false, clear_it = false;

	Current_sexp_network_packet.get_string(new_string);

	// if the hash is at the beginning, the string might be truncated to empty, so check this first
	string_was_empty = (*new_string == '\0');

	if (op_num == OP_SHIP_CHANGE_ALT_NAME || op_num == OP_SHIP_CHANGE_DISPLAY_NAME)
	{
		// truncate at a single hash
		end_string_at_first_hash_symbol(new_string, true);

		// ## -> #
		consolidate_double_characters(new_string, '#');
	}

	if (string_was_empty || !stricmp(new_string, SEXP_ANY_STRING))
	{
		clear_it = true;
	}
	else
	{
		if (op_num == OP_SHIP_CHANGE_ALT_NAME)
		{
			new_index = mission_parse_lookup_alt(new_string);
			if (new_index < 0)
				new_index = mission_parse_add_alt(new_string);
		}
		else if (op_num == OP_SHIP_CHANGE_CALLSIGN)
		{
			new_index = mission_parse_lookup_callsign(new_string);
			if (new_index < 0)
				new_index = mission_parse_add_callsign(new_string);
		}
	}

	while (Current_sexp_network_packet.get_int(type)) 
	{
		switch ((oswpt_type)type)
		{
			case OSWPT_TYPE_SHIP:
			{
				ship *shipp;
				if (Current_sexp_network_packet.get_ship(shipp))
					sexp_ingame_ship_alt_name_or_display_name_or_callsign(shipp, clear_it, new_index, new_string, op_num);
				break;
			}

			case OSWPT_TYPE_PARSE_OBJECT:
			{
				p_object *pobjp;
				if (Current_sexp_network_packet.get_parse_object(pobjp))
					sexp_parse_ship_alt_name_or_display_name_or_callsign(pobjp, clear_it, new_index, new_string, op_num);
				break;
			}

			case OSWPT_TYPE_WING:
			case OSWPT_TYPE_WING_NOT_PRESENT:
			{
				wing *wingp;
				if (Current_sexp_network_packet.get_wing(wingp))
				{
					// current ships
					for (int i = 0; i < wingp->current_count; i++)
						sexp_ingame_ship_alt_name_or_display_name_or_callsign(&Ships[wingp->ship_index[i]], clear_it, new_index, new_string, op_num);

					// 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))
					{
						if (p_objp->wingnum == WING_INDEX(wingp))
							sexp_parse_ship_alt_name_or_display_name_or_callsign(p_objp, clear_it, new_index, new_string, op_num);
					}
				}
				break;
			}

			default:
				break;
		}
	}
}

// Goober5000
void sexp_set_death_message(int n)
{
	int i;

	// we'll suppose it's the string for now
	Player->death_message = CTEXT(n);

	// but use an actual message if one exists
	for (i=0; i<Num_messages; i++)
	{
		if (!stricmp(Messages[i].name, Player->death_message.c_str()))
		{
			Player->death_message = Messages[i].message;
			break;
		}
	}

	// apply localization
	lcl_replace_stuff(Player->death_message);

	sexp_replace_variable_names_with_values(Player->death_message);
	sexp_container_replace_refs_with_values(Player->death_message);
}

int sexp_key_pressed(int node)
{
	int z, t;
	bool is_nan, is_nan_forever;

	Assert(node != -1);
	z = translate_key_to_index(CTEXT(node), false);
	if (z < 0){
		return SEXP_FALSE;
	}

	if (!Control_config[z].digital_used.isValid()){
		return SEXP_FALSE;
	}

	if (CDR(node) < 0){
		return SEXP_TRUE;
	}

	t = eval_num(CDR(node), is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	return timestamp_since(Control_config[z].digital_used) >= t * MILLISECONDS_PER_SECOND;
}

void sexp_key_reset(int node)
{
	int n, z;

	for (n = node; n != -1; n = CDR(n))
	{
		z = translate_key_to_index(CTEXT(n), false);
		if (z >= 0)
		{
			// note: this is only for digital controls like button presses,
			// so we don't need to clear the analog value
			Control_config[z].digital_used = TIMESTAMP::invalid();
		}
	}
}
				
void sexp_ignore_key(int node)
{
	int ignore_count, ignored_key;
	bool is_nan, is_nan_forever;

	ignore_count = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(ignore_count);

	node = CDR(node);
	while (node >= 0) {
		// get the key
		ignored_key = translate_key_to_index(CTEXT(node), false);

		if (ignored_key >= 0) {
			Ignored_keys[ignored_key] = ignore_count;
		}

		Current_sexp_network_packet.send_int(ignored_key);

		node = CDR(node);
	}

	Current_sexp_network_packet.end_callback();
}

void multi_sexp_ignore_key()
{
	int ignored_key, ignore_count;

	Current_sexp_network_packet.get_int(ignore_count);
	
	while (Current_sexp_network_packet.get_int(ignored_key)) {
		Ignored_keys[ignored_key] = ignore_count;
	}
}

int sexp_targeted(int node)
{
	int z;
	bool is_nan, is_nan_forever;
	ship_subsys *ptr;

	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_FALSE;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_KNOWN_FALSE;

	if (!Player_ai || (ship_entry->objnum != Players_target)){
		return SEXP_FALSE;
	}

	if (CDR(node) >= 0) {
		z = eval_num(CDR(node), is_nan, is_nan_forever);
		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;

		if (!Players_target_timestamp.isValid() || timestamp_since(Players_target_timestamp) < z * MILLISECONDS_PER_SECOND){
			return SEXP_FALSE;
		}

		if (CDR(CDR(node)) >= 0) {
			ptr = Players_targeted_subsys;
			if (!ptr || subsystem_stricmp(ptr->system_info->subobj_name, CTEXT(CDR(CDR(node))))){
				return SEXP_FALSE;
			}
		}
	}

	return SEXP_TRUE;
}

int sexp_node_targeted(int node)
{
	int z;
	bool is_nan, is_nan_forever;

	auto jnp = jumpnode_get_by_name(CTEXT(node));

	if (jnp==nullptr || !Player_ai || (jnp->GetSCPObjectNumber() != Players_target)){
		return SEXP_FALSE;
	}

	if (CDR(node) >= 0) {
		z = eval_num(CDR(node), is_nan, is_nan_forever);
		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;

		if (timestamp_since(Players_target_timestamp) < z * MILLISECONDS_PER_SECOND){
			return SEXP_FALSE;
		}
	}

	return SEXP_TRUE;
}

int sexp_speed(int node)
{
	int z;
	bool is_nan, is_nan_forever;

	if (Training_context & TRAINING_CONTEXT_SPEED) {
		if (Training_context_speed_set) {
			z = eval_num(node, is_nan, is_nan_forever);
			if (is_nan)
				return SEXP_FALSE;
			if (is_nan_forever)
				return SEXP_KNOWN_FALSE;

			if (timestamp_since(Training_context_speed_timestamp) >= z * MILLISECONDS_PER_SECOND){
				return SEXP_KNOWN_TRUE;
			}
		}
	}

	return SEXP_FALSE;
}

int sexp_get_throttle_speed(int node)
{
	auto the_player = get_player_from_ship_node(node); 

	if (the_player) {
		float max_speed = Ship_info[Ships[Objects[the_player->objnum].instance].ship_info_index].max_speed;
		return (int)(the_player->ci.forward_cruise_percent / 100.0f * max_speed);
	}

	return 0;
}

// CommanderDJ
void sexp_set_player_throttle_speed(int node)
{
	bool is_nan, is_nan_forever;

	//get and sanity check the player first
	auto the_player = get_player_from_ship_node(node); 

	if (the_player)
	{
		//now the throttle percentage
		node = CDR(node);
		int throttle_percent = eval_num(node, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
		CLAMP(throttle_percent, 0, 100);
		
		//now actually set the throttle
		the_player->ci.forward_cruise_percent = (float) throttle_percent;
	}
}

// Goober5000
int sexp_weapons_depleted(int node, bool primary)
{
	int num_banks, num_tested_banks, num_depleted_banks;
	int *ammo;

	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	// get bank stuff
	if (primary) {
		num_banks = ship_entry->shipp()->weapons.num_primary_banks;
		ammo = ship_entry->shipp()->weapons.primary_bank_ammo;
	} else {
		num_banks = ship_entry->shipp()->weapons.num_secondary_banks;
		ammo = ship_entry->shipp()->weapons.secondary_bank_ammo;
	}
	num_tested_banks = 0;
	num_depleted_banks = 0;

	// get number of depleted banks
	for (int idx=0; idx<num_banks; idx++) {
		if (primary) {
			// is this a ballistic bank?
			if (!(Weapon_info[ship_entry->shipp()->weapons.primary_bank_weapons[idx]].wi_flags[Weapon::Info_Flags::Ballistic])) {
				continue;
			}
		}
		else {
			if (Weapon_info[ship_entry->shipp()->weapons.secondary_bank_weapons[idx]].wi_flags[Weapon::Info_Flags::SecondaryNoAmmo]) {
				continue;
			}
		}
		num_tested_banks++;

		// is this bank out of ammo?
		if (ammo[idx] == 0)	{
			num_depleted_banks++;
		}
	}

	// are they all depleted?
	return (num_depleted_banks == num_tested_banks) ? SEXP_TRUE : SEXP_FALSE;
}

int sexp_facing(int node)
{
	int angle;
	bool is_nan, is_nan_forever;
	float a1, a2;
	vec3d v1, v2;

	if (!Player_obj) {
		return SEXP_CANT_EVAL;
	}

	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_FALSE;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_KNOWN_FALSE;
	auto target_objp = ship_entry->objp();
	node = CDR(node);

	angle = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	v1 = Player_obj->orient.vec.fvec;
	vm_vec_normalize(&v1);

	vm_vec_sub(&v2, &target_objp->pos, &Player_obj->pos);
	vm_vec_normalize(&v2);

	a1 = vm_vec_dot(&v1, &v2);
	a2 = cosf(fl_radians(angle % 360));
	if (a1 >= a2){
		return SEXP_TRUE;
	}

	return SEXP_FALSE;
}

int sexp_is_facing(int node)
{
	int angle;
	bool is_nan, is_nan_forever;
	float a1, a2;
	vec3d v1, v2;

	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_FALSE;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_KNOWN_FALSE;
	auto origin_objp = ship_entry->objp();
	node = CDR(node);

	object_ship_wing_point_team oswpt;
	eval_object_ship_wing_point_team(&oswpt, node);

	// check if target has departed or not yet arrived
	if (oswpt.type == OSWPT_TYPE_EXITED) {
		return SEXP_KNOWN_FALSE;
	}
	if (oswpt.type == OSWPT_TYPE_PARSE_OBJECT) {
		return SEXP_FALSE;
	}

	if (!oswpt.has_objp())
		return SEXP_FALSE;

	auto target_objp = oswpt.objp();
	node = CDR(node);

	angle = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	node = CDR(node);

	// check optional distance argument
	if (node >= 0) {
		int threshold = eval_num(node, is_nan, is_nan_forever);
		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;

		if (sexp_retail_distance3(origin_objp, target_objp) > threshold) {
			return SEXP_FALSE;
		}
	}

	v1 = origin_objp->orient.vec.fvec;	
	vm_vec_normalize(&v1);

	vm_vec_sub(&v2, &target_objp->pos, &origin_objp->pos);
	vm_vec_normalize(&v2);

	a1 = vm_vec_dot(&v1, &v2);
	a2 = cosf(fl_radians(angle % 360));
	if (a1 >= a2){
		return SEXP_TRUE;
	}

	return SEXP_FALSE;
}

// is ship facing first waypoint in waypoint path
int sexp_facing2(int node)
{
	int angle;
	bool is_nan, is_nan_forever;
	float a1, a2;
	vec3d v1, v2;

	// bail if Player_obj is not good
	if (!Player_obj) {
		return SEXP_CANT_EVAL;
	}

	// get position of first waypoint
	waypoint_list *wp_list = find_matching_waypoint_list(CTEXT(node));
	if (wp_list == nullptr) {
		return SEXP_KNOWN_FALSE;
	}

	// get angle
	angle = eval_num(CDR(node), is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	v1 = Player_obj->orient.vec.fvec;
	vm_vec_normalize(&v1);

	vm_vec_sub(&v2, wp_list->get_waypoints().front().get_pos(), &Player_obj->pos);
	vm_vec_normalize(&v2);

	a1 = vm_vec_dot(&v1, &v2);
	a2 = cosf(fl_radians(angle % 360));
	if (a1 >= a2){
		return SEXP_TRUE;
	}

	return SEXP_FALSE;
}

int sexp_order(int n)
{
	auto order_to = CTEXT(n);
	auto order = CTEXT(CDR(n));
	const char *target = nullptr;

	//target
	n = CDDR(n); 
	if (n != -1)
		target = CTEXT(n);
	
	return hud_query_order_issued(order_to, order, target);
}

int sexp_query_orders (int n)
{
	auto order_to = CTEXT(n);
	auto order = CTEXT(CDR(n));
	const char *target = nullptr;
	const char *order_from = nullptr;
	const char *special = nullptr;
	int timestamp = 0;
	bool is_nan, is_nan_forever;

	// delay
	n = CDDR(n); 
	if (n != -1) {
		timestamp = eval_num(n, is_nan, is_nan_forever);
		n = CDR(n); 

		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;
	}
	
	//target
	if (n != -1) {
		target = CTEXT(n);
		n = CDR(n); 
	}

	// player order comes from
	if (n != -1) {
		order_from = CTEXT(n);
		n = CDR(n); 
	}

	// optional special argument
	if (n != -1)
		special = CTEXT(n);

	return hud_query_order_issued(order_to, order, target, timestamp, order_from, special);
}

int sexp_time_to_goal(int n)
{
	auto ship_entry = eval_ship(n);

	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;

	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	auto shipp = ship_entry->shipp();
	int time = ship_return_seconds_to_goal(shipp);
	
	if (time < 0) {
		return SEXP_NAN;
	}

	return time;
}

void sexp_set_hud_time_pad(int n)
{
	bool is_nan, is_nan_forever;
	int pad;

	pad = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	The_mission.HUD_timer_padding = pad;
}

// Karajorma
void sexp_reset_orders (int  /*n*/)
{
	Squadmsg_history.clear();
}

int sexp_waypoint_missed()
{
	if (Training_context & TRAINING_CONTEXT_FLY_PATH) {
		if (Training_context_at_waypoint > Training_context_goal_waypoint){
			return SEXP_TRUE;
		}
	}

	return SEXP_FALSE;
}

int sexp_waypoint_twice()
{
	if (Training_context & TRAINING_CONTEXT_FLY_PATH) {
		if (Training_context_at_waypoint < Training_context_goal_waypoint - 1){
			return SEXP_TRUE;
		}
	}

	return SEXP_FALSE;
}

int sexp_path_flown()
{
	if (Training_context & TRAINING_CONTEXT_FLY_PATH) {
		auto wp_list = find_waypoint_list_at_index(Training_context_waypoint_path);
		if (wp_list && (Training_context_goal_waypoint == static_cast<int>(wp_list->get_waypoints().size()))) {
			return SEXP_TRUE;
		}
	}

	return SEXP_FALSE;
}

void sexp_send_training_message(int node)
{
	bool is_nan, is_nan_forever;
	int n = node, count, delay, duration;

	if(physics_paused){
		return;
	}

	Assert(node >= 0);
	Assert(Event_index >= 0);

	auto primary_message = CTEXT(n);
	n = CDR(n);

	auto secondary_message = (n >= 0) ? CTEXT(n) : nullptr;
	n = CDR(n);

	count = eval_nums(n, is_nan, is_nan_forever, delay, duration);
	if (is_nan || is_nan_forever)
		return;
	if (count > 0) {
		delay *= 1000;
	}
	if (count < 2) {
		duration = -1;
	}

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(duration);
	Current_sexp_network_packet.send_int(delay);

	if ((Mission_events[Event_index].repeat_count > 1) || (secondary_message == nullptr)) {
		message_training_queue(primary_message, _timestamp(delay), duration);
		Current_sexp_network_packet.send_string(primary_message);
	} else {
		message_training_queue(secondary_message, _timestamp(delay), duration);
		Current_sexp_network_packet.send_string(secondary_message);
	}
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_send_training_message()
{	
	int duration, delay;
	char message[TOKEN_LENGTH];

	Current_sexp_network_packet.get_int(duration);
	Current_sexp_network_packet.get_int(delay);
	if (Current_sexp_network_packet.get_string(message)) {
		message_training_queue(message, _timestamp(delay), duration);
	}
}

int sexp_gse_recharge_pct(int node, int op_num)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	int index;
	if (op_num == OP_WEAPON_RECHARGE_PCT)
		index = ship_entry->shipp()->weapon_recharge_index;
	else if (op_num == OP_SHIELD_RECHARGE_PCT)
		index = ship_entry->shipp()->shield_recharge_index;
	else if (op_num == OP_ENGINE_RECHARGE_PCT)
		index = ship_entry->shipp()->engine_recharge_index;
	else
		return SEXP_NAN_FOREVER;

	// recharge pct
	return (int)(100.0f * ets_power_factor(ship_entry->objp(), false) * Energy_levels[index]);
}

/*
* Get a ship's power output
*/
int sexp_get_power_output(int node) 
{
	auto ship_entry = eval_ship(node);

	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	return (int)(std::lround(Ship_info[ship_entry->shipp()->ship_info_index].power_output));
}


/**
 * retrieve one ETS index from a ship
 */
int sexp_get_ets_value(int node)
{
	auto ets_type = CTEXT(node);
	node = CDR(node);

	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	if (!stricmp(ets_type, "engine")) {
		return ship_entry->shipp()->engine_recharge_index;
	} else if (!stricmp(ets_type, "shield")) {
		return ship_entry->shipp()->shield_recharge_index;
	} else if (!stricmp(ets_type, "weapon")) {
		return ship_entry->shipp()->weapon_recharge_index;
	} else {
		return SEXP_NAN_FOREVER;
	}
}

/**
 * set all ETS indexes for one or more ships
 */
void sexp_set_ets_values(int node)
{
	bool is_nan, is_nan_forever;
	int ets_idx[num_retail_ets_gauges];

	//get inputs
	eval_nums(node, is_nan, is_nan_forever, ets_idx[ENGINES], ets_idx[SHIELDS], ets_idx[WEAPONS]);
	if (is_nan || is_nan_forever)
		return;

	// sanity check inputs
	sanity_check_ets_inputs(ets_idx);

	Current_sexp_network_packet.start_callback();

	// apply ETS settings to specified ships
	for ( ; node != -1; node = CDR(node)) {
		auto ship_entry = eval_ship(node);
		if (!ship_entry || !ship_entry->has_shipp())
			return;

		if (validate_ship_ets_indxes(ship_entry->shipnum, ets_idx)) {
			ship_entry->shipp()->engine_recharge_index = ets_idx[ENGINES];
			ship_entry->shipp()->shield_recharge_index = ets_idx[SHIELDS];
			ship_entry->shipp()->weapon_recharge_index = ets_idx[WEAPONS];

			Current_sexp_network_packet.send_ship(ship_entry->shipp());
			Current_sexp_network_packet.send_int(ets_idx[ENGINES]);
			Current_sexp_network_packet.send_int(ets_idx[SHIELDS]);
			Current_sexp_network_packet.send_int(ets_idx[WEAPONS]);
		}
	}
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_ets_values()
{
	int sindex;
	int ets_idx[num_retail_ets_gauges];

	while (Current_sexp_network_packet.get_ship(sindex)) {
		Current_sexp_network_packet.get_int(ets_idx[ENGINES]);
		Current_sexp_network_packet.get_int(ets_idx[SHIELDS]);
		Current_sexp_network_packet.get_int(ets_idx[WEAPONS]);

		set_recharge_rates(&Objects[Ships[sindex].objnum], ets_idx[SHIELDS], ets_idx[WEAPONS], ets_idx[ENGINES]);
	}
}

int sexp_shield_quad_low(int node)
{
	float max_quad, check;
	bool is_nan, is_nan_forever;

	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	auto sip = &Ship_info[ship_entry->shipp()->ship_info_index];
	if (!sip->is_small_ship()) {
		return SEXP_KNOWN_FALSE;
	}
	max_quad = shield_get_max_quad(ship_entry->objp());	

	// shield pct
	check = i2fl(eval_num(CDR(node), is_nan, is_nan_forever));
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	// check his quadrants
	for (float quadrant: ship_entry->objp()->shield_quadrant) {
		if ((quadrant / max_quad * 100.0f) <= check) {
			return SEXP_TRUE;
		}
	}

	// all good
	return SEXP_FALSE;	
}

int sexp_get_ammo_sub(ship_weapon *swp, int bank_to_check, bool primary, bool do_percent)
{
	int sum = 0;
	int total = 0;

	int num_banks, *ammo, *start_ammo, *weapons;
	if (primary)
	{
		num_banks = swp->num_primary_banks;
		ammo = swp->primary_bank_ammo;
		start_ammo = swp->primary_bank_start_ammo;
		weapons = swp->primary_bank_weapons;
	}
	else
	{
		num_banks = swp->num_secondary_banks;
		ammo = swp->secondary_bank_ammo;
		start_ammo = swp->secondary_bank_start_ammo;
		weapons = swp->secondary_bank_weapons;
	}

	for (int i = 0; i < num_banks; ++i)
	{
		// if this is primary, skip non-ballistic weapons
		if (primary && !Weapon_info[weapons[i]].wi_flags[Weapon::Info_Flags::Ballistic])
			continue;

		if (!primary && Weapon_info[weapons[i]].wi_flags[Weapon::Info_Flags::SecondaryNoAmmo])
			continue;

		// if we are checking a specific bank, and this isn't it, skip it
		if (bank_to_check >= 0 && bank_to_check < num_banks && i != bank_to_check)
			continue;

		sum += ammo[i];
		total += start_ammo[i];
	}

	if (do_percent)
	{
		if (total == 0)
			return 100;

		return (int)(((float)sum / (float)total) * 100.0f);
	}

	return sum;
}

// Goober5000 & Karajorma
int sexp_get_ammo(int node, bool for_turret, bool primary, bool do_percent)
{
	ship_weapon *swp;
	bool is_nan, is_nan_forever;

	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;
	node = CDR(node);

	// Get the turret
	if (for_turret)
	{
		auto turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if (!turret || !(turret->system_info->flags[Model::Subsystem_Flags::Turret_use_ammo]))
			return 0;
		node = CDR(node);

		swp = &turret->weapons;
	}
	else
		swp = &ship_entry->shipp()->weapons;
	
	// bank to check
	int check = eval_num(node, is_nan, is_nan_forever);

	// bogus check?
	if (is_nan || is_nan_forever || check < 0)
		return 0;

	return sexp_get_ammo_sub(swp, check, primary, do_percent);
}

//Karajorma - Helper function for the set-*-ammo and weapon functions
void sexp_set_ammo_sub(ship_weapon *swp, int requested_bank, int requested_ammo, int rearm_limit, bool primary)
{
	int num_banks, *ammo_capacity, *ammo, *start_ammo, *weapons;
	if (primary)
	{
		num_banks = swp->num_primary_banks;
		ammo_capacity = swp->primary_bank_capacity;
		ammo = swp->primary_bank_ammo;
		start_ammo = swp->primary_bank_start_ammo;
		weapons = swp->primary_bank_weapons;
	}
	else
	{
		num_banks = swp->num_secondary_banks;
		ammo_capacity = swp->secondary_bank_capacity;
		ammo = swp->secondary_bank_ammo;
		start_ammo = swp->secondary_bank_start_ammo;
		weapons = swp->secondary_bank_weapons;
	}

	// Can only set one bank at a time. Check that someone hasn't asked for the contents of all 
	// the banks or a non-existant bank
	if ((requested_bank >= num_banks) || requested_bank < 0)
		return;

	if (primary)
	{
		// Check that this isn't a non-ballistic bank as it's pointless to set the amount of ammo for those
		if (!(Weapon_info[weapons[requested_bank]].wi_flags[Weapon::Info_Flags::Ballistic]))
			return;
	}
	else {
		if (Weapon_info[weapons[requested_bank]].wi_flags[Weapon::Info_Flags::SecondaryNoAmmo])
			return;
	}

	int maximum_allowed = (int)std::lround(ammo_capacity[requested_bank] / Weapon_info[weapons[requested_bank]].cargo_size);

	//Check that a valid number of weapons have been specified. 
	if (requested_ammo >= 0)
	{
		// Is the number requested larger than the maximum allowed for that particular bank? 
		if (requested_ammo > maximum_allowed)
			requested_ammo = maximum_allowed;

		// Set the number of weapons
		ammo[requested_bank] = requested_ammo;
	}
	else if (ammo[requested_bank] > maximum_allowed)
	{
		// Make sure the current ammo doesn't exceed the maximum (this can otherwise happen with the set-*-weapon SEXPs)
		ammo[requested_bank] = maximum_allowed;
	}

	// Check rearm validity
	if (rearm_limit >= 0)
	{
		// Don't allow more weapons than the bank can actually hold.
		if (rearm_limit > maximum_allowed)
			rearm_limit = maximum_allowed;

		start_ammo[requested_bank] = rearm_limit;
	}
	else	// Even if no rearm limit is explicitly set, don't allow more weapons than the bank can actually hold (this can otherwise happen with the set-*-weapon SEXPs).
	{
		start_ammo[requested_bank] = maximum_allowed;
	}
}

// Karajorma - sets the amount of ammo in a certain ballistic or secondary weapon bank to the specified value
void sexp_set_ammo(int node, bool for_turret, bool primary)
{
	const char *turret_name = nullptr;
	ship_weapon *swp;
	bool is_nan, is_nan_forever;

	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	node = CDR(node);

	// Get the turret
	if (for_turret)
	{
		turret_name = CTEXT(node);
		auto turret = ship_get_subsys(ship_entry->shipp(), turret_name);
		if (!turret || !(turret->system_info->flags[Model::Subsystem_Flags::Turret_use_ammo]))
			return;
		node = CDR(node);

		swp = &turret->weapons;
	}
	else
		swp = &ship_entry->shipp()->weapons;

	// Get the bank to set the number on
	int requested_bank = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever || requested_bank < 0)
		return;
	node = CDR(node);

	//  Get the number of weapons requested
	int requested_weapons = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever || requested_weapons < 0)
		return;
	node = CDR(node);

	// If a rearm limit hasn't been specified simply change the ammo. Otherwise read in the rearm limit
	int rearm_limit = -1;
	if (node >= 0) {
		rearm_limit = eval_num(node, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
	}

	sexp_set_ammo_sub(swp, requested_bank, requested_weapons, rearm_limit, primary);

	// do the multiplayer callback here
	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_ship(ship_entry->shipp());
		if (for_turret)
			Current_sexp_network_packet.send_string(turret_name);
		Current_sexp_network_packet.send_int(requested_bank);
		Current_sexp_network_packet.send_int(requested_weapons);
		Current_sexp_network_packet.send_int(rearm_limit);
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_set_ammo(bool for_turret, bool primary)
{
	ship *shipp;
	char turret_name[TOKEN_LENGTH];
	ship_weapon *swp;
	int requested_bank = -1;
	int requested_weapons = -1;
	int rearm_limit = -1;

	Current_sexp_network_packet.get_ship(shipp);
	if (for_turret)
		Current_sexp_network_packet.get_string(turret_name);
	Current_sexp_network_packet.get_int(requested_bank);
	Current_sexp_network_packet.get_int(requested_weapons);
	Current_sexp_network_packet.get_int(rearm_limit);

	if (for_turret)
	{
		auto turret = ship_get_subsys(shipp, turret_name);
		if (!turret)
			return;
		swp = &turret->weapons;
	}
	else
		swp = &shipp->weapons;

	sexp_set_ammo_sub(swp, requested_bank, requested_weapons, rearm_limit, primary);
}

// Karajorma - Changes the weapon in the requested bank to the one supplied. Optionally sets the ammo and 
// rearm limit too.  
void sexp_set_weapon(int node, bool primary)
{
	int requested_bank, windex, requested_ammo = -1, rearm_limit = -1;
	bool is_nan, is_nan_forever;

	Assert(node != -1);

	// Check that a ship has been supplied
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	node = CDR(node);

	// Get the bank to change the weapon of
	requested_bank = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	node = CDR(node);

	// Get the weapon to change to
	windex = weapon_info_lookup(CTEXT(node));
	if (windex < 0)
		return;
	node = CDR(node);

	if (primary)
	{
		// Be sure it's a valid bank
		if (requested_bank < 0 || requested_bank >= ship_entry->shipp()->weapons.num_primary_banks)
			return;

		// Be sure it's a valid type
		if (!Weapon_info[windex].is_primary())
			return;
	}
	else
	{
		// Be sure it's a valid bank
		if (requested_bank < 0 || requested_bank >= ship_entry->shipp()->weapons.num_secondary_banks)
			return;

		// Be sure it's a valid type
		if (!Weapon_info[windex].is_secondary())
			return;
	}

	// Change the weapon
	if (primary)
		ship_entry->shipp()->weapons.primary_bank_weapons[requested_bank] = windex;
	else
		ship_entry->shipp()->weapons.secondary_bank_weapons[requested_bank] = windex;

	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_ship(ship_entry->shipp());
		Current_sexp_network_packet.send_bool(primary);
		Current_sexp_network_packet.send_int(requested_bank);
		Current_sexp_network_packet.send_int(windex);
	}

	// Check to see if the optional ammo and rearm_limit settings were supplied
	if (node >= 0) {
		requested_ammo = eval_num(node, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;

		if (requested_ammo < 0) {
			requested_ammo = 0;
		}
		node = CDR(node);

		// If nothing was supplied then set the rearm limit to a negative value so that it is ignored
		if (node >= 0) {
			rearm_limit = eval_num(node, is_nan, is_nan_forever);
			if (is_nan || is_nan_forever)
				return;
		}
	}

	// Set the ammo -- No matter what - Cyborg17
	sexp_set_ammo_sub(&ship_entry->shipp()->weapons, requested_bank, requested_ammo, rearm_limit, primary);

	// Now pass this info on to clients.
	if (MULTIPLAYER_MASTER)
	{
		Current_sexp_network_packet.send_int(requested_ammo);
		Current_sexp_network_packet.send_int(rearm_limit);
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_set_weapon()
{
	ship *shipp;
	int requested_bank;
	int windex;
	int requested_ammo;
	int rearm_limit;
	bool primary;

	Current_sexp_network_packet.get_ship(shipp);
	Current_sexp_network_packet.get_bool(primary);
	Current_sexp_network_packet.get_int(requested_bank);
	Current_sexp_network_packet.get_int(windex);
	Current_sexp_network_packet.get_int(requested_ammo);
	Current_sexp_network_packet.get_int(rearm_limit);

	// Change the weapon
	if (primary)
		shipp->weapons.primary_bank_weapons[requested_bank] = windex;
	else
		shipp->weapons.secondary_bank_weapons[requested_bank] = windex;

	// Set the ammo
	sexp_set_ammo_sub(&shipp->weapons, requested_bank, requested_ammo, rearm_limit, primary);
}

int sexp_get_countermeasures(int node) 
{
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::DEATH_ROLL || ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	return ship_entry->shipp()->cmeasure_count;
}

void sexp_set_countermeasures(int node)
{
	ship *shipp;
	int num_cmeasures;
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	shipp = ship_entry->shipp();
	node = CDR(node);

	float cm_cargo_size = 1.0f;
	if (Countermeasures_use_capacity)
		cm_cargo_size = Weapon_info[shipp->current_cmeasure].cargo_size;

	num_cmeasures = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever) {
		return;
	}
	if (num_cmeasures < 0) {
		num_cmeasures = 0;
	} else {
		if (Countermeasures_use_capacity) {
			if (num_cmeasures > fl2i(Ship_info[shipp->ship_info_index].cmeasure_max / cm_cargo_size)) {
				num_cmeasures = fl2i(Ship_info[shipp->ship_info_index].cmeasure_max / cm_cargo_size);
			}
		} else {
			if (num_cmeasures > Ship_info[shipp->ship_info_index].cmeasure_max) {
				num_cmeasures = Ship_info[shipp->ship_info_index].cmeasure_max;
			}
		}
	}

	shipp->cmeasure_count = num_cmeasures;

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_ship(shipp);
	Current_sexp_network_packet.send_int(num_cmeasures);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_countermeasures()
{
	int num_cmeasures = 0;
	ship *shipp;

	Current_sexp_network_packet.get_ship(shipp);
	if (shipp == nullptr) {
		return;
	}
	if (Current_sexp_network_packet.get_int(num_cmeasures)) {
		shipp->cmeasure_count = num_cmeasures;
	}
}

// KeldorKatarn - Locks or unlocks the afterburner on the requested ship
void sexp_deal_with_afterburner_lock (int node, bool lock)
{
	Assert (node != -1);
	sexp_deal_with_ship_flag(node, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Afterburner_locked, Mission::Parse_Object_Flags::NUM_VALUES, lock, true);
}

// Karajorma - locks or unlocks primary weapons on the requested ship
void sexp_deal_with_primary_lock (int node, bool lock)
{
	Assert (node != -1);	
	sexp_deal_with_ship_flag(node, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Primaries_locked, Mission::Parse_Object_Flags::SF_Primaries_locked, lock, true);

}

void sexp_deal_with_secondary_lock (int node, bool lock)
{
	Assert (node != -1);	
	sexp_deal_with_ship_flag(node, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Secondaries_locked, Mission::Parse_Object_Flags::SF_Secondaries_locked, lock, true);

}

//Karajorma - Changes the subsystem name displayed on the HUD.  
void sexp_change_subsystem_name(int node) 
{
	Assert (node != -1);

	// Check that a ship has been supplied
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	node = CDR(node);

	auto new_name = CTEXT(node);
	node = CDR(node);
	
	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_ship(ship_entry->shipp()); 
		Current_sexp_network_packet.send_string(new_name); 
	}

	// loop through all the subsystems the SEXP has provided
	while (node >= 0) {

		//Get the new subsystem name
		auto subsystem_to_rename = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if (subsystem_to_rename != nullptr) {
			ship_subsys_set_name(subsystem_to_rename, new_name); 
	
			if (MULTIPLAYER_MASTER) {
				Current_sexp_network_packet.send_string(CTEXT(node)); 
			}
		}

		node = CDR(node);
	}
	
	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_change_subsystem_name()
{
	ship *shipp = nullptr;
	char new_name[TOKEN_LENGTH];
	char subsys_name[MAX_NAME_LEN];
	ship_subsys *subsystem_to_rename;

	Current_sexp_network_packet.get_ship(shipp);
	Current_sexp_network_packet.get_string(new_name);
	while (Current_sexp_network_packet.get_string(subsys_name)) {
		subsystem_to_rename = ship_get_subsys(shipp, subsys_name);
		if (subsystem_to_rename != nullptr) {
			ship_subsys_set_name(subsystem_to_rename, new_name);
		}
	}
}

// Goober5000
void sexp_change_ship_class(int n)
{
	int class_num = ship_info_lookup(CTEXT(n));
	if (class_num < 0)
		return;
	n = CDR(n);

	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_int(class_num);
	}

	// all ships in the sexp
	for ( ; n != -1; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || ship_entry->status == ShipStatus::EXITED)
			continue;

		// If the ship hasn't arrived we still want the ability to change its class.
		if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		{
			swap_parse_object(ship_entry->p_objp(), class_num);

			if (MULTIPLAYER_MASTER) {
				Current_sexp_network_packet.send_bool(false); 
				Current_sexp_network_packet.send_parse_object(ship_entry->p_objp());
			}
		}
		// If the ship is already in the mission
		else 
		{
			// don't mess with a ship that's occupied
			if (!ship_entry->shipp()->is_arriving() && !ship_entry->shipp()->is_dying_or_departing())
			{
				change_ship_type(ship_entry->shipnum, class_num, 1);
				if (ship_entry->shipnum == SHIP_INDEX(Player_ship)) {
					// update HUD and RTT cockpit gauges if applicable
					set_current_hud();
					ship_close_cockpit_displays(Player_ship);
					ship_init_cockpit_displays(Player_ship);
				}

				if (MULTIPLAYER_MASTER) {
					Current_sexp_network_packet.send_bool(true); 
					Current_sexp_network_packet.send_ship(ship_entry->shipp()); 
				}
			}
		}
	}

	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_change_ship_class()
{
	int class_num = -1;
	int ship_num = -1;
	bool ship_arrived;
	p_object *pobjp = nullptr;

	Current_sexp_network_packet.get_int(class_num);
	while (Current_sexp_network_packet.get_bool(ship_arrived)) {
		if (ship_arrived) {
			Current_sexp_network_packet.get_ship(ship_num);
			if ((class_num >= 0) && (ship_num >= 0)) {
				change_ship_type(ship_num, class_num, 1);
				if (&Ships[ship_num] == Player_ship) {
					// update HUD and RTT cockpit gauges if applicable
					set_current_hud();
					ship_close_cockpit_displays(Player_ship);
					ship_init_cockpit_displays(Player_ship);
				}
			}
		}
		else {
			Current_sexp_network_packet.get_parse_object(pobjp); 
			if ((class_num >= 0) && (pobjp != nullptr)) {
				swap_parse_object(pobjp, class_num);
			}
		}
	}
}

// Goober5000
void ship_copy_damage(const ship_registry_entry *target, const ship_registry_entry *source)
{
	auto target_shipp = target->shipp();
	auto target_objp = target->objp();
	auto source_shipp = source->shipp();
	auto source_objp = source->objp();

	if (target_shipp->ship_info_index != source_shipp->ship_info_index)
	{
		nprintf(("SEXP", "Copying damage of ship %s to ship %s which has a different ship class.  Strange results might occur.\n", source_shipp->ship_name, target_shipp->ship_name));
	}


	// copy hull...
	target_shipp->special_hitpoints = source_shipp->special_hitpoints;
	target_shipp->ship_max_hull_strength = source_shipp->ship_max_hull_strength;
	target_objp->hull_strength = source_objp->hull_strength;

	// ...and shields
	target_shipp->special_shield = source_shipp->special_shield;
	target_shipp->ship_max_shield_strength = source_shipp->ship_max_shield_strength;
	int n_quadrants = static_cast<int>(target_objp->shield_quadrant.size());
	if (source_objp->shield_quadrant.size() == target_objp->shield_quadrant.size())
	{
		for (int i = 0; i < n_quadrants; i++)
			target_objp->shield_quadrant[i] = source_objp->shield_quadrant[i];
	}
	else
	{
		float quad_strength = target_shipp->ship_max_shield_strength / n_quadrants;
		for (int i = 0; i < n_quadrants; i++)
			target_objp->shield_quadrant[i] = quad_strength;
	}


	// search through all subsystems on source ship and map them onto target ship
	for (auto source_ss: list_range(&source_shipp->subsys_list))
	{
		// find subsystem to configure
		auto target_ss = ship_get_subsys(target_shipp, source_ss->system_info->subobj_name);
		if (target_ss == nullptr)
			continue;

		// copy
		target_ss->max_hits = source_ss->max_hits;
		target_ss->current_hits = source_ss->current_hits;
		if (target_ss->submodel_instance_1 && source_ss->submodel_instance_1)
			target_ss->submodel_instance_1->blown_off = source_ss->submodel_instance_1->blown_off;
		if (target_ss->submodel_instance_2 && source_ss->submodel_instance_2)
			target_ss->submodel_instance_2->blown_off = source_ss->submodel_instance_2->blown_off;
	}
}

void sexp_replace_texture(int n, bool skybox)
{
	auto old_name = CTEXT(n);
	n = CDR(n);

	auto new_name = CTEXT(n);
	n = CDR(n);

	if (skybox) {
		if (Nmodel_instance_num < 0) {
			mprintf(("Tried to replace texture of a non-existant skybox\n"));
			return;
		}
		polymodel_instance* skybox_pmi = model_get_instance(Nmodel_instance_num);
		modelinstance_replace_active_texture(skybox_pmi, old_name, new_name);
		stars_invalidate_environment_map();
		return;
	}

	for (; n != -1; n = CDR(n))
	{

		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		switch (oswpt.type)
		{
		case OSWPT_TYPE_PARSE_OBJECT:
		case OSWPT_TYPE_SHIP:
		{
			auto ship_entry = oswpt.ship_entry();

			if (ship_entry->status == ShipStatus::EXITED)
				continue;

			if (ship_entry->status == ShipStatus::NOT_YET_PRESENT)
			{
				p_object* pobjp = ship_entry->p_objp();

				texture_replace replace;

				strcpy(replace.ship_name, ship_entry->name);
				strcpy(replace.old_texture, old_name);
				strcpy(replace.new_texture, new_name);
				replace.from_table = false;

				if (!stricmp(new_name, "invisible"))
					replace.new_texture_id = REPLACE_WITH_INVISIBLE;
				else
					replace.new_texture_id = bm_load_either(new_name);

				pobjp->replacement_textures.push_back(replace);
			}
			// If the ship is already in the mission
			else
			{
				ship_replace_active_texture(ship_entry->shipnum, old_name, new_name);
			}
			break;
		}

		case OSWPT_TYPE_WING:
		{
			auto wp = oswpt.wingp();
			for (int i = 0; i < wp->current_count; ++i)
			{
				if (wp->ship_index[i] >= 0)
				{
					ship_replace_active_texture(wp->ship_index[i], old_name, new_name);
				}
			}
			break;
		}

		case OSWPT_TYPE_WING_NOT_PRESENT:
		{
			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 == oswpt.wingnum)
				{
					texture_replace replace;

					strcpy(replace.ship_name, p_objp->name);
					strcpy(replace.old_texture, old_name);
					strcpy(replace.new_texture, new_name);
					replace.from_table = false;

					if (!stricmp(new_name, "invisible"))
						replace.new_texture_id = REPLACE_WITH_INVISIBLE;
					else
						replace.new_texture_id = bm_load_either(new_name);

					p_objp->replacement_textures.push_back(replace);
				}
			}
			break;
		}

		default:
			mprintf(("Invalid Shipname in SEXP ship-effect\n"));
		}
	}
}


void sexp_set_alphamult(int n)
{
	bool is_nan, is_nan_forever;

	int newAlpha = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever) {
		return;
	}

	for (n = CDR(n); n != -1; n = CDR(n))
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		// we only handle ships and wings that are present
		switch (oswpt.type)
		{
		case OSWPT_TYPE_SHIP:
		{
			auto ship_entry = oswpt.ship_entry();
			auto shipp = ship_entry->shipp();

			shipp->flags.remove(Ship::Ship_Flags::Cloaked);
			if (newAlpha >= 100) {
				shipp->alpha_mult = 1.0f;
				shipp->flags.remove(Ship::Ship_Flags::Render_with_alpha_mult);
			}
			else if (newAlpha <= 0) {
				shipp->alpha_mult = 0.0f;
				shipp->flags.set(Ship::Ship_Flags::Cloaked);
			}
			else {
				shipp->alpha_mult = ((float)newAlpha) / 100.f;
				shipp->flags.set(Ship::Ship_Flags::Render_with_alpha_mult);
			}
			break;
		}

		case OSWPT_TYPE_WING:
		{
			auto wp = oswpt.wingp();
			for (int i = 0; i < wp->current_count; ++i)
			{
				if (wp->ship_index[i] >= 0)
				{
					ship* shipp = &Ships[wp->ship_index[i]];
					shipp->flags.remove(Ship::Ship_Flags::Cloaked);
					if (newAlpha >= 100) {
						shipp->alpha_mult = 1.0f;
						shipp->flags.remove(Ship::Ship_Flags::Render_with_alpha_mult);
					}
					else if (newAlpha <= 0) {
						shipp->alpha_mult = 0.0f;
						shipp->flags.set(Ship::Ship_Flags::Cloaked);
					}
					else {
						shipp->alpha_mult = ((float)newAlpha) / 100.f;
						shipp->flags.set(Ship::Ship_Flags::Render_with_alpha_mult);
					}
				}
			}
			break;
		}

		case OSWPT_TYPE_PARSE_OBJECT:
		case OSWPT_TYPE_WING_NOT_PRESENT:
		default:
			mprintf(("Invalid Shipname in SEXP set-alpha-multiplier. Ship / Wing must be present.\n"));
		}
	}
}

extern int insert_subsys_status(p_object *pobjp);

// Goober5000
void parse_copy_damage(const ship_registry_entry *target, const ship_registry_entry *source_entry)
{
	auto target_pobjp = target->p_objp();
	auto source_shipp = source_entry->shipp();

	if (target_pobjp->ship_class != source_shipp->ship_info_index)
	{
		nprintf(("SEXP", "Copying damage of ship %s to ship %s which has a different ship class.  Strange results might occur.\n", source_shipp->ship_name, target_pobjp->name));
	}


	// copy hull...
	target_pobjp->special_hitpoints = source_shipp->special_hitpoints;
	target_pobjp->ship_max_hull_strength = source_shipp->ship_max_hull_strength;
	target_pobjp->initial_hull = fl2i(get_hull_pct(source_entry) * 100.0f);

	// ...and shields
	target_pobjp->ship_max_shield_strength = source_shipp->ship_max_shield_strength;
	target_pobjp->initial_shields = fl2i(get_shield_pct(source_entry) * 100.0f);
	target_pobjp->max_shield_recharge = source_shipp->max_shield_recharge;


	// search through all subsystems on source ship and map them onto target ship
	for (auto source_ss: list_range(&source_shipp->subsys_list))
	{
		// find subsystem to configure
		auto target_sssp = parse_get_subsys_status(target_pobjp, source_ss->system_info->subobj_name);

		// gak... none allocated; we need to allocate one!
		if (target_sssp == nullptr)
		{
			// jam in the new subsystem at the end of the existing list for this parse object
			int new_idx = insert_subsys_status(target_pobjp);
			target_sssp = &Subsys_status[new_idx];

			strcpy_s(target_sssp->name, source_ss->system_info->subobj_name);
		}

		// copy
		if (source_ss->max_hits == 0.0f)
		{
			target_sssp->percent = 100.0f;
		}
		else
		{
			target_sssp->percent = 100.0f - (source_ss->current_hits / source_ss->max_hits) * 100.0f;
		}
	}
}

// Goober5000
void sexp_ship_copy_damage(int node)
{
	int n;

	// source ship must be present
	auto source = eval_ship(node);
	if (!source || !source->has_shipp())
		return;

	// loop through all subsequent arguments
	for (n = CDR(node); n != -1; n = CDR(n))
	{
		auto target = eval_ship(n);
		if (!target)
			continue;

		// maybe it's present in-mission
		if (target->has_shipp())
		{
			ship_copy_damage(target, source);
			continue;
		}

		// maybe it's on the arrival list
		if (target->status == ShipStatus::NOT_YET_PRESENT)
		{
			parse_copy_damage(target, source);
			continue;
		}

		// must have departed or not even existed... do nothing
	}
}

//-Bobboau
void sexp_activate_deactivate_glow_points(int n, bool activate)
{
	for ( ; n != -1; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp())
			continue;

		for (auto &bank_active : ship_entry->shipp()->glow_point_bank_active)
			bank_active = activate;
	}
}

//-Bobboau
void sexp_activate_deactivate_glow_point_bank(int n, bool activate)
{
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	for ( n = CDR(n); n != -1; n = CDR(n))
	{
		bool is_nan, is_nan_forever;
		int num = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			continue;

		if (num >= 0 && num < (int)ship_entry->shipp()->glow_point_bank_active.size())
			ship_entry->shipp()->glow_point_bank_active[num] = activate;
	}
}

//-Bobboau
void sexp_activate_deactivate_glow_maps(int n, bool activate)
{
	for ( ; n != -1; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp())
			continue;

		ship_entry->shipp()->flags.set(Ship::Ship_Flags::Glowmaps_disabled, !activate);
	}
}

void sexp_set_ambient_light(int node)
{
	int red, green, blue, level = 0; 
	bool is_nan, is_nan_forever;

	Assert(node >= 0);

	eval_nums(node, is_nan, is_nan_forever, red, green, blue);
	if (is_nan || is_nan_forever)
		return;

	if (red < 0 || red > 255)
		red = 0;
	if (green < 0 || green > 255)
		green = 0;
	if (blue < 0 || blue > 255)
		blue = 0;

	level |= red; 
	level |= green << 8;
	level |= blue << 16;

	// setting the ambient light level for the mission won't actually do anything as it is parsed at mission 
	// start but it should be updated in case someone changes that later
	The_mission.ambient_light_level = level; 

	// call the graphics function to actually set the level
	gr_set_ambient_light(red, green, blue);

	// do the multiplayer callback
	if (MULTIPLAYER_MASTER) {
		Current_sexp_network_packet.start_callback();
		Current_sexp_network_packet.send_int(level);
		Current_sexp_network_packet.end_callback();
	}
}

void multi_sexp_set_ambient_light()
{
	int level = 0;
	if (Current_sexp_network_packet.get_int(level)) {
		The_mission.ambient_light_level = level;
		gr_set_ambient_light((level & 0xff),((level >> 8) & 0xff), ((level >> 16) & 0xff));
	}
}

void sexp_set_post_effect(int node)
{
	std::array<float, 3> a1d;
	vec3d rgb;
	bool is_nan, is_nan_forever;

	auto name = CTEXT(node);
	if (name == nullptr || *name == '\0')
		return;
	node = CDR(node);

	int amount = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	if (amount < 0 || amount > 100)
		amount = 0;
	node = CDR(node);

	eval_array<float>(a1d, node, is_nan, is_nan_forever, [](int num)->float
	{
		float f = static_cast<float>(num) / 255.0f;
		CAP(f, 0.0f, 1.0f);
		return f;
	});
	if (is_nan || is_nan_forever)
		return;
	std::copy(a1d.begin(), a1d.end(), std::begin(rgb.a1d));

	gr_post_process_set_effect(name, amount, &rgb);
}

void sexp_reset_post_effects()
{
	gr_post_process_set_defaults();
}

// Goober5000
void sexp_set_skybox_orientation(int n)
{
	matrix m;
	angles a;
	bool is_nan, is_nan_forever;

	eval_angles(&a, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	vm_angles_2_matrix(&m, &a);
	stars_set_background_orientation(&m);
}

// Goober5000
void sexp_set_skybox_alpha(int n)
{
	int alphax1000;
	bool is_nan, is_nan_forever;

	alphax1000 = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	stars_set_background_alpha(alphax1000 / 1000.0f);
}

// taylor - load and set a skybox model
void sexp_set_skybox_model(int n)
{
	char new_skybox_model[TOKEN_LENGTH+1]; //max input is TOKEN_LENGTH, +1 for NUL
	strcpy_s(new_skybox_model, CTEXT(n));
	int new_skybox_model_flags = DEFAULT_NMODEL_FLAGS;

	// check if we need to reset the animated texture timestamp
	n = CDR(n);
	if (n == -1 || is_sexp_true(n)) {
		Skybox_timestamp = _timestamp();
	}

	if (n != -1) n = CDR(n);

	// gather any flags
	while (n != -1) {
		// this should check all entries in Skybox_flags
		if ( !stricmp("add-lighting", CTEXT(n) )) {
			new_skybox_model_flags &= ~MR_NO_LIGHTING;
		}
		else if ( !stricmp("no-transparency", CTEXT(n) )) {
			new_skybox_model_flags &= ~MR_ALL_XPARENT;
		}
		else if ( !stricmp("add-zbuffer", CTEXT(n) )) {
			new_skybox_model_flags &= ~MR_NO_ZBUFFER;
		}
		else if ( !stricmp("add-culling", CTEXT(n) )) {
			new_skybox_model_flags &= ~MR_NO_CULL;
		}
		else if ( !stricmp("no-glowmaps", CTEXT(n) )) {
			new_skybox_model_flags |= MR_NO_GLOWMAPS;
		}
		else if ( !stricmp("force-clamp", CTEXT(n) )) {
			new_skybox_model_flags |= MR_FORCE_CLAMP;
		}
		else {
			Warning(LOCATION, "Invalid flag passed to set-skybox-model: %s\n", CTEXT(n));
		}
		n = CDR(n);
	}
	if ( !stricmp("default", new_skybox_model )) {
		stars_set_background_model( The_mission.skybox_model, nullptr, new_skybox_model_flags );
	} else {
		// stars_level_init() will set the actual mission skybox after this gets
		// evaluated during parse. by setting it now we get everything loaded so
		// there is less slowdown when it actually swaps out - taylor
		stars_set_background_model( new_skybox_model, nullptr, new_skybox_model_flags );
	}
}

// taylor - preload a skybox model.  this doesn't set anything as viewable, just loads it into memory
void sexp_set_skybox_model_preload(const char *name)
{
	int i;

	if (!stricmp("default", name)) {
		name = The_mission.skybox_model;
	}

	// if there isn't a skybox model then don't load one
	if ( strlen(name) && stricmp(name, "none") != 0 ) {
		i = model_load( name );
		model_page_in_textures( i );
	}
}

void sexp_set_thrusters(int node)
{
	bool activate = is_sexp_true(node);
	node = CDR(node);

	for (; node >= 0; node = CDR(node))
	{
		auto ship_entry = eval_ship(node);
		if (!ship_entry || !ship_entry->has_shipp())
			continue;

		ship_entry->shipp()->flags.set(Ship::Ship_Flags::No_thrusters, !activate);
	}
}

void sexp_beam_fire(int node, bool at_coords)
{
	int idx, n = node;
	beam_fire_info fire_info;
	bool is_nan, is_nan_forever;

	// zero stuff out
	memset(&fire_info, 0, sizeof(beam_fire_info));
	fire_info.accuracy = 0.000001f;							// this will guarantee a hit

	// get the firing ship
	auto shooter = eval_ship(n);
	if (!shooter || !shooter->has_shipp()) {
		return;
	}
	n = CDR(n);
	fire_info.shooter = shooter->objp();

	// get the subsystem
	fire_info.turret = ship_get_subsys(shooter->shipp(), CTEXT(n));
	if (fire_info.turret == nullptr) {
		return;
	}
	n = CDR(n);

	if (at_coords) {
		// get the target coordinates
		eval_vec3d(&fire_info.target_pos1, n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever) {
			return;
		}

		fire_info.bfi_flags |= BFIF_TARGETING_COORDS;
		fire_info.target = nullptr;
		fire_info.target_subsys = nullptr;
	} else {
		// get the target
		auto target = eval_ship(n);
		if (!target || !target->has_shipp()) {
			return;
		}
		n = CDR(n);
		fire_info.target = target->objp();

		// see if the optional subsystem can be found	
		fire_info.target_subsys = nullptr;
		if (n >= 0) {
			fire_info.target_subsys = ship_get_subsys(target->shipp(), CTEXT(n));
			n = CDR(n);
		}
	}

	// optionally force firing
	if (n >= 0 && is_sexp_true(n)) {
		fire_info.bfi_flags |= BFIF_FORCE_FIRING;
		n = CDR(n);
	}

	// get the second set of coordinates
	if (at_coords) {
		int count = eval_vec3d(&fire_info.target_pos2, n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever) {
			return;
		}

		if (count == 0) {
			fire_info.target_pos2 = fire_info.target_pos1;
		}
	}

	// --- done getting arguments ---

	// if it has no primary weapons
	if (fire_info.turret->weapons.num_primary_banks <= 0) {
		Warning(LOCATION, "Couldn't fire turret on ship %s; subsystem %s has no primary weapons", CTEXT(node), CTEXT(CDR(node)));
		return;
	}

	// if the turret is destroyed
	if (!(fire_info.bfi_flags & BFIF_FORCE_FIRING) && fire_info.turret->current_hits <= 0.0f) {
		return;
	}

	// hmm, this could be wacky. Let's just simply select the first beam weapon in the turret
	fire_info.beam_info_index = -1;	
	for (idx=0; idx<fire_info.turret->weapons.num_primary_banks; idx++) {
		Assertion(fire_info.turret->weapons.primary_bank_weapons[idx] >= 0 && fire_info.turret->weapons.primary_bank_weapons[idx] < weapon_info_size(),
				"sexp_beam_fire: found invalid weapon index (%i), get a coder\n!", fire_info.turret->weapons.primary_bank_weapons[idx]);
		// store the weapon info index
		if (Weapon_info[fire_info.turret->weapons.primary_bank_weapons[idx]].wi_flags[Weapon::Info_Flags::Beam]) {
			fire_info.beam_info_index = fire_info.turret->weapons.primary_bank_weapons[idx];
		}
	}

		// fire the beam
		if (fire_info.beam_info_index != -1) {
			fire_info.fire_method = BFM_TURRET_FORCE_FIRED;
			for ( int i = 0; i < Weapon_info[fire_info.beam_info_index].shots; i++ ) {
				fire_info.burst_index = i;
				beam_fire(&fire_info);
				fire_info.turret->turret_next_fire_pos++;
			}
		} else {
			// it would appear the turret doesn't have any beam weapons
			Warning(LOCATION, "Couldn't fire turret on ship %s; subsystem %s has no beam weapons", CTEXT(node), CTEXT(CDR(node)));
		}
}	

void sexp_beam_floating_fire(int n)
{
	bool is_nan, is_nan_forever;
	beam_fire_info fire_info;
	memset(&fire_info, 0, sizeof(beam_fire_info));
	fire_info.accuracy = 0.000001f;							// this will guarantee a hit
	fire_info.bfi_flags |= BFIF_FLOATING_BEAM;
	fire_info.turret = nullptr;		// A free-floating beam isn't fired from a subsystem.
	fire_info.burst_index = 0;

	fire_info.beam_info_index = weapon_info_lookup(CTEXT(n));
	n = CDR(n);
	if (fire_info.beam_info_index < 0)
	{
		Warning(LOCATION, "Invalid weapon class passed to beam-create; weapon type '%s' does not exist!\n", CTEXT(n));
		return;
	}
	if (!(Weapon_info[fire_info.beam_info_index].wi_flags[Weapon::Info_Flags::Beam]))
	{
		Warning(LOCATION, "Invalid weapon class passed to beam-create; weapon type '%s' is not a beam!\n", CTEXT(n));
		return;
	}

	fire_info.shooter = nullptr;
	if (stricmp(CTEXT(n), SEXP_NONE_STRING) != 0)
	{
		auto shooter = eval_ship(n);
		if (!shooter || !shooter->has_shipp())
			return;

		fire_info.shooter = shooter->objp();
	}
	n = CDR(n);

	fire_info.team = static_cast<char>(iff_lookup(CTEXT(n)));
	n = CDR(n);

	eval_vec3d(&fire_info.starting_pos, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	fire_info.target = nullptr;
	fire_info.target_subsys = nullptr;

	const ship_registry_entry *target = nullptr;
	if (stricmp(CTEXT(n), SEXP_NONE_STRING) != 0)
	{
		target = eval_ship(n);
		if (!target || !target->has_shipp())
			return;

		fire_info.target = target->objp();
	}
	else
		fire_info.bfi_flags |= BFIF_TARGETING_COORDS;
	n = CDR(n);

	if (n >= 0)
	{
		if (stricmp(CTEXT(n), SEXP_NONE_STRING) != 0)
		{
			if (target && target->has_shipp())
				fire_info.target_subsys = ship_get_subsys(target->shipp(), CTEXT(n));
		}

		n = CDR(n);
	}

	// if the nodes do not exist, the vector will be set to 0
	eval_vec3d(&fire_info.target_pos1, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	// if these nodes do not exist, the vector should be the same as the first
	int count = eval_vec3d(&fire_info.target_pos2, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	if (count == 0)
		fire_info.target_pos2 = fire_info.target_pos1;

	fire_info.fire_method = BFM_SEXP_FLOATING_FIRED;

	beam_fire(&fire_info);
}

void sexp_beam_or_turret_free_one(ship_subsys *turret, bool is_beam, bool free)
{
	if (is_beam)
	{
		if (free)
		{
			// flag it as beam free :)
			if (!(turret->weapons.flags[Ship::Weapon_Flags::Beam_Free]))
			{
				turret->weapons.flags.set(Ship::Weapon_Flags::Beam_Free);
				turret->turret_next_fire_stamp = timestamp(Random::next(50, 4000));
			}
		}
		else
		{
			// flag it as not beam free
			turret->weapons.flags.remove(Ship::Weapon_Flags::Beam_Free);
		}
	}
	else
	{
		if (free)
		{
			// flag turret as no longer locked :)
			if (turret->weapons.flags[Ship::Weapon_Flags::Turret_Lock])
			{
				turret->weapons.flags.remove(Ship::Weapon_Flags::Turret_Lock);
				turret->turret_next_fire_stamp = timestamp(Random::next(50, 4000));
			}
		}
		else
		{
			// flag turret as locked
			turret->weapons.flags.set(Ship::Weapon_Flags::Turret_Lock);
		}
	}
}

void sexp_beam_or_turret_free_or_lock(int node, bool is_beam, bool free)
{
	// get the firing ship
	auto shooter = eval_ship(node);
	if (!shooter || !shooter->has_shipp())
		return;
	node = CDR(node);

	for ( ; node >= 0; node = CDR(node) )
	{
		// get the subsystem
		auto turret = ship_get_subsys(shooter->shipp(), CTEXT(node));
		if (!turret || turret->system_info->type != SUBSYSTEM_TURRET)
			continue;

		sexp_beam_or_turret_free_one(turret, is_beam, free);
	}
}

void sexp_beam_or_turret_free_or_lock_all(int node, bool is_beam, bool free)
{
	for (int n = node; n >= 0; n = CDR(n))
	{
		// get the firing ship
		auto shooter = eval_ship(n);
		if (!shooter || !shooter->has_shipp())
			continue;

		// visit all beam weapons
		for (auto turret: list_range(&shooter->shipp()->subsys_list))
		{
			if (turret->system_info->type != SUBSYSTEM_TURRET)
				continue;

			sexp_beam_or_turret_free_one(turret, is_beam, free);
		}
	}
}

void sexp_turret_tagged_or_clear_specific(int node, bool set_it)
{
	// get the firing ship
	auto shooter = eval_ship(node);
	if (!shooter || !shooter->has_shipp())
		return;
	node = CDR(node);

	for ( ; node >= 0; node = CDR(node))
	{
		// get the subsystem
		auto turret = ship_get_subsys(shooter->shipp(), CTEXT(node));
		if (!turret || turret->system_info->type != SUBSYSTEM_TURRET)
			continue;

		// flag turret as slaved to tag
		turret->weapons.flags.set(Ship::Weapon_Flags::Tagged_Only, set_it);
	}
}

void sexp_turret_tagged_only_or_clear_all(int node, bool set_it)
{
	for (int n = node; n >= 0; n = CDR(n))
	{
		// get the firing ship
		auto shooter = eval_ship(n);
		if (!shooter || !shooter->has_shipp())
			continue;

		// visit all turrets
		for (auto turret: list_range(&shooter->shipp()->subsys_list))
		{
			if (turret->system_info->type != SUBSYSTEM_TURRET)
				continue;

			turret->weapons.flags.set(Ship::Weapon_Flags::Tagged_Only, set_it);
		}
	}
}

void sexp_turret_change_weapon(int node)
{
	int windex;	//hehe
	ship_subsys *turret;
	ship_weapon *swp;

	// get the firing ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	node = CDR(node);

	//Get subsystem
	turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
	if (!turret) {
		return;
	}
	swp = &turret->weapons;
	node = CDR(node);

	windex = weapon_info_lookup(CTEXT(node));
	if (windex < 0) {
		return;
	}
	node = CDR(node);

	//Get the slot
	float capacity, size;
	int prim_slot, sec_slot;
	bool is_nan, is_nan_forever;

	eval_nums(node, is_nan, is_nan_forever, prim_slot, sec_slot);
	if (is_nan || is_nan_forever)
		return;

	if (prim_slot)
	{
		if (prim_slot > MAX_SHIP_PRIMARY_BANKS) {
			return;
		}

		if (prim_slot > swp->num_primary_banks) {
			swp->num_primary_banks++;
			prim_slot = swp->num_primary_banks;
		}

		//Get an index
		prim_slot--;

		//Get the max capacity
		capacity = (float)swp->primary_bank_capacity[prim_slot];
		size = (float)Weapon_info[windex].cargo_size;

		//Set various vars
		swp->primary_bank_start_ammo[prim_slot] = (int)(capacity / size);
		swp->primary_bank_ammo[prim_slot] = swp->primary_bank_start_ammo[prim_slot];
		swp->primary_bank_weapons[prim_slot] = windex;
		swp->primary_bank_rearm_time[prim_slot] = timestamp(0);
		swp->primary_bank_fof_cooldown[prim_slot] = 0.0f;
	}
	else if (sec_slot)
	{
		if (sec_slot > MAX_SHIP_SECONDARY_BANKS) {
			return;
		}

		if (sec_slot > swp->num_secondary_banks) {
			swp->num_secondary_banks++;
			sec_slot = swp->num_secondary_banks;
		}

		//Get an index
		sec_slot--;

		//Get the max capacity
		capacity = (float)swp->secondary_bank_capacity[sec_slot];
		size = (float)Weapon_info[windex].cargo_size;

		//Set various vars
		if (Weapon_info[windex].wi_flags[Weapon::Info_Flags::SecondaryNoAmmo])
			swp->secondary_bank_start_ammo[sec_slot] = 0;
		else
			swp->secondary_bank_start_ammo[sec_slot] = (int)(capacity / size);
		swp->secondary_bank_ammo[sec_slot] = swp->secondary_bank_start_ammo[sec_slot];
		swp->secondary_bank_weapons[sec_slot] = windex;
		swp->secondary_bank_rearm_time[sec_slot] = timestamp(0);
	}
}

void sexp_set_armor_type(int node)
{
	int armor;
	bool rset;
	ship *shipp = nullptr;
	ship_info *sip = nullptr;

	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	shipp = ship_entry->shipp();
	sip = &Ship_info[shipp->ship_info_index];
	node = CDR(node);

	// set or reset
	rset = is_sexp_true(node);
	node = CDR(node);

	// get armor
	auto armor_name = CTEXT(node);
	if (!stricmp(SEXP_NONE_STRING, armor_name)) {
		armor = -1;
	} else {
		armor = armor_type_get_idx(armor_name);
	}
	node = CDR(node);

	//Set armor
	process_ship_subsystems(ship_entry, true, node, true, "set-armor-type", [&](ProcessSubsystemType type, ship_subsys* ss)
	{
		if (type == ProcessSubsystemType::HULL || type == ProcessSubsystemType::SIM_HULL)
		{
			// we are setting the ship itself
			if (!rset)
				shipp->armor_type_idx = sip->armor_type_idx;
			else
				shipp->armor_type_idx = armor;
		}
		else if (type == ProcessSubsystemType::SHIELDS)
		{
			// we are setting the ships shields
			if (!rset)
				shipp->shield_armor_type_idx = sip->shield_armor_type_idx;
			else
				shipp->shield_armor_type_idx = armor;
		}
		else
		{
			// set the range
			if (!rset)
				ss->armor_type_idx = ss->system_info->armor_type_idx;
			else
				ss->armor_type_idx = armor;
		}
	});
}

void sexp_weapon_set_damage_type(int node)
{
	int windex, damage;
	bool swave, rset;
	size_t t;

	// weapon or shockwave
	swave = is_sexp_true(node);

	// get damage type
	node = CDR(node);
	if (!stricmp(SEXP_NONE_STRING, CTEXT(node)))
		damage = -1;
	else 
	{
		for(t = 0; t < Damage_types.size(); t++) 
		{
			if ( !stricmp(Damage_types[t].name, CTEXT(node)))  
				break;
		}
		if (t == Damage_types.size()) 
			return;
		damage = (int)t;
	}

	//Set or reset to default
	node = CDR(node);
	rset = is_sexp_true(node);
	
	//Set Damage
	node = CDR(node);
	while(node != -1)
	{
		// get the weapon
		windex = weapon_info_lookup(CTEXT(node));
		if(windex >= 0) 
		{
			// set the damage type
			if (swave)
				if (!rset)
					Weapon_info[windex].damage_type_idx = Weapon_info[windex].damage_type_idx_sav;
				else
					Weapon_info[windex].damage_type_idx = damage;
			else
				if (!rset)
					Weapon_info[windex].shockwave.damage_type_idx = Weapon_info[windex].shockwave.damage_type_idx_sav;
				else
					Weapon_info[windex].shockwave.damage_type_idx = damage;
		// next
		}
		node = CDR(node);
	}
}

void sexp_ship_set_damage_type(int node)
{
	int damage;
	bool set_collision, rset;
	size_t t;

	// collision or debris
	set_collision = is_sexp_true(node);

	// get damage type
	node = CDR(node);
	if (!stricmp(SEXP_NONE_STRING, CTEXT(node)))
		damage = -1;
	else 
	{
		for(t = 0; t < Damage_types.size(); t++) 
		{
			if ( !stricmp(Damage_types[t].name, CTEXT(node)))  
				break;
		}
		if (t == Damage_types.size()) 
			return;
		damage = (int)t;
	}

	//Set or reset to default
	node = CDR(node);
	rset = is_sexp_true(node);
	
	//Set Damage
	node = CDR(node);
	while(node != -1)
	{
		// get the ship
		auto ship_entry = eval_ship(node);
		if (ship_entry && ship_entry->has_shipp())
		{
			auto shipp = ship_entry->shipp();

			// set the damage type
			if (set_collision)
			{
				if (!rset)
					shipp->collision_damage_type_idx = Ship_info[shipp->ship_info_index].collision_damage_type_idx;
				else
					shipp->collision_damage_type_idx = damage;
			}
			else 
			{
				if (!rset)
					shipp->debris_damage_type_idx = Ship_info[shipp->ship_info_index].debris_damage_type_idx;
				else
					shipp->debris_damage_type_idx = damage;
			}
		}

		// next
		node = CDR(node);
	}
}

void sexp_ship_shockwave_set_damage_type(int node)
{
	int sindex, damage;
	bool rset;
	size_t t;

	// get damage type
	if (!stricmp(SEXP_NONE_STRING, CTEXT(node)))
		damage = -1;
	else 
	{
		for(t = 0; t < Damage_types.size(); t++) 
		{
			if ( !stricmp(Damage_types[t].name, CTEXT(node)))  
				break;
		}
		if (t == Damage_types.size()) 
			return;
		damage = (int)t;
	}

	//Set or reset to default
	node = CDR(node);
	rset = is_sexp_true(node);
	
	//Set Damage
	node = CDR(node);
	while(node != -1)
	{
		// get the ship
		sindex = ship_info_lookup(CTEXT(node));
		if(sindex >= 0) 
		{
			// set the damage type
			if (!rset)
				Ship_info[sindex].shockwave.damage_type_idx = Ship_info[sindex].shockwave.damage_type_idx_sav;
			else
				Ship_info[sindex].shockwave.damage_type_idx = damage;
		}

		// next
		node = CDR(node);
	}
}

void sexp_field_set_damage_type(int node)
{
	int damage, rset;
	size_t t;

	// get damage type
	if (!stricmp(SEXP_NONE_STRING, CTEXT(node)))
		damage = -1;
	else 
	{
		for(t = 0; t < Damage_types.size(); t++) 
		{
			if ( !stricmp(Damage_types[t].name, CTEXT(node)))  
				break;
		}
		if (t == Damage_types.size()) 
			return;
		damage = (int)t;
	}

	//Set or reset to default
	node = CDR(node);
	rset = is_sexp_true(node);
	
	//Set Damage
	node = CDR(node);
	for(t = 0; t < Asteroid_info.size(); t++) 
	if (!rset)
		Asteroid_info[t].damage_type_idx = Asteroid_info[t].damage_type_idx_sav;
	else
		Asteroid_info[t].damage_type_idx = damage;
}

void sexp_turret_set_target_order(int node)
{
	int i, oindex;

	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	//Get turret subsys
	node = CDR(node);
	auto turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
	if(turret == nullptr){
		return;
	}

	//Reset order
	for(i = 0; i < NUM_TURRET_ORDER_TYPES; i++) {
		turret->turret_targeting_order[i] = -1;
	}
	
	oindex = 0;
	node = CDR(node);
	while(node != -1)
	{
		if(oindex >= NUM_TURRET_ORDER_TYPES) {
			break;
		}

		for(i = 0; i < NUM_TURRET_ORDER_TYPES; i++) {
			if(!stricmp(Turret_target_order_names[i], CTEXT(node))) {
				turret->turret_targeting_order[oindex] = i;
			}
		}

		oindex++;
		node = CDR(node);
	}
}

void sexp_turret_set_direction_preference(int node)
{
	bool is_nan, is_nan_forever;
	
	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	//store direction preference
	node = CDR(node);
	int dirpref = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	node = CDR(node);

	//Set range
	for (; node != -1; node = CDR(node)) {
		// get the subsystem
		auto turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if(turret == nullptr){
			continue;
		}

		// set the range
		if(dirpref < 0)
			turret->optimum_range = turret->system_info->optimum_range;
		else
			if (dirpref == 0) {
				turret->favor_current_facing = 0.0f;
			} else {
				CAP(dirpref, 1, 100);
				turret->favor_current_facing = 1.0f + (((float) (100 - dirpref)) / 10.0f);
			}
	}
}

void sexp_turret_set_rate_of_fire(int node)
{
	float rof;
	bool is_nan, is_nan_forever;

	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	//store rof
	node = CDR(node);
	rof = (float)eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	node = CDR(node);

	//Set rof
	while (node >= 0)
	{
		// get the subsystem
		auto turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if (turret != nullptr)
		{
			// set the rate
			if (rof < 0)
				turret->rof_scaler = turret->system_info->turret_rof_scaler;
			else
				turret->rof_scaler = rof / 100;
		}

		// next
		node = CDR(node);
	}
}

void sexp_turret_set_optimum_range(int node)
{
	float range;
	bool is_nan, is_nan_forever;

	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	// store range
	node = CDR(node);
	range = (float)eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	node = CDR(node);

	//Set range
	while (node >= 0)
	{
		// get the subsystem
		auto turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if (turret != nullptr)
		{
			// set the range
			if (range < 0)
				turret->optimum_range = turret->system_info->optimum_range;
			else
				turret->optimum_range = range;
		}

		// next
		node = CDR(node);
	}
}

void sexp_turret_set_target_priorities(int node)
{
	int i, j;

	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	//Get turret subsys
	node = CDR(node);
	auto turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
	if(turret == nullptr){
		return;
	}

	//Reset or new list
	node = CDR(node);   
	if(!(is_sexp_true(node))) {	//Reset
		turret->num_target_priorities = turret->system_info->num_target_priorities;
		for (j = 0; j < 32; j++) {
			turret->target_priority[j] = turret->system_info->target_priority[j];
		}
	}
	else {					//New List
		node = CDR(node);
		//clear the list
		turret->num_target_priorities = 0;
		for (i = 0; i < 32; i++) {
			turret->target_priority[i] = -1;
		}
		int num_groups = (int)Ai_tp_list.size();
		// set the target priorities
		while(node != -1){
			if(turret->num_target_priorities < 32){
				for(j = 0; j < num_groups; j++) {
					if ( !stricmp(Ai_tp_list[j].name, CTEXT(node)))  {
							turret->target_priority[turret->num_target_priorities] = j;
							turret->num_target_priorities++;
					}
				}
			}
		// next
		node = CDR(node);
		}
	}
}

void sexp_turret_set_forced_target(int node, bool targeting_subsys)
{
	// get target
	auto target = eval_ship(node);
	if (!target || !target->has_shipp())
		return;
	node = CDR(node);

	// maybe get target subsys
	ship_subsys* target_subsys = nullptr;
	if (targeting_subsys) {
		target_subsys = ship_get_subsys(target->shipp(), CTEXT(node));
		if (target_subsys == nullptr)
			return;
		node = CDR(node);
	}

	// get ship
	auto shooter = eval_ship(node);
	if (!shooter || !shooter->has_shipp()) {
		return;
	}
	node = CDR(node);

	while (node >= 0)
	{
		// get the turret
		auto turret = ship_get_subsys(shooter->shipp(), CTEXT(node));
		if (turret != nullptr)
		{
			turret->turret_enemy_objnum = target->objnum;
			turret->turret_enemy_sig = target->objp()->signature;
			turret->flags.set(Ship::Subsystem_Flags::Forced_target);
			turret->targeted_subsys = nullptr;

			if (targeting_subsys) {
				turret->targeted_subsys = target_subsys;
				turret->flags.set(Ship::Subsystem_Flags::Forced_subsys_target);
			}
		}

		// next
		node = CDR(node);
	}
}

void sexp_turret_clear_forced_target(int node)
{
	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	//Set range
	while (node >= 0)
	{
		// get the subsystem
		auto turret = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if (turret != nullptr)
		{
			turret->turret_enemy_objnum = -1;
			turret->targeted_subsys = nullptr;
			turret->flags.remove(Ship::Subsystem_Flags::Forced_target);
			turret->flags.remove(Ship::Subsystem_Flags::Forced_subsys_target);
		}

		// next
		node = CDR(node);
	}
}

void sexp_turret_set_inaccuracy(int node)
{
	bool is_nan, is_nan_forever;
	// get ship
	auto shooter = eval_ship(node);
	if (!shooter || !shooter->has_shipp()) {
		return;
	}

	// get inaccuracy
	node = CDR(node);
	auto inaccuracy = (float)eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	if (inaccuracy < 0.0f)
		inaccuracy = 0.0f;

	// input is in tenths so divide by 10
	inaccuracy /= 10.0f;

	node = CDR(node);
	if (node < 0) { // affect all turrets
		for (auto turret: list_range(&shooter->shipp()->subsys_list))
		{
			if (turret->system_info->type != SUBSYSTEM_TURRET)
				continue;

			turret->turret_inaccuracy = inaccuracy;
		}
	} else { // affect just some particular turret(s)
		while (node >= 0)
		{
			// get the subsystem
			ship_subsys* turret = ship_get_subsys(shooter->shipp(), CTEXT(node));
			if (turret != nullptr)
				turret->turret_inaccuracy = inaccuracy;

			// next
			node = CDR(node);
		}
	}
}

void sexp_ship_turret_target_order(int node)
{
	int oindex;
	int i;
	int new_target_order[NUM_TURRET_ORDER_TYPES];

	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	//Reset order
	for(i = 0; i < NUM_TURRET_ORDER_TYPES; i++) {
		new_target_order[i] = -1;
	}

	oindex = 0;
	node = CDR(node);
	while(node != -1)
	{
		if(oindex >= NUM_TURRET_ORDER_TYPES) {
			break;
		}

		for(i = 0; i < NUM_TURRET_ORDER_TYPES; i++) {
			if(!stricmp(Turret_target_order_names[i], CTEXT(node))) {
				new_target_order[oindex] = i;
			}
		}

		oindex++;
		node = CDR(node);
	}

	for (auto turret: list_range(&ship_entry->shipp()->subsys_list))
	{
		memcpy(turret->turret_targeting_order, new_target_order, NUM_TURRET_ORDER_TYPES*sizeof(int));
	}
}

// Goober5000
int sexp_is_in_turret_fov(int node)
{
	int n = node, range;
	bool is_nan, is_nan_forever;
	vec3d tpos, tvec;

	auto target_ship = eval_ship(n);
	if (!target_ship || target_ship->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_FALSE;
	if (target_ship->status == ShipStatus::EXITED)
		return SEXP_KNOWN_FALSE;
	n = CDR(n);

	auto turret_ship = eval_ship(n);
	if (!turret_ship || turret_ship->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_FALSE;
	if (turret_ship->status == ShipStatus::EXITED)
		return SEXP_KNOWN_FALSE;
	n = CDR(n);

	auto turret_subsys_name = CTEXT(n);
	n = CDR(n);

	if (n >= 0) {
		range = eval_num(n, is_nan, is_nan_forever);
		if (is_nan)
			return SEXP_FALSE;
		if (is_nan_forever)
			return SEXP_KNOWN_FALSE;
	}
	else
		range = -1;

	// find the turret
	auto turret_subsys = ship_get_subsys(turret_ship->shipp(), turret_subsys_name);
	if (turret_subsys == nullptr) {
		Warning(LOCATION, "Couldn't find turret subsystem '%s' on ship '%s' in sexp_is_in_turret_fov!", turret_subsys_name, turret_ship->name);
		return SEXP_KNOWN_FALSE;
	}

	// find out where the turret is
	ship_get_global_turret_info(turret_ship->objp(), turret_subsys->system_info, &tpos, &tvec);

	// see how far away is the target (this isn't used for a range check, only for vector math)
	float dist = vm_vec_dist(&target_ship->objp()->pos, &tpos);

	// but we can still use it for the range check if we are optionally checking that
	if (range >= 0 && dist > range)
		return SEXP_FALSE;

	// perform the check
	return object_in_turret_fov(target_ship->objp(), turret_subsys, &tvec, &tpos, dist) != 0 ? SEXP_TRUE : SEXP_FALSE;
}

// Goober5000
void sexp_set_subsys_motion_lock_free(int node, bool is_rotation, bool locked)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	node = CDR(node);

	// loop for all specified subsystems
	for ( ; node >= 0; node = CDR(node) )
	{
		// get the moving subsystem
		auto subsys = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if (subsys == nullptr)
			continue;

		// skip if it's already at the state we want; if not, set the flag
		if (is_rotation)
		{
			if (subsys->flags[Ship::Subsystem_Flags::Rotates] == !locked)
				continue;
			subsys->flags.set(Ship::Subsystem_Flags::Rotates, !locked);
		}
		else
		{
			if (subsys->flags[Ship::Subsystem_Flags::Translates] == !locked)
				continue;
			subsys->flags.set(Ship::Subsystem_Flags::Translates, !locked);
		}

		// set moving or not, depending on flag
		if (locked)
		{   
			if (is_rotation && subsys->subsys_snd_flags[Ship::Subsys_Sound_Flags::Rotate])
			{
				obj_snd_delete_type(ship_entry->objnum, subsys->system_info->rotation_snd, subsys);
				subsys->subsys_snd_flags.remove(Ship::Subsys_Sound_Flags::Rotate);
			}

			// bit of a hack... set the timestamp to just the delta so that we can restore it properly later
			auto &stamp = is_rotation ? subsys->submodel_instance_1->stepped_rotation_started : subsys->submodel_instance_1->stepped_translation_started;
			if (stamp.isValid())
				stamp = TIMESTAMP(timestamp_since(stamp));
		}
		else
		{
			if (is_rotation && subsys->system_info->rotation_snd.isValid())
			{
				obj_snd_assign(ship_entry->objnum, subsys->system_info->rotation_snd, &subsys->system_info->pnt, OS_SUBSYS_ROTATION, subsys);
				subsys->subsys_snd_flags.set(Ship::Subsys_Sound_Flags::Rotate);
			}

			// and restore the timestamp from the delta
			auto &stamp = is_rotation ? subsys->submodel_instance_1->stepped_rotation_started : subsys->submodel_instance_1->stepped_translation_started;
			if (stamp.isValid())
				stamp = timestamp_delta(_timestamp(), -stamp.value());
		}
	}
}

// Goober5000
void sexp_reverse_moving_subsystem(int node, bool is_rotation)
{
	// get the ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	node = CDR(node);

	// loop for all specified subsystems
	for ( ; node >= 0; node = CDR(node) )
	{
		// get the moving subsystem
		auto subsys = ship_get_subsys(ship_entry->shipp(), CTEXT(node));
		if (subsys == nullptr || subsys->submodel_instance_1 == nullptr)
			continue;

		// switch direction of motion
		if (is_rotation)
		{
			subsys->submodel_instance_1->current_turn_rate *= -1.0f;
			subsys->submodel_instance_1->desired_turn_rate *= -1.0f;
		}
		else
		{
			subsys->submodel_instance_1->current_shift_rate *= -1.0f;
			subsys->submodel_instance_1->desired_shift_rate *= -1.0f;
		}
	}
}

// Goober5000
void sexp_moving_subsys_set_turn_time_or_speed(int node, bool is_rotation)
{
	int n = node;
	bool is_nan, is_nan_forever;
	float time_or_speed, accel;

	// get the ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	// get the moving subsystem
	auto subsys = ship_get_subsys(ship_entry->shipp(), CTEXT(n));
	if (subsys == nullptr || subsys->submodel_instance_1 == nullptr)
		return;
	n = CDR(n);

	// get and set the value
	time_or_speed = eval_num(n, is_nan, is_nan_forever) / 1000.0f;
	if (is_nan || is_nan_forever)
		return;
	if (fl_near_zero(time_or_speed))
	{
		Warning(LOCATION, "In %s, %s cannot be zero!", is_rotation ? "rotating-subsys-set-turn-time" : "translating-subsys-set-speed", is_rotation ? "turn time" : "speed");
		return;
	}
	if (is_rotation)
		subsys->submodel_instance_1->desired_turn_rate = PI2 / time_or_speed;
	else
		subsys->submodel_instance_1->desired_shift_rate = time_or_speed;
	n = CDR(n);

	// maybe get and set the accel
	if (n != -1)
	{
		accel = eval_num(n, is_nan, is_nan_forever) / 1000.0f;
		if (is_nan || is_nan_forever)
			return;
		if (fl_near_zero(accel))
		{
			Warning(LOCATION, "In %s, acceleration cannot be zero!", is_rotation ? "rotating-subsys-set-turn-time" : "translating-subsys-set-speed");
			return;
		}
		if (is_rotation)
			subsys->submodel_instance_1->turn_accel = PI2 / accel;
		else
			subsys->submodel_instance_1->shift_accel = accel;
	}
	else
	{
		if (is_rotation)
			subsys->submodel_instance_1->current_turn_rate = PI2 / time_or_speed;
		else
			subsys->submodel_instance_1->current_shift_rate = time_or_speed;
	}
}

void sexp_trigger_submodel_animation(int node)
{
	int animation_subtype, direction, n = node;
	bool instant, is_nan, is_nan_forever;

	// get the ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	// get the type
	auto animation_type = animation::anim_match_type(CTEXT(n));
	if (animation_type == animation::ModelAnimationTriggerType::None)
	{
		Warning(LOCATION, "Unable to match animation type \"%s\"!", CTEXT(n));
		return;
	}
	n = CDR(n);

	// get the subtype and direction
	eval_nums(n, is_nan, is_nan_forever, animation_subtype, direction);
	if (is_nan || is_nan_forever)
		return;

	if (direction != 1 && direction != -1)
	{
		Warning(LOCATION, "Direction is %d; it must be 1 or -1!", direction);
		return;
	}

	// instant or not
	if (n >= 0)
	{
		//TODO Respect in new animations
		instant = is_sexp_true(n);
		n = CDR(n);
	}
	else
		instant = false;

	// do we narrow it to a specific subsystem?
	if (n >= 0)
	{
		ship_subsys *ss = ship_get_subsys(ship_entry->shipp(), CTEXT(n));
		if (!ss)
		{
			Warning(LOCATION, "Subsystem \"%s\" not found on ship \"%s\"!", CTEXT(n), CTEXT(node));
			return;
		}

		Ship_info[ship_entry->shipp()->ship_info_index].animations.get(model_get_instance(ship_entry->shipp()->model_instance_num), animation_type, animation::anim_name_from_subsys(ss->system_info), animation_subtype)
			.start(direction == -1 ? animation::ModelAnimationDirection::RWD : animation::ModelAnimationDirection::FWD, instant, instant, false);
	}
	else
	{
		Ship_info[ship_entry->shipp()->ship_info_index].animations.getAll(model_get_instance(ship_entry->shipp()->model_instance_num), animation_type, animation_subtype)
			.start(direction == -1 ? animation::ModelAnimationDirection::RWD : animation::ModelAnimationDirection::FWD, instant, instant, false);
	}
}

void sexp_trigger_submodel_animation_new(int n)
{
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	auto animation_type = animation::anim_match_type(CTEXT(n));
	if (animation_type == animation::ModelAnimationTriggerType::None)
	{
		Warning(LOCATION, "Unable to match animation type \"%s\"!", CTEXT(n));
		return;
	}
	n = CDR(n);

	SCP_string triggeredBy(CTEXT(n));
	n = CDR(n);

	animation::ModelAnimationDirection direction;
	if (n >= 0)
	{
		direction = is_sexp_true(n) ? animation::ModelAnimationDirection::FWD : animation::ModelAnimationDirection::RWD;
		n = CDR(n);
	}
	else
		direction = animation::ModelAnimationDirection::FWD;


	bool forced;
	if (n >= 0)
	{
		forced = is_sexp_true(n);
		n = CDR(n);
	}
	else
		forced = false;

	bool instant;
	if (n >= 0)
	{
		instant = is_sexp_true(n);
		n = CDR(n);
	}
	else
		instant = false;

	bool pause;
	if (n >= 0)
	{
		pause = is_sexp_true(n);
		n = CDR(n);
	}
	else
		pause = false;

	const auto& list = Ship_info[ship_entry->shipp()->ship_info_index].animations.parseScripted(model_get_instance(ship_entry->shipp()->model_instance_num), animation_type, triggeredBy);
	list.start(direction, forced || instant, instant, pause);
}

void sexp_stop_looping_animation(int n)
{
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;
	n = CDR(n);

	auto animation_type = animation::anim_match_type(CTEXT(n));
	if (animation_type == animation::ModelAnimationTriggerType::None)
	{
		Warning(LOCATION, "Unable to match animation type \"%s\"!", CTEXT(n));
		return;
	}
	n = CDR(n);

	SCP_string triggeredBy(CTEXT(n));
	n = CDR(n);

	const auto& list = Ship_info[ship_entry->shipp()->ship_info_index].animations.parseScripted(model_get_instance(ship_entry->shipp()->model_instance_num), animation_type, triggeredBy);
	list.setFlag(animation::Animation_Instance_Flags::Stop_after_next_loop);
}

void sexp_update_moveable_animation(int node)
{
	bool is_nan, is_nan_forever;

	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	node = CDR(node);

	SCP_string name(CTEXT(node));

	node = CDR(node);

	//For now this only contains integers. It is very much feasible though that certain moveables might be updateable with strings and other non-numbers. For this, the C-side of the moveable code already supports other types
	SCP_vector<std::any> args;

	while(node >= 0) {
		args.emplace_back(eval_num(node, is_nan, is_nan_forever));
		
		if(is_nan || is_nan_forever)
			Warning(LOCATION, "Value for moveable %s on ship %s was NaN!", name.c_str(), ship_entry->name);
		
		node = CDR(node);
	}

	Ship_info[ship_entry->shipp()->ship_info_index].animations.updateMoveable(model_get_instance(ship_entry->shipp()->model_instance_num), name, args);
}

void sexp_advance_moveable_animation(int node)
{
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	node = CDR(node);

	SCP_string name(CTEXT(node));

	Ship_info[ship_entry->shipp()->ship_info_index].animations.advanceMoveableToFinal(model_get_instance(ship_entry->shipp()->model_instance_num), name);
}

void sexp_add_remove_escort(int node)
{
	int flag;
	bool is_nan, is_nan_forever;

	// get the firing ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}

	// determine whether to add or remove it
	flag = eval_num(CDR(node), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever) {
		return;
	}

	// add/remove
	if (flag) {
		ship_entry->shipp()->escort_priority = flag;
		hud_add_ship_to_escort(ship_entry->objnum, 1);
	} else {
		hud_remove_ship_from_escort(ship_entry->objnum);
	}

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_ship(ship_entry->shipp());
	Current_sexp_network_packet.send_int(flag); 
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_add_remove_escort()
{
	ship *shipp;
	int flag;

	Current_sexp_network_packet.get_ship(shipp);
	if (!(Current_sexp_network_packet.get_int(flag))) {
		return;
	}

	// add/remove
	if (flag) {
		shipp->escort_priority = flag;
		hud_add_ship_to_escort(shipp->objnum, 1);
	} else {
		hud_remove_ship_from_escort(shipp->objnum);
	}
}

//given: two escort priorities and a list of ships
//do:    sets the most damaged one to the first priority and the rest to the second.
void sexp_damage_escort_list(int node)
{
	int n = node;
	int priority1;		//escort priority to set the most damaged ship
	int priority2;		//""         ""   to set the other ships
	bool is_nan, is_nan_forever;

	float smallest_hull_pct=1;		//smallest hull pct found
	ship *small_shipp=nullptr;		//entry in Ships[] of the above
	float current_hull_pct;			//hull pct of current ship we are evaluating

	eval_nums(n, is_nan, is_nan_forever, priority1, priority2);
	if (is_nan || is_nan_forever)
		return;

	//loop through the ships
	for ( ; n != -1; n = CDR(n) )
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp())
			continue;

		//calc hull integrity and compare
		current_hull_pct = get_hull_pct(ship_entry);

		if (current_hull_pct < smallest_hull_pct)
		{
			if (small_shipp)	// avoid null during 1st loop iteration
			{
				small_shipp->escort_priority=priority2;				//give the previous smallest the lower priority
			}
			
			smallest_hull_pct = current_hull_pct;
			small_shipp = ship_entry->shipp();
	
			ship_entry->shipp()->escort_priority = priority1;			//give the new smallest the higher priority
			hud_add_ship_to_escort(ship_entry->objnum, 1);
		}
		else														//if its bigger to begin with give it lower priority
		{
			ship_entry->shipp()->escort_priority = priority2;
			hud_add_ship_to_escort(ship_entry->objnum, 1);
		}
	}
}

// Goober5000 - set stuff for mission support ship
void sexp_set_support_ship(int n)
{
	int temp_val;
	bool is_nan, is_nan_forever;

	// get arrival location
	auto arrival_location = ArrivalLocation::AT_LOCATION;
	int index = string_lookup(CTEXT(n), Arrival_location_names, MAX_ARRIVAL_NAMES);
	if (index >= 0)
		arrival_location = static_cast<ArrivalLocation>(index);
	else
	{
		Warning(LOCATION, "Support ship arrival location '%s' not found.\n", CTEXT(n));
		return;
	}
	The_mission.support_ships.arrival_location = arrival_location;

	// get arrival anchor
	n = CDR(n);
	if (!stricmp(CTEXT(n), "<no anchor>"))
	{
		// if no anchor, set arrival location to hyperspace
		The_mission.support_ships.arrival_location = ArrivalLocation::AT_LOCATION;
	}
	else
	{
		// find the anchor
		The_mission.support_ships.arrival_anchor = anchor_t(ship_registry_get_index(CTEXT(n)));
	}

	// get departure location
	n = CDR(n);
	auto departure_location = DepartureLocation::AT_LOCATION;
	index = string_lookup(CTEXT(n), Departure_location_names, MAX_DEPARTURE_NAMES);
	if (index >= 0)
		departure_location = static_cast<DepartureLocation>(index);
	else
	{
		Warning(LOCATION, "Support ship departure location '%s' not found.\n", CTEXT(n));
		return;
	}
	The_mission.support_ships.departure_location = departure_location;

	// get departure anchor
	n = CDR(n);
	if (!stricmp(CTEXT(n), "<no anchor>"))
	{
		// if no anchor, set departure location to hyperspace
		The_mission.support_ships.departure_location = DepartureLocation::AT_LOCATION;
	}
	else
	{
		// find the anchor
		The_mission.support_ships.departure_anchor = anchor_t(ship_registry_get_index(CTEXT(n)));
	}

	// get ship class
	n = CDR(n);
	temp_val = ship_info_lookup(CTEXT(n));
	if ((temp_val < 0) && ((stricmp(CTEXT(n), "<species support ship class>") != 0) && (stricmp(CTEXT(n), "<any support ship class>") != 0)))
	{
		Warning(LOCATION, "Support ship class '%s' not found.\n", CTEXT(n));
		return;
	}
	if ((temp_val >= 0) && !(Ship_info[temp_val].flags[Ship::Info_Flags::Support]))
	{
		Warning(LOCATION, "Ship %s is not a support ship!", Ship_info[temp_val].name);
		return;
	}
	The_mission.support_ships.ship_class = temp_val;

	// get max number of ships allowed
	n = CDR(n);
	temp_val = eval_num(n, is_nan, is_nan_forever);
	if (!is_nan && !is_nan_forever) {
		The_mission.support_ships.max_support_ships = temp_val;
	}

	// get the number of concurrent ships allowed
	n = CDR(n);
	if ( n == -1 ) {
		// 7th arg not specified, set default
		The_mission.support_ships.max_concurrent_ships = 1;
	} else {
		temp_val = eval_num(n, is_nan, is_nan_forever);
		if (!is_nan && !is_nan_forever) {
			The_mission.support_ships.max_concurrent_ships = temp_val;
		}
	}
}

// Goober5000 - set stuff for arriving ships or wings
void sexp_set_arrival_info(int node)
{
	int arrival_mask, arrival_distance, arrival_delay, n = node;
	bool show_warp, adjust_warp_when_docked, is_nan, is_nan_forever;
	object_ship_wing_point_team oswpt;

	// get ship or wing
	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	// get arrival location
	auto arrival_location = ArrivalLocation::AT_LOCATION;
	int index = string_lookup(CTEXT(n), Arrival_location_names, MAX_ARRIVAL_NAMES);
	if (index >= 0)
		arrival_location = static_cast<ArrivalLocation>(index);
	else
	{
		Warning(LOCATION, "Arrival location '%s' not found.\n", CTEXT(n));
		return;
	}
	n = CDR(n);

	// get arrival anchor
	anchor_t arrival_anchor;
	if ((n < 0) || !stricmp(CTEXT(n), "<no anchor>"))
	{
		// if no anchor, set arrival location to hyperspace
		arrival_location = ArrivalLocation::AT_LOCATION;
	}
	else
	{
		// find the anchor
		arrival_anchor = anchor_t(ship_registry_get_index(CTEXT(n)));
	}
	n = CDR(n);

	// get arrival path mask, distance, and delay
	eval_nums(n, is_nan, is_nan_forever, arrival_mask, arrival_distance, arrival_delay);
	if (is_nan || is_nan_forever)
		return;

	// get warp effect
	show_warp = true;
	if (n >= 0)
	{
		show_warp = is_sexp_true(n);
		n = CDR(n);
	}

	// another flag
	adjust_warp_when_docked = true;
	if (n >= 0)
	{
		adjust_warp_when_docked = is_sexp_true(n);
		n = CDR(n);
	}

	// now set all that information depending on the first argument
	if (oswpt.type == OSWPT_TYPE_SHIP)
	{
		auto shipp = oswpt.shipp();

		shipp->arrival_location = arrival_location;
		shipp->arrival_anchor = arrival_anchor;
		shipp->arrival_path_mask = arrival_mask;
		shipp->arrival_distance = arrival_distance;
		shipp->arrival_delay = arrival_delay;

		shipp->flags.set(Ship::Ship_Flags::No_arrival_warp, !show_warp);
		shipp->flags.set(Ship::Ship_Flags::Same_arrival_warp_when_docked, !adjust_warp_when_docked);
	}
	else if (oswpt.type == OSWPT_TYPE_WING || oswpt.type == OSWPT_TYPE_WING_NOT_PRESENT)
	{
		auto wingp = oswpt.wingp();

		wingp->arrival_location = arrival_location;
		wingp->arrival_anchor = arrival_anchor;
		wingp->arrival_path_mask = arrival_mask;
		wingp->arrival_distance = arrival_distance;
		wingp->arrival_delay = arrival_delay;

		wingp->flags.set(Ship::Wing_Flags::No_arrival_warp, !show_warp);
		wingp->flags.set(Ship::Wing_Flags::Same_arrival_warp_when_docked, !adjust_warp_when_docked);
	}
	else if (oswpt.type == OSWPT_TYPE_PARSE_OBJECT)
	{
		auto p_objp = oswpt.p_objp();

		p_objp->arrival_location = arrival_location;
		p_objp->arrival_anchor = arrival_anchor;
		p_objp->arrival_path_mask = arrival_mask;
		p_objp->arrival_distance = arrival_distance;
		p_objp->arrival_delay = arrival_delay;

		p_objp->flags.set(Mission::Parse_Object_Flags::SF_No_arrival_warp, !show_warp);
		p_objp->flags.set(Mission::Parse_Object_Flags::SF_Same_arrival_warp_when_docked, !adjust_warp_when_docked);
	}
}

// Goober5000 - set stuff for departing ships or wings
void sexp_set_departure_info(int node)
{
	int departure_mask, departure_delay, n = node;
	bool show_warp, adjust_warp_when_docked, is_nan, is_nan_forever;
	object_ship_wing_point_team oswpt;

	// get ship or wing
	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	// get departure location
	auto departure_location = DepartureLocation::AT_LOCATION;
	int index = string_lookup(CTEXT(n), Departure_location_names, MAX_DEPARTURE_NAMES);
	if (index >= 0)
		departure_location = static_cast<DepartureLocation>(index);
	else
	{
		Warning(LOCATION, "Departure location '%s' not found.\n", CTEXT(n));
		return;
	}
	n = CDR(n);

	// get departure anchor
	anchor_t departure_anchor;
	if ((n < 0) || !stricmp(CTEXT(n), "<no anchor>"))
	{
		// if no anchor, set departure location to hyperspace
		departure_location = DepartureLocation::AT_LOCATION;
	}
	else
	{
		// find the anchor
		departure_anchor = anchor_t(ship_registry_get_index(CTEXT(n)));
	}
	n = CDR(n);

	// get departure path mask and delay
	eval_nums(n, is_nan, is_nan_forever, departure_mask, departure_delay);
	if (is_nan || is_nan_forever)
		return;

	// get warp effect
	show_warp = true;
	if (n >= 0)
	{
		show_warp = is_sexp_true(n);
		n = CDR(n);
	}

	// another flag
	adjust_warp_when_docked = true;
	if (n >= 0)
	{
		adjust_warp_when_docked = is_sexp_true(n);
		n = CDR(n);
	}

	// now set all that information depending on the first argument
	if (oswpt.type == OSWPT_TYPE_SHIP)
	{
		auto shipp = oswpt.shipp();

		shipp->departure_location = departure_location;
		shipp->departure_anchor = departure_anchor;
		shipp->departure_path_mask = departure_mask;
		shipp->departure_delay = departure_delay;

		shipp->flags.set(Ship::Ship_Flags::No_departure_warp, !show_warp);
		shipp->flags.set(Ship::Ship_Flags::Same_departure_warp_when_docked, !adjust_warp_when_docked);
	}
	else if (oswpt.type == OSWPT_TYPE_WING || oswpt.type == OSWPT_TYPE_WING_NOT_PRESENT)
	{
		auto wingp = oswpt.wingp();

		wingp->departure_location = departure_location;
		wingp->departure_anchor = departure_anchor;
		wingp->departure_path_mask = departure_mask;
		wingp->departure_delay = departure_delay;

		wingp->flags.set(Ship::Wing_Flags::No_departure_warp, !show_warp);
		wingp->flags.set(Ship::Wing_Flags::Same_departure_warp_when_docked, !adjust_warp_when_docked);
	}
	else if (oswpt.type == OSWPT_TYPE_PARSE_OBJECT)
	{
		auto p_objp = oswpt.p_objp();

		p_objp->departure_location = departure_location;
		p_objp->departure_anchor = departure_anchor;
		p_objp->departure_path_mask = departure_mask;
		p_objp->departure_delay = departure_delay;

		p_objp->flags.set(Mission::Parse_Object_Flags::SF_No_departure_warp, !show_warp);
		p_objp->flags.set(Mission::Parse_Object_Flags::SF_Same_departure_warp_when_docked, !adjust_warp_when_docked);
	}
}

// Goober5000
// set *all* the escort priorities of ships in escort list as follows: most damaged ship gets
// first priority in the argument list, next damaged gets next priority, etc.; if there are more
// ships than priorities, all remaining ships get the final priority on the list
// -- As indicated in the argument specification, there must be at least one argument but no more
// than MAX_COMPLETE_ESCORT_LIST arguments
void sexp_damage_escort_list_all(int n)
{
	typedef struct
	{
		int index;
		float hull;
	} my_escort_ship;

	std::array<int, MAX_COMPLETE_ESCORT_LIST> priority;
	my_escort_ship escort_ship[MAX_COMPLETE_ESCORT_LIST];
	int i, j, num_escort_ships, num_priorities, temp_i;
	bool is_nan, is_nan_forever;
	float temp_f;

	// build list of priorities
	num_priorities = eval_array(priority, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	// build custom list of escort ships
	num_escort_ships = 0;
	for (auto so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so))
	{
		auto objp = &Objects[so->objnum];
		if (objp->flags[Object::Object_Flags::Should_be_dead])
			continue;
		Assertion(objp->type == OBJ_SHIP, "All objects on Ship_obj_list must be ships!");

		// make sure it's on the escort list
		if ( !(Ships[objp->instance].flags[Ship::Ship_Flags::Escort]) )
			continue;

		// set index
		escort_ship[num_escort_ships].index = objp->instance;

		// calc and set hull integrity
		escort_ship[num_escort_ships].hull = get_hull_pct(objp);

		num_escort_ships++;
	}

	// sort it bubbly, lowest hull to highest hull
	for (i = 0; i < num_escort_ships; i++)
	{
		for (j = 0; j < i; j++)
		{
			if (escort_ship[i].hull < escort_ship[j].hull)
			{
				// swap
				temp_i = escort_ship[i].index;
				temp_f = escort_ship[i].hull;
				escort_ship[i].index = escort_ship[j].index;
				escort_ship[i].hull = escort_ship[j].hull;
				escort_ship[j].index = temp_i;
				escort_ship[j].hull = temp_f;
			}
		}
	}

	// loop through and assign priorities
	for (i = 0; i < num_escort_ships; i++)
	{
		if (i >= num_priorities)
			Ships[escort_ship[i].index].escort_priority = priority[num_priorities - 1];
		else
			Ships[escort_ship[i].index].escort_priority = priority[i];
	}

	// reorder the escort list
	hud_resort_escort_list();
}

void sexp_awacs_set_radius(int node)
{
	int radius;
	bool is_nan, is_nan_forever;

	// get the firing ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	// get the awacs subsystem
	auto awacs = ship_get_subsys(ship_entry->shipp(), CTEXT(CDR(node)));
	if (!awacs || !(awacs->system_info->flags[Model::Subsystem_Flags::Awacs]))
		return;

	// set the new awacs radius
	radius = eval_num(CDDR(node), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	awacs->awacs_radius = (float)radius;
}

// Goober5000
void sexp_primitive_sensors_set_range(int n)
{
	int range;
	bool is_nan, is_nan_forever;

	// get the ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	// set the new range
	range = eval_num(CDR(n), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	ship_entry->shipp()->primitive_sensor_range = range;
}

//*************************************************************************************************
// Kazan
// AutoNav/AutoPilot system SEXPS

//text: set-nav-carry
//text: unset-nav-carry
//args: 1+, Ship/Wing name
void set_unset_nav_carry_status(int node, bool set_it)
{
	for (int n = node; n >= 0; n = CDR(n))
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		switch (oswpt.type)
		{
			case OSWPT_TYPE_SHIP:
				oswpt.shipp()->flags.set(Ship::Ship_Flags::Navpoint_carry, set_it);
				break;

			case OSWPT_TYPE_WING:
			case OSWPT_TYPE_WING_NOT_PRESENT:
				oswpt.wingp()->flags.set(Ship::Wing_Flags::Nav_carry, set_it);
				break;

			default:
				break;
		}
	}
}

//text: set-nav-needslink
//text: unset-nav-needslink
//args: 1+, Ship name
void set_unset_nav_needslink(int node, bool set_it)
{
	for (int n = node; n >= 0; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp())
			continue;

		if (set_it)
			ship_entry->shipp()->flags.remove(Ship::Ship_Flags::Navpoint_carry);

		ship_entry->shipp()->flags.set(Ship::Ship_Flags::Navpoint_needslink, set_it);
	}
}

void add_nav_waypoint(object_ship_wing_point_team *oswpt, const char *nav, const char *WP_path, int vert)
{	
	bool add_for_this_player = true; 

	if (oswpt)
	{
		// we can't assume this nav should be visible to the player any more
		add_for_this_player = false;

		switch (oswpt->type)
		{
			case OSWPT_TYPE_WHOLE_TEAM:
				if (oswpt->team == Player_ship->team)
					add_for_this_player = true; 
				break;

			case OSWPT_TYPE_SHIP:
				if (oswpt->objnum == Player_ship->objnum)
					add_for_this_player = true; 
				break;

			case OSWPT_TYPE_WING:
				for (int i = 0; i < oswpt->wingp()->current_count; ++i)
					if (Ships[oswpt->wingp()->ship_index[i]].objnum == Player_ship->objnum)
						add_for_this_player = true; 
				break;

			// for all other oswpt types we simply ignore this
			default:
				break;
		}
	}
	
	AddNav_Waypoint(nav, WP_path, vert, add_for_this_player ? 0 : NP_HIDDEN);
}

//text: add-nav-waypoint
//args: 4, Nav Name, Waypoint Path Name, Waypoint Path point, ShipWingTeam
void add_nav_waypoint(int node)
{
	object_ship_wing_point_team oswpt, *oswptp = nullptr;
	bool is_nan, is_nan_forever;

	auto nav_name = CTEXT(node);
	node = CDR(node);
	auto way_name = CTEXT(node);
	node = CDR(node);

	int vert = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever) {
		return;
	}
	node = CDR(node);

	if (node >= 0) {
		eval_object_ship_wing_point_team(&oswpt, node);
		oswptp = &oswpt;
	}

	add_nav_waypoint(oswptp, nav_name, way_name, vert);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(nav_name);
	Current_sexp_network_packet.send_string(way_name);
	Current_sexp_network_packet.send_int(vert);
	if (oswptp) {
		Current_sexp_network_packet.send_string(oswptp->object_name);
	}
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_add_nav_waypoint()
{
	char nav_name[TOKEN_LENGTH];
	char way_name[TOKEN_LENGTH];
	char oswpt_name[TOKEN_LENGTH];
	int vert;
	object_ship_wing_point_team *oswptp = nullptr;

	if (!Current_sexp_network_packet.get_string(nav_name))
		return;
	if (!Current_sexp_network_packet.get_string(way_name))
		return;
	if (!Current_sexp_network_packet.get_int(vert))
		return;

	object_ship_wing_point_team oswpt;
	if (Current_sexp_network_packet.get_string(oswpt_name)) {
		eval_object_ship_wing_point_team(&oswpt, -1, oswpt_name);
		oswptp = &oswpt;
	}

	add_nav_waypoint(oswptp, nav_name, way_name, vert);
}

//text: add-nav-ship
//args: 2, Nav Name, Ship Name
void add_nav_ship(int node)
{
	auto nav_name = CTEXT(node);
	auto ship_name = CTEXT(CDR(node));
	AddNav_Ship(nav_name, ship_name, 0);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(nav_name);
	Current_sexp_network_packet.send_string(ship_name);
	Current_sexp_network_packet.end_callback();
}

void multi_add_nav_ship()
{
	char nav_name[TOKEN_LENGTH];
	char ship_name[TOKEN_LENGTH];

	if (!Current_sexp_network_packet.get_string(nav_name)) {
		return; 
	}
	
	if (!Current_sexp_network_packet.get_string(ship_name)) {
		return;
	}

	AddNav_Ship(nav_name, ship_name, 0);
}

//text: del-nav
//args: 1, Nav Name
void del_nav(int node)
{
	auto nav_name = CTEXT(node);
	DelNavPoint(nav_name);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(nav_name);
	Current_sexp_network_packet.end_callback();
}

void multi_del_nav()
{
	char nav_name[TOKEN_LENGTH];

	if (!Current_sexp_network_packet.get_string(nav_name)) {
		return; 
	}
	
	DelNavPoint(nav_name);
}

//text: use-nav-cinematics
//args: 1, boolean enable/disable
void set_use_ap_cinematics(int node)
{
	The_mission.flags.set(Mission::Mission_Flags::Use_ap_cinematics, is_sexp_true(node));
}

//text: use-autopilot
//args: 1, boolean enable/disable
void set_use_ap(int node)
{
	The_mission.flags.set(Mission::Mission_Flags::Deactivate_ap, !is_sexp_true(node));
}

//text: hide-nav
//text: unhide-nav
//args: 1, Nav Name
void hide_unhide_nav(int node, bool hide_it)
{
	auto nav_name = CTEXT(node);
	if (hide_it)
		Nav_Set_Hidden(nav_name);
	else
		Nav_UnSet_Hidden(nav_name);
}

//text: restrict-nav
//text: unrestrict-nav
//args: 1, nav name
void restrict_unrestrict_nav(int node, bool restrict_it)
{
	auto nav_name = CTEXT(node);
	if (restrict_it)
		Nav_Set_NoAccess(nav_name);
	else
		Nav_UnSet_NoAccess(nav_name);
}

//text: set-nav-visited
//text: unset-nav-visited
//args: 1, Nav Name
void set_unset_nav_visited(int node, bool set_it)
{
	auto nav_name = CTEXT(node);
	if (set_it)
		Nav_Set_Visited(nav_name);
	else
		Nav_UnSet_Visited(nav_name);
}

//text: is-nav-visited
//args: 1, Nav Name
//rets: true/false
int is_nav_visited(int node)
{
	auto nav_name = CTEXT(node);
	return IsVisited(nav_name);
}

//text: is-nav_linked
//args: 1, Ship name
//rets: true/false
int is_nav_linked(int node)
{
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	return ship_entry->shipp()->flags[Ship::Ship_Flags::Navpoint_carry] ? SEXP_TRUE : SEXP_FALSE;
}

//text: distance-to-nav
//args: 1, Nav Name
//rets: distance to nav
int distance_to_nav(int node)
{
	auto nav_name = CTEXT(node);
	return DistanceTo(nav_name);
}

void select_unselect_nav(int node, bool select_it)
{
	if (select_it)
	{
		auto nav_name = CTEXT(node);
		SelectNav(nav_name);
	}
	else
		DeselectNav();
}

void set_nav_color(int node, bool visited)
{
	std::array<ubyte, 3> rgb;
	bool is_nan, is_nan_forever;

	eval_array<ubyte>(rgb, node, is_nan, is_nan_forever, [](int num)->ubyte {
		CLAMP(num, 0, 255);
		return static_cast<ubyte>(num);
		});

	while (node >= 0)
	{
		auto nav_name = CTEXT(node);
		Nav_SetColor(nav_name, visited, rgb[0], rgb[1], rgb[2]);
		node = CDR(node);
	}
}

//*************************************************************************************************

int sexp_is_tagged(int node)
{
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	return ship_is_tagged(ship_entry->objp()) ? SEXP_TRUE : SEXP_FALSE;
}

// Joint effort of Sesquipedalian and Goober5000.  Sesq found the code, mucked around making
// sexps with it and learned things, Goober taught Sesq and made the sexp work properly. =D
// Returns true so long as the player has held a missile lock for the specified time.
// If the optional ship and/or ship's subsystem are specified, returns true when that
// has been locked onto, but otherwise returns as long as anything has been locked onto.
int sexp_missile_locked(int node)
{
	int z;
	bool is_nan, is_nan_forever;

	// if we aren't targeting anything, it's false
	if ((Players_target == -1) || (Players_target == UNINITIALIZED))
		return SEXP_FALSE;

	// if we aren't locked on to anything, it's false
	if (!Players_mlocked)
		return SEXP_FALSE;

	// do we have a specific ship?
	if (CDR(node) != -1)
	{
		// if we're not targeting the specific ship, it's false
		if (stricmp(Ships[Objects[Players_target].instance].ship_name, CTEXT(CDR(node))) != 0)
			return SEXP_FALSE;

		// do we have a specific subsystem?
		if (CDR(CDR(node)) != -1)
		{
			// if we aren't targeting a subsystem at all, it's false
			if (!Player_ai->targeted_subsys)
				return SEXP_FALSE;

			// if we're not targeting the specific subsystem, it's false
			if (subsystem_stricmp(Player_ai->targeted_subsys->system_info->subobj_name, CTEXT(CDR(CDR(node)))))
				return SEXP_FALSE;
		}
	}

	// if we've gotten this far, we must have satisfied whatever conditions the sexp imposed
	// finally, test if we've locked for a certain period of time
	z = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;

	if (!Players_mlocked_timestamp.isValid() || timestamp_since(Players_mlocked_timestamp) < z * MILLISECONDS_PER_SECOND)
		return SEXP_FALSE;

	return SEXP_TRUE;
}

int sexp_is_player(int node)
{
	bool standard_check = is_sexp_true(node);
	node = CDR(node);

	while (node >= 0) {
		auto player = get_player_from_ship_node(node, true);
		if (player) {
			// in single-player mode, maybe check for AI-controlled player ships
			if (!(Game_mode & GM_MULTIPLAYER) && standard_check && Player_use_ai) {
				return SEXP_FALSE;
			}
		} else {
			// ship is not a player
			return SEXP_FALSE;
		}

		node = CDR(node);
	}

	// if we reached this far they all checked out
	return SEXP_TRUE;
}

void sexp_set_respawns(int node)
{
	int num_respawns;
	bool is_nan, is_nan_forever;

	// we're wasting our time if you can't respawn
	if (!(Game_mode & GM_MULTIPLAYER)) {
		return;
	}

	num_respawns = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever) {
		return;
	}
	node = CDR(node);

	// send the information to clients
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(num_respawns); 

	for ( ; node >= 0; node = CDR(node)) {
		// get the parse object for the ship
		auto ship_entry = eval_ship(node);
		if (!ship_entry || !ship_entry->has_p_objp()) {
			continue;
		}

		ship_entry->p_objp()->respawn_count = num_respawns;
		Current_sexp_network_packet.send_parse_object(ship_entry->p_objp()); 
	}
	
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_respawns()
{
	int num_respawns;
	p_object *p_objp;

	Current_sexp_network_packet.get_int(num_respawns);

	while (Current_sexp_network_packet.get_parse_object(p_objp)) {
		p_objp->respawn_count = num_respawns;
	}
}

/**
 * get a hotkey for a one ship or wing
 */
int sexp_get_hotkey(int node)
{
	object_ship_wing_point_team oswpt;
	eval_object_ship_wing_point_team(&oswpt, node);
	int hotkey;

	// returns the hotkey of the ship or wing
	// if argument is not ship or wing returns -1
	if (oswpt.type == OSWPT_TYPE_SHIP) {
		hotkey = oswpt.shipp()->hotkey;
	} else if (oswpt.type == OSWPT_TYPE_WING) {
		hotkey = oswpt.wingp()->hotkey;
	} else {
		hotkey = -1;
	}

	return hotkey;
}

/**
 * set a hotkey for one or more ships/wings
 */
void sexp_add_remove_hotkey(int node)
{
	int objnum, setnum, n = node;
	bool is_adding, is_nan, is_nan_forever;

	// True for add, False for remove
	is_adding = is_sexp_true(n);
	n = CDR(n);

	setnum = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever) {
		return;
	}
	n = CDR(n);
	
	// first, only proceed if setnum number is a valid hotkey
	// look for ship name -- if found, then add or remove hotkey to ship
	// if not a ship, look for wing name, then add or remove hotkey to each ship in wing
	// note, hud_target_hotkey_add_remove() checks if the object has arrived or is destroyed or departed
	if (setnum >= 0 && setnum < MAX_KEYED_TARGETS) {
		for (; n != -1; n = CDR(n)) {
			object_ship_wing_point_team oswpt;
			eval_object_ship_wing_point_team(&oswpt, n);
			if (oswpt.type == OSWPT_TYPE_SHIP) {
				objnum = oswpt.objnum;
				// check if the ship already has this hot-key and if sexp is adding or removing it
				if (((oswpt.shipp()->hotkey == setnum) && !is_adding) ||
					((oswpt.shipp()->hotkey != setnum) && is_adding) ) {
					hud_target_hotkey_add_remove(setnum, &Objects[objnum], HOTKEY_USER_ADDED);
				}
			}
			else if (oswpt.type == OSWPT_TYPE_WING) {
				for (int i = 0; i < oswpt.wingp()->current_count; i++) {
					auto shipp = &Ships[oswpt.wingp()->ship_index[i]];
					objnum = shipp->objnum;
					// check if the ship already has this hot-key and if sexp is adding or removing it
					if (((shipp->hotkey == setnum) && !is_adding) ||
						((shipp->hotkey != setnum) && is_adding)) {
						hud_target_hotkey_add_remove(setnum, &Objects[objnum], HOTKEY_USER_ADDED);
					}
				}
			}
		}
	}
}

// helper function for the clear-weapons and clear-debris SEXPs
void actually_clear_weapons_or_debris(int op_num, int class_index)
{
	for (auto objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp))
	{
		if (objp->flags[Object::Object_Flags::Should_be_dead])
			continue;

		if (op_num == OP_CLEAR_WEAPONS && objp->type != OBJ_WEAPON)
			continue;
		if (op_num == OP_CLEAR_DEBRIS && objp->type != OBJ_DEBRIS)
			continue;

		if (class_index >= 0)
		{
			// weapon doesn't match the class
			if (op_num == OP_CLEAR_WEAPONS && (Weapons[objp->instance].weapon_info_index != class_index))
				continue;

			// debris doesn't match the class
			if (op_num == OP_CLEAR_DEBRIS && (Debris[objp->instance].ship_info_index != class_index))
				continue;
		}

		objp->flags.set(Object::Object_Flags::Should_be_dead);
	}
}

void sexp_clear_weapons_or_debris(int node, int op_num)
{ 
	int class_index = -1;

	// if we have the optional argument, read it in
	if (node >= 0) {
		if (op_num == OP_CLEAR_WEAPONS) {
			class_index = weapon_info_lookup(CTEXT(node));
			if (class_index == -1) {
				Warning(LOCATION, "Clear-weapons attempted to remove %s. Weapon class not found. Clear-weapons will remove all weapons currently in the mission\n", CTEXT(node));
			}
		} else if (op_num == OP_CLEAR_DEBRIS) {
			class_index = ship_info_lookup(CTEXT(node));
			if (class_index == -1) {
				Warning(LOCATION, "Clear-debris attempted to remove %s. Ship class not found. Clear-debris will remove all debris currently in the mission\n", CTEXT(node));
			}
		}
	}

	actually_clear_weapons_or_debris(op_num, class_index);
	
	// send the information to clients
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(class_index); 
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_clear_weapons_or_debris(int op_num)
{
	int class_index = -1; 

	Current_sexp_network_packet.get_int(class_index);
	
	actually_clear_weapons_or_debris(op_num, class_index);
}

int sexp_return_player_data(int node, int type)
{
	int np_index;
	auto p = get_player_from_ship_node(node, true, &np_index);

	// now, if we have a valid player, return his kills
	if (p) {
		switch (type) {
			case OP_NUM_KILLS:
				return p->stats.m_kill_count_ok;

			case OP_NUM_ASSISTS:
				return p->stats.m_assists;

			case OP_SHIP_SCORE: 
				return p->stats.m_score;

			case OP_SHIP_DEATHS: 
				return p->stats.m_player_deaths;

			case OP_RESPAWNS_LEFT:
				// Dogfight missions have no respawn limit
				if (MULTI_NOT_DOGFIGHT) {
					if ( Net_players[np_index].flags & NETINFO_FLAG_RESPAWNING ) {						
						// since the player hasn't actually respawned yet he hasn't used up a number or spawns equal to his deaths
						// so add an extra life back. 
						return Netgame.respawn - p->stats.m_player_deaths + 1;
					}
					else {
						return Netgame.respawn - p->stats.m_player_deaths;
					}
				}
				break;

			default:
				Error(LOCATION, "return-player-data was called with invalid type %d on node %d!", type, node);
		}
	}
	// AI ships also have a respawn count so we can return valid data for that at least
	else if ( (Game_mode & GM_MULTIPLAYER) && (type == OP_SHIP_DEATHS || type == OP_RESPAWNS_LEFT ) ) {
		auto ship_entry = eval_ship(node);
		if (!ship_entry || !ship_entry->has_p_objp()) {
			return 0;
		}

		if (ship_entry->p_objp()->flags[Mission::Parse_Object_Flags::OF_Player_start]) {
			switch (type) {				
				case OP_SHIP_DEATHS: 
					// when an AI ship is finally killed its respawn count won't be updated so get the number of deaths 
					// from the log instead
					return mission_log_get_count(LOG_SHIP_DESTROYED, ship_entry->p_objp()->name, nullptr) + mission_log_get_count(LOG_SELF_DESTRUCTED, ship_entry->p_objp()->name, nullptr);
					
				case OP_RESPAWNS_LEFT:
					return Netgame.respawn - ship_entry->p_objp()->respawn_count;

				default:
					// We should never reach this.
					Assert(false);
			}
		}
	}

	// AI ships 
	return 0;
}

int sexp_num_type_kills(int node)
{
	auto p = get_player_from_ship_node(node, true);
	if (!p)
		return 0;

	// look up ship type name
	auto name = CTEXT(CDR(node));
	int constraint_type = ship_type_name_lookup(name);
	if (constraint_type < 0)
	{
		if (Fighter_bomber_valid && !stricmp(name, Fighter_bomber_type_name))
			constraint_type = Ship_type_fighter_bomber;
		else
			return 0;
	}

	// look stuff up	
	int total = 0;
	for (int idx = 0; idx < ship_info_size(); idx++)
	{
		if (p->stats.m_okKills[idx] <= 0)
			continue;

		int ship_type = ship_class_query_general_type(idx);

		if (constraint_type == Ship_type_fighter_bomber)
		{
			if (ship_type != Ship_type_fighter && ship_type != Ship_type_bomber)
				continue;
		}
		else
		{
			if (ship_type != constraint_type)
				continue;
		}

		total += p->stats.m_okKills[idx];
	}

	// total
	return total;
}

int sexp_num_class_kills(int node)
{
	auto p = get_player_from_ship_node(node, true);
	if (!p) {
		return 0;
	}

	// get the ship type we're looking for
	int si_index = ship_info_lookup(CTEXT(CDR(node)));
	if ((si_index < 0) || (si_index >= ship_info_size())) {
		return 0;
	}

	// return the count	
	return p->stats.m_okKills[si_index];
}

void sexp_subsys_set_random(int node)
{
	int low, high, n = node, idx, rand, exclusion_list[MAX_MODEL_SUBSYSTEMS];
	bool is_nan, is_nan_forever;
	ship_subsys *subsys;
	ship *shipp;

	// get ship
	auto ship_entry = eval_ship(n);
	if (!ship_entry || !ship_entry->has_shipp()) {
		return;
	}
	shipp = ship_entry->shipp();
	n = CDR(n);

	// get low and high
	eval_nums(n, is_nan, is_nan_forever, low, high);
	if (is_nan || is_nan_forever) {
		return;
	}
	if (low < 0) {
		low = 0;
	}
	if (high > 100) {
		high = 100;
	}
	if (low > high) {
		Warning(LOCATION, "subsys-set-random was passed an invalid range (%d ... %d)!", low, high);
		return;
	}

	// init exclusion list
	memset(exclusion_list, 0, sizeof(int) * Ship_info[shipp->ship_info_index].n_subsystems);

	// get exclusion list
	while( n != -1) {
		int exclude_index = ship_find_subsys(shipp, CTEXT(n));
		if (exclude_index >= 0) {
			exclusion_list[exclude_index] = 1;
		}

		n = CDR(n);
	}

	// apply to all others
	for (idx=0; idx<Ship_info[shipp->ship_info_index].n_subsystems; idx++) {
		if ( exclusion_list[idx] == 0 ) {
			// get non excluded subsystem
			subsys = ship_get_indexed_subsys(shipp, idx);
			if (subsys == nullptr) {
				nprintf(("Warning", "Nonexistent subsystem for index %d on ship %s for subsys set random\n", idx, shipp->ship_name));
				continue;
			}

			// randomize its hit points
			rand = rand_internal(low, high);
			set_subsys_strength_and_maybe_ancestors(shipp, subsys, nullptr, &rand, nullptr, nullptr, true, true);
		}
	}

	// needed to keep aggregate info correct
	ship_recalc_subsys_strength(shipp);
}

void sexp_supernova_start(int node)
{
	bool is_nan, is_nan_forever;
	int countdown = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	supernova_start(countdown);
}

void sexp_supernova_stop(int  /*node*/)
{
	supernova_stop();
}

int sexp_get_supernova_stage(int /*node*/)
{
	return static_cast<int>(supernova_stage());
}

int sexp_is_weapon_selected(int node, bool primary)
{
	int bank, num_banks;
	bool is_nan, is_nan_forever;

	// lookup ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	num_banks = primary ? ship_entry->shipp()->weapons.num_primary_banks : ship_entry->shipp()->weapons.num_secondary_banks;

	// bogus value?
	bank = eval_num(CDR(node), is_nan, is_nan_forever);
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	if (is_nan || bank >= num_banks)
		return SEXP_FALSE;

	// is this the bank currently selected
	if (primary)
	{
		if (ship_entry->shipp()->flags[Ship::Ship_Flags::Primary_linked])
			return SEXP_TRUE;
		if (bank == ship_entry->shipp()->weapons.current_primary_bank)
			return SEXP_TRUE;
	}
	else
	{
		if (bank == ship_entry->shipp()->weapons.current_secondary_bank)
			return SEXP_TRUE;
	}

	// nope
	return SEXP_FALSE;
}

//	Return SEXP_TRUE if quadrant quadnum is near max.
int shield_quad_near_max(int quadnum)
{
	int n_quadrants = static_cast<int>(Player_obj->shield_quadrant.size());
	if (quadnum < 0 || quadnum >= n_quadrants)
		return SEXP_FALSE;

	float	remaining = 0.0f;
	for (int i=0; i<n_quadrants; i++) {
		if (i == quadnum){
			continue;
		}
		remaining += Player_obj->shield_quadrant[i];
	}

	if ((remaining < 2.0f) || (Player_obj->shield_quadrant[quadnum] > shield_get_max_quad(Player_obj) - 5.0f)) {
		return SEXP_TRUE;
	} else {
		return SEXP_FALSE;
	}
}

//	Return truth value for special SEXP.
//	Used in training#5, perhaps in other missions.
int process_special_sexps(int index)
{
	switch (index) {
	case 0:	//	Ship "Freighter 1" is aspect locked by player.
		if (Player_ai->target_objnum != -1) {
			if (!(stricmp(Ships[Objects[Player_ai->target_objnum].instance].ship_name, "Freighter 1"))) {
				if (Player_ai->current_target_is_locked)
					return SEXP_TRUE;
			}
		}
		return SEXP_FALSE;

	case 1:	//	Fired Interceptors
		for (auto mop: list_range(&Missile_obj_list)) {
			auto objp = &Objects[mop->objnum];
			if (objp->flags[Object::Object_Flags::Should_be_dead])
				continue;

			if (objp->type == OBJ_WEAPON) {
				if (!stricmp(Weapon_info[Weapons[objp->instance].weapon_info_index].name, "Interceptor#weak")) {
					int target = Weapons[objp->instance].target_num;
					if (target != -1) {
						if (Objects[target].type == OBJ_SHIP) {
							if (!(stricmp(Ships[Objects[target].instance].ship_name, "Freighter 1")))
								return SEXP_TRUE;
						}
					}
				}
			}
		}
		return SEXP_FALSE;

	case 2:	//	Ship "Freighter 1", subsystem "Weapons" is aspect locked by player.
		if (Player_ai->target_objnum != -1) {
			if (!(stricmp(Ships[Objects[Player_ai->target_objnum].instance].ship_name, "Freighter 1"))) {
				if (!(subsystem_stricmp(Player_ai->targeted_subsys->system_info->name, "Weapons"))) {
					if (Player_ai->current_target_is_locked){
						return SEXP_TRUE;
					}
				}
			}
		}
		return SEXP_FALSE;

	case 3:	//	Player ship suffering shield damage on front.
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			shield_apply_damage(Player_obj, FRONT_QUAD, 10.0f);
			hud_shield_quadrant_hit(Player_obj, FRONT_QUAD);
			return SEXP_TRUE;
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		break;

	case 4:	//	Player ship suffering much damage.
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			nprintf(("AI", "Frame %i\n", Framecount));
			shield_apply_damage(Player_obj, FRONT_QUAD, 10.0f);
			hud_shield_quadrant_hit(Player_obj, FRONT_QUAD);
			if (Player_obj->shield_quadrant[FRONT_QUAD] < 2.0f)
				return SEXP_TRUE;
			else
				return SEXP_FALSE;
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		break;

	case 5:	//	Player's shield is quick repaired
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			nprintf(("AI", "Frame %i, recharged to %7.3f\n", Framecount, Player_obj->shield_quadrant[FRONT_QUAD]));

			shield_apply_damage(Player_obj, FRONT_QUAD, -flFrametime*200.0f);

			if (Player_obj->shield_quadrant[FRONT_QUAD] > shield_get_max_quad(Player_obj))
				Player_obj->shield_quadrant[FRONT_QUAD] = shield_get_max_quad(Player_obj);

			if (Player_obj->shield_quadrant[FRONT_QUAD] > Player_obj->shield_quadrant[(FRONT_QUAD+1)%DEFAULT_SHIELD_SECTIONS] - 2.0f)
				return SEXP_TRUE;
			else
				return SEXP_FALSE;
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		break;

	case 6:	//	3 of player's shield quadrants are reduced to 0.
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			Player_obj->shield_quadrant[1] = 1.0f;
			Player_obj->shield_quadrant[2] = 1.0f;
			Player_obj->shield_quadrant[3] = 1.0f;
			hud_shield_quadrant_hit(Player_obj, FRONT_QUAD);
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		return SEXP_TRUE;

	case 7:	//	Make sure front quadrant has been maximized, or close to it.
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			if (shield_quad_near_max(FRONT_QUAD)) return SEXP_TRUE; else return SEXP_FALSE;
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		break;

	case 8:	//	Make sure rear quadrant has been maximized, or close to it.
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			if (shield_quad_near_max(REAR_QUAD)) return SEXP_TRUE; else return SEXP_FALSE;
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		break;
	
	case 9:	//	Zero left and right quadrants in preparation for maximizing rear quadrant.
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			Player_obj->shield_quadrant[LEFT_QUAD] = 0.0f;
			Player_obj->shield_quadrant[RIGHT_QUAD] = 0.0f;
			hud_shield_quadrant_hit(Player_obj, LEFT_QUAD);
			return SEXP_TRUE;
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		break;

	case 10:	//	Return true if player is low on Interceptors.
		if (Player_ship->weapons.secondary_bank_ammo[0] + Player_ship->weapons.secondary_bank_ammo[1] < 8)
			return SEXP_TRUE;
		else
			return SEXP_FALSE;
		break;

	case 11:	//	Return true if player has plenty of Interceptors.
		if (Player_ship->weapons.secondary_bank_ammo[0] + Player_ship->weapons.secondary_bank_ammo[1] >= 8)
			return SEXP_TRUE;
		else
			return SEXP_FALSE;
		break;

	case 12:	//	Return true if player is low on Interceptors.
		if (Player_ship->weapons.secondary_bank_ammo[0] + Player_ship->weapons.secondary_bank_ammo[1] < 4)
			return SEXP_TRUE;
		else
			return SEXP_FALSE;
		break;

	case 13:	// Zero front shield quadrant.  Added for Jim Boone on August 26, 1999 by MK.
		if (!(Ship_info[Player_ship->ship_info_index].flags[Ship::Info_Flags::Model_point_shields])) {
			Player_obj->shield_quadrant[FRONT_QUAD] = 0.0f;
			hud_shield_quadrant_hit(Player_obj, FRONT_QUAD);
			return SEXP_TRUE;
		} else {
			nprintf(("Warning", "Shield-related Special-check SEXPs do not work on ship %s because it uses model point shields.\n", Player_ship->ship_name));
			return SEXP_FALSE;
		}
		break;

	case 100:	//	Return true if player is out of countermeasures.
		if (Player_ship->cmeasure_count <= 0)
			return SEXP_TRUE;
		else
			return SEXP_FALSE;

	default:
		Assertion(false, "Special sexp processing code was called for an unsupported node type!");
	}

	return SEXP_FALSE;
}

// Karajorma / Goober5000
int sexp_string_to_int(int n)
{
	bool first_ch = true;
	char *buf_ch, buf[TOKEN_LENGTH];
	Assert (n != -1);

	if (Sexp_nodes[n].cache)
		return Sexp_nodes[n].cache->numeric_literal;

	// maybe forward to a special-arg node
	if (Sexp_nodes[n].flags & SNF_SPECIAL_ARG_IN_NODE)
	{
		auto current_argument = Sexp_replacement_arguments.back();
		int arg_node = current_argument.second;

		if (arg_node >= 0)
			return sexp_string_to_int(arg_node);
	}

	// copy all numeric characters to buf
	// also, copy a sign symbol if we haven't copied numbers yet
	buf_ch = buf;
	for (auto ch = CTEXT(n); *ch != 0; ch++)
	{
		if ((first_ch && (*ch == '-' || *ch == '+')) || strchr("0123456789", *ch))
		{
			*buf_ch = *ch;
			buf_ch++;

			first_ch = false;
		}

		// don't save the fractional parts of decimal numbers
		if (*ch == '.')
			break;
	}

	// terminate string
	*buf_ch = '\0';

	int num = atoi(buf);

	// cache the value if it can't change later
	if (!is_node_value_dynamic(n))
		Sexp_nodes[n].cache = new sexp_cached_data(OPF_NUMBER, num, -1);

	return num;
}

// Goober5000
void sexp_int_to_string(int n)
{
	int i, sexp_variable_index;
	bool is_nan, is_nan_forever;
	char new_text[TOKEN_LENGTH];

	// Only do single player or multi host
	if (MULTIPLAYER_CLIENT)
		return;

	i = eval_num(n, is_nan, is_nan_forever);
	n = CDR(n);

	// get sexp_variable index
	sexp_variable_index = sexp_get_variable_index(n);
	if (sexp_variable_index < 0)
	{
		Warning(LOCATION, "int-to-string: Variable %s does not exist!", Sexp_nodes[n].text);
		return;
	}

	// check variable type
	if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING))
	{
		Warning(LOCATION, "int-to-string: Cannot assign a string to a non-string variable %s!", Sexp_variables[sexp_variable_index].variable_name);
		return;
	}

	// write string
	if (is_nan || is_nan_forever)
		sprintf(new_text, "NaN");
	else
		sprintf(new_text, "%d", i);

	// assign to variable
	sexp_modify_variable(new_text, sexp_variable_index);
}

// Goober5000
void sexp_string_concatenate(int n)
{
	int sexp_variable_index;
	char new_text[TOKEN_LENGTH * 2];

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	auto str1 = CTEXT(n);
	n = CDR(n);
	auto str2 = CTEXT(n);
	n = CDR(n);

	// get sexp_variable index
	sexp_variable_index = sexp_get_variable_index(n);
	if (sexp_variable_index < 0)
	{
		Warning(LOCATION, "string-concatenate: Variable %s does not exist!", Sexp_nodes[n].text);
		return;
	}

	// check variable type
	if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING))
	{
		Warning(LOCATION, "string-concatenate: Cannot assign a string to a non-string variable %s!", Sexp_variables[sexp_variable_index].variable_name);
		return;
	}

	// concatenate strings
	strcpy_s(new_text, str1);
	strcat_s(new_text, str2);

	// check length
	if (strlen(new_text) >= TOKEN_LENGTH)
	{
		Warning(LOCATION, "string-concatenate: Concatenated string '%s' has " SIZE_T_ARG " characters, but the maximum is %d.  The string will be truncated.", new_text, strlen(new_text), TOKEN_LENGTH - 1);
		new_text[TOKEN_LENGTH] = 0;
	}

	// assign to variable
	sexp_modify_variable(new_text, sexp_variable_index);
}

// Goober5000
void sexp_string_concatenate_block(int n)
{
	int sexp_variable_index;
	SCP_string new_text;

	// Only do single player or multi host
	if (MULTIPLAYER_CLIENT)
		return;

	// get sexp_variable index
	sexp_variable_index = sexp_get_variable_index(n);
	if (sexp_variable_index < 0)
	{
		Warning(LOCATION, "string-concatenate-block: Variable %s does not exist!", Sexp_nodes[n].text);
		return;
	}
	n = CDR(n);

	// check variable type
	if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING))
	{
		Warning(LOCATION, "string-concatenate-block: Cannot assign a string to a non-string variable %s!", Sexp_variables[sexp_variable_index].variable_name);
		return;
	}

	// concatenate strings
	while (n >= 0)
	{
		new_text.append(CTEXT(n));
		n = CDR(n);
	}

	// check length
	if (new_text.length() >= TOKEN_LENGTH)
	{
		Warning(LOCATION, "string-concatenate-block: Concatenated string '%s' has " SIZE_T_ARG " characters, but the maximum is %d.  The string will be truncated.", new_text.c_str(), new_text.length(), TOKEN_LENGTH - 1);
		new_text.resize(TOKEN_LENGTH - 1);
	}

	// assign to variable
	sexp_modify_variable(new_text.c_str(), sexp_variable_index);
}

// Goober5000
int sexp_string_get_length(int node)
{
	auto text = CTEXT(node);
	return (int)unicode::num_codepoints(text, text + strlen(text));
}

// Goober5000
void sexp_string_get_substring(int node)
{
	int n = node, pos, len, sexp_variable_index;
	bool is_nan, is_nan_forever;
	char new_text[TOKEN_LENGTH];
	memset(new_text, 0, TOKEN_LENGTH);

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	auto parent = CTEXT(n);
	n = CDR(n);

	eval_nums(n, is_nan, is_nan_forever, pos, len);

	// get sexp_variable index
	sexp_variable_index = sexp_get_variable_index(n);
	if (sexp_variable_index < 0)
	{
		Warning(LOCATION, "string-get-substring: Variable %s does not exist!", Sexp_nodes[n].text);
		return;
	}

	// check variable type
	if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING))
	{
		Warning(LOCATION, "string-get-substring: Cannot assign a string to a non-string variable %s!", Sexp_variables[sexp_variable_index].variable_name);
		return;
	}

	// now actually get the substring
	if (!is_nan && !is_nan_forever)
	{
		auto parent_byte_len = strlen(parent);
		auto parent_end = parent + parent_byte_len;
		auto parent_len = (int)unicode::num_codepoints(parent, parent_end);

		// sanity
		if (pos >= parent_len)
		{
			Warning(LOCATION, "( string-get-substring %s %d %d ) failed: starting position is larger than the string length!", parent, pos, len);
			return;
		}

		// sanity
		if (pos + len > parent_len)
			len = parent_len - pos;

		// copy substring
		auto start_ptr = parent;
		// Advance the pointer by n codepoints to the start of our substring
		unicode::advance(start_ptr, static_cast<size_t>(pos), parent_end);

		auto end_ptr = start_ptr;
		unicode::advance(end_ptr, static_cast<size_t>(len), parent_end);

		auto byte_diff = end_ptr - start_ptr;

		strncpy(new_text, start_ptr, byte_diff);
	}

	// assign to variable
	sexp_modify_variable(new_text, sexp_variable_index);
}

// Goober5000
void sexp_string_set_substring(int node)
{
	int n = node, pos, len, sexp_variable_index;
	bool is_nan, is_nan_forever;

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	auto parent = CTEXT(n);
	n = CDR(n);

	eval_nums(n, is_nan, is_nan_forever, pos, len);
	if (is_nan || is_nan_forever)
		return;

	auto new_substring = CTEXT(n);
	n = CDR(n);

	// get sexp_variable index
	sexp_variable_index = sexp_get_variable_index(n);
	if (sexp_variable_index < 0)
	{
		Warning(LOCATION, "string-set-substring: Variable %s does not exist!", Sexp_nodes[n].text);
		return;
	}

	// check variable type
	if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING))
	{
		Warning(LOCATION, "string-set-substring: Cannot assign a string to a non-string variable %s!", Sexp_variables[sexp_variable_index].variable_name);
		return;
	}

	int parent_byte_len = (int)strlen(parent);

	auto parent_len = (int)unicode::num_codepoints(parent, parent + parent_byte_len);

	// sanity
	if (pos >= parent_len)
	{
		Warning(LOCATION, "( string-set-substring %s %d %d %s ) failed: starting position is larger than the string length!", parent, pos, len, new_substring);
		return;
	}

	SCP_string new_text = parent;

	auto range = unicode::codepoint_range(parent);
	auto end_iter = range.end();

	size_t substring_begin_byte = 0;
	size_t substring_end_byte = 0;
	auto i = 0;
	for (auto iter = range.begin(); iter != end_iter; ++iter, ++i) {
		if (i == pos) {
			substring_begin_byte = iter.pos() - parent;
		} else if (i == pos + len) {
			substring_end_byte = iter.pos() - parent;
			// We have reached the end byte so we have done everything we need to
			break;
		}
	}

	// This shouldn't happen
	Assertion(substring_begin_byte < substring_end_byte,
			  "string-set-substring: The begin position of the substring must be less than the end position!");

	new_text.replace(substring_begin_byte, substring_end_byte - substring_begin_byte, new_substring);

	if (new_text.size() >= TOKEN_LENGTH) {
		Warning(LOCATION, "string-set-substring: Concatenated string is too long and will be truncated.");

		new_text.resize(TOKEN_LENGTH - 1);

		// This might have broken the UTF-8 sequence so that needs to be fixed in Unicode mode
		if (Unicode_text_mode) {
			auto invalid = utf8::find_invalid(new_text.begin(), new_text.end());

			if (invalid != new_text.end()) {
				// Found an invalid sequence. End the string right before that to make sure the string is still valid
				new_text.erase(invalid, new_text.end());
			}
		}
	}

	// assign to variable
	sexp_modify_variable(new_text.c_str(), sexp_variable_index);
}

void sexp_modify_variable_xstr(int n)
{
	Assert(n >= 0);

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	// get sexp_variable index
	auto sexp_variable_index = sexp_get_variable_index(n);
	if (sexp_variable_index < 0)
	{
		Warning(LOCATION, "modify-variable-xstr: Variable %s does not exist!", Sexp_nodes[n].text);
		return;
	}
	n = CDR(n);

	if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING)) {
		Warning(LOCATION, "modify-variable-xstr: Variable %s must be a string variable!", Sexp_variables[sexp_variable_index].variable_name);
		return;
	}

	// get new string
	const char* new_text = Sexp_nodes[n].text;

	// assign to variable
	sexp_modify_variable(new_text, sexp_variable_index);
}

// breaks into the code or sends a warning
void sexp_debug(int node)
{
	int i;
	SCP_string warning_message;

	#ifdef NDEBUG
	bool no_release_message = is_sexp_true(node); 
	#endif

	node = CDR(node); 
	Assertion (node >= 0, "No message defined in debug SEXP");

	// we'll suppose it's the string for now
	warning_message = CTEXT(node);

	// but use an actual message if one exists
	for (i=0; i<Num_messages; i++) {
		// find the message
		if ( !stricmp(Messages[i].name, warning_message.c_str()) ) {
			warning_message = Messages[i].message;
			break;
		}
	}

	// replace variables and container references if necessary
	sexp_replace_variable_names_with_values(warning_message);
	sexp_container_replace_refs_with_values(warning_message);

	//send the message
	#ifndef NDEBUG
		Warning(LOCATION, "%s", warning_message.c_str());
	#else
	if (!no_release_message) {	
		ReleaseWarning(LOCATION, "%s", warning_message.c_str());
	}
	#endif
}

// custom sexp operator for handling misc training stuff
int sexp_special_training_check(int node)
{
	int num, rtn;
	bool is_nan, is_nan_forever;

	num = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	if (num == SPECIAL_CHECK_TRAINING_FAILURE)
		return Training_failure ? SEXP_TRUE : SEXP_FALSE;

	// To MK: do whatever you want with this number here.
	rtn = process_special_sexps(num);

	return rtn;
}

// sexpression to flash a hud gauge.  gauge name is text value of node
void sexp_flash_hud_gauge( int node )
{
	auto name = CTEXT(node);
	bool match = false;

	// see if this is specified the new way, according to the HUD type #define
	int type = hud_gauge_type_lookup(name);

	// now go through and find out which gauge (index i in the legacy list) to flash
	for (int i = 0; i < NUM_HUD_GAUGES; ++i) {
		if (type < 0) {
			if (stricmp(name, Legacy_HUD_gauges[i].hud_gauge_text) == 0) {
				match = true;
			}
		} else if (type == Legacy_HUD_gauges[i].hud_gauge_type) {
			match = true;
		}

		if (match) {
			hud_gauge_start_flash(i);	// call HUD function to flash gauge

			Current_sexp_network_packet.start_callback();
			Current_sexp_network_packet.send_int(i);
			Current_sexp_network_packet.end_callback();

			break;
		}
	}

	if (!match && type >= 0) {
		Warning(LOCATION, "HUD gauge '%s' is not a legacy gauge; flashing is not supported", name);
	}
}

void multi_sexp_flash_hud_gauge()
{
	int i; 

	if (Current_sexp_network_packet.get_int(i)) {
		hud_gauge_start_flash(i);
	}
}

void sexp_set_training_context_fly_path(int node)
{
	bool is_nan, is_nan_forever;

	int wp_list = find_matching_waypoint_list_index(CTEXT(node));
	if (wp_list < 0)
		return;

	int distance = eval_num(CDR(node), is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	Training_context |= TRAINING_CONTEXT_FLY_PATH;
	Training_context_waypoint_path = wp_list;
	Training_context_distance = static_cast<float>(distance);
	Training_context_goal_waypoint = 0;
	Training_context_at_waypoint = -1;
}

void sexp_set_training_context_speed(int node)
{
	int min, max;
	bool is_nan, is_nan_forever;

	eval_nums(node, is_nan, is_nan_forever, min, max);
	if (is_nan || is_nan_forever)
		return;

	Training_context |= TRAINING_CONTEXT_SPEED;
	Training_context_speed_min = min;
	Training_context_speed_max = max;
	Training_context_speed_set = 0;
}

void sexp_scramble_messages(int node, bool scramble)
{
	if (node < 0)
	{
		// scramble messages on player ship... this isn't multi compatible, but neither was the old version of the sexp
		Player_ship->flags.set(Ship::Ship_Flags::Scramble_messages, scramble);
		return;
	}

	sexp_deal_with_ship_flag(node, true, Object::Object_Flags::NUM_VALUES, Ship::Ship_Flags::Scramble_messages, Mission::Parse_Object_Flags::SF_Scramble_messages, scramble, false);
}

void toggle_cutscene_bars(float delta_speed, int set) 
{
	//Do we want the bars?
	if (set) {
		Cutscene_bar_flags |= CUB_CUTSCENE;
	}
	else {
		Cutscene_bar_flags &= ~CUB_CUTSCENE;
	}

	if(delta_speed > 0.0f) {
		Cutscene_bars_progress = 0.0f;
		Cutscene_bar_flags |= CUB_GRADUAL;
		Cutscene_delta_time = delta_speed;
	}
	else {
		Cutscene_bar_flags &= ~CUB_GRADUAL;
	}
}

void sexp_toggle_cutscene_bars(int node, int set)
{
	bool is_nan, is_nan_forever;
	float delta_speed = 0.0f;

	if (node != -1) {
		delta_speed = eval_num(node, is_nan, is_nan_forever) / 1000.0f;
		if (is_nan || is_nan_forever) {
			return;
		}
	}

	toggle_cutscene_bars(delta_speed, set);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_float(delta_speed);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_toggle_cutscene_bars(int set)
{
	float delta_speed;

	if(Current_sexp_network_packet.get_float(delta_speed) ) {
		toggle_cutscene_bars(delta_speed, set);
	}
}

void sexp_fade(bool fade_in, int duration, ubyte R, ubyte G, ubyte B)
{
	if (duration > 0)
	{
		Fade_start_timestamp = _timestamp();
		Fade_end_timestamp = _timestamp(duration);
		Fade_type = fade_in ? FadeType::FI_FADEIN : FadeType::FI_FADEOUT;
		gr_create_shader(&Viewer_shader, R, G, B, Viewer_shader.c);
	}
	else
	{
		Fade_type = FadeType::FI_NONE;
		gr_create_shader(&Viewer_shader, R, G, B, fade_in ? 0 : 255);
	}
}

void sexp_fade(int n, bool fade_in)
{
	int duration = 0, R, G, B;
	bool is_nan, is_nan_forever;
	std::array<int, 3> temp_rgb;

	// get duration if present
	if (n >= 0)
	{
		duration = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
		n = CDR(n);
	}

	// get color if present, with range checks, defaulting to -1
	eval_array<int>(temp_rgb, n, is_nan, is_nan_forever, [](int num) {
		return (num < 0 || num > 255) ? -1 : num;
	}, -1);
	if (is_nan || is_nan_forever)
		return;
	R = temp_rgb[0];
	G = temp_rgb[1];
	B = temp_rgb[2];

	// select legacy (or default) fade color
	if (R < 0 || G < 0 || B < 0)
	{
		// fade white
		if (R == 1)
		{
			R = G = B = 255;
		}
		// fade red
		else if (R == 2)
		{
			R = 255;
			G = B = 0;
		}
		// default: fade black or previous fadeout color
		else
		{
			// Mantis #2944: if we're fading in, use the same color we used to fade out with
			if (fade_in)
			{
				R = Fade_out_r;
				G = Fade_out_g;
				B = Fade_out_b;
			}
			else
			{
				R = G = B = 0;
			}
		}
	}

	// Mantis #2944
	// If we're fading out, save its color so we can fade-in with the same color
	if (!fade_in)
	{
		Fade_out_r = R;
		Fade_out_g = G;
		Fade_out_b = B;
	}

	sexp_fade(fade_in, duration, (ubyte) R, (ubyte) G, (ubyte) B);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(duration);
	Current_sexp_network_packet.send_int(R);
	Current_sexp_network_packet.send_int(G);
	Current_sexp_network_packet.send_int(B);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_fade(bool fade_in)
{
	int duration = 0;
	int R = 0;
	int G = 0;
	int B = 0;

	if (Current_sexp_network_packet.get_int(duration))
		if (Current_sexp_network_packet.get_int(R))
			if (Current_sexp_network_packet.get_int(G))
				Current_sexp_network_packet.get_int(B);

	sexp_fade(fade_in, duration, (ubyte) R, (ubyte) G, (ubyte) B);
}

camera* sexp_get_set_camera(bool reset = false)
{
	static camid sexp_camera;
	if (!reset)
	{
		if (Viewer_mode & VM_FREECAMERA)
		{
			camera *cam = cam_get_current().getCamera();
			if (cam != nullptr)
				return cam;
		}
	}
	if (!sexp_camera.isValid())
	{
		sexp_camera = cam_create("SEXP camera");
	}

	cam_set_camera(sexp_camera);

	return sexp_camera.getCamera();
}

void sexp_set_camera(int node)
{
	game_set_photo_mode_allowed(false);
	
	if (node < 0)
	{
		sexp_get_set_camera(true);
		return;
	}

	auto cam_name = CTEXT(node);
	camid cid = cam_lookup(cam_name);
	if (!cid.isValid())
	{
		cid = cam_create(cam_name);
	}
	cam_set_camera(cid);
}

void sexp_set_camera_position(int n)
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	bool is_nan, is_nan_forever;
	vec3d camera_vec;
	float camera_time, camera_acc_time, camera_dec_time;

	eval_vec3d(&camera_vec, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	int count = eval_nums(n, is_nan, is_nan_forever, camera_time, camera_acc_time, camera_dec_time);
	if (is_nan || is_nan_forever)
		return;
	if (count == 2)
		camera_dec_time = camera_acc_time;
	camera_time /= 1000.0f;
	camera_acc_time /= 1000.0f;
	camera_dec_time /= 1000.0f;

	cam->set_position(&camera_vec, camera_time, camera_acc_time, camera_dec_time);

	//multiplayer callback
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_float(camera_vec.xyz.x);
	Current_sexp_network_packet.send_float(camera_vec.xyz.y);
	Current_sexp_network_packet.send_float(camera_vec.xyz.z);
	Current_sexp_network_packet.send_float(camera_time);
	Current_sexp_network_packet.send_float(camera_acc_time);
	Current_sexp_network_packet.send_float(camera_dec_time);
	Current_sexp_network_packet.end_callback();
}

//CommanderDJ
void multi_sexp_set_camera_position()
{
	camera *cam = sexp_get_set_camera();
	Assert(cam != nullptr);

	vec3d camera_vec;
	float camera_time = 0.0f;
	float camera_acc_time = 0.0f;
	float camera_dec_time = 0.0f;

	Current_sexp_network_packet.get_float(camera_vec.xyz.x);
	Current_sexp_network_packet.get_float(camera_vec.xyz.y);
	Current_sexp_network_packet.get_float(camera_vec.xyz.z);
	Current_sexp_network_packet.get_float(camera_time);
	Current_sexp_network_packet.get_float(camera_acc_time);
	Current_sexp_network_packet.get_float(camera_dec_time);

	cam->set_position(&camera_vec, camera_time, camera_acc_time, camera_dec_time);
}

void sexp_set_camera_rotation(int n)
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	bool is_nan, is_nan_forever;
	angles rot_angles;
	float rot_time, rot_acc_time, rot_dec_time;

	// Angles are in degrees
	eval_angles(&rot_angles, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	int count = eval_nums(n, is_nan, is_nan_forever, rot_time, rot_acc_time, rot_dec_time);
	if (is_nan || is_nan_forever)
		return;
	if (count == 2)
		rot_dec_time = rot_acc_time;
	rot_time /= 1000.0f;
	rot_acc_time /= 1000.0f;
	rot_dec_time /= 1000.0f;

	cam->set_rotation(&rot_angles, rot_time, rot_acc_time, rot_dec_time);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_float(rot_angles.b);
	Current_sexp_network_packet.send_float(rot_angles.h);
	Current_sexp_network_packet.send_float(rot_angles.p);
	Current_sexp_network_packet.send_float(rot_time);
	Current_sexp_network_packet.send_float(rot_acc_time);
	Current_sexp_network_packet.send_float(rot_dec_time);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_camera_rotation()
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	angles rot_angles;
	float rot_time = 0.0f;
	float rot_acc_time = 0.0f;
	float rot_dec_time = 0.0f;

	Current_sexp_network_packet.get_float(rot_angles.b);
	Current_sexp_network_packet.get_float(rot_angles.h);
	Current_sexp_network_packet.get_float(rot_angles.p);
	Current_sexp_network_packet.get_float(rot_time);
	Current_sexp_network_packet.get_float(rot_acc_time);
	Current_sexp_network_packet.get_float(rot_dec_time);

	cam->set_rotation(&rot_angles, rot_time, rot_acc_time, rot_dec_time);
}

void sexp_set_camera_facing(int n)
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	bool is_nan, is_nan_forever;
	vec3d location;
	float rot_time, rot_acc_time, rot_dec_time;

	eval_vec3d(&location, n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	int count = eval_nums(n, is_nan, is_nan_forever, rot_time, rot_acc_time, rot_dec_time);
	if (is_nan || is_nan_forever)
		return;
	if (count == 2)
		rot_dec_time = rot_acc_time;
	rot_time /= 1000.0f;
	rot_acc_time /= 1000.0f;
	rot_dec_time /= 1000.0f;

	cam->set_rotation_facing(&location, rot_time, rot_acc_time, rot_dec_time);

	//multiplayer callback
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_float(location.xyz.x);
	Current_sexp_network_packet.send_float(location.xyz.y);
	Current_sexp_network_packet.send_float(location.xyz.z);
	Current_sexp_network_packet.send_float(rot_time);
	Current_sexp_network_packet.send_float(rot_acc_time);
	Current_sexp_network_packet.send_float(rot_dec_time);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_camera_facing()
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	vec3d location;
	float rot_time = 0.0f;
	float rot_acc_time = 0.0f;
	float rot_dec_time = 0.0f;

	Current_sexp_network_packet.get_float(location.xyz.x);
	Current_sexp_network_packet.get_float(location.xyz.y);
	Current_sexp_network_packet.get_float(location.xyz.z);
	Current_sexp_network_packet.get_float(rot_time);
	Current_sexp_network_packet.get_float(rot_acc_time);
	Current_sexp_network_packet.get_float(rot_dec_time);
 
	cam->set_rotation_facing(&location, rot_time, rot_acc_time, rot_dec_time);
}

void sexp_set_camera_facing_object(int n)
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	object_ship_wing_point_team oswpt;
	vec3d *pos;

	eval_object_ship_wing_point_team(&oswpt, n);
	switch (oswpt.type)
	{
		case OSWPT_TYPE_EXITED:
		{
			Warning(LOCATION, "Camera tried to face destroyed/departed object %s", oswpt.object_name);
			return;
		}

		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WING:
		case OSWPT_TYPE_WAYPOINT:
		{
			pos = &oswpt.objp()->pos;
			break;
		}

		default:
			return;
	}
	n = CDR(n);

	bool is_nan, is_nan_forever;
	float rot_time, rot_acc_time, rot_dec_time;

	// Now get the rotation time values
	int count = eval_nums(n, is_nan, is_nan_forever, rot_time, rot_acc_time, rot_dec_time);
	if (is_nan || is_nan_forever)
		return;
	if (count == 2)
		rot_dec_time = rot_acc_time;
	rot_time /= 1000.0f;
	rot_acc_time /= 1000.0f;
	rot_dec_time /= 1000.0f;

	cam->set_rotation_facing(pos, rot_time, rot_acc_time, rot_dec_time);

	//multiplayer callback
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_vec3d(pos);
	Current_sexp_network_packet.send_float(rot_time);
	Current_sexp_network_packet.send_float(rot_acc_time);
	Current_sexp_network_packet.send_float(rot_dec_time);
	Current_sexp_network_packet.end_callback();
}

//CommanderDJ
void multi_sexp_set_camera_facing_object()
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	vec3d pos;
	float rot_time = 0.0f;
	float rot_acc_time = 0.0f;
	float rot_dec_time = 0.0f;

	Current_sexp_network_packet.get_vec3d(&pos);
	Current_sexp_network_packet.get_float(rot_time);
	Current_sexp_network_packet.get_float(rot_acc_time);
	Current_sexp_network_packet.get_float(rot_dec_time);

	cam->set_rotation_facing(&pos, rot_time, rot_acc_time, rot_dec_time);
}

void sexp_set_camera_fov(int n)
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	bool is_nan, is_nan_forever;
	int int_fov;
	float camera_fov, camera_time, camera_acc_time, camera_dec_time = 0.0f;

	int_fov = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);
	camera_fov = fl_radians(int_fov % 360);

	int count = eval_nums(n, is_nan, is_nan_forever, camera_time, camera_acc_time, camera_dec_time);
	if (is_nan || is_nan_forever)
		return;
	if (count == 2)
		camera_dec_time = camera_acc_time;
	camera_time /= 1000.0f;
	camera_acc_time /= 1000.0f;
	camera_dec_time /= 1000.0f;

	cam->set_fov(camera_fov, camera_time, camera_acc_time, camera_dec_time);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_float(camera_fov);
	Current_sexp_network_packet.send_float(camera_time);
	Current_sexp_network_packet.send_float(camera_acc_time);
	Current_sexp_network_packet.send_float(camera_dec_time);
	Current_sexp_network_packet.end_callback();
}

//CommanderDJ
void multi_sexp_set_camera_fov()
{
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	float camera_fov = g3_get_hfov(VIEWER_ZOOM_DEFAULT);
	float camera_time = 0.0f;
	float camera_acc_time = 0.0f;
	float camera_dec_time = 0.0f;

	Current_sexp_network_packet.get_float(camera_fov);
	Current_sexp_network_packet.get_float(camera_time);
	Current_sexp_network_packet.get_float(camera_acc_time);
	Current_sexp_network_packet.get_float(camera_dec_time);

	cam->set_fov(camera_fov, camera_time, camera_acc_time, camera_dec_time);
}

//Internal helper function for set-target and set-host
object *sexp_camera_get_objsub(int node, int *o_submodel)
{
	//Important variables
	object *objp = nullptr;
	int submodel = -1;
	
	//Get arguments
	int n = node;
	
	object_ship_wing_point_team oswpt;
	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	const char *sub_name = nullptr;
	if (n >= 0)
		sub_name = CTEXT(n);

	//*****Process object
	switch (oswpt.type)
	{
		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WING:
		case OSWPT_TYPE_WAYPOINT:
			objp = oswpt.objp();
			break;

		default:
			objp = nullptr;
	}

	//*****Process submodel
	if(objp != nullptr && sub_name != nullptr && oswpt.type == OSWPT_TYPE_SHIP)
	{
		if(stricmp(sub_name, SEXP_NONE_STRING) != 0)
		{
			ship_subsys *ss = ship_get_subsys(&Ships[objp->instance], sub_name);
			if (ss != nullptr)
			{
				submodel = ss->system_info->subobj_num;
			}
		}
	}
	
	if(o_submodel != nullptr)
		*o_submodel = submodel;
	
	return objp;
}

void sexp_set_camera_host(int node)
{
	//Try to get current camera
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;
	
	//*****Get variables
	int submodel = -1;
	object *objp = sexp_camera_get_objsub(node, &submodel);
	
	//*****Set
	cam->set_object_host(objp, submodel);
}

void sexp_set_camera_target(int node)
{
	//Try to get current camera
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;
	
	//*****Get variables
	int submodel = -1;
	object *objp = sexp_camera_get_objsub(node, &submodel);
	
	//*****Set
	cam->set_object_target(objp, submodel);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_object(objp);
	Current_sexp_network_packet.send_int(submodel);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_camera_target()
{
	int submodel;
	object *objp;
	
	//Try to get current camera
	camera *cam = sexp_get_set_camera();
	if (cam == nullptr)
		return;

	Current_sexp_network_packet.get_object(objp);
	Current_sexp_network_packet.get_int(submodel);
	
	cam->set_object_target(objp, submodel);
}

void sexp_set_fov(int n)
{
	camera *cam = Main_camera.getCamera();
	if (cam == nullptr) {
		game_render_frame_setup();
		cam = Main_camera.getCamera();
	}

	bool is_nan, is_nan_forever;
	int int_fov = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	//Cap FOV to something reasonable.
	float new_fov = i2fl(int_fov % 360);
	Sexp_fov = fl_radians(new_fov);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_float(new_fov);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_fov()
{
	float new_fov;

	camera *cam = Main_camera.getCamera();
	if (cam == nullptr) {
		game_render_frame_setup();
		cam = Main_camera.getCamera();
	}

	Current_sexp_network_packet.get_float(new_fov);
	Sexp_fov = fl_radians(new_fov);
}

int sexp_get_fov()
{
	camera *cam = Main_camera.getCamera();
	if (cam == nullptr)
		return -1;
	else if (Sexp_fov > 0.0f)
		// SEXP override has been set
		return (int)fl_degrees(Sexp_fov);
	else
		return (int)fl_degrees(g3_get_hfov(cam->get_fov()));
}

/**
 * @todo Check VIEWER_ZOOM_DEFAULT
 */
void sexp_reset_fov()
{
	camera *cam = Main_camera.getCamera();
	if (cam == nullptr)
		return;

	Sexp_fov = 0.0;
	//cam->set_fov(VIEWER_ZOOM_DEFAULT);

	Current_sexp_network_packet.do_callback();
}

void multi_sexp_reset_fov()
{
	camera *cam = Main_camera.getCamera();
	if (cam == nullptr)
		return;

	Sexp_fov = 0.0;
}

void sexp_reset_camera(int node)
{
	game_set_photo_mode_allowed(true);
	
	bool cam_reset = false;
	camera *cam = cam_get_current().getCamera();
	if (cam != nullptr)
	{
		if (is_sexp_true(node))
		{
			cam->reset();
			cam_reset = true;
		}
	}
	cam_reset_camera();
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_bool(cam_reset);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_reset_camera()
{
	game_set_photo_mode_allowed(true);
	
	camera *cam = cam_get_current().getCamera();
	bool cam_reset = false;

	Current_sexp_network_packet.get_bool(cam_reset);
	if ((cam != nullptr) && cam_reset) {
		cam->reset();
	}
	cam_reset_camera();
}

void sexp_show_subtitle(int node)
{
	//These should be set to the default if not required to be explicitly defined
	int x_pos, y_pos, width = 0, line_height_modifier = 0;
	const char *text, *imageanim = nullptr;
	float display_time, fade_time = 0.0f;
	int n = node;
	std::array<int, 3> rgb = { 255, 255, 255 };
	bool is_nan, is_nan_forever, center_x = false, center_y = false, post_shaded = false;

	eval_nums(n, is_nan, is_nan_forever, x_pos, y_pos);
	if (is_nan || is_nan_forever)
		return;

	if (gr_screen.center_w != 1024)
		x_pos = (int)((x_pos / 1024.0f) * gr_screen.center_w);
	if (gr_screen.center_h != 768)
		y_pos = (int)((y_pos / 768.0f) * gr_screen.center_h);

	text = CTEXT(n);
	n = CDR(n);

	display_time = eval_num(n, is_nan, is_nan_forever) / 1000.0f;	//is in ms
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	if (n != -1)
	{
		imageanim = CTEXT(n);
		n = CDR(n);

		if (n != -1)
		{
			fade_time = eval_num(n, is_nan, is_nan_forever) / 1000.0f; //also in ms
			if (is_nan || is_nan_forever)
				return;
			n = CDR(n);

			if (n != -1)
			{
				center_x = is_sexp_true(n);
				n = CDR(n);

				if (n != -1)
				{
					center_y = is_sexp_true(n);
					n = CDR(n);

					if (n != -1)
					{
						width = eval_num(n, is_nan, is_nan_forever);
						if (is_nan || is_nan_forever)
							return;
						n = CDR(n);

						if (n != -1)
						{
							eval_array<int>(rgb, n, is_nan, is_nan_forever, [](int num)->int {
								CLAMP(num, 0, 255);
								return num;
							}, 255);
							if (is_nan || is_nan_forever)
								return;

							if (n != -1)
							{
								post_shaded = is_sexp_true(n);
								n = CDR(n);

								if (n != -1)
								{
									line_height_modifier = eval_num(n, is_nan, is_nan_forever);
									if (is_nan || is_nan_forever)
										return;
								}
							}
						}
					}
				}
			}
		}
	}

	//FINALLY !!
	color new_color;
	gr_init_alphacolor(&new_color, rgb[0], rgb[1], rgb[2], 255);

	subtitle new_subtitle(x_pos, y_pos, text, imageanim, display_time, fade_time, &new_color, -1, center_x, center_y, width, 0, post_shaded, line_height_modifier);
	Subtitles.push_back(new_subtitle);
}

void sexp_clear_subtitles() 
{
	Subtitles.clear();

	Current_sexp_network_packet.do_callback();
}

void multi_sexp_clear_subtitles() 
{
	Subtitles.clear();
}

void get_subtitle_screen_size(int& w, int& h)
{
	// if we are doing screen scaling, then set the size to the base resolution
	// (it will be adjusted when the subtitle is constructed)
	if (Show_subtitle_screen_base_res[0] > 0 && Show_subtitle_screen_base_res[1] > 0)
	{
		w = Show_subtitle_screen_base_res[0];
		h = Show_subtitle_screen_base_res[1];
	}
	// otherwise use the actual screen size
	else
	{
		w = gr_screen.center_w;
		h = gr_screen.center_h;
	}
}

void sexp_show_subtitle_text(int node)
{
	bool is_nan, is_nan_forever;
	int i, n = node, message_index = -1;
	SCP_string text;

	// we'll suppose it's the string for now
	auto ctext = CTEXT(n);
	n = CDR(n);

	// but use an actual message if one exists
	for (i=0; i<Num_messages; i++)
	{
		if (!stricmp(Messages[i].name, ctext))
		{
			ctext = Messages[i].message;
			message_index = i;
			break;
		}
	}

	// translate things like keypresses, e.g. $T$ for targeting key
	// (we don't need to do variable replacements because the subtitle code already does that)
	text = message_translate_tokens(ctext);

	std::array<int, 2> xy_input;
	eval_array<int>(xy_input, n, is_nan, is_nan_forever, [](int num)->int {
		if (!Show_subtitle_uses_pixels)
			CLAMP(num, -100, 100);
		return num;
	});
	if (is_nan || is_nan_forever)
		return;

	bool center_x = is_sexp_true(n);
	n = CDR(n);

	bool center_y = is_sexp_true(n);
	n = CDR(n);

	float display_time, fade_time;
	int width_input;
	eval_nums(n, is_nan, is_nan_forever, display_time, fade_time, width_input);
	if (is_nan || is_nan_forever)
		return;
	display_time /= 1000.0f;
	fade_time /= 1000.0f;
	if (!Show_subtitle_uses_pixels)
	{
		// note: width_input is OPF_POSITIVE so checking for < 0 is not necessary
		if (width_input > 100)
			width_input = 100;
	}

	std::array<int, 3> rgb;
	eval_array<int>(rgb, n, is_nan, is_nan_forever, [](int num)->int {
		CLAMP(num, 0, 255);
		return num;
	}, 255);
	if (is_nan || is_nan_forever)
		return;

	int fontnum = -1;
	if (n >= 0)
	{
		auto font_name = CTEXT(n);
		n = CDR(n);

		fontnum = font::FontManager::getFontIndex(font_name);
	}

	bool post_shaded = false;
	if (n >= 0)
	{
		post_shaded = is_sexp_true(n);
		n = CDR(n);
	}

	int line_height_modifier = 0;
	if (n >= 0)
	{
		line_height_modifier = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
		n = CDR(n);
	}

	bool adjust_wh = true;
	if (n >= 0)
	{
		adjust_wh = is_sexp_true(n);
		n = CDR(n);
	}

	color new_color;
	gr_init_alphacolor(&new_color, rgb[0], rgb[1], rgb[2], 255);

	// calculate pixel positions
	int x_pos, y_pos, width;
	if (Show_subtitle_uses_pixels)
	{
		x_pos = xy_input[0];
		y_pos = xy_input[1];
		width = width_input;
	}
	else
	{
		int screen_w, screen_h;
		get_subtitle_screen_size(screen_w, screen_h);

		x_pos = fl2i(screen_w * (xy_input[0] / 100.0f));
		y_pos = fl2i(screen_h * (xy_input[1] / 100.0f));
		width = fl2i(screen_w * (width_input / 100.0f));
	}

	// add the subtitle
	subtitle new_subtitle(x_pos, y_pos, text.c_str(), nullptr, display_time, fade_time, &new_color, fontnum, center_x, center_y, width, 0, post_shaded, line_height_modifier, adjust_wh);
	Subtitles.push_back(new_subtitle);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(xy_input[0]);
	Current_sexp_network_packet.send_int(xy_input[1]);
	Current_sexp_network_packet.send_int(message_index);
	// only send the text if it is not a message. If it is a message, we've already sent the index anyway. 
	if (message_index == -1) {
		Current_sexp_network_packet.send_string(text);
	}
	Current_sexp_network_packet.send_float(display_time);
	Current_sexp_network_packet.send_float(fade_time);
	Current_sexp_network_packet.send_int(rgb[0]);
	Current_sexp_network_packet.send_int(rgb[1]);
	Current_sexp_network_packet.send_int(rgb[2]);
	Current_sexp_network_packet.send_int(fontnum);
	Current_sexp_network_packet.send_bool(center_x);
	Current_sexp_network_packet.send_bool(center_y);
	Current_sexp_network_packet.send_int(width_input);
	Current_sexp_network_packet.send_bool(post_shaded);
	Current_sexp_network_packet.send_int(line_height_modifier);
	Current_sexp_network_packet.send_bool(adjust_wh);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_show_subtitle_text()
{
	std::array<int, 2> xy_input;
	int width_input, line_height_modifier = 0, fontnum, message_index = -1;
	SCP_string text;
	float display_time, fade_time=0.0f;
	int red=255, green=255, blue=255;
	bool center_x=false, center_y=false;
	bool post_shaded = false, adjust_wh = true;
	color new_color;

	Current_sexp_network_packet.get_int(xy_input[0]);
	Current_sexp_network_packet.get_int(xy_input[1]);
	Current_sexp_network_packet.get_int(message_index); 
	if (message_index == -1) {
		Current_sexp_network_packet.get_string(text);
	}
	else {
		auto ctext = Messages[message_index].message;
		text = message_translate_tokens(ctext);
	}
	Current_sexp_network_packet.get_float(display_time);
	Current_sexp_network_packet.get_float(fade_time);
	Current_sexp_network_packet.get_int(red);
	Current_sexp_network_packet.get_int(green);
	Current_sexp_network_packet.get_int(blue);
	Current_sexp_network_packet.get_int(fontnum);
	Current_sexp_network_packet.get_bool(center_x);
	Current_sexp_network_packet.get_bool(center_y);
	Current_sexp_network_packet.get_int(width_input);
	Current_sexp_network_packet.get_bool(post_shaded);
	Current_sexp_network_packet.get_int(line_height_modifier);
	Current_sexp_network_packet.get_bool(adjust_wh);

	gr_init_alphacolor(&new_color, red, green, blue, 255);

	// calculate pixel positions
	int x_pos, y_pos, width;
	if (Show_subtitle_uses_pixels)
	{
		x_pos = xy_input[0];
		y_pos = xy_input[1];
		width = width_input;
	}
	else
	{
		int screen_w, screen_h;
		get_subtitle_screen_size(screen_w, screen_h);

		x_pos = fl2i(screen_w * (xy_input[0] / 100.0f));
		y_pos = fl2i(screen_h * (xy_input[1] / 100.0f));
		width = fl2i(screen_w * (width_input / 100.0f));
	}

	// add the subtitle
	subtitle new_subtitle(x_pos, y_pos, text.c_str(), nullptr, display_time, fade_time, &new_color, fontnum, center_x, center_y, width, 0, post_shaded, line_height_modifier, adjust_wh);
	Subtitles.push_back(new_subtitle);	
}

void sexp_show_subtitle_image(int node)
{
	bool is_nan, is_nan_forever;
	int n = node;

	auto image = CTEXT(n);
	n = CDR(n);

	std::array<int, 2> xy_input;
	eval_array<int>(xy_input, n, is_nan, is_nan_forever, [](int num)->int {
		if (!Show_subtitle_uses_pixels)
			CLAMP(num, -100, 100);
		return num;
	});
	if (is_nan || is_nan_forever)
		return;

	bool center_x = is_sexp_true(n);
	n = CDR(n);

	bool center_y = is_sexp_true(n);
	n = CDR(n);

	int width_input, height_input;
	float display_time, fade_time;
	eval_nums(n, is_nan, is_nan_forever, width_input, height_input, display_time, fade_time);
	if (is_nan || is_nan_forever)
		return;
	if (!Show_subtitle_uses_pixels)
	{
		// note: width_input and height_input are OPF_POSITIVE so checking for < 0 is not necessary
		if (width_input > 100)
			width_input = 100;
		if (height_input > 100)
			height_input = 100;
	}
	display_time /= 1000.0f;
	fade_time /= 1000.0f;

	bool post_shaded = false;
	if (n >= 0)
	{
		post_shaded = is_sexp_true(n);
		n = CDR(n);
	}

	bool adjust_wh = true;
	if (n >= 0)
	{
		adjust_wh = is_sexp_true(n);
		n = CDR(n);
	}

	// calculate pixel positions
	int x_pos, y_pos, width, height;
	if (Show_subtitle_uses_pixels)
	{
		x_pos = xy_input[0];
		y_pos = xy_input[1];
		width = width_input;
		height = height_input;
	}
	else
	{
		int screen_w, screen_h;
		get_subtitle_screen_size(screen_w, screen_h);

		x_pos = fl2i(screen_w * (xy_input[0] / 100.0f));
		y_pos = fl2i(screen_h * (xy_input[1] / 100.0f));
		width = fl2i(screen_w * (width_input / 100.0f));
		height = fl2i(screen_h * (height_input / 100.0f));
	}

	// add the subtitle
	subtitle new_subtitle(x_pos, y_pos, nullptr, image, display_time, fade_time, nullptr, -1, center_x, center_y, width, height, post_shaded, 0, adjust_wh);
	Subtitles.push_back(new_subtitle);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(x_pos);
	Current_sexp_network_packet.send_int(y_pos);
	Current_sexp_network_packet.send_string(image);
	Current_sexp_network_packet.send_float(display_time);
	Current_sexp_network_packet.send_float(fade_time);
	Current_sexp_network_packet.send_bool(center_x);
	Current_sexp_network_packet.send_bool(center_y);
	Current_sexp_network_packet.send_int(width);
	Current_sexp_network_packet.send_int(height);
	Current_sexp_network_packet.send_bool(post_shaded);
	Current_sexp_network_packet.send_bool(adjust_wh);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_show_subtitle_image()
{
	std::array<int, 2> xy_input;
	int width_input = 0, height_input = 0;
	char image[TOKEN_LENGTH];
	float display_time, fade_time=0.0f;
	bool center_x=false, center_y=false;
	bool post_shaded = false, adjust_wh = true;

	Current_sexp_network_packet.get_int(xy_input[0]);
	Current_sexp_network_packet.get_int(xy_input[1]);
	Current_sexp_network_packet.get_string(image);
	Current_sexp_network_packet.get_float(display_time);
	Current_sexp_network_packet.get_float(fade_time);
	Current_sexp_network_packet.get_bool(center_x);
	Current_sexp_network_packet.get_bool(center_y);
	Current_sexp_network_packet.get_int(width_input);
	Current_sexp_network_packet.get_int(height_input);
	Current_sexp_network_packet.get_bool(post_shaded);
	Current_sexp_network_packet.get_bool(adjust_wh);

	// calculate pixel positions
	int x_pos, y_pos, width, height;
	if (Show_subtitle_uses_pixels)
	{
		x_pos = xy_input[0];
		y_pos = xy_input[1];
		width = width_input;
		height = height_input;
	}
	else
	{
		int screen_w, screen_h;
		get_subtitle_screen_size(screen_w, screen_h);

		x_pos = fl2i(screen_w * (xy_input[0] / 100.0f));
		y_pos = fl2i(screen_h * (xy_input[1] / 100.0f));
		width = fl2i(screen_w * (width_input / 100.0f));
		height = fl2i(screen_h * (height_input / 100.0f));
	}

	// add the subtitle
	subtitle new_subtitle(x_pos, y_pos, nullptr, image, display_time, fade_time, nullptr, -1, center_x, center_y, width, height, post_shaded, 0, adjust_wh);
	Subtitles.push_back(new_subtitle);	
}

void sexp_set_time_compression(int n)
{
	bool is_nan, is_nan_forever;
	float new_multiplier, new_change_time, starting_multiplier;

	int count = eval_nums(n, is_nan, is_nan_forever, new_multiplier, new_change_time, starting_multiplier);
	if (is_nan || is_nan_forever)
		return;

	Current_sexp_network_packet.start_callback();

	new_multiplier /= 100.0f;			//percent->decimal
	Current_sexp_network_packet.send_float(new_multiplier);

	//Time to change
	if (count > 1) {
		new_change_time /= 1000.0f;		//ms->seconds
		Current_sexp_network_packet.send_float(new_change_time);
	}

	//Override current time compression with this value
	if (count > 2) {
		starting_multiplier /= 100.0f;	//percent->decimal
		set_time_compression(starting_multiplier);
		Current_sexp_network_packet.send_float(starting_multiplier);
	}

	Current_sexp_network_packet.end_callback();

	set_time_compression(new_multiplier, new_change_time);
	lock_time_compression(true);
}

void multi_sexp_set_time_compression()
{
	float new_change_time = 0.0f;
	float new_multiplier = 0.0f;
	float current_multiplier = 0.0f;

	Current_sexp_network_packet.get_float(new_multiplier);
	Current_sexp_network_packet.get_float(new_change_time);
	if (Current_sexp_network_packet.get_float(current_multiplier)) {
		set_time_compression(current_multiplier);
	}

	set_time_compression(new_multiplier, new_change_time);
	lock_time_compression(true);
}

void sexp_reset_time_compression()
{
	set_time_compression(1);
	lock_time_compression(false);

	 Current_sexp_network_packet.start_callback();
	 Current_sexp_network_packet.end_callback();
}

void multi_sexp_reset_time_compression()
{
	set_time_compression(1);
	lock_time_compression(false);
}

void sexp_force_perspective(int n)
{
	Perspective_locked = is_sexp_true(n);
	n=CDR(n);

	if(n != -1)
	{
		bool is_nan, is_nan_forever;
		int option = eval_num(n, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			return;
		switch(option)
		{
			case -1:
				break;
			case 0:
				Viewer_mode = 0;
				break;
			case 1:
				Viewer_mode = VM_CHASE;
				break;
			case 2:
				Viewer_mode = VM_EXTERNAL;
				break;
			case 3:
				Viewer_mode = VM_TOPDOWN;
				break;
			default:
				Warning(LOCATION, "Unhandled option %d supplied to lock-perspective!", option);
				break;
		}
		n = CDR(n);

		// optionally prevent hat/slew/free-look
		if (n != -1)
		{
			Slew_locked = is_sexp_true(n);

			if (Slew_locked)
				Viewer_mode |= VM_CENTERING;	// start centering so that we don't get stuck in a slewed position
		}
	}
}

void sexp_allow_photo_mode(int n)
{
	game_set_photo_mode_allowed(is_sexp_true(n));
}

void sexp_set_camera_shudder(int n)
{
	int time;
	float intensity;
	bool perpetual = false;
	bool everywhere = false;
	bool is_nan, is_nan_forever;

	eval_nums(n, is_nan, is_nan_forever, time, intensity);
	if (is_nan || is_nan_forever)
		return;
	intensity *= 0.01f;

	if (n >= 0)
	{
		perpetual = is_sexp_true(n);
		n = CDR(n);
	}
	if (n >= 0)
	{
		everywhere = is_sexp_true(n);
		n = CDR(n);
	}

	game_shudder_apply(time, intensity, perpetual, everywhere);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_int(time);
	Current_sexp_network_packet.send_float(intensity);
	Current_sexp_network_packet.send_bool(perpetual);
	Current_sexp_network_packet.send_bool(everywhere);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_camera_shudder()
{
	int time;
	float intensity;
	bool perpetual = false;
	bool everywhere = false;

	Current_sexp_network_packet.get_int(time);
	if (Current_sexp_network_packet.get_float(intensity)) {
		Current_sexp_network_packet.get_bool(perpetual);
		Current_sexp_network_packet.get_bool(everywhere);
		game_shudder_apply(time, intensity, perpetual, everywhere);
	}
}

void sexp_set_jumpnode_name(int n) //CommanderDJ
{
	auto old_name = CTEXT(n);
	n = CDR(n);

	auto jnp = jumpnode_get_by_name(old_name);
	if (!jnp)
		return;

	auto new_name = CTEXT(n);
	jnp->SetName(new_name);

	//multiplayer callback
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(old_name);
	Current_sexp_network_packet.send_string(new_name);
	Current_sexp_network_packet.end_callback();
}

void sexp_set_jumpnode_display_name(int n) //MjnMixael
{
	auto node = CTEXT(n);
	n = CDR(n);

	auto jnp = jumpnode_get_by_name(node);
	if (!jnp)
		return;

	auto display_name = CTEXT(n);
	jnp->SetDisplayName(display_name);

	//multiplayer callback
	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(node);
	Current_sexp_network_packet.send_string(display_name);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_jumpnode_name() //CommanderDJ
{
	char old_name[TOKEN_LENGTH];
	char new_name[TOKEN_LENGTH];

	Current_sexp_network_packet.get_string(old_name);
	Current_sexp_network_packet.get_string(new_name);

	CJumpNode *jnp = jumpnode_get_by_name(old_name);
	if (jnp == nullptr)
		return;

	jnp->SetName(new_name);
}

void multi_sexp_set_jumpnode_display_name()
{
	char node[TOKEN_LENGTH];
	char display_name[TOKEN_LENGTH];

	Current_sexp_network_packet.get_string(node);
	Current_sexp_network_packet.get_string(display_name);

	CJumpNode* jnp = jumpnode_get_by_name(node);
	if (jnp == nullptr)
		return;

	jnp->SetDisplayName(display_name);
}

void sexp_set_jumpnode_color(int n)
{
	auto jumpnode_name = CTEXT(n);
	n = CDR(n);

	auto jnp = jumpnode_get_by_name(jumpnode_name);
	if (!jnp)
		return;

	bool is_nan, is_nan_forever;
	std::array<int, 4> rgba;
	eval_array<int>(rgba, n, is_nan, is_nan_forever, [](int num)->int {
		CLAMP(num, 0, 255);
		return num;
	});
	if (is_nan || is_nan_forever)
		return;

	jnp->SetAlphaColor(rgba[0], rgba[1], rgba[2], rgba[3]);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(jumpnode_name);
	Current_sexp_network_packet.send_int(rgba[0]);
	Current_sexp_network_packet.send_int(rgba[1]);
	Current_sexp_network_packet.send_int(rgba[2]);
	Current_sexp_network_packet.send_int(rgba[3]);
	Current_sexp_network_packet.end_callback();
}

//CommanderDJ
void multi_sexp_set_jumpnode_color()
{
	char jumpnode_name[TOKEN_LENGTH];
	int red, blue, green, alpha;

	Current_sexp_network_packet.get_string(jumpnode_name);

	CJumpNode *jnp = jumpnode_get_by_name(jumpnode_name);
	if (jnp == nullptr) {
		Current_sexp_network_packet.discard_remaining_callback_data();
		return;
	}

	Current_sexp_network_packet.get_int(red);
	Current_sexp_network_packet.get_int(green);
	Current_sexp_network_packet.get_int(blue);
	Current_sexp_network_packet.get_int(alpha);

	jnp->SetAlphaColor(red, green, blue, alpha);
}

void sexp_set_jumpnode_model(int n)
{
	auto jumpnode_name = CTEXT(n);
	n = CDR(n);

	auto jnp = jumpnode_get_by_name(jumpnode_name);
	if (!jnp)
		return;

	auto model_name = CTEXT(n);
	n = CDR(n);

	bool show_polys = is_sexp_true(n);

	jnp->SetModel(model_name, show_polys);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(jumpnode_name);
	Current_sexp_network_packet.send_string(model_name);
	Current_sexp_network_packet.send_bool(show_polys);
	Current_sexp_network_packet.end_callback();
}

void multi_sexp_set_jumpnode_model()
{
	char jumpnode_name[TOKEN_LENGTH];
	char model_name[TOKEN_LENGTH];
	bool show_polys;

	Current_sexp_network_packet.get_string(jumpnode_name);
	Current_sexp_network_packet.get_string(model_name);

	CJumpNode *jnp = jumpnode_get_by_name(jumpnode_name);
	if (jnp == nullptr) {
		Current_sexp_network_packet.discard_remaining_callback_data();
		return;
	}

	show_polys = Current_sexp_network_packet.get_bool(show_polys);

	jnp->SetModel(model_name, show_polys);
}

void sexp_show_hide_jumpnode(int node, bool show)
{
	Current_sexp_network_packet.start_callback();

	for (int n = node; n >= 0; n = CDR(n))
	{
		auto jnp = jumpnode_get_by_name(CTEXT(n));
		if (jnp)
		{
			jnp->SetVisibility(show);
			Current_sexp_network_packet.send_string(CTEXT(n));
		}
	}

	Current_sexp_network_packet.end_callback();
}

void multi_sexp_show_hide_jumpnode(bool show)
{
	char jumpnode_name[TOKEN_LENGTH];

	while (Current_sexp_network_packet.get_string(jumpnode_name))
	{
		CJumpNode *jnp = jumpnode_get_by_name(jumpnode_name);
		if (jnp != nullptr)
			jnp->SetVisibility(show);
	}
}

//WMC - This is a bit of a hack, however, it's easier than
//coding in a whole new Script_system function.
int sexp_script_eval(int node, int return_type, bool concat_args = false)
{
	int n = node;

	switch(return_type)
	{
		case OPR_BOOL:
		{
			SCP_string script_cmd;
			for (; n != -1; n = CDR(n))
				script_cmd.append(CTEXT(n));

			bool r = false;
			bool success = Script_system.EvalStringWithReturn(script_cmd.c_str(), "|b", &r);

			if (!success)
				Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", script_cmd.c_str());

			return r ? SEXP_TRUE : SEXP_FALSE;
		}

		case OPR_NUMBER:
		{
			SCP_string script_cmd;
			for (; n != -1; n = CDR(n))
				script_cmd.append(CTEXT(n));

			int r = -1;
			bool success = Script_system.EvalStringWithReturn(script_cmd.c_str(), "|i", &r);

			if (!success)
				Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", script_cmd.c_str());

			return r;
		}

		case OPR_STRING:
			{
				const char* ret = nullptr;
				auto s = CTEXT(n);

				bool success = Script_system.EvalStringWithReturn(s, "|s", &ret);
				n = CDR(n);

				if(!success)
					Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", s);

				if (n != -1 && success)
				{
					int variable_index = sexp_get_variable_index(n);

					if (variable_index < 0)
					{
						Warning(LOCATION, "script-eval: Variable %s does not exist!", Sexp_nodes[n].text);
					}
					else if (!(Sexp_variables[variable_index].type & SEXP_VARIABLE_STRING))
					{
						Warning(LOCATION, "script-eval: Variable %s must be a string variable!", Sexp_variables[variable_index].variable_name);
					}
					else if (ret != nullptr)
					{
						// assign to variable
						sexp_modify_variable(ret, variable_index);
					}

					n = CDR(n);
				}
				break;
			}
		case OPR_NULL:
			{
				SCP_string script_cmd;
				while (n != -1)
				{
					auto s = CTEXT(n);

					if (concat_args)
					{
						script_cmd.append(s);
					}
					else
					{
						bool success = Script_system.EvalString(s);

						if (!success)
							Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", s);
					}

					n = CDR(n);
				}

				if (concat_args)
				{
					bool success = Script_system.EvalString(script_cmd.c_str());

					if (!success)
						Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", script_cmd.c_str());
				}
				break;
			}
		default:
			Error(LOCATION, "Bad type passed to sexp_script_eval - get a coder");
			break;
	}

	return SEXP_TRUE;
}

void sexp_script_eval_multi(int node)
{
	char script[TOKEN_LENGTH];
	bool success = true;
	bool execute_on_server;
	player *p;

	strcpy_s(script, CTEXT(node));
	node = CDR(node);

	execute_on_server = is_sexp_true(node);
	node = CDR(node);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(script);
	// evalutate on all clients
	if (node == -1) {
		Current_sexp_network_packet.send_bool(true);			
		execute_on_server = true;
	}
	// we have to send to all clients but we need to send a list of ships so that they know if they evaluate or not
	else {
		Current_sexp_network_packet.send_bool(false);

		for (; node != -1; node = CDR(node)) {
			p = get_player_from_ship_node(node, true);

			// not a player ship so skip it
			if (p == nullptr ){
				continue;
			}
			else {
				// if this is me, flag that we should execute the script
				if (p == Player) {
					execute_on_server = true;
				}
				// otherwise notify the clients
				else {
					Current_sexp_network_packet.send_string(CTEXT(node));
				}
			}
		}
	}

	Current_sexp_network_packet.end_callback();

	if (execute_on_server) {		
		success = Script_system.EvalString(script, script);
	}

	if(!success) {
		Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", script);
	}
}

void multi_sexp_script_eval_multi()
{
	char script[TOKEN_LENGTH];
	char ship_name[TOKEN_LENGTH];
	bool sent_to_all = false;
	bool success = true;

	Current_sexp_network_packet.get_string(script);
	Current_sexp_network_packet.get_bool(sent_to_all);

	if (sent_to_all) {
		success = Script_system.EvalString(script, script);
	}
	// go through all the ships that were sent and see if any of them match this client.
	else {
		while (Current_sexp_network_packet.get_string(ship_name)) {
			auto ship_entry = ship_registry_get(ship_name);
			Assertion(ship_entry, "Illegal value for the ship name sent in multi_sexp_script_eval_multi()! Ship %s does not exist!", ship_name);
			if (Player == get_player_from_ship_entry(ship_entry)) {
				success = Script_system.EvalString(script, script);
			}
		}
	}

	if(!success) {
		Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", script);
	}
}

void sexp_force_glide(int node)
{
	// get ship
	auto ship_entry = eval_ship(node);
	if (!ship_entry || !ship_entry->has_shipp())
		return;

	//Can this ship glide?
	if (!Ship_info[ship_entry->shipp()->ship_info_index].can_glide)
		return;

	bool glide = is_sexp_true(CDR(node));

	object_set_gliding(ship_entry->objp(), glide, true);
}

bool test_point_within_box(vec3d *test_point, vec3d *box_corner_1, vec3d *box_corner_2, object *reference_ship_obj)
{
	vec3d tempv, test_point_buf;

	// If reference_ship is specified, rotate test_point into its reference frame
	if (reference_ship_obj != nullptr) 
	{
		vm_vec_sub(&tempv, test_point, &reference_ship_obj->pos);
		vm_vec_rotate(&test_point_buf, &tempv, &reference_ship_obj->orient);

		test_point = &test_point_buf;
	}

	// Check to see if the test point is within the specified box as defined by two extreme corners
	return ((test_point->xyz.x >= box_corner_1->xyz.x && test_point->xyz.x <= box_corner_2->xyz.x) &&
			(test_point->xyz.y >= box_corner_1->xyz.y && test_point->xyz.y <= box_corner_2->xyz.y) &&
			(test_point->xyz.z >= box_corner_1->xyz.z && test_point->xyz.z <= box_corner_2->xyz.z));
}

int sexp_is_in_box(int n)
{
	int i;
	float x1, x2, y1, y2, z1, z2;
	bool is_nan, is_nan_forever;
	Assert(n >= 0);

	object_ship_wing_point_team oswpt;
	eval_object_ship_wing_point_team(&oswpt, n);
	n = CDR(n);

	// Get box corners
	eval_nums(n, is_nan, is_nan_forever, x1, x2, y1, y2, z1, z2);
	if (is_nan)
		return SEXP_FALSE;
	if (is_nan_forever)
		return SEXP_KNOWN_FALSE;
	vec3d box_corner_1;
	box_corner_1.xyz.x = MIN(x1, x2);
	box_corner_1.xyz.y = MIN(y1, y2);
	box_corner_1.xyz.z = MIN(z1, z2);
	vec3d box_corner_2;
	box_corner_2.xyz.x = MAX(x1, x2);
	box_corner_2.xyz.y = MAX(y1, y2);
	box_corner_2.xyz.z = MAX(z1, z2);

	// Ship to define reference frame is optional
	object* reference_ship_obj = nullptr;
	if (n != -1)
	{
		auto reference = eval_ship(n);
		if (!reference || reference->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_FALSE;
		if (reference->status == ShipStatus::EXITED)
			return SEXP_KNOWN_FALSE;

		reference_ship_obj = reference->objp();
	}

	// Check position of object
	switch (oswpt.type)
	{
		case OSWPT_TYPE_EXITED:
			return SEXP_KNOWN_FALSE;

		case OSWPT_TYPE_WING:
			for (i = 0; i < oswpt.wingp()->current_count; i++)
				if (!test_point_within_box(&Objects[Ships[oswpt.wingp()->ship_index[i]].objnum].pos, &box_corner_1, &box_corner_2, reference_ship_obj))
					return SEXP_FALSE;
			return SEXP_TRUE;

		case OSWPT_TYPE_SHIP:
		case OSWPT_TYPE_WAYPOINT:
			return test_point_within_box(&oswpt.objp()->pos, &box_corner_1, &box_corner_2, reference_ship_obj);

		default:
			return SEXP_FALSE;
	}
}

int sexp_is_in_mission(int node)
{
	// For this sexp, we do not short-circuit known-true or known-false.
	for (int n = node; n != -1; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (ship_entry)
		{
			if (ship_entry->has_shipp())
				continue;

			return SEXP_FALSE;	// we know the ship isn't present
		}

		auto wingp = eval_wing(n);
		if (wingp)
		{
			if (wingp->current_count > 0)
				continue;

			return SEXP_FALSE;	// we know no ships in the wing are present
		}

		// this is neither a ship nor a wing
		return SEXP_FALSE;
	}

	return SEXP_TRUE;
}

int sexp_has_armor_type(int node)
{
	// get ship from sexp
	auto ship_entry = eval_ship(node);
	if (!ship_entry || ship_entry->status == ShipStatus::NOT_YET_PRESENT)
		return SEXP_NAN;
	if (ship_entry->status == ShipStatus::EXITED)
		return SEXP_NAN_FOREVER;

	ship* shipp = ship_entry->shipp();
	node = CDR(node);

	// get armor from sexp
	int armor;
	if (!stricmp(SEXP_NONE_STRING, CTEXT(node))) {
		armor = -1;
	} else {
		armor = armor_type_get_idx(CTEXT(node));

		//warn and return if the armor is not found
		if (armor < 0) {
			Warning(LOCATION, "Armor %s is not a valid entry in armor.tbl!", CTEXT(node));
			return SEXP_NAN;
		}
	}
	node = CDR(node);

	// get the armor to check against
	int currentArmor = -1;

	if (!stricmp(SEXP_HULL_STRING, CTEXT(node))) {
		currentArmor = shipp->armor_type_idx;
	} else if (!stricmp(SEXP_SHIELD_STRING, CTEXT(node))) {
		currentArmor = shipp->shield_armor_type_idx;
	} else {
		// get the subsystem
		ship_subsys* ss = ship_get_subsys(shipp, CTEXT(node));

		if (ss == nullptr) {
			Warning(LOCATION, "Subsystem %s not found on ship %s", CTEXT(node), shipp->ship_name);
			return SEXP_NAN;
		}

		currentArmor = ss->armor_type_idx;
	}

	if (currentArmor == armor)
		return SEXP_TRUE;

	return SEXP_FALSE;
}

int sexp_is_docked(int node)
{
	object *host_objp = nullptr;

	for (int n = node; n != -1; n = CDR(n))
	{
		auto current_entry = eval_ship(n);
		if (!current_entry || current_entry->status == ShipStatus::NOT_YET_PRESENT)
			return SEXP_NAN;
		if (current_entry->status == ShipStatus::EXITED)
			return SEXP_NAN_FOREVER;

		// if host_objp is a nullptr then we're on the 1st loop iteration
		if (host_objp == nullptr)
		{
			// if the host isn't docked to anything, no need to check each ship individually
			if (!object_is_docked(current_entry->objp()))
				return SEXP_FALSE;

			host_objp = current_entry->objp();
			continue;
		}

		// if we are not docked, do a quick out
		if (!dock_check_find_direct_docked_object(host_objp, current_entry->objp()))
			return SEXP_FALSE;
	}

	// all ships are docked
	return SEXP_TRUE;
}

void sexp_manipulate_colgroup(int node, bool add_to_group)
{
	int colgroup_id = 0;
	const ship_registry_entry* ship_entry = nullptr;
	const prop* prop_entry = nullptr;
	SCP_string name;

	// Ship
	ship_entry = eval_ship(node);
	if (ship_entry) {
		if (ship_entry->status == ShipStatus::EXITED)
			return;

		colgroup_id = (ship_entry->has_objp()) ? ship_entry->objp()->collision_group_id
											   : ship_entry->p_objp()->collision_group_id;
		name = ship_entry->name;

	// Prop
	} else {
		prop_entry = eval_prop(node);

		if (prop_entry) {
			colgroup_id = Objects[prop_entry->objnum].collision_group_id;
			name = prop_entry->prop_name;
		} else { // Not a prop or a ship so warn and return
			Warning(LOCATION, "Invalid object %s specified in manipulate-colgroup SEXP", CTEXT(node));
			return;
		}
	}

	node = CDR(node);

	// Set the colgroup
	for (; node != -1; node = CDR(node)) {
		bool is_nan, is_nan_forever;
		int group = eval_num(node, is_nan, is_nan_forever);
		if (is_nan || is_nan_forever) {
			continue;
		}

		if (group < 0 || group > 31) {
			WarningEx(LOCATION,
				"Invalid collision group id %d specified for object %s. Valid IDs range from 0 to 31.\n",
				group,
				name.c_str());
		} else {
			if (add_to_group) {
				colgroup_id |= (1 << group);
			} else {
				colgroup_id &= ~(1 << group);
			}
		}
	}

	// Apply it to ship
	if (ship_entry) {
		if (ship_entry->has_objp()) {
			ship_entry->objp()->collision_group_id = colgroup_id;
		} else {
			ship_entry->p_objp()->collision_group_id = colgroup_id;
		}

		return;
	}

	// Apply it to prop
	if (prop_entry) {
		Objects[prop_entry->objnum].collision_group_id = colgroup_id;
	}
}

void sexp_manipulate_colgroup_new(int node, bool add_to_group)
{
	bool is_nan, is_nan_forever;
	int group = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	node = CDR(node);

	if (group < 0 || group > 31) {
		WarningEx(LOCATION, "Invalid collision group id %d specified. Valid IDs range from 0 to 31.\n", group);
		return;
	}

	for (; node != -1; node = CDR(node)) {

		// Ship
		auto ship_entry = eval_ship(node);
		if (ship_entry) {
			if (ship_entry->status == ShipStatus::EXITED)
				continue;

			if (ship_entry->has_objp()) {
				if (add_to_group)
					ship_entry->objp()->collision_group_id |= (1 << group);
				else
					ship_entry->objp()->collision_group_id &= ~(1 << group);
			} else {
				if (add_to_group)
					ship_entry->p_objp()->collision_group_id |= (1 << group);
				else
					ship_entry->p_objp()->collision_group_id &= ~(1 << group);
			}

		// Prop
		} else {
			auto prop_entry = eval_prop(node);

			if (prop_entry) {
				object& obj = Objects[prop_entry->objnum];

				if (add_to_group)
					obj.collision_group_id |= (1 << group);
				else
					obj.collision_group_id &= ~(1 << group);
			}
		}
	}
}

int sexp_get_colgroup(int node)
{
	// Ship
	auto ship_entry = eval_ship(node);
	if (ship_entry) {
		if (ship_entry->status == ShipStatus::EXITED)
			return SEXP_NAN_FOREVER;

		if (ship_entry->has_objp())
			return ship_entry->objp()->collision_group_id;
		else
			return ship_entry->p_objp()->collision_group_id;

	// Prop
	} else {
		auto prop_entry = eval_prop(node);
		if (prop_entry) {
			const object& obj = Objects[prop_entry->objnum];

			return obj.collision_group_id;
		}
	}

	return SEXP_NAN;
}

int get_effect_from_name(const char* name)
{
	int i = 0;
	for (SCP_vector<ship_effect>::iterator sei = Ship_effects.begin(); sei != Ship_effects.end(); ++sei) {
		if (!stricmp(name, sei->name))
			return i;
		i++;
	}
	return -1;
}

void sexp_ship_effect(int n)
{
	bool is_nan, is_nan_forever;
	
	int effect_num = get_effect_from_name(CTEXT(n));
	if (effect_num == -1) {
		WarningEx(LOCATION, "Invalid effect name passed to ship-effect\n");
		return;
	}
	n = CDR(n);

	int effect_duration = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	for (; n != -1; n = CDR(n))
	{
		object_ship_wing_point_team oswpt;
		eval_object_ship_wing_point_team(&oswpt, n);

		// we only handle ships and wings that are present
		switch (oswpt.type)
		{
			case OSWPT_TYPE_SHIP:
			{
				auto sp = oswpt.shipp();
				sp->shader_effect_num = effect_num;
				sp->shader_effect_duration = effect_duration;
				sp->shader_effect_timestamp = _timestamp(effect_duration);
				break;
			}

			case OSWPT_TYPE_WING:
			{
				auto wp = oswpt.wingp();
				for (int i = 0; i < wp->current_count; ++i)
				{
					if (wp->ship_index[i] >= 0)
					{
						auto sp = &Ships[wp->ship_index[i]];
						sp->shader_effect_num = effect_num;
						sp->shader_effect_duration = effect_duration;
						sp->shader_effect_timestamp = _timestamp(effect_duration);
					}
				}
				break;
			}

			default:
				mprintf(("Invalid Shipname in SEXP ship-effect\n"));
		}
	}
}

void sexp_change_team_color(int n)
{
	auto new_color = CTEXT(n);
	n = CDR(n);

	bool is_nan, is_nan_forever;
	int fade_time = eval_num(n, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;
	n = CDR(n);

	Current_sexp_network_packet.start_callback();
	Current_sexp_network_packet.send_string(new_color);
	Current_sexp_network_packet.send_int(fade_time);

	for ( ; n >= 0; n = CDR(n))
	{
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_shipp())
			continue;
		auto shp = ship_entry->shipp();

		Current_sexp_network_packet.send_ship(shp);

		if (fade_time == 0) {
			shp->team_name = new_color;
		} else {
			shp->secondary_team_name = new_color;
			shp->team_change_time = fade_time;
			shp->team_change_timestamp = Missiontime;
		}
	}

	Current_sexp_network_packet.end_callback();
}

void multi_sexp_change_team_color()
{
	SCP_string new_color = "<none>";
	int fade_time = 0;
	ship* shipp;

	Current_sexp_network_packet.get_string(new_color);
	Current_sexp_network_packet.get_int(fade_time);

	while (Current_sexp_network_packet.get_ship(shipp))
	{
		if (fade_time == 0) {
			shipp->team_name = new_color;
		} else {
			shipp->secondary_team_name = new_color;
			shipp->team_change_time = fade_time;
			shipp->team_change_timestamp = Missiontime;
		}
	}
}

void sexp_call_ssm_strike(int node)
{
	int ssm_index = ssm_info_lookup(CTEXT(node));
	node = CDR(node);
	int calling_team = iff_lookup(CTEXT(node));
	if (ssm_index < 0 || calling_team < 0)
		return;

	for (int n = node; n != -1; n = CDR(n))
	{
		// don't do anything if the ship isn't there
		auto ship_entry = eval_ship(n);
		if (!ship_entry || !ship_entry->has_objp())
			continue;

		auto target_objp = ship_entry->objp();
		auto start = &target_objp->pos;

		vm_vec_scale_add(start, start, &target_objp->orient.vec.fvec, -1);

		ssm_create(target_objp, start, ssm_index, nullptr, calling_team);
	}
}

extern int Cheats_enabled;
int sexp_player_is_cheating_bastard()
{
	if (Cheats_enabled) {
		return SEXP_KNOWN_TRUE;	
	}

	return SEXP_FALSE;
}

int sexp_is_language(int node)
{
	// we don't check the language for validity because it could be a language the engine doesn't know about
	auto lang = CTEXT(node);

	// easy lookup
	auto li = &Lcl_languages[lcl_get_current_lang_index()];

	// easy comparison
	if (stricmp(li->lang_name, lang) == 0)
		return SEXP_KNOWN_TRUE;
	else
		return SEXP_KNOWN_FALSE;
}

extern SCP_string CheatUsed;
int sexp_cheat_used(int node)
{
	auto len = CheatUsed.length();
	if (len <= 0) 
		return SEXP_KNOWN_FALSE;
	auto text = CTEXT(node);
	if (strcmp(text, CheatUsed.c_str()) == 0)
		return SEXP_KNOWN_TRUE;
	return SEXP_UNKNOWN;
}

void sexp_set_motion_debris(int node)
{
	if (Motion_debris_ptr != nullptr)
		Motion_debris_override = is_sexp_true(node);
}

/**
 * Returns the subsystem type if the name of a subsystem is actually a generic type (e.g \<all engines\> or \<all turrets\>
 * NOTE: this can return SUBSYSTEM_ALL which is not in the normal range of subsystem types
 */
int get_generic_subsys(const char *subsys_name)
{
	char buffer[NAME_LENGTH];

	for (int i = 0; i < SUBSYSTEM_MAX; ++i)
	{
		sprintf(buffer, SEXP_ALL_GENERIC_SUBSYSTEM_STRING, Subsystem_types[i]);
		if (!stricmp(subsys_name, buffer))
			return i;
	}

	if (!stricmp(subsys_name, SEXP_ALL_SUBSYSTEMS_STRING))
		return SUBSYSTEM_ALL;

	return SUBSYSTEM_NONE;
}

// Karajorma - returns false if the ship class has changed since the mission was parsed in
// Changes can be from use of the change-ship-class SEXP, loadout or any future method
bool ship_class_unchanged(const ship_registry_entry *ship_entry)
{
	Assertion(ship_entry && ship_entry->has_shipp(), "The ship_class_unchanged function requires an active ship!");
	if (!ship_entry || !ship_entry->has_shipp() || !ship_entry->has_p_objp())
		return false;

	return ship_entry->p_objp()->ship_class == ship_entry->shipp()->ship_info_index;
}

// Goober5000 - needed because any nonzero integer value is "true"
bool is_sexp_true(int cur_node, int referenced_node)
{
	int result = eval_sexp(cur_node, referenced_node);

	// any SEXP_KNOWN_TRUE result will return SEXP_TRUE from eval_sexp, but let's be defensive
	return (result == SEXP_TRUE) || (result == SEXP_KNOWN_TRUE);
}

/**
* 
*/
int generate_event_log_flags_mask(int result)
{
	int matches = 0; 
	mission_event *current_event = &Mission_events[Event_index];

	switch (result) {
		case SEXP_TRUE:
			matches |= MLF_SEXP_TRUE; 
			break; 

		case SEXP_FALSE:
			matches |= MLF_SEXP_FALSE; 
			break;

		default:
			Error(LOCATION, "SEXP has a value which isn't true or false.");	
	}

	if (( result == SEXP_TRUE ) || (result == SEXP_KNOWN_TRUE)) {
		// now deal with the flags depending on repeat and trigger counts
		switch (current_event->mission_log_flags) {
			case MLF_FIRST_REPEAT_ONLY: 
				if (current_event->repeat_count > 1) {			
					matches |= MLF_FIRST_REPEAT_ONLY; 
				}
				break;

			case MLF_LAST_REPEAT_ONLY: 
				if (current_event->repeat_count == 1) {			
					matches |= MLF_LAST_REPEAT_ONLY; 
				}
				break;

			case MLF_FIRST_TRIGGER_ONLY: 
				if (current_event->trigger_count > 1) {			
					matches |= MLF_FIRST_TRIGGER_ONLY; 
				}
				break;

			case MLF_LAST_TRIGGER_ONLY: 
				if ((current_event->trigger_count == 1) && (current_event->flags & MEF_USING_TRIGGER_COUNT)) {			
					matches |= MLF_LAST_TRIGGER_ONLY; 
				}
				break;
		}
	}

	return matches;
}

void current_log_to_backup_log_buffer()
{
	Mission_events[Event_index].backup_log_buffer.clear();
	if (!(Mission_events[Event_index].mission_log_flags & MLF_STATE_CHANGE)){
		return;
	}

	for (int i = 0; i < (int)Current_event_log_buffer->size(); i++) {
		Mission_events[Event_index].backup_log_buffer.push_back(Current_event_log_buffer->at(i));
	}
}

void maybe_write_previous_event_to_log(int result) 
{
	mission_event *this_event = &Mission_events[Event_index];

	// if the old log is empty, all we do is record the result for the next evaluation
	// the old log should only be empty at mission start
	if (this_event->backup_log_buffer.empty()) {
		this_event->previous_result = result;
		return;
	}

	// if there's no change in state, we don't write the previous state to the log
	if ((this_event->mission_log_flags & MLF_STATE_CHANGE) && (result == this_event->previous_result)) {
		current_log_to_backup_log_buffer();
		return;
	}

	log_string(LOGFILE_EVENT_LOG, "Event has changed state. Old state");
	while (!this_event->backup_log_buffer.empty()) {
		log_string(LOGFILE_EVENT_LOG, this_event->backup_log_buffer.back().c_str());
		this_event->backup_log_buffer.pop_back();
	}
	log_string(LOGFILE_EVENT_LOG, "New state");

	// backup the current buffer as this may be a repeating event
	current_log_to_backup_log_buffer();
}

/**
* Checks the mission logs flags for this event and writes to the log if this has been asked for
*/
void maybe_write_to_event_log(int result)
{
	char buffer [256]; 

	int mask = generate_event_log_flags_mask(result);
	sprintf(buffer, "Event: %s at mission time %d seconds (%d milliseconds)", Mission_events[Event_index].name.c_str(), f2i(Missiontime), static_cast<int>(f2fl(Missiontime) * MILLISECONDS_PER_SECOND));
	Current_event_log_buffer->push_back(buffer);
		
	if (!Snapshot_all_events && (!(mask &=  Mission_events[Event_index].mission_log_flags))) {
		current_log_to_backup_log_buffer();
		Current_event_log_buffer->clear();
		return;
	}

	// remove some of the flags	
	if (mask & (MLF_FIRST_REPEAT_ONLY | MLF_FIRST_TRIGGER_ONLY)) {
		Mission_events[Event_index].mission_log_flags &= ~(MLF_FIRST_REPEAT_ONLY | MLF_FIRST_TRIGGER_ONLY) ; 
	}

	if (Mission_events[Event_index].mission_log_flags & MLF_STATE_CHANGE) {
		maybe_write_previous_event_to_log(result);
	}

	while (!Current_event_log_buffer->empty()) {
		log_string(LOGFILE_EVENT_LOG, Current_event_log_buffer->back().c_str());
		Current_event_log_buffer->pop_back();
	}
	log_string(LOGFILE_EVENT_LOG, "");

}

/**
* Returns the constant used as a SEXP's result as text for printing to the event log
*/
const char *sexp_get_result_as_text(int result)
{
	switch (result) {
		case SEXP_TRUE:
			return "TRUE";

		case SEXP_FALSE:
			return "FALSE";

		case SEXP_KNOWN_FALSE:
			return "ALWAYS FALSE";

		case SEXP_KNOWN_TRUE:
			return "ALWAYS TRUE";

		case SEXP_UNKNOWN:	
			return "UNKNOWN";
				
		case SEXP_NAN:	
			return "NOT A NUMBER";

		case SEXP_NAN_FOREVER:
			return "CAN NEVER BE A NUMBER";

		case SEXP_CANT_EVAL:
			return "CAN'T EVALUATE";

		default:
			return nullptr;
	}
}

/**
* Checks the mission logs flags for this event and writes to the log if this has been asked for
*/
void add_to_event_log_buffer(int node, int op_num, int result)
{
	Assertion ((Current_event_log_buffer != nullptr) &&
				(Current_event_log_variable_buffer != nullptr)&& 
				(Current_event_log_container_buffer != nullptr) &&
				(Current_event_log_argument_buffer != nullptr), "Attempting to write to a non-existent log buffer");

	if (op_num == -1) {
		nprintf(("SEXP", "ERROR: add_to_event_log_buffer() function called with op_num of %i; this should not happen. Contact a coder.\n", op_num));
		return; //How does this happen?
	}

	char buffer[TOKEN_LENGTH];
	SCP_string tmp; 

	// indent the operators according to their depth
	int n = node;
	while (n >= 0) {
		tmp.append("  ");
		n = find_parent_operator(n);
	}

	tmp.append(Operators[op_num].text);
	tmp.append(" returned ");

	if ((Operators[op_num].type == sexp_oper_type::INTEGER) || (Operators[op_num].type == sexp_oper_type::ARITHMETIC) || (sexp_get_result_as_text(result) == nullptr)) {
		sprintf(buffer, "%d", result);
		tmp.append(buffer);
	}
	else {
		tmp.append(sexp_get_result_as_text(result));
	}

	if (True_loop_argument_sexps && !Sexp_replacement_arguments.empty()) {
		tmp.append(" for argument ");
		tmp.append(Sexp_replacement_arguments.back().first);
	}
	
	if (!Current_event_log_argument_buffer->empty()) {
		tmp.append(" for the following arguments");
		while (!Current_event_log_argument_buffer->empty()) {
			tmp.append("\n");
			tmp.append(Current_event_log_argument_buffer->back());
			Current_event_log_argument_buffer->pop_back();
		}
	}

	if (!Current_event_log_variable_buffer->empty()) {
		tmp.append("\nVariables:");
		while (!Current_event_log_variable_buffer->empty()) {
			tmp.append("\n");
			tmp.append(Current_event_log_variable_buffer->back());
			Current_event_log_variable_buffer->pop_back();
			tmp.append("[");
			tmp.append(Current_event_log_variable_buffer->back());
			Current_event_log_variable_buffer->pop_back();
			tmp.append("]");
		}
	}

	if (!Current_event_log_container_buffer->empty()) {
		tmp.append("\nContainers:");
		while (!Current_event_log_container_buffer->empty()) {
			tmp.append("\n");
			tmp.append(Current_event_log_container_buffer->back());
			Current_event_log_container_buffer->pop_back();
		}
	}

	Current_event_log_buffer->push_back(std::move(tmp));
}

/**
 * High-level sexpression evaluator
 */
int eval_sexp(int cur_node, int referenced_node)
{
	int node, type, sexp_val = UNINITIALIZED;
	if (cur_node == -1)  // empty list, i.e. sexp: ( )
		return SEXP_FALSE;

	Assert(cur_node >= 0);			// we have special sexp nodes <= -1!!!  MWA
									// which should be intercepted before we get here.  HOFFOSS
	type = SEXP_NODE_TYPE(cur_node);
	Assert( (type == SEXP_LIST) || (type == SEXP_ATOM) );

	// Trap known true and known false sexpressions.  We don't trap on SEXP_NAN sexpressions since
	// they may yet evaluate to true or false.  If the sexp is part of a when-argument tree,
	// we can't 'know' its value since the sexp nodes may be evaluated in different ways for
	// different arguments, so we skip this behaviour.

	if (!is_descendant_of_when_argument_op(cur_node)) {
		// we want to log event values for KNOWN_X or FOREVER_X before returning
		if (Log_event && ((Sexp_nodes[cur_node].value == SEXP_KNOWN_TRUE) || (Sexp_nodes[cur_node].value == SEXP_KNOWN_FALSE) || (Sexp_nodes[cur_node].value == SEXP_NAN_FOREVER))) {
			// if this is a node that has been assigned the value by short-circuiting,
			// it might not be the operator that returned the value
			int op_index = get_operator_index(cur_node);
			if (op_index < 0)
				op_index = get_operator_index(CAR(cur_node));

			// log the known value
			add_to_event_log_buffer(cur_node, op_index, Sexp_nodes[cur_node].value);
		}

		// now do a quick return whether or not we log, per the comment above about trapping known sexpressions
		if (Sexp_nodes[cur_node].value == SEXP_KNOWN_TRUE) {
			return SEXP_TRUE;
		}
		else if (Sexp_nodes[cur_node].value == SEXP_KNOWN_FALSE) {
			return SEXP_FALSE;
		}
		else if (Sexp_nodes[cur_node].value == SEXP_NAN_FOREVER) {
			return SEXP_FALSE;
		}
	}

	// ignore for container data, because their "first" is a container modifier
	if ((Sexp_nodes[cur_node].first != -1) && (Sexp_nodes[cur_node].subtype != SEXP_ATOM_CONTAINER_DATA)) {
		node = CAR(cur_node);
		sexp_val = eval_sexp(node);
		Sexp_nodes[cur_node].value = Sexp_nodes[node].value;	// higher level node gets node value
		return sexp_val;

	} else {
		int op_num;

		node = CDR(cur_node);		// makes reading the next bit of code a little easier.

		op_num = get_operator_const(cur_node);
		// add the op_num to the stack if it is an actual operator rather than a number
		if (op_num) {
			Current_sexp_operator.push_back(op_num); 
		}
		switch ( op_num ) {
		// arithmetic operators will always return just their value
			case OP_PLUS:
				sexp_val = add_sexps(node);
				break;

			case OP_MINUS:
				sexp_val = sub_sexps(node);
				break;

			case OP_MUL:
				sexp_val = mul_sexps(node);
				break;

			case OP_MOD:
				sexp_val = mod_sexps(node);
				break;

			case OP_DIV:
				sexp_val = div_sexps(node);
				break;

			case OP_RAND:
			case OP_RAND_MULTIPLE:
				sexp_val = rand_sexp( node, (op_num == OP_RAND_MULTIPLE) );
				break;

			case OP_ABS:
				sexp_val = abs_sexp(node);
				break;

			case OP_MIN:
				sexp_val = min_sexp(node);
				break;

			case OP_MAX:
				sexp_val = max_sexp(node);
				break;

			case OP_AVG:
				sexp_val = avg_sexp(node);
				break;

			case OP_POW:
				sexp_val = pow_sexp(node);
				break;

			case OP_SIGNUM:
				sexp_val = signum_sexp(node);
				break;

			case OP_IS_NAN:
				sexp_val = sexp_is_nan(node);
				break;

			case OP_NAN_TO_NUMBER:
				sexp_val = sexp_nan_to_number(node);
				break;

			case OP_SET_BIT:
			case OP_UNSET_BIT:
				sexp_val = sexp_set_bit(node, op_num == OP_SET_BIT);
				break;

			case OP_IS_BIT_SET:
				sexp_val = sexp_is_bit_set(node);
				break;

			case OP_BITWISE_AND:
				sexp_val = sexp_bitwise_and(node);
				break;

			case OP_BITWISE_OR:
				sexp_val = sexp_bitwise_or(node);
				break;

			case OP_BITWISE_NOT:
				sexp_val = sexp_bitwise_not(node);
				break;

			case OP_BITWISE_XOR:
				sexp_val = sexp_bitwise_xor(node);
				break;

			case OP_ANGLE_VECTORS:
				sexp_val = sexp_angle_vectors(node);
				break;

			// boolean operators can have one of the special sexp values (known true, known false, unknown)
			case OP_TRUE:
				sexp_val = SEXP_KNOWN_TRUE;
				break;

			case OP_FALSE:
				sexp_val = SEXP_KNOWN_FALSE;
				break;

			case OP_OR:
				sexp_val = sexp_or(node);
				break;

			case OP_AND:
				sexp_val = sexp_and(node);
				break;

			case OP_AND_IN_SEQUENCE:
				sexp_val = sexp_and_in_sequence(node);
				break;

			case OP_XOR:
				sexp_val = sexp_xor(node);
				break;

			case OP_EQUALS:
			case OP_GREATER_THAN:
			case OP_LESS_THAN:
			case OP_NOT_EQUAL:
			case OP_GREATER_OR_EQUAL:
			case OP_LESS_OR_EQUAL:
				sexp_val = sexp_number_compare( node, op_num );
				break;

			case OP_STRING_GREATER_THAN:
			case OP_STRING_LESS_THAN:
			case OP_STRING_EQUALS:
				sexp_val = sexp_string_compare( node, op_num );
				break;

			case OP_IS_TRUE_FOR_DURATION:
				sexp_val = sexp_is_true_for_duration(cur_node, node);
				break;

			case OP_IS_IFF:
				sexp_val = sexp_is_iff_or_species(node, true);
				break;

			case OP_IS_SPECIES:
				sexp_val = sexp_is_iff_or_species(node, false);
				break;

			case OP_NOT:
				sexp_val = sexp_not(node);
				break;

			case OP_PREVIOUS_GOAL_TRUE:
				sexp_val = sexp_previous_goal_status( node, GOAL_COMPLETE );
				break;

			case OP_PREVIOUS_GOAL_FALSE:
				sexp_val = sexp_previous_goal_status( node, GOAL_FAILED );
				break;

			case OP_PREVIOUS_GOAL_INCOMPLETE:
				sexp_val = sexp_previous_goal_status( node, GOAL_INCOMPLETE );
				break;

			case OP_PREVIOUS_EVENT_TRUE:
				sexp_val = sexp_previous_event_status( node, EventStatus::SATISFIED );
				break;

			case OP_PREVIOUS_EVENT_FALSE:
				sexp_val = sexp_previous_event_status( node, EventStatus::FAILED );
				break;

			case OP_PREVIOUS_EVENT_INCOMPLETE:
				sexp_val = sexp_previous_event_status( node, EventStatus::INCOMPLETE );
				break;

			case OP_EVENT_TRUE:
			case OP_EVENT_FALSE:
				sexp_val = sexp_event_status( node, (op_num == OP_EVENT_TRUE?1:0) );
				break;

			case OP_EVENT_TRUE_DELAY:
			case OP_EVENT_FALSE_DELAY:
				sexp_val = sexp_event_delay_status( node, (op_num == OP_EVENT_TRUE_DELAY?1:0) );
				break;

			case OP_EVENT_TRUE_MSECS_DELAY:
			case OP_EVENT_FALSE_MSECS_DELAY:
				sexp_val = sexp_event_delay_status( node, (op_num == OP_EVENT_TRUE_MSECS_DELAY?1:0), true );
				break;

			case OP_GOAL_TRUE_DELAY:
			case OP_GOAL_FALSE_DELAY:
				sexp_val = sexp_goal_delay_status( node, (op_num == OP_GOAL_TRUE_DELAY?1:0) );
				break;

			case OP_EVENT_INCOMPLETE:
				sexp_val = sexp_event_incomplete(node);
				break;

			case OP_GOAL_INCOMPLETE:
				sexp_val = sexp_goal_incomplete(node);
				break;

			case OP_RESET_EVENT:
				sexp_reset_event(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_RESET_GOAL:
				sexp_reset_goal(node);
				sexp_val = SEXP_TRUE;
				break;

			// destroy type sexpressions
			case OP_IS_DESTROYED:
				sexp_val = sexp_is_destroyed(node, nullptr);
				break;

			case OP_WAS_DESTROYED_BY_DELAY:
				sexp_val = sexp_was_destroyed_by_delay(node);
				break;

			case OP_IS_SUBSYSTEM_DESTROYED:
				sexp_val = sexp_is_subsystem_destroyed(node, nullptr);
				break;

			case OP_HAS_ARRIVED:
				sexp_val = sexp_has_arrived( node, nullptr );
				break;

			case OP_HAS_DEPARTED:
				sexp_val = sexp_has_departed( node, nullptr );
				break;

			case OP_IS_DISABLED:
				sexp_val = sexp_is_disabled_xor_disarmed( node, true, nullptr );
				break;

			case OP_IS_DISARMED:
				sexp_val = sexp_is_disabled_xor_disarmed( node, false, nullptr );
				break;

			case OP_WAYPOINTS_DONE:
				sexp_val = sexp_are_waypoints_done( node, nullptr );
				break;

			// objective operators that use a delay
			case OP_IS_DESTROYED_DELAY:
				sexp_val = sexp_is_destroyed_delay(node);
				break;

			case OP_IS_SUBSYSTEM_DESTROYED_DELAY:
				sexp_val = sexp_is_subsystem_destroyed_delay(node);
				break;

			case OP_HAS_DOCKED:
			case OP_HAS_UNDOCKED:
			case OP_HAS_DOCKED_DELAY:
			case OP_HAS_UNDOCKED_DELAY:
				sexp_val = sexp_has_docked_or_undocked(node, op_num);
				break;

			case OP_HAS_ARRIVED_DELAY:
				sexp_val = sexp_has_arrived_delay(node);
				break;

			case OP_HAS_DEPARTED_DELAY:
				sexp_val = sexp_has_departed_delay(node);
				break;

			case OP_IS_DISABLED_DELAY:
				sexp_val = sexp_is_disabled_xor_disarmed_delay(node, true);
				break;

			case OP_IS_DISARMED_DELAY:
				sexp_val = sexp_is_disabled_xor_disarmed_delay(node, false);
				break;

			case OP_WAYPOINTS_DONE_DELAY:
				sexp_val = sexp_are_waypoints_done_delay(node);
				break;

			case OP_SHIP_TYPE_DESTROYED:
				sexp_val = sexp_ship_type_destroyed(node);
				break;

			// time based sexpressions
			case OP_HAS_TIME_ELAPSED:
			case OP_HAS_TIME_ELAPSED_MSECS:
				sexp_val = sexp_has_time_elapsed(node, op_num == OP_HAS_TIME_ELAPSED_MSECS);
				break;

			case OP_MODIFY_VARIABLE:
				sexp_modify_variable(node);
				sexp_val = SEXP_TRUE;	// SEXP_TRUE means only do once.
				break;

			case OP_MODIFY_VARIABLE_XSTR:
				sexp_modify_variable_xstr(node);
				sexp_val = SEXP_TRUE;	// SEXP_TRUE means only do once.
				break;

			case OP_GET_VARIABLE_BY_INDEX:
				sexp_val = sexp_get_variable_by_index(node);
				break;

			case OP_SET_VARIABLE_BY_INDEX:
				sexp_set_variable_by_index(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_COPY_VARIABLE_FROM_INDEX:
				sexp_copy_variable_from_index(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_COPY_VARIABLE_BETWEEN_INDEXES:
				sexp_copy_variable_between_indexes(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CONTAINER_ADD_TO_LIST:
			case OP_CONTAINER_REMOVE_FROM_LIST:
			case OP_CONTAINER_ADD_TO_MAP:
			case OP_CONTAINER_REMOVE_FROM_MAP:
			case OP_CONTAINER_GET_MAP_KEYS:
			case OP_CLEAR_CONTAINER:
			case OP_COPY_CONTAINER:
			case OP_APPLY_CONTAINER_FILTER:
				sexp_val = sexp_container_eval_change_sexp(op_num, node);
				break;

			case OP_TIME_SHIP_DESTROYED:
			case OP_TIME_WING_DESTROYED:
			case OP_TIME_SHIP_DEPARTED:
			case OP_TIME_WING_DEPARTED:
				sexp_val = sexp_time_exited(node, op_num);
				break;

			case OP_TIME_SHIP_ARRIVED:
			case OP_TIME_WING_ARRIVED:
				sexp_val = sexp_time_arrived(node, op_num == OP_TIME_SHIP_ARRIVED);
				break;

			case OP_MISSION_TIME:
				sexp_val = sexp_mission_time();
				break;

			case OP_MISSION_TIME_MSECS:
				sexp_val = sexp_mission_time_msecs();
				break;

			case OP_TIME_DOCKED:
			case OP_TIME_UNDOCKED:
				sexp_val = sexp_time_docked_or_undocked(node, op_num == OP_TIME_DOCKED);
				break;

			case OP_AFTERBURNER_LEFT:
			case OP_WEAPON_ENERGY_LEFT:
				sexp_val = sexp_get_energy_pct(node, op_num);
				break;

			case OP_SHIELDS_LEFT:
				sexp_val = sexp_shields_left(node);
				break;

			case OP_HITS_LEFT:
			case OP_SIM_HITS_LEFT:
				sexp_val = sexp_hits_left(node, op_num == OP_SIM_HITS_LEFT);
				break;

			case OP_HITS_LEFT_SUBSYSTEM:
				sexp_val = sexp_hits_left_subsystem(node);
				break;

			case OP_HITS_LEFT_SUBSYSTEM_GENERIC:
				sexp_val = sexp_hits_left_subsystem_generic(node);
				break;

			case OP_HITS_LEFT_SUBSYSTEM_SPECIFIC:
				sexp_val = sexp_hits_left_subsystem_specific(node);
				break;

			case OP_SPECIAL_WARP_DISTANCE:
				sexp_val = sexp_special_warp_dist(node);
				break;

			case OP_DISTANCE:
				sexp_val = sexp_distance(node, sexp_retail_distance3);
				break;
			case OP_DISTANCE_CENTER:
				sexp_val = sexp_distance(node, sexp_center_distance3);
				break;
			case OP_DISTANCE_BBOX:
				sexp_val = sexp_distance(node, sexp_bbox_distance3);
				break;

			case OP_DISTANCE_CENTER_SUBSYSTEM:
				sexp_val = sexp_distance_subsystem(node, sexp_center_distance_point);
				break;
			case OP_DISTANCE_BBOX_SUBSYSTEM:
				sexp_val = sexp_distance_subsystem(node, sexp_bbox_distance_point);
				break;

			case OP_ANGLE_FVEC_TARGET:
				sexp_val = sexp_angle_fvec_target(node);
				break;

			case OP_NUM_WITHIN_BOX:
				sexp_val = sexp_num_within_box(node);
				break;

			case OP_IS_IN_BOX:
				sexp_val = sexp_is_in_box(node);
				break;

			case OP_IS_IN_MISSION:
				sexp_val = sexp_is_in_mission(node);
				break;

			case OP_HAS_ARMOR_TYPE:
				sexp_val = sexp_has_armor_type(node);
				break;

			case OP_IS_DOCKED:
				sexp_val = sexp_is_docked(node);
				break;

			case OP_IS_SHIP_VISIBLE:
				sexp_val = sexp_is_ship_visible(node);
				break;

			case OP_IS_SHIP_STEALTHY:
				sexp_val = sexp_check_ship_flag(node, Ship::Ship_Flags::Stealth);
				break;

			case OP_IS_FRIENDLY_STEALTH_VISIBLE:
				sexp_val = sexp_check_ship_flag(node, Ship::Ship_Flags::Friendly_stealth_invis);
				break;

			case OP_TEAM_SCORE:
				sexp_val = sexp_team_score(node);
				break;

			case OP_LAST_ORDER_TIME:
				sexp_val = sexp_last_order_time(node);
				break;

			case OP_NUM_PLAYERS:
				sexp_val = sexp_num_players();
				break;

			case OP_SKILL_LEVEL_AT_LEAST:
				sexp_val = sexp_skill_level_at_least(node);
				break;

			case OP_IS_CARGO_KNOWN:
			case OP_CARGO_KNOWN_DELAY:
				sexp_val = sexp_is_cargo_known( node, op_num == OP_CARGO_KNOWN_DELAY );
				break;

			case OP_HAS_BEEN_TAGGED_DELAY:
				sexp_val = sexp_has_been_tagged_delay(node);
				break;

			case OP_ARE_SHIP_FLAGS_SET:
				sexp_val = sexp_are_ship_flags_set(node);
				break;

			case OP_ARE_WING_FLAGS_SET:
				sexp_val = sexp_are_wing_flags_set(node);
				break;

			case OP_IS_SHIP_EMP_ACTIVE:
				sexp_val = sexp_is_ship_emp_active(node);
				break;

			case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY:
				sexp_val = sexp_cap_subsys_cargo_known_delay(node);
				break;

			case OP_WAS_PROMOTION_GRANTED:
				sexp_val = sexp_was_promotion_granted(node);
				break;

			case OP_WAS_MEDAL_GRANTED:
				sexp_val = sexp_was_medal_granted(node);
				break;

			case OP_GET_DAMAGE_CAUSED:
				sexp_val = sexp_get_damage_caused(node);
				break;

			case OP_PERCENT_SHIPS_ARRIVED:
			case OP_PERCENT_SHIPS_DEPARTED:
			case OP_PERCENT_SHIPS_DESTROYED:
			case OP_PERCENT_SHIPS_DISARMED:
			case OP_PERCENT_SHIPS_DISABLED:
			case OP_PERCENT_SHIPS_SCANNED:
				sexp_val = sexp_percent_ships_arrive_depart_destroy_disarm_disable_scan(node, op_num);
				break;

			case OP_DEPART_NODE_DELAY:
				sexp_val = sexp_depart_node_delay(node);
				break;

			case OP_DESTROYED_DEPARTED_DELAY:
				sexp_val = sexp_destroyed_departed_delay(node);
				break;

			// conditional sexpressions
			case OP_WHEN:
			case OP_WHEN_ARGUMENT:
			case OP_IF_THEN_ELSE:
				sexp_val = eval_when( node, op_num );
				break;

			case OP_PERFORM_ACTIONS_BOOL_FIRST:
			case OP_PERFORM_ACTIONS_BOOL_LAST:
			case OP_FUNCTIONAL_WHEN:
			case OP_ON_MISSION_SKIP:
				sexp_val = eval_perform_actions( node, op_num );
				break;

			case OP_SWITCH:
				eval_switch(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_COND:
				sexp_val = eval_cond(node);
				break;

			// Goober5000: special case; evaluate like when, but flush the sexp tree
			// and return SEXP_NAN so this will always be re-evaluated
			case OP_EVERY_TIME:
			case OP_EVERY_TIME_ARGUMENT:
				eval_when( node, op_num );
				flush_sexp_tree(node);
				sexp_val = SEXP_NAN;
				break;

			// Goober5000
			case OP_ANY_OF:
				sexp_val = eval_any_of( cur_node, referenced_node );
				break;

			// Goober5000
			case OP_EVERY_OF:
				sexp_val = eval_every_of( cur_node, referenced_node );
				break;

			// Goober5000
			case OP_RANDOM_OF:
				sexp_val = eval_random_of( cur_node, referenced_node );
				break;

			// Goober5000 and Karajorma
			case OP_RANDOM_MULTIPLE_OF:
				sexp_val = eval_random_multiple_of( cur_node, referenced_node );
				break;

			// Goober5000
			case OP_NUMBER_OF:
				sexp_val = eval_number_of( cur_node, referenced_node );
				break;

			// Karajorma
			case OP_IN_SEQUENCE:
				sexp_val = eval_in_sequence( cur_node, referenced_node );
				break;

			// Goober5000
			case OP_FOR_COUNTER:
				sexp_val = eval_for_counter( cur_node, referenced_node );
				break;

			// Goober5000
			case OP_FOR_SHIP_CLASS:
			case OP_FOR_SHIP_TYPE:
			case OP_FOR_SHIP_TEAM:
			case OP_FOR_SHIP_SPECIES:
				sexp_val = eval_for_ship_collection( cur_node, referenced_node, op_num );
				break;

			// Goober5000
			case OP_FOR_PLAYERS:
				sexp_val = eval_for_players( cur_node, referenced_node );
				break;

			// Goober5000
			case OP_FOR_SUBSYSTEMS:
				sexp_val = eval_for_subsystems( cur_node, referenced_node );
				break;

			// jg18
			case OP_FOR_CONTAINER_DATA:
			case OP_FOR_MAP_CONTAINER_KEYS:
				sexp_val = eval_for_container( cur_node, referenced_node, op_num );
				break;

			// MageKing17
			case OP_FIRST_OF:
				sexp_val = eval_first_of( cur_node, referenced_node );
				break;

			// Karajorma
			case OP_INVALIDATE_ALL_ARGUMENTS:
			case OP_VALIDATE_ALL_ARGUMENTS:
				sexp_change_all_argument_validity(cur_node, (op_num == OP_INVALIDATE_ALL_ARGUMENTS));
				sexp_val = SEXP_TRUE;
			break;

			// Goober5000
			case OP_INVALIDATE_ARGUMENT:
			case OP_VALIDATE_ARGUMENT:
				sexp_change_argument_validity(node, (op_num == OP_INVALIDATE_ARGUMENT));
				sexp_val = SEXP_TRUE;
				break;

			case OP_DO_FOR_VALID_ARGUMENTS:
				// do-for-valid-arguments should only ever be called within eval_when()
				Warning(LOCATION, "do-for-valid-arguments was encountered in eval_sexp()!");
				break;

			case OP_NUM_VALID_ARGUMENTS:
				sexp_val = sexp_num_valid_arguments( cur_node );
				break;

			// this is a "conditional", but it's not really treated the same way as when, etc.
			case OP_FUNCTIONAL_IF_THEN_ELSE:
				sexp_val = sexp_functional_if_then_else(node);
				break;

			// this too
			case OP_FUNCTIONAL_SWITCH:
				sexp_val = sexp_functional_switch(node);
				break;

			// sexpressions with side effects
			case OP_CHANGE_IFF:
				sexp_change_iff(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ADD_SHIP_GOAL:
			case OP_ADD_WING_GOAL:
			case OP_ADD_GOAL:
				sexp_add_goal(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_REMOVE_GOAL:
				sexp_remove_goal(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CLEAR_SHIP_GOALS:
			case OP_CLEAR_WING_GOALS:
			case OP_CLEAR_GOALS:
				sexp_clear_goals(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_PROTECT_SHIP:
			case OP_UNPROTECT_SHIP:
				sexp_protect_ships(node, (op_num == OP_PROTECT_SHIP));
				sexp_val = SEXP_TRUE;
				break;

			case OP_BEAM_PROTECT_SHIP:
			case OP_BEAM_UNPROTECT_SHIP:
				sexp_beam_protect_ships(node, (op_num == OP_BEAM_PROTECT_SHIP));
				sexp_val = SEXP_TRUE;
				break;

			case OP_TURRET_PROTECT_SHIP:
			case OP_TURRET_UNPROTECT_SHIP:
				sexp_turret_protect_ships(node, (op_num == OP_TURRET_PROTECT_SHIP));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_STEALTHY:
			case OP_SHIP_UNSTEALTHY:
				sexp_ships_stealthy(node, (op_num == OP_SHIP_STEALTHY));
				sexp_val = SEXP_TRUE;
				break;

			case OP_FRIENDLY_STEALTH_INVISIBLE:
			case OP_FRIENDLY_STEALTH_VISIBLE:
				sexp_friendly_stealth_invisible(node, (op_num == OP_FRIENDLY_STEALTH_INVISIBLE));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_INVISIBLE:
			case OP_SHIP_VISIBLE:
				sexp_ships_visible(node, (op_num == OP_SHIP_VISIBLE));
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_SHIP_TAG:
			case OP_SHIP_UNTAG:
				sexp_ship_tag(node, (op_num == OP_SHIP_TAG));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_CHANGE_ALT_NAME:
			case OP_SHIP_CHANGE_DISPLAY_NAME:
			case OP_SHIP_CHANGE_CALLSIGN:
				sexp_ship_change_alt_name_or_display_name_or_callsign(node, op_num);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_DEATH_MESSAGE:
				sexp_set_death_message(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ALTER_SHIP_FLAG:
				sexp_alter_ship_flag(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ALTER_WING_FLAG:
				sexp_alter_wing_flag(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CANCEL_FUTURE_WAVES:
				sexp_cancel_future_waves(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_VULNERABLE:
			case OP_SHIP_INVULNERABLE:
				sexp_ships_invulnerable(node, (op_num == OP_SHIP_INVULNERABLE));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_BOMB_TARGETABLE:
			case OP_SHIP_BOMB_UNTARGETABLE:
				sexp_ships_bomb_targetable(node, (op_num == OP_SHIP_BOMB_TARGETABLE));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_GUARDIAN:
			case OP_SHIP_NO_GUARDIAN:
				sexp_ships_guardian(node, (op_num == OP_SHIP_GUARDIAN));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_GUARDIAN_THRESHOLD:
				sexp_ship_guardian_threshold(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD:
				sexp_ship_subsys_guardian_threshold(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_GUARD_RANGE:
				sexp_set_guard_range(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_SUBSYS_TARGETABLE:
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::Untargetable, true, false);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_SUBSYS_UNTARGETABLE:
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::Untargetable, true, true);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TURRET_SUBSYS_TARGET_DISABLE:
				sexp_val = SEXP_TRUE;
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::No_SS_targeting, false, true);
				break;

			case OP_TURRET_SUBSYS_TARGET_ENABLE:
				sexp_val = SEXP_TRUE;
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::No_SS_targeting, false, false);
				break;

			case OP_SHIP_SUBSYS_NO_REPLACE:
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::No_replace, true);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_SUBSYS_NO_LIVE_DEBRIS:
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::No_live_debris, true);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_SUBSYS_VANISHED:
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::Vanished, true);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_SUBSYS_IGNORE_IF_DEAD:
				sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::Missiles_ignore_if_dead, false);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_CREATE:
				sexp_ship_create(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_PROP_CREATE:
				sexp_prop_create(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_WEAPON_CREATE:
				sexp_weapon_create(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_VANISH:
				sexp_ship_vanish(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_PROP_VANISH:
				sexp_prop_vanish(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_DESTROY_INSTANTLY:
			case OP_DESTROY_INSTANTLY_WITH_DEBRIS:
				sexp_destroy_instantly(node, (op_num == OP_DESTROY_INSTANTLY_WITH_DEBRIS));
				sexp_val = SEXP_TRUE;
				break;

			//-Sesquipedalian
			case OP_SHIELDS_ON: 
			case OP_SHIELDS_OFF:
				sexp_shields_off(node, (op_num == OP_SHIELDS_OFF));
				sexp_val = SEXP_TRUE;
				break;

			//-Sesquipedalian
			case OP_KAMIKAZE: 
				sexp_kamikaze(node, (op_num == OP_KAMIKAZE));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_CARGO:
				sexp_set_cargo(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_IS_CARGO:
				sexp_val = sexp_is_cargo(node);
				break;

			case OP_CHANGE_AI_CLASS:
				sexp_change_ai_class(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_IS_AI_CLASS:
				sexp_val = sexp_is_ai_class(node);
				break;
				
			case OP_IS_SHIP_TYPE:
			case OP_IS_SHIP_CLASS:
				sexp_val = sexp_is_ship_class_or_type(node, op_num == OP_IS_SHIP_CLASS);
				break;

			case OP_CHANGE_SOUNDTRACK:
				sexp_change_soundtrack(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_PLAY_SOUND_FROM_TABLE:
				sexp_play_sound_from_table(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_PLAY_SOUND_FROM_FILE:
				sexp_play_sound_from_file(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CLOSE_SOUND_FROM_FILE:
				sexp_close_sound_from_file(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_PAUSE_SOUND_FROM_FILE:
				sexp_pause_sound_from_file(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_SOUND_ENVIRONMENT:
				sexp_set_sound_environment(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_UPDATE_SOUND_ENVIRONMENT:
				sexp_update_sound_environment(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ADJUST_AUDIO_VOLUME:
				sexp_adjust_audio_volume(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_DISABLE:
				sexp_hud_disable(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_DISABLE_EXCEPT_MESSAGES:
				sexp_hud_disable_except_messages(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_TEXT:
				sexp_hud_set_text(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_XSTR:
				sexp_hud_set_xstr(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_TEXT_NUM:
				sexp_hud_set_text_num(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_COORDS:
				sexp_hud_set_coords(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_FRAME:
				sexp_hud_set_frame(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_COLOR:
				sexp_hud_set_color(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_RESET_COLOR:
				sexp_hud_reset_color(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_MAX_TARGETING_RANGE:
				sexp_hud_set_max_targeting_range(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_DISPLAY_GAUGE:
				sexp_hud_display_gauge(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_SET_MESSAGE:
				sexp_hud_set_message(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_FORCE_SENSOR_STATIC:
				sexp_hud_force_static(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_HUD_FORCE_EMP_EFFECT:
				sexp_hud_force_emp(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_SQUADRON_WINGS:
				sexp_set_squadron_wings(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_PLAYER_USE_AI:
			case OP_PLAYER_NOT_USE_AI:
				sexp_player_use_ai(op_num == OP_PLAYER_USE_AI);
				sexp_val = SEXP_TRUE;
				break;

			// LuytenKy
			case OP_SET_PLAYER_TARGET:
				sexp_set_player_target(node);
				sexp_val = SEXP_TRUE;
				break;
			case OP_CLEAR_PLAYER_TARGET:
				sexp_clear_player_target();
				sexp_val = SEXP_TRUE;
				break;

			// Kestrellius
			case OP_SET_FRIENDLY_DAMAGE_CAPS:
				sexp_set_friendly_damage_caps(node);
				sexp_val = SEXP_TRUE;
				break; 

			//Karajorma
			case OP_ALLOW_TREASON:
				sexp_allow_treason(node);
				sexp_val = SEXP_TRUE;
				break; 

			//Karajorma
			case OP_SET_PLAYER_ORDERS:
				sexp_set_player_orders(node);
				sexp_val = SEXP_TRUE;
				break; 

			//MjnMixael
			case OP_SET_ORDER_ALLOWED_TARGET:
				sexp_set_order_allowed_for_target(node);
				sexp_val = SEXP_TRUE;
				break;

			//MjnMixael
			case OP_ENABLE_GENERAL_ORDERS:
				sexp_enable_or_validate_general_orders(node, true);
				sexp_val = SEXP_TRUE;
				break;

			//MjnMixael
			case OP_VALIDATE_GENERAL_ORDERS:
				sexp_enable_or_validate_general_orders(node, false);
				sexp_val = SEXP_TRUE;
				break;

			// MjnMixael
			case OP_CREATE_BOLT:
				sexp_create_bolt(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_EXPLOSION_EFFECT:
				sexp_explosion_effect(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_WARP_EFFECT:
				sexp_warp_effect(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SEND_MESSAGE:
				sexp_send_message(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SEND_BUILTIN_MESSAGE:
				sexp_send_builtin_message(node);
				sexp_val = SEXP_TRUE;
				break;

			// Karajorma
			case OP_ENABLE_BUILTIN_MESSAGES:
			case OP_DISABLE_BUILTIN_MESSAGES:
				sexp_toggle_builtin_messages (node, op_num == OP_ENABLE_BUILTIN_MESSAGES);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_PERSONA:
				sexp_set_persona (node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_MISSION_MOOD:
				sexp_set_mission_mood (node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SEND_MESSAGE_LIST:
			case OP_SEND_MESSAGE_CHAIN:
				sexp_send_message_list(node, op_num == OP_SEND_MESSAGE_CHAIN);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SEND_RANDOM_MESSAGE:
				sexp_send_random_message(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SELF_DESTRUCT:
				sexp_self_destruct(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NEXT_MISSION:
				sexp_next_mission(node);
				sexp_val = SEXP_TRUE;
				break;
				
			case OP_END_OF_CAMPAIGN:
				sexp_end_of_campaign(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_END_CAMPAIGN:
				sexp_end_campaign(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SABOTAGE_SUBSYSTEM:
				sexp_sabotage_subsystem(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_REPAIR_SUBSYSTEM:
				sexp_repair_subsystem(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_SUBSYSTEM_STRNGTH:
				sexp_set_subsystem_strength(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_DESTROY_SUBSYS_INSTANTLY:
				sexp_destroy_subsys_instantly(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_INVALIDATE_GOAL:
			case OP_VALIDATE_GOAL:
				sexp_change_goal_validity( node, (op_num==OP_INVALIDATE_GOAL?0:1) );
				sexp_val = SEXP_TRUE;
				break;

			case OP_TRANSFER_CARGO:
				sexp_transfer_cargo(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_EXCHANGE_CARGO:
				sexp_exchange_cargo(node);
				sexp_val = SEXP_TRUE;
				break;


			case OP_JETTISON_CARGO_DELAY:
			case OP_JETTISON_CARGO_NEW:
				sexp_jettison_cargo(node, (op_num==OP_JETTISON_CARGO_NEW));
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_DOCKED:
				sexp_set_docked(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CARGO_NO_DEPLETE:
				sexp_cargo_no_deplete(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_SCANNED:	// Goober5000
			case OP_SET_UNSCANNED:
				sexp_set_scanned_unscanned(node, op_num == OP_SET_SCANNED);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_SPECIAL_WARPOUT_NAME:
				sexp_special_warpout_name(node);
				sexp_val = SEXP_TRUE;
				break;

			//-WMC
			case OP_MISSION_SET_NEBULA:
				sexp_mission_set_nebula(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_MISSION_SET_SUBSPACE:
				sexp_mission_set_subspace(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CHANGE_BACKGROUND:
				sexp_change_background(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ADD_SUN_BITMAP:
			case OP_ADD_SUN_BITMAP_NEW:
			case OP_ADD_BACKGROUND_BITMAP:
			case OP_ADD_BACKGROUND_BITMAP_NEW:
				sexp_add_background_bitmap(node, op_num == OP_ADD_SUN_BITMAP || op_num == OP_ADD_SUN_BITMAP_NEW, op_num == OP_ADD_SUN_BITMAP_NEW || op_num == OP_ADD_BACKGROUND_BITMAP_NEW);
				sexp_val = SEXP_TRUE;
				break;

			case OP_REMOVE_SUN_BITMAP:
			case OP_REMOVE_BACKGROUND_BITMAP:
				sexp_remove_background_bitmap(node, op_num == OP_REMOVE_SUN_BITMAP);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NEBULA_CHANGE_STORM:
				sexp_nebula_change_storm(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NEBULA_TOGGLE_POOF:
			case OP_NEBULA_SET_POOFS:
				sexp_nebula_set_poofs(node, op_num == OP_NEBULA_TOGGLE_POOF);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NEBULA_FADE_POOF:
			case OP_NEBULA_FADE_POOFS:
				sexp_nebula_fade_poofs(node, op_num == OP_NEBULA_FADE_POOF);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NEBULA_CHANGE_PATTERN:
				sexp_nebula_change_pattern(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NEBULA_CHANGE_FOG_COLOR:
				sexp_nebula_change_fog_color(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NEBULA_SET_RANGE:
				sexp_nebula_set_range(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_VOLUMETRICS_TOGGLE:
				sexp_volumetrics_toggle(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TOGGLE_ASTEROID_FIELD:
				sexp_toggle_asteroid_field(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_ASTEROID_FIELD:
				sexp_set_asteroid_field(node, false);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_DEBRIS_FIELD:
				sexp_set_debris_field(node, false);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CONFIG_ASTEROID_FIELD:
				sexp_set_asteroid_field(node, true);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CONFIG_DEBRIS_FIELD:
				sexp_set_debris_field(node, true);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CONFIG_FIELD_TARGETS:
				sexp_config_field_targets(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_MOTION_DEBRIS:
				sexp_set_motion_debris_type(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_END_MISSION:
				sexp_end_mission(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_SET_DEBRIEFING_TOGGLED:
				sexp_set_debriefing_toggled(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_SET_DEBRIEFING_PERSONA:
				sexp_set_debriefing_persona(node);
				sexp_val = SEXP_TRUE;
				break;

			// MjnMixael
			case OP_SET_TRAITOR_OVERRIDE:
				sexp_set_traitor_override(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_FORCE_JUMP:
				sexp_force_jump();
				sexp_val = SEXP_TRUE;
				break;

				// sexpressions for setting flag for good/bad time for someone to rearm
			case OP_GOOD_REARM_TIME:
			case OP_BAD_REARM_TIME:
				sexp_good_bad_time_to_rearm(node, op_num == OP_GOOD_REARM_TIME);
				sexp_val = SEXP_TRUE;
				break;

			case OP_GRANT_PROMOTION:
				sexp_grant_promotion();
				sexp_val = SEXP_TRUE;
				break;

			case OP_GRANT_MEDAL:
				sexp_grant_medal(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_VAPORIZE:
			case OP_SHIP_NO_VAPORIZE:
				sexp_ships_vaporize( node, (op_num == OP_SHIP_VAPORIZE) );
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_EXPLOSION_OPTION:
				sexp_set_explosion_option(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_DONT_COLLIDE_INVISIBLE:
			case OP_COLLIDE_INVISIBLE:
				sexp_dont_collide_invisible( node, (op_num == OP_DONT_COLLIDE_INVISIBLE) );
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_MOBILE:
			case OP_SET_IMMOBILE:
				sexp_set_immobile(node, (op_num == OP_SET_IMMOBILE));
				sexp_val = SEXP_TRUE;
				break;

			case OP_IGNORE_KEY:
				sexp_ignore_key(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000 - sigh, was this messed up all along?
			case OP_WARP_BROKEN:
			case OP_WARP_NOT_BROKEN:
			case OP_WARP_NEVER:
			case OP_WARP_ALLOWED:
				sexp_deal_with_warp( node, (op_num==OP_WARP_BROKEN) || (op_num==OP_WARP_NOT_BROKEN),
					(op_num==OP_WARP_BROKEN) || (op_num==OP_WARP_NEVER) );
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_SET_SUBSPACE_DRIVE:
				sexp_set_subspace_drive(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_GOOD_PRIMARY_TIME:
				sexp_good_primary_time(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_GOOD_SECONDARY_TIME:
				sexp_good_secondary_time(node);
				sexp_val = SEXP_TRUE;
				break;

			// sexpressions to allow shpis/weapons during the course of a mission
			case OP_ALLOW_SHIP:
				sexp_allow_ship(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ALLOW_WEAPON:
				sexp_allow_weapon(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TECH_ADD_SHIP:
				sexp_tech_add_ship(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TECH_ADD_WEAPON:
				sexp_tech_add_weapon(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TECH_ADD_INTEL:
			case OP_TECH_ADD_INTEL_XSTR:
				sexp_tech_toggle_intel(node, true, op_num == OP_TECH_ADD_INTEL_XSTR);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TECH_REMOVE_INTEL:
			case OP_TECH_REMOVE_INTEL_XSTR:
				sexp_tech_toggle_intel(node, false, op_num == OP_TECH_REMOVE_INTEL_XSTR);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TECH_RESET_TO_DEFAULT:
				sexp_tech_reset_to_default();
				sexp_val = SEXP_TRUE;
				break;

			case OP_CHANGE_PLAYER_SCORE:
				sexp_change_player_score(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CHANGE_TEAM_SCORE:
				sexp_change_team_score(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_RED_ALERT:
				sexp_red_alert();
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_OBJECT_SPEED_X:
			case OP_SET_OBJECT_SPEED_Y:
			case OP_SET_OBJECT_SPEED_Z:
				sexp_set_object_speed(node, op_num - OP_SET_OBJECT_SPEED_X);
				sexp_val = SEXP_TRUE;
				break;

			case OP_GET_OBJECT_SPEED_X:
			case OP_GET_OBJECT_SPEED_Y:
			case OP_GET_OBJECT_SPEED_Z:
				sexp_val = sexp_get_object_speed(node, op_num - OP_GET_OBJECT_SPEED_X);
				break;

			case OP_GET_OBJECT_X:
			case OP_GET_OBJECT_Y:
			case OP_GET_OBJECT_Z:
				sexp_val = sexp_get_object_coordinate(node, op_num - OP_GET_OBJECT_X);
				break;

			case OP_GET_OBJECT_PITCH:
			case OP_GET_OBJECT_BANK:
			case OP_GET_OBJECT_HEADING:
				sexp_val = sexp_get_object_angle(node, op_num - OP_GET_OBJECT_PITCH);
				break;

			case OP_SET_OBJECT_POSITION:
				sexp_set_object_position(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_OBJECT_ORIENTATION:
				sexp_set_object_orientation(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_OBJECT_FACING:
			case OP_SET_OBJECT_FACING_OBJECT:
				sexp_set_object_facing(node, op_num == OP_SET_OBJECT_FACING_OBJECT);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SHIP_MANEUVER:
			case OP_SHIP_ROT_MANEUVER:
			case OP_SHIP_LAT_MANEUVER:
				sexp_set_ship_maneuver(node, op_num);
				sexp_val = SEXP_TRUE;
				break;

			// training operators
			case OP_KEY_PRESSED:
				sexp_val = sexp_key_pressed(node);
				break;

			case OP_SPECIAL_CHECK:
				sexp_val = sexp_special_training_check(node);
				break;

			case OP_KEY_RESET:
				sexp_key_reset(node);
				sexp_val = SEXP_KNOWN_TRUE;  // only do it first time in repeating events.
				break;

			case OP_KEY_RESET_MULTIPLE:
				sexp_key_reset(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_MISSILE_LOCKED:
				sexp_val = sexp_missile_locked(node);
				break;

			case OP_TARGETED:
				sexp_val = sexp_targeted(node);
				break;

			case OP_NODE_TARGETED:
				sexp_val = sexp_node_targeted(node);
				break;

			case OP_SPEED:
				sexp_val = sexp_speed(node);
				break;

			case OP_GET_THROTTLE_SPEED:
				sexp_val = sexp_get_throttle_speed(node);
				break;

			case OP_SET_PLAYER_THROTTLE_SPEED:
				sexp_set_player_throttle_speed(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_PRIMARIES_DEPLETED:
			case OP_SECONDARIES_DEPLETED:
				sexp_val = sexp_weapons_depleted(node, op_num == OP_PRIMARIES_DEPLETED);
				break;

			case OP_FACING:
				sexp_val = sexp_facing(node);
				break;

			case OP_IS_FACING:
				sexp_val = sexp_is_facing(node);
				break;

			case OP_FACING2:
				sexp_val = sexp_facing2(node);
				break;

			case OP_ORDER:
				sexp_val = sexp_order(node);
				break;

			case OP_QUERY_ORDERS:
				sexp_val = sexp_query_orders(node);
				break;

			case OP_TIME_TO_GOAL:
				sexp_val = sexp_time_to_goal(node);
				break;

			case OP_SET_HUD_TIME_PAD:
				sexp_set_hud_time_pad(node);
				sexp_val = SEXP_TRUE;
				break;


			// Karajorma
			case OP_RESET_ORDERS:
				sexp_reset_orders(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_WAYPOINT_MISSED:
				sexp_val = sexp_waypoint_missed();
				break;

			case OP_WAYPOINT_TWICE:
				sexp_val = sexp_waypoint_twice();
				break;

			case OP_PATH_FLOWN:
				sexp_val = sexp_path_flown();
				break;

			case OP_TRAINING_MSG:
				sexp_send_training_message(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_FLASH_HUD_GAUGE:
				sexp_flash_hud_gauge(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_TRAINING_CONTEXT_FLY_PATH:
				sexp_set_training_context_fly_path(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_TRAINING_CONTEXT_SPEED:
				sexp_set_training_context_speed(node);
				sexp_val = SEXP_TRUE;
				break;
			
			// Karajorma
			case OP_STRING_TO_INT:
				sexp_val = sexp_string_to_int(node);
				break;

			// Goober5000
			case OP_INT_TO_STRING:
				sexp_int_to_string(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_STRING_CONCATENATE:
				sexp_string_concatenate(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_STRING_CONCATENATE_BLOCK:
				sexp_string_concatenate_block(node);
				sexp_val = SEXP_TRUE;
				break;

				// Goober5000
			case OP_STRING_GET_SUBSTRING:
				sexp_string_get_substring(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_STRING_SET_SUBSTRING:
				sexp_string_set_substring(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_STRING_GET_LENGTH:
				sexp_val = sexp_string_get_length(node);
				break;

			// Karajorma/jg18
			case OP_IS_CONTAINER_EMPTY:
			case OP_GET_CONTAINER_SIZE:
			case OP_LIST_HAS_DATA:
			case OP_LIST_DATA_INDEX:
			case OP_MAP_HAS_KEY:
			case OP_MAP_HAS_DATA_ITEM:
				sexp_val = sexp_container_eval_status_sexp(op_num, node);
				break;

			// Karajorma
			case OP_DEBUG:
				sexp_debug(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NOT_AN_OP: // zero represents a non-operator
				return sexp_atoi(cur_node);

			case OP_NOP:
				sexp_val = SEXP_TRUE;
				break;

			case OP_BEAM_FIRE:
			case OP_BEAM_FIRE_COORDS:
				sexp_beam_fire(node, op_num == OP_BEAM_FIRE_COORDS);
				sexp_val = SEXP_TRUE;
				break;

			case OP_BEAM_FLOATING_FIRE:
				sexp_beam_floating_fire(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_IS_TAGGED:
				sexp_val = sexp_is_tagged(node);
				break;

			case OP_IS_PLAYER:
				sexp_val = sexp_is_player(node);
				break;

			case OP_NUM_KILLS:
			case OP_NUM_ASSISTS:
			case OP_SHIP_SCORE: 
			case OP_SHIP_DEATHS: 
			case OP_RESPAWNS_LEFT:
				sexp_val = sexp_return_player_data(node, op_num);
				break;

			case OP_SET_RESPAWNS:
				sexp_set_respawns(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ADD_REMOVE_HOTKEY:
				sexp_add_remove_hotkey(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CLEAR_WEAPONS:
			case OP_CLEAR_DEBRIS:
				sexp_clear_weapons_or_debris(node, op_num);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NUM_TYPE_KILLS:
				sexp_val = sexp_num_type_kills(node);
				break;

			case OP_NUM_CLASS_KILLS:
				sexp_val = sexp_num_class_kills(node);
				break;

			case OP_BEAM_FREE:
			case OP_BEAM_LOCK:
			case OP_TURRET_FREE:
			case OP_TURRET_LOCK:
				sexp_beam_or_turret_free_or_lock(node, op_num == OP_BEAM_FREE || op_num == OP_BEAM_LOCK, op_num == OP_BEAM_FREE || op_num == OP_TURRET_FREE);
				sexp_val = SEXP_TRUE;
				break;

			case OP_BEAM_FREE_ALL:
			case OP_BEAM_LOCK_ALL:
			case OP_TURRET_FREE_ALL:
			case OP_TURRET_LOCK_ALL:
				sexp_beam_or_turret_free_or_lock_all(node, op_num == OP_BEAM_FREE_ALL || op_num == OP_BEAM_LOCK_ALL, op_num == OP_BEAM_FREE_ALL || op_num == OP_TURRET_FREE_ALL);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TURRET_CHANGE_WEAPON:
				sexp_val = SEXP_TRUE;
				sexp_turret_change_weapon(node);
				break;

			case OP_TURRET_SET_DIRECTION_PREFERENCE:
				sexp_val = SEXP_TRUE;
				sexp_turret_set_direction_preference(node);
				break;

			case OP_TURRET_SET_RATE_OF_FIRE:
				sexp_val = SEXP_TRUE;
				sexp_turret_set_rate_of_fire(node);
				break;

			case OP_TURRET_SET_OPTIMUM_RANGE:
				sexp_val = SEXP_TRUE;
				sexp_turret_set_optimum_range(node);
				break;

			case OP_TURRET_SET_TARGET_PRIORITIES:
				sexp_val = SEXP_TRUE;
				sexp_turret_set_target_priorities(node);
				break;

			case OP_TURRET_SET_TARGET_ORDER:
				sexp_val = SEXP_TRUE;
				sexp_turret_set_target_order(node);
				break;

			case OP_TURRET_SET_FORCED_SUBSYS_TARGET:
			case OP_TURRET_SET_FORCED_TARGET:
				sexp_val = SEXP_TRUE;
				sexp_turret_set_forced_target(node, op_num == OP_TURRET_SET_FORCED_SUBSYS_TARGET);
				break;

			case OP_TURRET_CLEAR_FORCED_TARGET:
				sexp_val = SEXP_TRUE;
				sexp_turret_clear_forced_target(node);
				break;

			case OP_TURRET_SET_INACCURACY:
				sexp_val = SEXP_TRUE;
				sexp_turret_set_inaccuracy(node);
				break;

			case OP_SET_ARMOR_TYPE:
				sexp_val = SEXP_TRUE;
				sexp_set_armor_type(node);
				break;

			case OP_WEAPON_SET_DAMAGE_TYPE:
				sexp_val = SEXP_TRUE;
				sexp_weapon_set_damage_type(node);
				break;

			case OP_SHIP_SET_DAMAGE_TYPE:
				sexp_val = SEXP_TRUE;
				sexp_ship_set_damage_type(node);
				break;

			case OP_SHIP_SHOCKWAVE_SET_DAMAGE_TYPE:
				sexp_val = SEXP_TRUE;
				sexp_ship_shockwave_set_damage_type(node);
				break;

			case OP_FIELD_SET_DAMAGE_TYPE:
				sexp_val = SEXP_TRUE;
				sexp_field_set_damage_type(node);
				break;

			case OP_SHIP_TURRET_TARGET_ORDER:
				sexp_val = SEXP_TRUE;
				sexp_ship_turret_target_order(node);
				break;

			case OP_ADD_REMOVE_ESCORT:
				sexp_val = SEXP_TRUE;
				sexp_add_remove_escort(node);
				break;
			
			case OP_DAMAGED_ESCORT_LIST:
				sexp_val = SEXP_TRUE;
				sexp_damage_escort_list(node);
				break;

			case OP_DAMAGED_ESCORT_LIST_ALL:
				sexp_val = SEXP_TRUE;
				sexp_damage_escort_list_all(node);
				break;

			case OP_AWACS_SET_RADIUS:
				sexp_val = SEXP_TRUE;
				sexp_awacs_set_radius(node);
				break;

			case OP_PRIMITIVE_SENSORS_SET_RANGE:
				sexp_primitive_sensors_set_range(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CAP_WAYPOINT_SPEED:
				sexp_val = SEXP_TRUE;
				sexp_cap_waypoint_speed(node);
				break;

			case OP_SET_WING_FORMATION:
				sexp_set_wing_formation(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TURRET_TAGGED_ONLY_ALL:
			case OP_TURRET_TAGGED_CLEAR_ALL:
				sexp_turret_tagged_only_or_clear_all(node, op_num == OP_TURRET_TAGGED_ONLY_ALL);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SUBSYS_SET_RANDOM:
				sexp_val = SEXP_TRUE;
				sexp_subsys_set_random(node);
				break;

			case OP_SUPERNOVA_START:
				sexp_val = SEXP_TRUE;
				sexp_supernova_start(node);
				break;

			case OP_SUPERNOVA_STOP:
				sexp_val = SEXP_TRUE;
				sexp_supernova_stop(node);
				break;

			case OP_GET_SUPERNOVA_STAGE:
				return sexp_get_supernova_stage(node);
				break;

			case OP_OVERRIDE_MOTION_DEBRIS:
				sexp_val = SEXP_TRUE;
				sexp_set_motion_debris(node);
				break;

			case OP_WEAPON_RECHARGE_PCT:
			case OP_SHIELD_RECHARGE_PCT:
			case OP_ENGINE_RECHARGE_PCT:
				sexp_val = sexp_gse_recharge_pct(node, op_num);
				break;

			case OP_GET_ETS_VALUE:
				sexp_val = sexp_get_ets_value(node);
				break;

			case OP_SET_ETS_VALUES:
				sexp_val = SEXP_TRUE;
				sexp_set_ets_values(node);
				break;

			case OP_GET_POWER_OUTPUT:
				sexp_val = sexp_get_power_output(node);
				break;

			case OP_SHIELD_QUAD_LOW:
				sexp_val = sexp_shield_quad_low(node);
				break;

			case OP_PRIMARY_AMMO_PCT:
			case OP_SECONDARY_AMMO_PCT:
			case OP_GET_PRIMARY_AMMO:
			case OP_GET_SECONDARY_AMMO:
				sexp_val = sexp_get_ammo(node, false, op_num == OP_PRIMARY_AMMO_PCT || op_num == OP_GET_PRIMARY_AMMO, op_num == OP_PRIMARY_AMMO_PCT || op_num == OP_SECONDARY_AMMO_PCT);
				break;

			case OP_TURRET_GET_PRIMARY_AMMO:
			case OP_TURRET_GET_SECONDARY_AMMO:
				sexp_val = sexp_get_ammo(node, true, op_num == OP_TURRET_GET_PRIMARY_AMMO, false);
				break;

			// Karajorma
			case OP_SET_PRIMARY_AMMO:
			case OP_SET_SECONDARY_AMMO:
				sexp_set_ammo(node, false, op_num == OP_SET_PRIMARY_AMMO);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TURRET_SET_PRIMARY_AMMO:
			case OP_TURRET_SET_SECONDARY_AMMO:
				sexp_set_ammo(node, true, op_num == OP_TURRET_SET_PRIMARY_AMMO);
				sexp_val = SEXP_TRUE;
				break;

			// Karajorma
			case OP_GET_NUM_COUNTERMEASURES:
				sexp_val = sexp_get_countermeasures(node);
				break;

			case OP_IS_PRIMARY_SELECTED:
			case OP_IS_SECONDARY_SELECTED:
				sexp_val = sexp_is_weapon_selected(node, op_num == OP_IS_PRIMARY_SELECTED);
				break;

			// Goober5000
			case OP_SET_SUPPORT_SHIP:
				sexp_set_support_ship(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_SET_ARRIVAL_INFO:
				sexp_set_arrival_info(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_SET_DEPARTURE_INFO:
				sexp_set_departure_info(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_CHANGE_SHIP_CLASS:
				sexp_change_ship_class(node);
				sexp_val = SEXP_TRUE;
				break;

			// Goober5000
			case OP_SHIP_COPY_DAMAGE:
				sexp_ship_copy_damage(node);
				sexp_val = SEXP_TRUE;
				break;

			//-Bobboau
			case OP_ACTIVATE_GLOW_POINTS:
			case OP_DEACTIVATE_GLOW_POINTS:
				sexp_activate_deactivate_glow_points(node, op_num == OP_ACTIVATE_GLOW_POINTS);
				sexp_val = SEXP_TRUE;
				break;

			//-Bobboau
			case OP_ACTIVATE_GLOW_MAPS:
			case OP_DEACTIVATE_GLOW_MAPS:
				sexp_activate_deactivate_glow_maps(node, op_num == OP_ACTIVATE_GLOW_MAPS);
				sexp_val = SEXP_TRUE;
				break;

			//-Bobboau
			case OP_ACTIVATE_GLOW_POINT_BANK:
			case OP_DEACTIVATE_GLOW_POINT_BANK:
				sexp_activate_deactivate_glow_point_bank(node, op_num == OP_ACTIVATE_GLOW_POINT_BANK);
				sexp_val = SEXP_TRUE;
				break;

			// taylor
			case OP_SET_SKYBOX_MODEL:
				sexp_set_skybox_model(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_SKYBOX_ORIENT:
				sexp_set_skybox_orientation(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_SKYBOX_ALPHA:
				sexp_set_skybox_alpha(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TURRET_TAGGED_SPECIFIC:
			case OP_TURRET_TAGGED_CLEAR_SPECIFIC:
				sexp_turret_tagged_or_clear_specific(node, op_num == OP_TURRET_TAGGED_SPECIFIC);
				sexp_val = SEXP_TRUE;
				break;

			case OP_LOCK_ROTATING_SUBSYSTEM:
			case OP_FREE_ROTATING_SUBSYSTEM:
			case OP_LOCK_TRANSLATING_SUBSYSTEM:
			case OP_FREE_TRANSLATING_SUBSYSTEM:
				sexp_set_subsys_motion_lock_free(node, op_num == OP_LOCK_ROTATING_SUBSYSTEM || op_num == OP_FREE_ROTATING_SUBSYSTEM, op_num == OP_LOCK_ROTATING_SUBSYSTEM || op_num == OP_LOCK_TRANSLATING_SUBSYSTEM);
				sexp_val = SEXP_TRUE;
				break;

			case OP_REVERSE_ROTATING_SUBSYSTEM:
			case OP_REVERSE_TRANSLATING_SUBSYSTEM:
				sexp_reverse_moving_subsystem(node, op_num == OP_REVERSE_ROTATING_SUBSYSTEM);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ROTATING_SUBSYS_SET_TURN_TIME:
			case OP_TRANSLATING_SUBSYS_SET_SPEED:
				sexp_moving_subsys_set_turn_time_or_speed(node, op_num == OP_ROTATING_SUBSYS_SET_TURN_TIME);
				sexp_val = SEXP_TRUE;
				break;

			case OP_TRIGGER_SUBMODEL_ANIMATION:
				sexp_trigger_submodel_animation(node);
				sexp_val = SEXP_TRUE;
				break;

			// Karajorma
			case OP_SET_PRIMARY_WEAPON:
			case OP_SET_SECONDARY_WEAPON:
				sexp_set_weapon(node, op_num == OP_SET_PRIMARY_WEAPON);
				sexp_val = SEXP_TRUE;
				break;

			// Karajorma
			case OP_SET_NUM_COUNTERMEASURES:
				sexp_set_countermeasures(node);
				sexp_val = SEXP_TRUE;
				break;

			// Karajorma
			case OP_LOCK_PRIMARY_WEAPON:
			case OP_UNLOCK_PRIMARY_WEAPON:
				sexp_deal_with_primary_lock(node, op_num == OP_LOCK_PRIMARY_WEAPON);
				sexp_val = SEXP_TRUE;
				break;

			case OP_LOCK_SECONDARY_WEAPON:
			case OP_UNLOCK_SECONDARY_WEAPON:
				sexp_deal_with_secondary_lock(node, op_num == OP_LOCK_SECONDARY_WEAPON);
				sexp_val = SEXP_TRUE;
				break;

			// KeldorKatarn
			case OP_LOCK_AFTERBURNER:
			case OP_UNLOCK_AFTERBURNER:
				sexp_deal_with_afterburner_lock(node, op_num == OP_LOCK_AFTERBURNER);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_AFTERBURNER_ENERGY: 
			case OP_SET_WEAPON_ENERGY:
			case OP_SET_SHIELD_ENERGY:
				sexp_set_energy_pct(node, op_num);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_AMBIENT_LIGHT: 
				sexp_set_ambient_light(node); 
				sexp_val = SEXP_TRUE;
				break;

			case OP_SET_POST_EFFECT:
				sexp_set_post_effect(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_RESET_POST_EFFECTS:
				sexp_reset_post_effects();
				sexp_val = SEXP_TRUE;
				break;

			case OP_TURRET_FIRED_SINCE:
				sexp_val = sexp_turret_fired_delay(node);
				break;

			case OP_PRIMARY_FIRED_SINCE:
			case OP_SECONDARY_FIRED_SINCE:
				sexp_val = sexp_weapon_fired_delay(node, op_num); 
				break;

			case OP_HAS_PRIMARY_WEAPON:
			case OP_HAS_SECONDARY_WEAPON:
			case OP_TURRET_HAS_PRIMARY_WEAPON:
			case OP_TURRET_HAS_SECONDARY_WEAPON:
				sexp_val = sexp_has_weapon(node, op_num);
				break;

			case OP_DIRECTIVE_VALUE:
				sexp_val = sexp_directive_value(node);
				break;

			case OP_GET_HOTKEY:
				sexp_val = sexp_get_hotkey(node);
				break;

			case OP_CHANGE_SUBSYSTEM_NAME:
				sexp_change_subsystem_name(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_NUM_SHIPS_IN_BATTLE:	// phreak
				sexp_val = sexp_num_ships_in_battle(node);
				break;

			// Karajorma
			case OP_NUM_SHIPS_IN_WING:
				sexp_val=sexp_num_ships_in_wing(node);
				break;

			case OP_CURRENT_SPEED:
				sexp_val = sexp_current_speed(node);
				break;

			case OP_NAV_IS_VISITED: //kazan
				sexp_val = is_nav_visited(node);
				break;

			case OP_NAV_DISTANCE: //kazan
				sexp_val = distance_to_nav(node);
				break;

			case OP_NAV_ADD_WAYPOINT: //kazan
				sexp_val = SEXP_TRUE;
				add_nav_waypoint(node);
				break;

			case OP_NAV_ADD_SHIP: //kazan
				sexp_val = SEXP_TRUE;
				add_nav_ship(node);
				break;

			case OP_NAV_DEL: //kazan
				sexp_val = SEXP_TRUE;
				del_nav(node);
				break;

			case OP_NAV_HIDE: //kazan
			case OP_NAV_UNHIDE: //kazan
				sexp_val = SEXP_TRUE;
				hide_unhide_nav(node, op_num == OP_NAV_HIDE);
				break;

			case OP_NAV_RESTRICT: //kazan
			case OP_NAV_UNRESTRICT: //kazan
				sexp_val = SEXP_TRUE;
				restrict_unrestrict_nav(node, op_num == OP_NAV_RESTRICT);
				break;

			case OP_NAV_SET_VISITED: //kazan
			case OP_NAV_UNSET_VISITED: //kazan
				sexp_val = SEXP_TRUE;
				set_unset_nav_visited(node, op_num == OP_NAV_SET_VISITED);
				break;

			case OP_NAV_SET_CARRY: //kazan
			case OP_NAV_UNSET_CARRY: //kazan
				sexp_val = SEXP_TRUE;
				set_unset_nav_carry_status(node, op_num == OP_NAV_SET_CARRY);
				break;

			case OP_NAV_SET_NEEDSLINK:
			case OP_NAV_UNSET_NEEDSLINK:
				sexp_val = SEXP_TRUE;
				set_unset_nav_needslink(node, op_num == OP_NAV_SET_NEEDSLINK);
				break;

			case OP_NAV_ISLINKED:
				sexp_val = is_nav_linked(node);
				break;
			
			case OP_NAV_USEAP:
				sexp_val = SEXP_TRUE;
				set_use_ap(node);
				break;

			case OP_NAV_USECINEMATICS:
				sexp_val = SEXP_TRUE;
				set_use_ap_cinematics(node);
				break;
			
			//Talon1024
			case OP_NAV_SELECT:
			case OP_NAV_UNSELECT:
				sexp_val = SEXP_TRUE;
				select_unselect_nav(node, op_num == OP_NAV_SELECT);
				break;

			case OP_NAV_SET_COLOR:
			case OP_NAV_SET_VISITED_COLOR:
				set_nav_color(node, op_num == OP_NAV_SET_VISITED_COLOR);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SCRAMBLE_MESSAGES:
			case OP_UNSCRAMBLE_MESSAGES:
				sexp_scramble_messages(node, op_num == OP_SCRAMBLE_MESSAGES );
				sexp_val = SEXP_TRUE;
				break;

			case OP_CUTSCENES_SET_CUTSCENE_BARS:
			case OP_CUTSCENES_UNSET_CUTSCENE_BARS:
				sexp_val = SEXP_TRUE;
				sexp_toggle_cutscene_bars(node, op_num == OP_CUTSCENES_SET_CUTSCENE_BARS);
				break;

			case OP_CUTSCENES_FADE_IN:
			case OP_CUTSCENES_FADE_OUT:
				sexp_val = SEXP_TRUE;
				sexp_fade(node, op_num == OP_CUTSCENES_FADE_IN);
				break;

			case OP_CUTSCENES_SET_CAMERA:
				sexp_val = SEXP_TRUE;
				sexp_set_camera(node);
				break;
			case OP_CUTSCENES_SET_CAMERA_FACING:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_facing(node);
				break;
			case OP_CUTSCENES_SET_CAMERA_FACING_OBJECT:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_facing_object(node);
				break;
			case OP_CUTSCENES_SET_CAMERA_FOV:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_fov(node);
				break;
			case OP_CUTSCENES_SET_CAMERA_HOST:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_host(node);
				break;
			case OP_CUTSCENES_SET_CAMERA_POSITION:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_position(node);
				break;
			case OP_CUTSCENES_SET_CAMERA_ROTATION:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_rotation(node);
				break;
			case OP_CUTSCENES_SET_CAMERA_TARGET:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_target(node);
				break;
			case OP_CUTSCENES_SET_FOV:
				sexp_val = SEXP_TRUE;
				sexp_set_fov(node);
				break;
			case OP_CUTSCENES_GET_FOV:	
				sexp_val = sexp_get_fov();
				break;
			case OP_CUTSCENES_RESET_FOV:
				sexp_val = SEXP_TRUE;
				sexp_reset_fov();
				break;
			case OP_CUTSCENES_RESET_CAMERA:
				sexp_val = SEXP_TRUE;
				sexp_reset_camera(node);
				break;
			case OP_CUTSCENES_SHOW_SUBTITLE:
				sexp_val = SEXP_TRUE;
				sexp_show_subtitle(node);
				break;
			case OP_CUTSCENES_SHOW_SUBTITLE_TEXT:
				sexp_val = SEXP_TRUE;
				sexp_show_subtitle_text(node);
				break;
			case OP_CUTSCENES_SHOW_SUBTITLE_IMAGE:
				sexp_val = SEXP_TRUE;
				sexp_show_subtitle_image(node);
				break;
			case OP_CUTSCENES_SET_TIME_COMPRESSION:
				sexp_val = SEXP_TRUE;
				sexp_set_time_compression(node);
				break;
			case OP_CUTSCENES_RESET_TIME_COMPRESSION:
				sexp_val = SEXP_TRUE;
				sexp_reset_time_compression();
				break;
			case OP_CUTSCENES_FORCE_PERSPECTIVE:
				sexp_val = SEXP_TRUE;
				sexp_force_perspective(node);
				break;
			case OP_ALLOW_PHOTO_MODE:
				sexp_val = SEXP_TRUE;
				sexp_allow_photo_mode(node);
				break;

			case OP_SET_CAMERA_SHUDDER:
				sexp_val = SEXP_TRUE;
				sexp_set_camera_shudder(node);
				break;

			case OP_JUMP_NODE_SET_JUMPNODE_NAME: //CommanderDJ
				sexp_val = SEXP_TRUE;
				sexp_set_jumpnode_name(node);
				break;

			case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME:
				sexp_val = SEXP_TRUE;
				sexp_set_jumpnode_display_name(node);
				break;

			case OP_JUMP_NODE_SET_JUMPNODE_COLOR:
				sexp_val = SEXP_TRUE;
				sexp_set_jumpnode_color(node);
				break;
			case OP_JUMP_NODE_SET_JUMPNODE_MODEL:
				sexp_val = SEXP_TRUE;
				sexp_set_jumpnode_model(node);
				break;
			case OP_JUMP_NODE_SHOW_JUMPNODE:
			case OP_JUMP_NODE_HIDE_JUMPNODE:
				sexp_show_hide_jumpnode(node, op_num == OP_JUMP_NODE_SHOW_JUMPNODE);
				sexp_val = SEXP_TRUE;
				break;

			case OP_SCRIPT_EVAL_BOOL:
				sexp_val = sexp_script_eval(node, OPR_BOOL);
				break;

			case OP_SCRIPT_EVAL_NUM:
				sexp_val = sexp_script_eval(node, OPR_NUMBER);
				break;

			case OP_SCRIPT_EVAL_STRING:
				sexp_val = sexp_script_eval(node, OPR_STRING);
				break;

			case OP_SCRIPT_EVAL:
				sexp_val = sexp_script_eval(node, OPR_NULL);
				break;
				
			case OP_SCRIPT_EVAL_BLOCK:
				sexp_val = sexp_script_eval(node, OPR_NULL, true);
				break;

			case OP_SCRIPT_EVAL_MULTI:
				sexp_script_eval_multi(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_CHANGE_IFF_COLOR:
				sexp_change_iff_color(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_DISABLE_ETS:
			case OP_ENABLE_ETS:
				sexp_disable_ets(node, (op_num == OP_DISABLE_ETS));
				sexp_val = SEXP_TRUE;
				break;

			case OP_FORCE_GLIDE:
				sexp_val = SEXP_TRUE;
				sexp_force_glide(node);
				break;

			case OP_HUD_SET_DIRECTIVE:
				sexp_val = SEXP_TRUE;
				sexp_hud_set_directive(node);
				break;

			case OP_HUD_GAUGE_SET_ACTIVE:
				sexp_val = SEXP_TRUE;
				sexp_hud_gauge_set_active(node);
				break;

			case OP_HUD_SET_CUSTOM_GAUGE_ACTIVE:
				sexp_val = SEXP_TRUE;
				sexp_hud_set_custom_gauge_active(node);
				break;

			case OP_HUD_CLEAR_MESSAGES:
				sexp_val = SEXP_TRUE;
				sexp_hud_clear_messages();
				break;

			case OP_HUD_ACTIVATE_GAUGE_TYPE:
				sexp_val = SEXP_TRUE;
				sexp_hud_activate_gauge_type(node);
				break;

			case OP_HUD_SET_BUILTIN_GAUGE_ACTIVE:
				sexp_val = SEXP_TRUE;
				sexp_hud_set_builtin_gauge_active(node);
				break;

			case OP_ADD_TO_COLGROUP:
			case OP_REMOVE_FROM_COLGROUP:
				sexp_manipulate_colgroup(node, op_num == OP_ADD_TO_COLGROUP);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ADD_TO_COLGROUP_NEW:
			case OP_REMOVE_FROM_COLGROUP_NEW:
				sexp_manipulate_colgroup_new(node, op_num == OP_ADD_TO_COLGROUP_NEW);
				sexp_val = SEXP_TRUE;
				break;

			case OP_GET_COLGROUP_ID:
				sexp_val = sexp_get_colgroup(node);
				break;

			case OP_SHIP_EFFECT:
				sexp_val = SEXP_TRUE;
				sexp_ship_effect(node);
				break;

			case OP_CLEAR_SUBTITLES:
				sexp_val = SEXP_TRUE;
				sexp_clear_subtitles();
				break;

			case OP_SET_THRUSTERS:
				sexp_val = SEXP_TRUE;
				sexp_set_thrusters(node);
				break;

			case OP_CHANGE_TEAM_COLOR:
				sexp_val = SEXP_TRUE;
				sexp_change_team_color(node);
				break;

			case OP_CALL_SSM_STRIKE:
				sexp_val = SEXP_TRUE;
				sexp_call_ssm_strike(node);
				break;

			case OP_PLAYER_IS_CHEATING_BASTARD:
				sexp_val = sexp_player_is_cheating_bastard();
				break;

			case OP_IS_LANGUAGE:
				sexp_val = sexp_is_language(node);
				break;

			case OP_USED_CHEAT:
				sexp_val = sexp_cheat_used(node);
				break;

			case OP_IS_IN_TURRET_FOV:
				sexp_val = sexp_is_in_turret_fov(node);
				break;

			case OP_REPLACE_TEXTURE:
			case OP_REPLACE_TEXTURE_SKYBOX:
				sexp_val = SEXP_TRUE;
				sexp_replace_texture(node, op_num == OP_REPLACE_TEXTURE_SKYBOX);
				break;

			case OP_SET_ALPHA_MULT:
				sexp_val = SEXP_TRUE;
				sexp_set_alphamult(node);
				break;

			case OP_TRIGGER_ANIMATION_NEW:
				sexp_val = SEXP_TRUE;
				sexp_trigger_submodel_animation_new(node);
				break;

			case OP_UPDATE_MOVEABLE:
				sexp_val = SEXP_TRUE;
				sexp_update_moveable_animation(node);
				break;

			case OP_ADVANCE_MOVEABLE:
				sexp_val = SEXP_TRUE;
				sexp_advance_moveable_animation(node);
				break;

			case OP_STOP_LOOPING_ANIMATION:
				sexp_val = SEXP_TRUE;
				sexp_stop_looping_animation(node);
				break;

			case OP_SET_GRAVITY_ACCEL:
				sexp_set_gravity_accel(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_FORCE_REARM:
				sexp_force_rearm(node);
				sexp_val = SEXP_TRUE;
				break;

			case OP_ABORT_REARM:
				sexp_abort_rearm(node);
				sexp_val = SEXP_TRUE;
				break;

			default:{
				// Check if we have a dynamic SEXP with this operator and if there is, execute that
				auto dynamicSEXP = sexp::get_dynamic_sexp(op_num);
				if (dynamicSEXP != nullptr) {
					sexp_val = dynamicSEXP->execute(node, cur_node);
				} else {
					Error(LOCATION, "Looking for SEXP operator, found '%s'.\n", CTEXT(cur_node));
				}
			}
		}

		if (Log_event) {
			add_to_event_log_buffer(cur_node, get_operator_index(cur_node), sexp_val);
		}

		Assert(!Current_sexp_operator.empty()); 
		Current_sexp_operator.pop_back();

		Assertion(sexp_val != UNINITIALIZED, "SEXP %s didn't return a value!", CTEXT(cur_node));

		// if we haven't returned, check the sexp value of the sexpression evaluation.  A special
		// value of known true or known false means that we should set the sexp.value field for
		// short circuit eval.
		if (sexp_val == SEXP_KNOWN_TRUE) {
			Sexp_nodes[cur_node].value = SEXP_KNOWN_TRUE;
			return SEXP_TRUE;
		}

		if (sexp_val == SEXP_KNOWN_FALSE) {
			Sexp_nodes[cur_node].value = SEXP_KNOWN_FALSE;
			return SEXP_FALSE;
		}

		if ( sexp_val == SEXP_NAN ) {
			Sexp_nodes[cur_node].value = SEXP_NAN;			// not a number values are false I would suspect
			return SEXP_FALSE;
		}

		if ( sexp_val == SEXP_NAN_FOREVER ) {
			Sexp_nodes[cur_node].value = SEXP_NAN_FOREVER;
			// Goober5000 changed from sexp_val to SEXP_FALSE on 2/21/2006 in accordance with above comment
			// NOTE: we return false rather than known-false to match the SEXP_KNOWN_FALSE case above
			return SEXP_FALSE;
		}

		if ( sexp_val == SEXP_CANT_EVAL ) {
			Sexp_nodes[cur_node].value = SEXP_CANT_EVAL;
			Assume_event_is_current = false;  // indicate sexp isn't current yet
			return SEXP_FALSE;
		}

		if ( Sexp_nodes[cur_node].value == SEXP_NAN ) {	// if we had a nan, but now don't, reset the value
			Sexp_nodes[cur_node].value = SEXP_UNKNOWN;
			return sexp_val;
		}

		if ( sexp_val ){
			Sexp_nodes[cur_node].value = SEXP_TRUE;
		} else {
			Sexp_nodes[cur_node].value = SEXP_FALSE;
		}

		return sexp_val;
	}
}

/**
 * Only runs on the client machines not the server. Evaluates the contents of a SEXP packet and calls the relevent multi_sexp_x 
 * function(s). 
 */
void multi_sexp_eval()
{
	int op_num; 

	Assert (MULTIPLAYER_CLIENT); 

	while (Current_sexp_network_packet.sexp_bytes_left > 0) {
		op_num = Current_sexp_network_packet.get_next_operator();

		Assert (Current_sexp_network_packet.sexp_bytes_left);

		if (op_num < 0) {
			Warning(LOCATION, "Received invalid operator number from host in multi_sexp_eval(). Entire packet may be corrupt. Discarding packet"); 
			return; 	
		}

		switch(op_num) {

			case OP_CHANGE_SOUNDTRACK:
				multi_sexp_change_soundtrack();
				break; 

			case OP_SET_PERSONA:
				multi_sexp_set_persona();
				break; 
			
			case OP_CHANGE_SUBSYSTEM_NAME:
				multi_sexp_change_subsystem_name();
				break;

			case OP_SHIP_SUBSYS_NO_REPLACE:
				multi_sexp_deal_with_subsys_flag(Ship::Subsystem_Flags::No_replace);
				break;
			case OP_SHIP_SUBSYS_NO_LIVE_DEBRIS:
				multi_sexp_deal_with_subsys_flag(Ship::Subsystem_Flags::No_live_debris);
				break;
			case OP_SHIP_SUBSYS_VANISHED:
				multi_sexp_deal_with_subsys_flag(Ship::Subsystem_Flags::Vanished);
				break;
			case OP_SHIP_SUBSYS_IGNORE_IF_DEAD:
				multi_sexp_deal_with_subsys_flag(Ship::Subsystem_Flags::Missiles_ignore_if_dead);
				break;
			case OP_SHIP_SUBSYS_TARGETABLE:
			case OP_SHIP_SUBSYS_UNTARGETABLE:
				multi_sexp_deal_with_subsys_flag(Ship::Subsystem_Flags::Untargetable);
				break;

			case OP_SHIP_CHANGE_ALT_NAME:
			case OP_SHIP_CHANGE_CALLSIGN:
			case OP_SHIP_CHANGE_DISPLAY_NAME:
				multi_sexp_ship_change_alt_name_or_display_name_or_callsign(op_num);
				break;

			case OP_SET_RESPAWNS:
				multi_sexp_set_respawns();
				break;

			case OP_CLEAR_WEAPONS:
			case OP_CLEAR_DEBRIS:
				multi_sexp_clear_weapons_or_debris(op_num);
				break;

			case OP_CHANGE_SHIP_CLASS:
				multi_sexp_change_ship_class();
				break;
					
			case OP_PLAY_SOUND_FROM_TABLE:
				multi_sexp_play_sound_from_table();
				break;
					
			case OP_PLAY_SOUND_FROM_FILE:
				multi_sexp_play_sound_from_file();
				break;

			case OP_CLOSE_SOUND_FROM_FILE:
				multi_sexp_close_sound_from_file();
				break;

			case OP_PAUSE_SOUND_FROM_FILE:
				multi_sexp_pause_sound_from_file();
				break;

			case OP_SHIP_BOMB_TARGETABLE:
			case OP_SHIP_BOMB_UNTARGETABLE:
			case OP_SHIP_INVISIBLE:
			case OP_SHIP_VISIBLE:
			case OP_SHIP_STEALTHY:
			case OP_SHIP_UNSTEALTHY:
			case OP_FRIENDLY_STEALTH_INVISIBLE:
			case OP_FRIENDLY_STEALTH_VISIBLE:
			case OP_LOCK_AFTERBURNER:
			case OP_UNLOCK_AFTERBURNER:
			case OP_LOCK_PRIMARY_WEAPON:
			case OP_UNLOCK_PRIMARY_WEAPON:
			case OP_LOCK_SECONDARY_WEAPON:
			case OP_UNLOCK_SECONDARY_WEAPON:
			case OP_SHIELDS_ON:
			case OP_SHIELDS_OFF:
				multi_sexp_deal_with_ship_flag();
				break;

			case OP_ALTER_SHIP_FLAG:
				multi_sexp_alter_ship_flag();
				break;

			case OP_ALTER_WING_FLAG:
				multi_sexp_alter_wing_flag();
				break;

			case OP_SET_AFTERBURNER_ENERGY:
				multi_sexp_set_energy_pct();
				break;

			case OP_SET_AMBIENT_LIGHT:
				multi_sexp_set_ambient_light();
				break;

			case OP_MODIFY_VARIABLE:
				multi_sexp_modify_variable();
				break;

			case OP_NAV_ADD_WAYPOINT:
				multi_sexp_add_nav_waypoint();
				break;

			case OP_CUTSCENES_FADE_IN:
			case OP_CUTSCENES_FADE_OUT:
				multi_sexp_fade(op_num == OP_CUTSCENES_FADE_IN);
				break;

			case OP_NAV_ADD_SHIP:
				multi_add_nav_ship();
				break;

			case OP_NAV_DEL:
				multi_del_nav();
				break;

			case OP_ADD_REMOVE_ESCORT:
				multi_sexp_add_remove_escort();
				break;

			case OP_CUTSCENES_SHOW_SUBTITLE_TEXT:
				 multi_sexp_show_subtitle_text();
				 break;

			case OP_CUTSCENES_SHOW_SUBTITLE_IMAGE:
				 multi_sexp_show_subtitle_image();
				 break;

			case OP_TRAINING_MSG:
				multi_sexp_send_training_message(); 
				break;

			case OP_HUD_DISABLE:
				multi_sexp_hud_disable();
				break;
			
			case OP_HUD_DISABLE_EXCEPT_MESSAGES:
				multi_sexp_hud_disable_except_messages();
				break;

			case OP_FLASH_HUD_GAUGE:
				multi_sexp_flash_hud_gauge();
				break;

			case OP_HUD_DISPLAY_GAUGE:
				multi_sexp_hud_display_gauge();
				break;

			case OP_CUTSCENES_SET_CUTSCENE_BARS:
			case OP_CUTSCENES_UNSET_CUTSCENE_BARS:
				multi_sexp_toggle_cutscene_bars(op_num == OP_CUTSCENES_SET_CUTSCENE_BARS );
				break;

			case OP_CUTSCENES_SET_CAMERA_FACING:
				multi_sexp_set_camera_facing();
				break;

			case OP_CUTSCENES_SET_CAMERA_FACING_OBJECT:
				multi_sexp_set_camera_facing_object();
				break;

			case OP_CUTSCENES_SET_CAMERA_TARGET :
				multi_sexp_set_camera_target();
				break;

			case OP_CUTSCENES_SET_CAMERA_ROTATION:
				multi_sexp_set_camera_rotation();
				break;

			case OP_CUTSCENES_SET_CAMERA_FOV:
				multi_sexp_set_camera_fov();
				break;

			case OP_CUTSCENES_SET_CAMERA_POSITION:
				multi_sexp_set_camera_position();
				break;

			case OP_SET_CAMERA_SHUDDER:
				multi_sexp_set_camera_shudder();
				break;

			case OP_CUTSCENES_RESET_CAMERA:
				multi_sexp_reset_camera();
				break;

			case OP_CUTSCENES_SET_FOV:
				multi_sexp_set_fov();
				break;

			case OP_CUTSCENES_RESET_FOV:
				multi_sexp_reset_fov();
				break;

			case OP_JUMP_NODE_SET_JUMPNODE_NAME:
				multi_sexp_set_jumpnode_name();
				break;

			case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME:
				multi_sexp_set_jumpnode_display_name();
				break;

			case OP_IGNORE_KEY:
				multi_sexp_ignore_key();
				break;

			case OP_JUMP_NODE_SET_JUMPNODE_COLOR:
				multi_sexp_set_jumpnode_color();
				break;

			case OP_JUMP_NODE_SET_JUMPNODE_MODEL:
				multi_sexp_set_jumpnode_model();
				break;

			case OP_JUMP_NODE_SHOW_JUMPNODE:
			case OP_JUMP_NODE_HIDE_JUMPNODE:
				multi_sexp_show_hide_jumpnode(op_num == OP_JUMP_NODE_SHOW_JUMPNODE);
				break;

			case OP_CLEAR_SUBTITLES:
				multi_sexp_clear_subtitles();
				break;

			case OP_SET_OBJECT_SPEED_X:
			case OP_SET_OBJECT_SPEED_Y:
			case OP_SET_OBJECT_SPEED_Z:
				multi_sexp_set_object_speed();
				break;

			case OP_SET_OBJECT_POSITION:
				multi_sexp_set_object_position();
				break;

			case OP_CHANGE_TEAM_COLOR:
				multi_sexp_change_team_color();
				break;

			case OP_HUD_SET_MAX_TARGETING_RANGE:
				multi_sexp_hud_set_max_targeting_range();
				break;

			case OP_CUTSCENES_SET_TIME_COMPRESSION:
				multi_sexp_set_time_compression();
				break;

			case OP_CUTSCENES_RESET_TIME_COMPRESSION:
				multi_sexp_reset_time_compression();
				break;

			case OP_SET_ETS_VALUES:
				multi_sexp_set_ets_values();
				break;

			case OP_SCRIPT_EVAL_MULTI:
				multi_sexp_script_eval_multi();
				break;

			case OP_SET_PRIMARY_AMMO:
			case OP_SET_SECONDARY_AMMO:
				multi_sexp_set_ammo(false, op_num == OP_SET_PRIMARY_AMMO);
				break;

			case OP_TURRET_SET_PRIMARY_AMMO:
			case OP_TURRET_SET_SECONDARY_AMMO:
				multi_sexp_set_ammo(true, op_num == OP_TURRET_SET_PRIMARY_AMMO);
				break;

			case OP_SET_PRIMARY_WEAPON:
			case OP_SET_SECONDARY_WEAPON:
				multi_sexp_set_weapon();
				break;

			case OP_SET_NUM_COUNTERMEASURES:
				multi_sexp_set_countermeasures();
				break;

			case OP_CHANGE_AI_CLASS:
				multi_sexp_change_ai_class();
				break;

			case OP_CHANGE_IFF:
				multi_sexp_change_iff();
				break;

			case OP_CHANGE_IFF_COLOR:
				multi_sexp_change_iff_color();
				break;
				
			case OP_DESTROY_INSTANTLY:
				multi_sexp_destroy_instantly();
				break;

			case OP_DESTROY_SUBSYS_INSTANTLY:
				multi_sexp_destroy_subsys_instantly();
				break;

			case OP_RED_ALERT:
				multi_sexp_red_alert();
				break;

			case OP_END_MISSION:
				multi_sexp_end_mission();
				break;

			// bad sexp in the packet
			default: 
				// probably just a version error where the host supports a SEXP but a client does not
				if (Current_sexp_network_packet.sexp_discard_operator()) {
					Warning(LOCATION, "Received invalid SEXP operator number from host. Operator number %d is not supported by this version.", op_num); 
				}
				// a more major problem
				else {
					Warning(LOCATION, "Received invalid SEXP packet from host. Function involving operator %d lacks termination. Entire packet may be corrupt. Discarding remaining packet", op_num); 
					return; 
				}			
		}

		Current_sexp_network_packet.finished_callback();
	}
}

//	get_sexp_main reads and builds the internal representation for a
//	symbolic expression.
//	On entry:
//		Mp points at first character in expression.
//	The symbolic expression is built in Sexp_nodes beginning at node 0.
int get_sexp_main()
{
	int	start_node, op;

	ignore_white_space();

	if (*Mp != '(')
	{
		char buf[512];
		error_display(0, "Expected to find an open parenthesis in the following sexp:\n%s", three_dot_truncate(buf, Mp, 512));
		return -1;
	}

	Mp++;
	start_node = get_sexp();

	// only need to check syntax if we have a operator
	if (!Fred_running && (start_node >= 0))
	{
		op = get_operator_index(start_node);
		if (op < 0)
		{
			error_display(0, "Can't find operator %s in operator list!\n", CTEXT(start_node));
			return -1;
		}
	}

	return start_node;
}

int run_sexp(const char* sexpression, bool run_eval_num, bool *is_nan_or_nan_forever)
{
	char* oldMp = Mp;
	int n, sexp_val = UNINITIALIZED;
	char buf[8192];

	strcpy_s(buf, sexpression);

	// HACK: ! -> "
	for (auto ch = buf; *ch; ++ch)
	{
		// convert single ! to ", but don't convert !!
		if (*ch == '!')
		{
			if (*(ch + 1) == '!')
				++ch;
			else
				*ch = '\"';
		}
	}

	// !! -> !
	consolidate_double_characters(buf, '!');

	Mp = buf;

	n = get_sexp_main();
	if (n != -1)
	{
		if (run_eval_num)
		{
			// if this sexp node is an operator, put it in another node so that CAR(node) will find it
			if (get_operator_index(n) >= 0)
				n = alloc_sexp("", 1, -1, n, -1);

			bool is_nan, is_nan_forever;
			sexp_val = eval_num(n, is_nan, is_nan_forever);
			if (is_nan_or_nan_forever != nullptr)
				*is_nan_or_nan_forever = (is_nan || is_nan_forever);
		}
		else
			sexp_val = eval_sexp(n);

		free_sexp2(n);
	}

	Mp = oldMp;

	return sexp_val;
}

DCF(sexpc, "Runs the given sexp command, wrapped as an action of a when-true condition")
{
	SCP_string sexp;
	SCP_string sexp_wrapped;
	
	if (dc_optional_string_either("help", "--help")) {
		dc_printf( "Usage: sexpc <sexpression>\n. Runs the given sexp as '( when ( true ) ( sexpression ) )' .\n" );
		return;
	}

	dc_stuff_string(sexp);

	sexp_wrapped = "( when ( true ) ( " + sexp + " ) )";

	int sexp_val = run_sexp(sexp_wrapped.c_str());
	dc_printf("SEXP '%s' run; sexp_val = %d\n", sexp_wrapped.c_str(), sexp_val);
}

DCF(sexp, "Runs the given sexp")
{
	SCP_string sexp;

	if (dc_optional_string_either("help", "--help")) {
		dc_printf( "Usage: sexp <sexpression>\n. Runs the given sexp.\n");
		return;
	}

	dc_stuff_string(sexp);

	int sexp_val = run_sexp(sexp.c_str());
	dc_printf("Result = %d\n", sexp_val);
}

DCF(script, "Runs the given script, without regard to its return value")
{
	SCP_string script;

	if (dc_optional_string_either("help", "--help")) {
		dc_printf( "Usage: script <script code>\n. Runs the given script.\n");
		return;
	}

	dc_stuff_string(script);

	bool success = Script_system.EvalString(script.c_str());
	dc_printf("Script %s\n", success ? "successful" : "unsuccessful");
}

template <typename T>
void script_eval_with_return(const char *help_text, const char *format, void (*to_scp_string)(T, SCP_string&))
{
	SCP_string script;

	if (dc_optional_string_either("help", "--help")) {
		dc_printf("%s", help_text);
		return;
	}

	dc_stuff_string(script);

	T result{};
	SCP_string result_string;

	bool success = Script_system.EvalStringWithReturn(script.c_str(), format, &result);
	to_scp_string(result, result_string);
	dc_printf("Script %s.  Result = %s\n", success ? "successful" : "unsuccessful", result_string.c_str());
}

DCF(script_eval_bool, "Evaluates the given script, returning a boolean result")
{
	script_eval_with_return<bool>("Usage: script_eval_bool <script code>\n. Runs the given script, returning a boolean result.\n",
		"|b",
		[](bool b, SCP_string& str) { str = b ? "true" : "false"; }
	);
}

DCF(script_eval_int, "Evaluates the given script, returning an integer result")
{
	script_eval_with_return<int>("Usage: script_eval_int <script code>\n. Runs the given script, returning an integer result.\n",
		"|i",
		[](int i, SCP_string& str) { sprintf(str, "%d", i); }
	);
}

DCF(script_eval_float, "Evaluates the given script, returning a float result")
{
	script_eval_with_return<float>("Usage: script_eval_float <script code>\n. Runs the given script, returning a float result.\n",
		"|f",
		[](float f, SCP_string& str) { sprintf(str, "%f", f); }
	);
}

DCF(script_eval_string, "Evaluates the given script, returning a string result")
{
	script_eval_with_return<const char*>("Usage: script_eval_string <script code>\n. Runs the given script, returning a string result.\n",
		"|s",
		[](const char* s, SCP_string& str) { sprintf(str, "'%s'", s ? s : ""); }
	);
}

bool map_opf_to_opr(sexp_opf_t opf_type, sexp_opr_t &opr_type)
{
	opr_type = OPR_NONE;

	switch (opf_type)
	{
		case OPF_NUMBER:
			opr_type = OPR_NUMBER;
			break;

		case OPF_POSITIVE:
			opr_type = OPR_POSITIVE;
			break;

		case OPF_BOOL:
			opr_type = OPR_BOOL;
			break;

		case OPF_NULL:
			opr_type = OPR_NULL;
			break;

		// Goober5000
		case OPF_FLEXIBLE_ARGUMENT:
			opr_type = OPR_FLEXIBLE_ARGUMENT;
			break;

		case OPF_AI_GOAL:
			opr_type = OPR_AI_GOAL;
			break;

		// special case for modify-variable
		case OPF_AMBIGUOUS:
			opr_type = OPR_AMBIGUOUS;
			break;

		// these types can accept either lists of strings or indexes
		case OPF_GAME_SND:
		case OPF_FIREBALL:
		case OPF_WEAPON_BANK_NUMBER:
			opr_type = OPR_POSITIVE;
			break;

		default:
			return false;	// no other return types available
	}

	Assertion(opr_type != OPR_NONE, "map_opf_to_opr should have found a type");
	return true;
}

const char *opr_type_name(sexp_opr_t opr_type)
{
	switch (opr_type)
	{
		case OPR_NONE:
			return "<invalid return type>";
		case OPR_NUMBER:
			return "number";
		case OPR_BOOL:
			return "boolean";
		case OPR_NULL:
			return "no value";
		case OPR_AI_GOAL:
			return "[ai goal type]";
		case OPR_POSITIVE:
			return "positive number";
		case OPR_STRING:
			return "string";
		case OPR_AMBIGUOUS:
			return "[variable/container type]";
		case OPR_FLEXIBLE_ARGUMENT:
			return "[argument type]";
		default:
			return "<unknown return type>";
	}
}

// returns the data type returned by an operator
int query_operator_return_type(int op)
{
	if (op < FIRST_OP)
	{
		Assert(op >= 0 && op < (int)Operators.size());
		op = Operators[op].value;
	}

	switch (op)
	{
		case OP_TRUE:
		case OP_FALSE:
		case OP_AND:
		case OP_AND_IN_SEQUENCE:
		case OP_OR:
		case OP_NOT:
		case OP_XOR:
		case OP_EQUALS:
		case OP_GREATER_THAN:
		case OP_LESS_THAN:
		case OP_NOT_EQUAL:
		case OP_GREATER_OR_EQUAL:
		case OP_LESS_OR_EQUAL:
		case OP_STRING_EQUALS:
		case OP_STRING_GREATER_THAN:
		case OP_STRING_LESS_THAN:
		case OP_PERFORM_ACTIONS_BOOL_FIRST:
		case OP_PERFORM_ACTIONS_BOOL_LAST:
		case OP_IS_TRUE_FOR_DURATION:
		case OP_IS_DESTROYED:
		case OP_IS_SUBSYSTEM_DESTROYED:
		case OP_IS_DISABLED:
		case OP_IS_DISARMED:
		case OP_HAS_DOCKED:
		case OP_HAS_UNDOCKED:
		case OP_HAS_ARRIVED:
		case OP_HAS_DEPARTED:
		case OP_IS_DESTROYED_DELAY:
		case OP_WAS_DESTROYED_BY_DELAY:
		case OP_IS_SUBSYSTEM_DESTROYED_DELAY:
		case OP_IS_DISABLED_DELAY:
		case OP_IS_DISARMED_DELAY:
		case OP_HAS_DOCKED_DELAY:
		case OP_HAS_UNDOCKED_DELAY:
		case OP_HAS_ARRIVED_DELAY:
		case OP_HAS_DEPARTED_DELAY:
		case OP_IS_IFF:
		case OP_IS_SPECIES:
		case OP_IS_AI_CLASS:
		case OP_IS_SHIP_TYPE:
		case OP_IS_SHIP_CLASS:
		case OP_HAS_TIME_ELAPSED:
		case OP_HAS_TIME_ELAPSED_MSECS:
		case OP_GOAL_INCOMPLETE:
		case OP_GOAL_TRUE_DELAY:
		case OP_GOAL_FALSE_DELAY:
		case OP_EVENT_INCOMPLETE:
		case OP_EVENT_TRUE_DELAY:
		case OP_EVENT_FALSE_MSECS_DELAY:
		case OP_EVENT_TRUE_MSECS_DELAY:
		case OP_EVENT_FALSE_DELAY:
		case OP_PREVIOUS_EVENT_TRUE:
		case OP_PREVIOUS_EVENT_FALSE:
		case OP_PREVIOUS_EVENT_INCOMPLETE:
		case OP_PREVIOUS_GOAL_TRUE:
		case OP_PREVIOUS_GOAL_FALSE:
		case OP_PREVIOUS_GOAL_INCOMPLETE:
		case OP_WAYPOINTS_DONE:
		case OP_WAYPOINTS_DONE_DELAY:
		case OP_SHIP_TYPE_DESTROYED:
		case OP_LAST_ORDER_TIME:
		case OP_KEY_PRESSED:
		case OP_TARGETED:
		case OP_NODE_TARGETED:
		case OP_SPEED:
		case OP_FACING:
		case OP_FACING2:
		case OP_ORDER:
		case OP_QUERY_ORDERS:
		case OP_WAYPOINT_MISSED:
		case OP_WAYPOINT_TWICE:
		case OP_PATH_FLOWN:
		case OP_EVENT_TRUE:
		case OP_EVENT_FALSE:
		case OP_SKILL_LEVEL_AT_LEAST:
		case OP_IS_CARGO_KNOWN:
		case OP_HAS_BEEN_TAGGED_DELAY:
		case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY:
		case OP_CARGO_KNOWN_DELAY:
		case OP_WAS_PROMOTION_GRANTED:
		case OP_WAS_MEDAL_GRANTED:
		case OP_PERCENT_SHIPS_DEPARTED:
		case OP_PERCENT_SHIPS_DESTROYED:
		case OP_PERCENT_SHIPS_DISARMED:
		case OP_PERCENT_SHIPS_DISABLED:
		case OP_PERCENT_SHIPS_ARRIVED:
		case OP_PERCENT_SHIPS_SCANNED:
		case OP_DEPART_NODE_DELAY:
		case OP_DESTROYED_DEPARTED_DELAY:
		case OP_SPECIAL_CHECK:
		case OP_IS_TAGGED:
		case OP_PRIMARIES_DEPLETED:
		case OP_SECONDARIES_DEPLETED:
		case OP_SHIELD_QUAD_LOW:
		case OP_IS_SECONDARY_SELECTED:
		case OP_IS_PRIMARY_SELECTED:
		case OP_IS_SHIP_STEALTHY:
		case OP_IS_FRIENDLY_STEALTH_VISIBLE:
		case OP_IS_CARGO:
		case OP_MISSILE_LOCKED:
		case OP_NAV_IS_VISITED:
		case OP_NAV_ISLINKED:
		case OP_IS_PLAYER:
		case OP_PRIMARY_FIRED_SINCE:
		case OP_TURRET_FIRED_SINCE:
		case OP_SECONDARY_FIRED_SINCE:
		case OP_IS_FACING:
		case OP_HAS_PRIMARY_WEAPON:
		case OP_HAS_SECONDARY_WEAPON:
		case OP_TURRET_HAS_PRIMARY_WEAPON:
		case OP_TURRET_HAS_SECONDARY_WEAPON:
		case OP_IS_BIT_SET:
		case OP_IS_NAN:
		case OP_DIRECTIVE_VALUE:
		case OP_IS_IN_BOX:
		case OP_IS_IN_MISSION:
		case OP_HAS_ARMOR_TYPE:
		case OP_IS_DOCKED:
		case OP_PLAYER_IS_CHEATING_BASTARD:
		case OP_ARE_SHIP_FLAGS_SET:
		case OP_ARE_WING_FLAGS_SET:
		case OP_IS_SHIP_EMP_ACTIVE:
		case OP_IS_IN_TURRET_FOV:
		case OP_IS_LANGUAGE:
		case OP_USED_CHEAT:
		case OP_FUNCTIONAL_WHEN:
		case OP_SCRIPT_EVAL_BOOL:
		case OP_IS_CONTAINER_EMPTY:
		case OP_LIST_HAS_DATA:
		case OP_MAP_HAS_KEY:
		case OP_MAP_HAS_DATA_ITEM:
			return OPR_BOOL;

		case OP_PLUS:
		case OP_MINUS:
		case OP_MOD:
		case OP_MUL:
		case OP_DIV:
		case OP_RAND:
		case OP_RAND_MULTIPLE:
		case OP_MIN:
		case OP_MAX:
		case OP_AVG:
		case OP_POW:
		case OP_SIGNUM:
		case OP_NAN_TO_NUMBER:
		case OP_GET_OBJECT_X:
		case OP_GET_OBJECT_Y:
		case OP_GET_OBJECT_Z:
		case OP_GET_OBJECT_PITCH:
		case OP_GET_OBJECT_BANK:
		case OP_GET_OBJECT_HEADING:
		case OP_GET_OBJECT_SPEED_X:
		case OP_GET_OBJECT_SPEED_Y:
		case OP_GET_OBJECT_SPEED_Z:
		case OP_SCRIPT_EVAL_NUM:
		case OP_STRING_TO_INT:
		case OP_GET_THROTTLE_SPEED:
		case OP_GET_VARIABLE_BY_INDEX:
		case OP_GET_COLGROUP_ID:
		case OP_FUNCTIONAL_IF_THEN_ELSE:
		case OP_FUNCTIONAL_SWITCH:
		case OP_GET_HOTKEY:
		case OP_GET_CONTAINER_SIZE:
		case OP_LIST_DATA_INDEX:
		case OP_ANGLE_VECTORS:
		case OP_ANGLE_FVEC_TARGET:
			return OPR_NUMBER;

		case OP_ABS:
		case OP_SET_BIT:
		case OP_UNSET_BIT:
		case OP_BITWISE_AND:
		case OP_BITWISE_OR:
		case OP_BITWISE_NOT:
		case OP_BITWISE_XOR:
		case OP_TIME_SHIP_DESTROYED:
		case OP_TIME_SHIP_ARRIVED:
		case OP_TIME_SHIP_DEPARTED:
		case OP_TIME_WING_DESTROYED:
		case OP_TIME_WING_ARRIVED:
		case OP_TIME_WING_DEPARTED:
		case OP_MISSION_TIME:
		case OP_MISSION_TIME_MSECS:
		case OP_TIME_DOCKED:
		case OP_TIME_UNDOCKED:
		case OP_TIME_TO_GOAL:
		case OP_AFTERBURNER_LEFT:
		case OP_WEAPON_ENERGY_LEFT:
		case OP_SHIELDS_LEFT:
		case OP_HITS_LEFT:
		case OP_HITS_LEFT_SUBSYSTEM:
		case OP_HITS_LEFT_SUBSYSTEM_GENERIC:
		case OP_HITS_LEFT_SUBSYSTEM_SPECIFIC:
		case OP_SIM_HITS_LEFT:
		case OP_DISTANCE:
		case OP_DISTANCE_CENTER:
		case OP_DISTANCE_BBOX:
		case OP_DISTANCE_CENTER_SUBSYSTEM:
		case OP_DISTANCE_BBOX_SUBSYSTEM:
		case OP_NUM_WITHIN_BOX:
		case OP_NUM_PLAYERS:
		case OP_NUM_KILLS:
		case OP_NUM_ASSISTS:
		case OP_SHIP_DEATHS: 
		case OP_RESPAWNS_LEFT:
		case OP_SHIP_SCORE:
		case OP_NUM_TYPE_KILLS:
		case OP_NUM_CLASS_KILLS:
		case OP_SHIELD_RECHARGE_PCT:
		case OP_ENGINE_RECHARGE_PCT:
		case OP_WEAPON_RECHARGE_PCT:
		case OP_PRIMARY_AMMO_PCT:
		case OP_SECONDARY_AMMO_PCT:
		case OP_GET_PRIMARY_AMMO:
		case OP_GET_SECONDARY_AMMO:
		case OP_GET_NUM_COUNTERMEASURES:
		case OP_SPECIAL_WARP_DISTANCE:
		case OP_IS_SHIP_VISIBLE:
		case OP_TEAM_SCORE:
		case OP_NUM_SHIPS_IN_BATTLE:
		case OP_NUM_SHIPS_IN_WING:
		case OP_CURRENT_SPEED:
		case OP_NAV_DISTANCE:
		case OP_GET_DAMAGE_CAUSED:
		case OP_CUTSCENES_GET_FOV:
		case OP_NUM_VALID_ARGUMENTS:
		case OP_STRING_GET_LENGTH:
		case OP_GET_ETS_VALUE:
		case OP_TURRET_GET_PRIMARY_AMMO:
		case OP_TURRET_GET_SECONDARY_AMMO:
		case OP_GET_POWER_OUTPUT:
		case OP_GET_SUPERNOVA_STAGE:
			return OPR_POSITIVE;

		case OP_COND:
		case OP_WHEN:
		case OP_WHEN_ARGUMENT:
		case OP_EVERY_TIME:
		case OP_EVERY_TIME_ARGUMENT:
		case OP_ON_MISSION_SKIP:
		case OP_IF_THEN_ELSE:
		case OP_SWITCH:
		case OP_INVALIDATE_ARGUMENT:
		case OP_VALIDATE_ARGUMENT:
		case OP_INVALIDATE_ALL_ARGUMENTS:
		case OP_VALIDATE_ALL_ARGUMENTS:
		case OP_DO_FOR_VALID_ARGUMENTS:
		case OP_CHANGE_IFF:
		case OP_CHANGE_AI_CLASS:
		case OP_CLEAR_SHIP_GOALS:
		case OP_CLEAR_WING_GOALS:
		case OP_CLEAR_GOALS:
		case OP_ADD_SHIP_GOAL:
		case OP_ADD_WING_GOAL:
		case OP_ADD_GOAL:
		case OP_REMOVE_GOAL:
		case OP_PROTECT_SHIP:
		case OP_UNPROTECT_SHIP:
		case OP_BEAM_PROTECT_SHIP:
		case OP_BEAM_UNPROTECT_SHIP:
		case OP_TURRET_PROTECT_SHIP:
		case OP_TURRET_UNPROTECT_SHIP:
		case OP_NOP:
		case OP_GOALS_ID:
		case OP_SEND_MESSAGE:
		case OP_SEND_BUILTIN_MESSAGE:
		case OP_SET_HUD_TIME_PAD:
		case OP_SELF_DESTRUCT:
		case OP_NEXT_MISSION:
		case OP_END_CAMPAIGN:
		case OP_END_OF_CAMPAIGN:
		case OP_SABOTAGE_SUBSYSTEM:
		case OP_REPAIR_SUBSYSTEM:
		case OP_INVALIDATE_GOAL:
		case OP_VALIDATE_GOAL:
		case OP_SEND_RANDOM_MESSAGE:
		case OP_TRANSFER_CARGO:
		case OP_EXCHANGE_CARGO:
		case OP_SET_CARGO:
		case OP_JETTISON_CARGO_DELAY:
		case OP_JETTISON_CARGO_NEW:
		case OP_SET_DOCKED:
		case OP_CARGO_NO_DEPLETE:
		case OP_SET_SCANNED:
		case OP_SET_UNSCANNED:
		case OP_KEY_RESET:
		case OP_KEY_RESET_MULTIPLE:
		case OP_TRAINING_MSG:
		case OP_SET_TRAINING_CONTEXT_FLY_PATH:
		case OP_SET_TRAINING_CONTEXT_SPEED:
		case OP_END_MISSION:
		case OP_SET_DEBRIEFING_TOGGLED:
		case OP_SET_DEBRIEFING_PERSONA:
		case OP_SET_TRAITOR_OVERRIDE:
		case OP_FORCE_JUMP:
		case OP_SET_SUBSYSTEM_STRNGTH:
		case OP_DESTROY_SUBSYS_INSTANTLY:
		case OP_GOOD_REARM_TIME:
		case OP_BAD_REARM_TIME:
		case OP_GRANT_PROMOTION:
		case OP_GRANT_MEDAL:
		case OP_ALLOW_SHIP:
		case OP_ALLOW_WEAPON:
		case OP_TECH_ADD_SHIP:
		case OP_TECH_ADD_WEAPON:
		case OP_TECH_ADD_INTEL:
		case OP_TECH_REMOVE_INTEL:
		case OP_TECH_ADD_INTEL_XSTR:
		case OP_TECH_REMOVE_INTEL_XSTR:
		case OP_TECH_RESET_TO_DEFAULT:
		case OP_CHANGE_PLAYER_SCORE:
		case OP_CHANGE_TEAM_SCORE:
		case OP_WARP_BROKEN:
		case OP_WARP_NOT_BROKEN:
		case OP_WARP_NEVER:
		case OP_WARP_ALLOWED:
		case OP_SET_SUBSPACE_DRIVE:
		case OP_FLASH_HUD_GAUGE:
		case OP_GOOD_PRIMARY_TIME:
		case OP_GOOD_SECONDARY_TIME:
		case OP_SHIP_VISIBLE:
		case OP_SHIP_INVISIBLE:
		case OP_SHIP_TAG:
		case OP_SHIP_UNTAG:
		case OP_SHIP_VULNERABLE:
		case OP_SHIP_INVULNERABLE:
		case OP_SHIP_BOMB_TARGETABLE:
		case OP_SHIP_BOMB_UNTARGETABLE:
		case OP_SHIP_GUARDIAN:
		case OP_SHIP_NO_GUARDIAN:
		case OP_SHIP_GUARDIAN_THRESHOLD:
		case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD:
		case OP_SET_GUARD_RANGE:
		case OP_SHIP_VANISH:
		case OP_PROP_VANISH:
		case OP_DESTROY_INSTANTLY:
		case OP_DESTROY_INSTANTLY_WITH_DEBRIS:
		case OP_SHIELDS_ON:
		case OP_SHIELDS_OFF:
		case OP_SHIP_STEALTHY:
		case OP_SHIP_UNSTEALTHY:
		case OP_FRIENDLY_STEALTH_INVISIBLE:
		case OP_FRIENDLY_STEALTH_VISIBLE:
		case OP_SHIP_SUBSYS_NO_REPLACE:
		case OP_SHIP_SUBSYS_NO_LIVE_DEBRIS:
		case OP_SHIP_SUBSYS_VANISHED:
		case OP_SHIP_SUBSYS_IGNORE_IF_DEAD:
		case OP_SHIP_SUBSYS_TARGETABLE:
		case OP_SHIP_SUBSYS_UNTARGETABLE:
		case OP_RED_ALERT:
		case OP_MODIFY_VARIABLE:
		case OP_MODIFY_VARIABLE_XSTR:
		case OP_SET_VARIABLE_BY_INDEX:
		case OP_BEAM_FIRE:
		case OP_BEAM_FIRE_COORDS:
		case OP_BEAM_FLOATING_FIRE:
		case OP_BEAM_FREE:
		case OP_BEAM_FREE_ALL:
		case OP_BEAM_LOCK:
		case OP_BEAM_LOCK_ALL:
		case OP_TURRET_FREE:
		case OP_TURRET_FREE_ALL:
		case OP_TURRET_LOCK:
		case OP_TURRET_LOCK_ALL:
		case OP_TURRET_CHANGE_WEAPON:
		case OP_TURRET_SET_DIRECTION_PREFERENCE:
		case OP_TURRET_SET_RATE_OF_FIRE:
		case OP_TURRET_SET_OPTIMUM_RANGE:
		case OP_TURRET_SET_TARGET_PRIORITIES:
		case OP_TURRET_SET_TARGET_ORDER:
		case OP_SET_ARMOR_TYPE:
		case OP_WEAPON_SET_DAMAGE_TYPE:
		case OP_SHIP_SET_DAMAGE_TYPE:
		case OP_SHIP_SHOCKWAVE_SET_DAMAGE_TYPE:
		case OP_FIELD_SET_DAMAGE_TYPE:
		case OP_SHIP_TURRET_TARGET_ORDER:
		case OP_TURRET_SUBSYS_TARGET_DISABLE:
		case OP_TURRET_SUBSYS_TARGET_ENABLE:
		case OP_ADD_REMOVE_ESCORT:
		case OP_DAMAGED_ESCORT_LIST:
		case OP_DAMAGED_ESCORT_LIST_ALL:
		case OP_AWACS_SET_RADIUS:
		case OP_PRIMITIVE_SENSORS_SET_RANGE:
		case OP_SEND_MESSAGE_LIST:
		case OP_SEND_MESSAGE_CHAIN:
		case OP_CAP_WAYPOINT_SPEED:
		case OP_SET_WING_FORMATION:
		case OP_TURRET_TAGGED_ONLY_ALL:
		case OP_TURRET_TAGGED_CLEAR_ALL:
		case OP_SUBSYS_SET_RANDOM:
		case OP_SUPERNOVA_START:
		case OP_SUPERNOVA_STOP:
		case OP_SET_SPECIAL_WARPOUT_NAME:
		case OP_SHIP_VAPORIZE:
		case OP_SHIP_NO_VAPORIZE:
		case OP_SET_EXPLOSION_OPTION:
		case OP_DONT_COLLIDE_INVISIBLE:
		case OP_COLLIDE_INVISIBLE:
		case OP_SET_MOBILE:
		case OP_SET_IMMOBILE:
		case OP_IGNORE_KEY:
		case OP_CHANGE_SHIP_CLASS:
		case OP_SHIP_COPY_DAMAGE:
		case OP_DEACTIVATE_GLOW_POINTS:
		case OP_ACTIVATE_GLOW_POINTS:
		case OP_DEACTIVATE_GLOW_MAPS:
		case OP_ACTIVATE_GLOW_MAPS:
		case OP_DEACTIVATE_GLOW_POINT_BANK:
		case OP_ACTIVATE_GLOW_POINT_BANK:
		case OP_SET_SKYBOX_MODEL:
		case OP_SET_SKYBOX_ORIENT:
		case OP_SET_SKYBOX_ALPHA:
		case OP_SET_SUPPORT_SHIP:
		case OP_SET_ARRIVAL_INFO:
		case OP_SET_DEPARTURE_INFO:
		case OP_CHANGE_SOUNDTRACK:
		case OP_PLAY_SOUND_FROM_FILE:
		case OP_CLOSE_SOUND_FROM_FILE:
		case OP_PAUSE_SOUND_FROM_FILE:
		case OP_PLAY_SOUND_FROM_TABLE:
		case OP_SET_SOUND_ENVIRONMENT:
		case OP_UPDATE_SOUND_ENVIRONMENT:
		case OP_ADJUST_AUDIO_VOLUME:
		case OP_CREATE_BOLT:
		case OP_EXPLOSION_EFFECT:
		case OP_WARP_EFFECT:
		case OP_SET_OBJECT_POSITION:
		case OP_SET_OBJECT_ORIENTATION:
		case OP_SET_OBJECT_FACING:
		case OP_SET_OBJECT_FACING_OBJECT:
		case OP_SHIP_MANEUVER:
		case OP_SHIP_ROT_MANEUVER:
		case OP_SHIP_LAT_MANEUVER:
		case OP_HUD_DISABLE:
		case OP_HUD_DISABLE_EXCEPT_MESSAGES:
		case OP_KAMIKAZE:
		case OP_TURRET_TAGGED_SPECIFIC:
		case OP_TURRET_TAGGED_CLEAR_SPECIFIC:
		case OP_LOCK_ROTATING_SUBSYSTEM:
		case OP_FREE_ROTATING_SUBSYSTEM:
		case OP_REVERSE_ROTATING_SUBSYSTEM:
		case OP_ROTATING_SUBSYS_SET_TURN_TIME:
		case OP_LOCK_TRANSLATING_SUBSYSTEM:
		case OP_FREE_TRANSLATING_SUBSYSTEM:
		case OP_REVERSE_TRANSLATING_SUBSYSTEM:
		case OP_TRANSLATING_SUBSYS_SET_SPEED:
		case OP_TRIGGER_SUBMODEL_ANIMATION:
		case OP_PLAYER_USE_AI:
		case OP_PLAYER_NOT_USE_AI:
		case OP_SET_PLAYER_TARGET:
		case OP_CLEAR_PLAYER_TARGET:
		case OP_SET_FRIENDLY_DAMAGE_CAPS:
		case OP_ALLOW_TREASON:
		case OP_SET_PLAYER_ORDERS:
		case OP_SET_ORDER_ALLOWED_TARGET:
		case OP_ENABLE_GENERAL_ORDERS:
		case OP_VALIDATE_GENERAL_ORDERS:
		case OP_NAV_ADD_WAYPOINT:
		case OP_NAV_ADD_SHIP:
		case OP_NAV_DEL:
		case OP_NAV_HIDE:
		case OP_NAV_RESTRICT:
		case OP_NAV_UNHIDE:
		case OP_NAV_UNRESTRICT:
		case OP_NAV_SET_VISITED:
		case OP_NAV_UNSET_VISITED:
		case OP_NAV_SET_CARRY:
		case OP_NAV_UNSET_CARRY:
		case OP_NAV_SET_NEEDSLINK:
		case OP_NAV_UNSET_NEEDSLINK:
		case OP_NAV_USECINEMATICS:
		case OP_NAV_USEAP:
		case OP_NAV_SELECT:
		case OP_NAV_UNSELECT:
		case OP_NAV_SET_COLOR:
		case OP_NAV_SET_VISITED_COLOR:
		case OP_HUD_SET_TEXT:
		case OP_HUD_SET_XSTR:
		case OP_HUD_SET_TEXT_NUM:
		case OP_HUD_SET_MESSAGE:
		case OP_HUD_SET_COORDS:
		case OP_HUD_SET_FRAME:
		case OP_HUD_SET_COLOR:
		case OP_HUD_RESET_COLOR:
		case OP_HUD_SET_MAX_TARGETING_RANGE:
		case OP_HUD_CLEAR_MESSAGES:
		case OP_HUD_FORCE_SENSOR_STATIC:
		case OP_HUD_FORCE_EMP_EFFECT:
		case OP_SET_SQUADRON_WINGS:
		case OP_SHIP_CHANGE_ALT_NAME:
		case OP_SHIP_CHANGE_CALLSIGN:
		case OP_SHIP_CHANGE_DISPLAY_NAME:
		case OP_SET_DEATH_MESSAGE:
		case OP_SCRAMBLE_MESSAGES:
		case OP_UNSCRAMBLE_MESSAGES:
		case OP_CUTSCENES_SET_CUTSCENE_BARS:
		case OP_CUTSCENES_UNSET_CUTSCENE_BARS:
		case OP_CUTSCENES_FADE_IN:
		case OP_CUTSCENES_FADE_OUT:
		case OP_CUTSCENES_SET_CAMERA:
		case OP_CUTSCENES_SET_CAMERA_FACING:
		case OP_CUTSCENES_SET_CAMERA_FACING_OBJECT:
		case OP_CUTSCENES_SET_CAMERA_FOV:
		case OP_CUTSCENES_SET_CAMERA_HOST:
		case OP_CUTSCENES_SET_CAMERA_POSITION:
		case OP_CUTSCENES_SET_CAMERA_ROTATION:
		case OP_CUTSCENES_SET_CAMERA_TARGET:
		case OP_CUTSCENES_SET_FOV:
		case OP_CUTSCENES_RESET_FOV:
		case OP_CUTSCENES_RESET_CAMERA:
		case OP_CUTSCENES_SHOW_SUBTITLE:
		case OP_CUTSCENES_SHOW_SUBTITLE_TEXT:
		case OP_CUTSCENES_SHOW_SUBTITLE_IMAGE:
		case OP_CUTSCENES_SET_TIME_COMPRESSION:
		case OP_CUTSCENES_RESET_TIME_COMPRESSION:
		case OP_CUTSCENES_FORCE_PERSPECTIVE:
		case OP_ALLOW_PHOTO_MODE:
		case OP_SET_CAMERA_SHUDDER:
		case OP_JUMP_NODE_SET_JUMPNODE_NAME:
		case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME:
		case OP_JUMP_NODE_SET_JUMPNODE_COLOR:
		case OP_JUMP_NODE_SET_JUMPNODE_MODEL:
		case OP_JUMP_NODE_SHOW_JUMPNODE:
		case OP_JUMP_NODE_HIDE_JUMPNODE:
		case OP_SET_OBJECT_SPEED_X:
		case OP_SET_OBJECT_SPEED_Y:
		case OP_SET_OBJECT_SPEED_Z:
		case OP_SHIP_CREATE:
		case OP_PROP_CREATE:
		case OP_WEAPON_CREATE:
		case OP_MISSION_SET_NEBULA:
		case OP_CHANGE_BACKGROUND:
		case OP_ADD_BACKGROUND_BITMAP:
		case OP_ADD_BACKGROUND_BITMAP_NEW:
		case OP_REMOVE_BACKGROUND_BITMAP:
		case OP_ADD_SUN_BITMAP:
		case OP_ADD_SUN_BITMAP_NEW:
		case OP_REMOVE_SUN_BITMAP:
		case OP_NEBULA_CHANGE_STORM:
		case OP_NEBULA_TOGGLE_POOF:
		case OP_NEBULA_SET_POOFS:
		case OP_NEBULA_FADE_POOF:
		case OP_NEBULA_FADE_POOFS:
		case OP_NEBULA_CHANGE_PATTERN:
		case OP_NEBULA_CHANGE_FOG_COLOR:
		case OP_NEBULA_SET_RANGE:
		case OP_VOLUMETRICS_TOGGLE:
		case OP_TOGGLE_ASTEROID_FIELD:
		case OP_SET_ASTEROID_FIELD:
		case OP_SET_DEBRIS_FIELD:
		case OP_CONFIG_ASTEROID_FIELD:
		case OP_CONFIG_DEBRIS_FIELD:
		case OP_CONFIG_FIELD_TARGETS:
		case OP_SET_MOTION_DEBRIS:
		case OP_SET_PRIMARY_AMMO:
		case OP_SET_SECONDARY_AMMO:
		case OP_SET_PRIMARY_WEAPON:
		case OP_SET_SECONDARY_WEAPON:
		case OP_SET_NUM_COUNTERMEASURES:
		case OP_SCRIPT_EVAL:
		case OP_SCRIPT_EVAL_BLOCK:
		case OP_SCRIPT_EVAL_STRING:
		case OP_SCRIPT_EVAL_MULTI:
		case OP_ENABLE_BUILTIN_MESSAGES:
		case OP_DISABLE_BUILTIN_MESSAGES:
		case OP_LOCK_PRIMARY_WEAPON:
		case OP_UNLOCK_PRIMARY_WEAPON:
		case OP_LOCK_SECONDARY_WEAPON:
		case OP_UNLOCK_SECONDARY_WEAPON:
		case OP_LOCK_AFTERBURNER:
		case OP_UNLOCK_AFTERBURNER:
		case OP_RESET_ORDERS:
		case OP_SET_PERSONA:
		case OP_SET_MISSION_MOOD:
		case OP_CHANGE_SUBSYSTEM_NAME:
		case OP_SET_RESPAWNS:
		case OP_ADD_REMOVE_HOTKEY:
		case OP_SET_AFTERBURNER_ENERGY: 
		case OP_SET_WEAPON_ENERGY:
		case OP_SET_SHIELD_ENERGY:
		case OP_SET_AMBIENT_LIGHT:
		case OP_SET_POST_EFFECT:
		case OP_RESET_POST_EFFECTS:
		case OP_CHANGE_IFF_COLOR:
		case OP_CLEAR_WEAPONS:
		case OP_CLEAR_DEBRIS:
		case OP_MISSION_SET_SUBSPACE:
		case OP_HUD_DISPLAY_GAUGE:
		case OP_FORCE_GLIDE:
		case OP_HUD_SET_DIRECTIVE:
		case OP_HUD_GAUGE_SET_ACTIVE:
		case OP_HUD_ACTIVATE_GAUGE_TYPE:
		case OP_STRING_CONCATENATE:
		case OP_STRING_CONCATENATE_BLOCK:
		case OP_INT_TO_STRING:
		case OP_DISABLE_ETS:
		case OP_ENABLE_ETS:
		case OP_STRING_GET_SUBSTRING:
		case OP_STRING_SET_SUBSTRING:
		case OP_ADD_TO_COLGROUP:
		case OP_REMOVE_FROM_COLGROUP:
		case OP_ADD_TO_COLGROUP_NEW:
		case OP_REMOVE_FROM_COLGROUP_NEW:
		case OP_SHIP_EFFECT:
		case OP_CLEAR_SUBTITLES:
		case OP_SET_THRUSTERS:
		case OP_SET_PLAYER_THROTTLE_SPEED:
		case OP_DEBUG:
		case OP_HUD_SET_CUSTOM_GAUGE_ACTIVE:
		case OP_HUD_SET_BUILTIN_GAUGE_ACTIVE:
		case OP_ALTER_SHIP_FLAG:
		case OP_ALTER_WING_FLAG:
		case OP_CANCEL_FUTURE_WAVES:
		case OP_CHANGE_TEAM_COLOR:
		case OP_COPY_VARIABLE_FROM_INDEX:
		case OP_COPY_VARIABLE_BETWEEN_INDEXES:
		case OP_SET_ETS_VALUES:
		case OP_CALL_SSM_STRIKE:
		case OP_OVERRIDE_MOTION_DEBRIS:
		case OP_TURRET_SET_PRIMARY_AMMO:
		case OP_TURRET_SET_SECONDARY_AMMO:
		case OP_TURRET_SET_FORCED_TARGET:
		case OP_TURRET_SET_FORCED_SUBSYS_TARGET:
		case OP_TURRET_CLEAR_FORCED_TARGET:
		case OP_TURRET_SET_INACCURACY:
		case OP_REPLACE_TEXTURE:
		case OP_REPLACE_TEXTURE_SKYBOX:
		case OP_SET_ALPHA_MULT:
		case OP_TRIGGER_ANIMATION_NEW:
		case OP_UPDATE_MOVEABLE:
		case OP_ADVANCE_MOVEABLE:
		case OP_STOP_LOOPING_ANIMATION:
		case OP_CONTAINER_ADD_TO_LIST:
		case OP_CONTAINER_REMOVE_FROM_LIST:
		case OP_CONTAINER_ADD_TO_MAP:
		case OP_CONTAINER_REMOVE_FROM_MAP:
		case OP_CONTAINER_GET_MAP_KEYS:
		case OP_CLEAR_CONTAINER:
		case OP_COPY_CONTAINER:
		case OP_APPLY_CONTAINER_FILTER:
		case OP_SET_GRAVITY_ACCEL:
		case OP_FORCE_REARM:
		case OP_ABORT_REARM:
		case OP_RESET_EVENT:
		case OP_RESET_GOAL:
			return OPR_NULL;

		case OP_AI_CHASE:
		case OP_AI_CHASE_WING:
		case OP_AI_CHASE_SHIP_CLASS:
		case OP_AI_CHASE_SHIP_TYPE:
		case OP_AI_CHASE_ANY:
		case OP_AI_DOCK:
		case OP_AI_UNDOCK:
		case OP_AI_WARP:						// this particular operator is obsolete
		case OP_AI_WARP_OUT:
		case OP_AI_WAYPOINTS:
		case OP_AI_WAYPOINTS_ONCE:
		case OP_AI_DESTROY_SUBSYS:
		case OP_AI_DISABLE_SHIP:
		case OP_AI_DISABLE_SHIP_TACTICAL:
		case OP_AI_DISARM_SHIP:
		case OP_AI_DISARM_SHIP_TACTICAL:
		case OP_AI_GUARD:
		case OP_AI_GUARD_WING:
		case OP_AI_EVADE_SHIP:
		case OP_AI_STAY_NEAR_SHIP:
		case OP_AI_KEEP_SAFE_DISTANCE:
		case OP_AI_IGNORE:
		case OP_AI_IGNORE_NEW:
		case OP_AI_STAY_STILL:
		case OP_AI_PLAY_DEAD:
		case OP_AI_PLAY_DEAD_PERSISTENT:
		case OP_AI_FORM_ON_WING:
		case OP_AI_FLY_TO_SHIP:
		case OP_AI_REARM_REPAIR:
			return OPR_AI_GOAL;

		case OP_ANY_OF:
		case OP_EVERY_OF:
		case OP_RANDOM_OF:
		case OP_RANDOM_MULTIPLE_OF:
		case OP_NUMBER_OF:
		case OP_IN_SEQUENCE:
		case OP_FOR_COUNTER:
		case OP_FOR_SHIP_CLASS:
		case OP_FOR_SHIP_TYPE:
		case OP_FOR_SHIP_TEAM:
		case OP_FOR_SHIP_SPECIES:
		case OP_FOR_PLAYERS:
		case OP_FOR_SUBSYSTEMS:
		case OP_FOR_CONTAINER_DATA:
		case OP_FOR_MAP_CONTAINER_KEYS:
		case OP_FIRST_OF:
			return OPR_FLEXIBLE_ARGUMENT;

		default: {
			auto dynamicSEXP = sexp::get_dynamic_sexp(op);
			if (dynamicSEXP != nullptr) {
				return dynamicSEXP->getReturnType();
			}

			Assertion(false, "query_operator_return_type() called for unsupported operator type %d!", op);
		}
	}

	return 0;
}

/**
 * Return the data type of a specified argument to an operator.  
 *
 * @param op operator index
 * @param argnum is 0 indexed.
 */
int query_operator_argument_type(int op_index, int argnum)
{
	if (op_index < 0)
		return OPF_NONE;

	int op_const;
	if (op_index < FIRST_OP)
	{
		Assertion(SCP_vector_inbounds(Operators, op_index), "Operator index is out of bounds!");
		op_const = Operators[op_index].value;
	}
	else
	{
		Warning(LOCATION, "Possible unnecessary search for operator index.  Trace out and see if this is necessary.\n");
		op_const = op_index;

		int count = sz2i(Operators.size());
		for (op_index=0; op_index<count; op_index++)
			if (Operators[op_index].value == op_const)
				break;

		if (op_index >= count)
			return OPF_NONE;
	}

	if (argnum >= Operators[op_index].max)
		return OPF_NONE;

	switch (op_const) {
		case OP_TRUE:
		case OP_FALSE:
		case OP_MISSION_TIME:
		case OP_MISSION_TIME_MSECS:
		case OP_NOP:
		case OP_WAYPOINT_MISSED:
		case OP_WAYPOINT_TWICE:
		case OP_PATH_FLOWN:
		case OP_GRANT_PROMOTION:
		case OP_WAS_PROMOTION_GRANTED:
		case OP_RED_ALERT:
		case OP_FORCE_JUMP:
		case OP_RESET_ORDERS:
		case OP_INVALIDATE_ALL_ARGUMENTS:
		case OP_VALIDATE_ALL_ARGUMENTS:
		case OP_NUM_VALID_ARGUMENTS:
		case OP_SUPERNOVA_STOP:
		case OP_GET_SUPERNOVA_STAGE:
		case OP_NAV_UNSELECT:
		case OP_PLAYER_IS_CHEATING_BASTARD:
		case OP_RESET_POST_EFFECTS:
		case OP_FOR_PLAYERS:
			return OPF_NONE;

		case OP_AND:
		case OP_AND_IN_SEQUENCE:
		case OP_OR:
		case OP_NOT:
		case OP_XOR:
			return OPF_BOOL;

		case OP_PLUS:
		case OP_MINUS:
		case OP_MOD:
		case OP_MUL:
		case OP_DIV:
		case OP_EQUALS:
		case OP_GREATER_THAN:
		case OP_LESS_THAN:
		case OP_NOT_EQUAL:
		case OP_GREATER_OR_EQUAL:
		case OP_LESS_OR_EQUAL:
		case OP_RAND:
		case OP_RAND_MULTIPLE:
		case OP_ABS:
		case OP_MIN:
		case OP_MAX:
		case OP_AVG:
		case OP_SIGNUM:
		case OP_IS_NAN:
		case OP_NAN_TO_NUMBER:
		case OP_ANGLE_VECTORS:
			return OPF_NUMBER;

		case OP_POW:
			if (argnum == 0)
				return OPF_NUMBER;
			else
				return OPF_POSITIVE;

		case OP_STRING_EQUALS:
		case OP_STRING_GREATER_THAN:
		case OP_STRING_LESS_THAN:
		case OP_STRING_TO_INT:		// Karajorma
		case OP_STRING_GET_LENGTH:	// Goober5000
			return OPF_STRING;

		case OP_STRING_CONCATENATE:
			if (argnum == 0 || argnum == 1) {
				return OPF_STRING;
			} else if (argnum == 2) {
				return OPF_VARIABLE_NAME;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_STRING_CONCATENATE_BLOCK:
			if (argnum == 0) {
				return OPF_VARIABLE_NAME;
			} else {
				return OPF_STRING;
			}

		case OP_INT_TO_STRING:
			if (argnum == 0) {
				return OPF_NUMBER;
			} else if (argnum == 1) {
				return OPF_VARIABLE_NAME;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_STRING_GET_SUBSTRING:
			if (argnum == 0) {
				return OPF_STRING;
			} else if (argnum == 1 || argnum == 2) {
				return OPF_POSITIVE;
			} else if (argnum == 3) {
				return OPF_VARIABLE_NAME;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_STRING_SET_SUBSTRING:
			if (argnum == 0) {
				return OPF_STRING;
			} else if (argnum == 1 || argnum == 2) {
				return OPF_POSITIVE;
			} else if (argnum == 3) {
				return OPF_STRING;
			} else if (argnum == 4) {
				return OPF_VARIABLE_NAME;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_DEBUG:
			if (argnum == 0) {
				return OPF_BOOL;
			} else {
				return OPF_MESSAGE_OR_STRING;
			}

		case OP_IS_TRUE_FOR_DURATION:
			if (argnum == 0)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_HAS_TIME_ELAPSED:
		case OP_HAS_TIME_ELAPSED_MSECS:
		case OP_SPEED:
		case OP_SET_TRAINING_CONTEXT_SPEED:
		case OP_SPECIAL_CHECK:
		case OP_AI_WARP_OUT:
		case OP_TEAM_SCORE:
		case OP_HUD_SET_MAX_TARGETING_RANGE:
		case OP_MISSION_SET_NEBULA:	//WMC
		case OP_MISSION_SET_SUBSPACE:
		case OP_SET_BIT:
		case OP_UNSET_BIT:
		case OP_IS_BIT_SET:
		case OP_BITWISE_AND:
		case OP_BITWISE_OR:
		case OP_BITWISE_NOT:
		case OP_BITWISE_XOR:
			return OPF_POSITIVE;

		case OP_AI_WARP:								// this operator is obsolete
		case OP_SET_TRAINING_CONTEXT_FLY_PATH:
			if ( !argnum )
				return OPF_WAYPOINT_PATH;
			else
				return OPF_NUMBER;
		
		case OP_AI_WAYPOINTS:
		case OP_AI_WAYPOINTS_ONCE:
			if (argnum == 0)
				return OPF_WAYPOINT_PATH;
			else if (argnum == 1 || argnum == 3)
				return OPF_POSITIVE;
			else if (argnum == 2 || argnum == 4)
				return OPF_BOOL;
			else
				return OPF_NONE;

		case OP_TURRET_PROTECT_SHIP:
		case OP_TURRET_UNPROTECT_SHIP:
			if (argnum == 0)
				return OPF_TURRET_TYPE;
			else
				return OPF_SHIP;

		case OP_IS_DISABLED:
		case OP_IS_DISARMED:
		case OP_TIME_SHIP_DESTROYED:
		case OP_TIME_SHIP_ARRIVED:
		case OP_TIME_SHIP_DEPARTED:
		case OP_AFTERBURNER_LEFT:
		case OP_WEAPON_ENERGY_LEFT:
		case OP_SHIELDS_LEFT:
		case OP_HITS_LEFT:
		case OP_SIM_HITS_LEFT:
		case OP_CLEAR_SHIP_GOALS:
		case OP_PROTECT_SHIP:
		case OP_UNPROTECT_SHIP:
		case OP_BEAM_PROTECT_SHIP:
		case OP_BEAM_UNPROTECT_SHIP:
		case OP_TRANSFER_CARGO:
		case OP_EXCHANGE_CARGO:
		case OP_SHIP_INVISIBLE:
		case OP_SHIP_VISIBLE:	
		case OP_SHIP_INVULNERABLE:
		case OP_SHIP_VULNERABLE:
		case OP_SHIP_BOMB_TARGETABLE:
		case OP_SHIP_BOMB_UNTARGETABLE:
		case OP_SHIP_GUARDIAN:
		case OP_SHIP_NO_GUARDIAN:
		case OP_SHIP_VANISH:
		case OP_DESTROY_INSTANTLY:
		case OP_DESTROY_INSTANTLY_WITH_DEBRIS:
		case OP_SHIELDS_ON:
		case OP_SHIELDS_OFF:
		case OP_SHIP_STEALTHY:
		case OP_SHIP_UNSTEALTHY:
		case OP_FRIENDLY_STEALTH_INVISIBLE:
		case OP_FRIENDLY_STEALTH_VISIBLE:
		case OP_PRIMARIES_DEPLETED:
		case OP_SECONDARIES_DEPLETED:
		case OP_SPECIAL_WARP_DISTANCE:
		case OP_SET_SPECIAL_WARPOUT_NAME:
		case OP_IS_SHIP_VISIBLE:
		case OP_IS_SHIP_STEALTHY:
		case OP_IS_FRIENDLY_STEALTH_VISIBLE:
		case OP_GET_DAMAGE_CAUSED:
		case OP_GET_THROTTLE_SPEED:
		case OP_FORCE_REARM:
		case OP_ABORT_REARM:
			return OPF_SHIP;

		case OP_PROP_VANISH:
			return OPF_PROP;

		case OP_ALTER_SHIP_FLAG:
			if(argnum == 0)
				return OPF_SHIP_FLAG;
			if(argnum == 1 || argnum == 2)
				return OPF_BOOL;
			else
				return OPF_SHIP_WING_WHOLETEAM;

		case OP_ALTER_WING_FLAG:
			if(argnum == 0)
				return OPF_WING_FLAG;
			if(argnum == 1)
				return OPF_BOOL;
			else
				return OPF_WING;

		case OP_CANCEL_FUTURE_WAVES:
			return OPF_WING;
		
		case OP_SET_PLAYER_THROTTLE_SPEED:
			if(argnum == 0)
				return OPF_SHIP;
			else
				return OPF_POSITIVE;

		case OP_SHIP_CREATE:
			if (argnum == 0)
				return OPF_STRING;
			else if (argnum == 1)
				return OPF_SHIP_CLASS_NAME;
			else if (argnum == 8)
				return OPF_IFF;
			else if (argnum == 9)
				return OPF_BOOL;
			else
				return OPF_NUMBER;

		case OP_PROP_CREATE:
			if (argnum == 0)
				return OPF_STRING;
			else if (argnum == 1)
				return OPF_PROP_CLASS_NAME;
			else
				return OPF_NUMBER;

		case OP_WEAPON_CREATE:
			if (argnum == 0)
				return OPF_SHIP_OR_NONE;
			else if (argnum == 1)
				return OPF_WEAPON_NAME;
			else if (argnum == 8)
				return OPF_SHIP;
			else if (argnum == 9)
				return OPF_SUBSYSTEM;
			else
				return OPF_NUMBER;

		case OP_CLEAR_WEAPONS:
			return OPF_WEAPON_NAME;

		case OP_CLEAR_DEBRIS:
			return OPF_SHIP_CLASS_NAME;

		case OP_SHIP_GUARDIAN_THRESHOLD:
			if (argnum == 0)
				return OPF_POSITIVE;
			else
				return OPF_SHIP;

		case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD:
			if (argnum == 0)
				return OPF_POSITIVE;
			else if (argnum == 1)
				return OPF_SHIP;
			else
				return OPF_SUBSYS_OR_GENERIC;

		case OP_SET_GUARD_RANGE:
			if (argnum == 0)
				return OPF_NUMBER;
			else
				return OPF_SHIP;

		case OP_SHIP_SUBSYS_TARGETABLE:
		case OP_SHIP_SUBSYS_UNTARGETABLE:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_SUBSYS_OR_GENERIC;

		case OP_SHIP_SUBSYS_NO_REPLACE:
		case OP_SHIP_SUBSYS_NO_LIVE_DEBRIS:
		case OP_SHIP_SUBSYS_VANISHED:
		case OP_SHIP_SUBSYS_IGNORE_IF_DEAD:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_BOOL;
			else
				return OPF_SUBSYS_OR_GENERIC;

		case OP_IS_DESTROYED:
		case OP_HAS_ARRIVED:
		case OP_HAS_DEPARTED:
		case OP_CLEAR_GOALS:
			return OPF_SHIP_WING;

		case OP_IS_DISABLED_DELAY:
		case OP_IS_DISARMED_DELAY:
			if ( argnum == 0 )
				return OPF_POSITIVE;
			else
				return OPF_SHIP;

		case OP_SHIP_TAG:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 3)
				return OPF_SSM_CLASS;
			else if (argnum == 7)
				return OPF_IFF;
			else if (argnum > 3)	// SSM origin coordinates shouldn't be limited to positive numbers
				return OPF_NUMBER;
			else
				return OPF_POSITIVE;

		case OP_SHIP_UNTAG:
			return OPF_SHIP;

		case OP_FACING:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_POSITIVE;

		case OP_FACING2:
			if (argnum == 0) {
				return OPF_WAYPOINT_PATH;
			} else {
				return OPF_POSITIVE;
			}

		case OP_ORDER:
			if (argnum == 1)
				return OPF_AI_ORDER;
			else
				return OPF_SHIP_WING;	// arg 0 or 2

		case OP_QUERY_ORDERS:
			if (argnum == 0)
				return OPF_ORDER_RECIPIENT;
			if (argnum == 1)
				return OPF_AI_ORDER;
			if (argnum == 2)
				return OPF_POSITIVE;
			if (argnum == 5)
				return OPF_SUBSYSTEM;
			else
				return OPF_SHIP_WING;

		case OP_TIME_TO_GOAL:
				return OPF_SHIP;

		case OP_SET_HUD_TIME_PAD:
			return OPF_NUMBER;

		case OP_WAS_DESTROYED_BY_DELAY:
			if (argnum == 0)
				return OPF_POSITIVE;
			else
				return OPF_SHIP;

		case OP_IS_DESTROYED_DELAY:
		case OP_HAS_ARRIVED_DELAY:
		case OP_HAS_DEPARTED_DELAY:
		case OP_LAST_ORDER_TIME:
			if ( argnum == 0 )
				return OPF_POSITIVE;
			else
				return OPF_SHIP_WING;

		case OP_SHIP_CHANGE_ALT_NAME:
			if (argnum == 0)
				return OPF_STRING;
			else
				return OPF_SHIP_WING;

		case OP_SHIP_CHANGE_CALLSIGN:
			if (argnum == 0)
				return OPF_STRING;
			else
				return OPF_SHIP;

		case OP_SHIP_CHANGE_DISPLAY_NAME:
			if (argnum == 0)
				return OPF_STRING;
			else
				return OPF_SHIP_WING;

		case OP_SET_DEATH_MESSAGE:
			return OPF_MESSAGE_OR_STRING;

		case OP_DISTANCE:
		case OP_DISTANCE_CENTER:
		case OP_DISTANCE_BBOX:
			return OPF_SHIP_WING_SHIPONTEAM_POINT;

		case OP_SET_OBJECT_SPEED_X:
		case OP_SET_OBJECT_SPEED_Y:
		case OP_SET_OBJECT_SPEED_Z:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_NUMBER;
			else
				return OPF_BOOL;

		case OP_GET_OBJECT_SPEED_X:
		case OP_GET_OBJECT_SPEED_Y:
		case OP_GET_OBJECT_SPEED_Z:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else
				return OPF_BOOL;

		case OP_GET_OBJECT_X:
		case OP_GET_OBJECT_Y:
		case OP_GET_OBJECT_Z:
			if (argnum == 0)
				return OPF_SHIP_WING_POINT;
			else if (argnum == 1)
				return OPF_SUBSYSTEM_OR_NONE;
			else
				return OPF_NUMBER;

		case OP_GET_OBJECT_PITCH:
		case OP_GET_OBJECT_BANK:
		case OP_GET_OBJECT_HEADING:
			return OPF_SHIP_WING;

		case OP_ANGLE_FVEC_TARGET:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_SHIP_WING_POINT;

		case OP_SET_OBJECT_POSITION:
			if(argnum == 0)
				return OPF_SHIP_WING_POINT;
			else
				return OPF_NUMBER;

		case OP_SET_OBJECT_ORIENTATION:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else
				return OPF_NUMBER;

		case OP_SET_OBJECT_FACING:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum < 4)
				return OPF_NUMBER;
			else
				return OPF_POSITIVE;

		case OP_SET_OBJECT_FACING_OBJECT:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_SHIP_WING_POINT;
			else
				return OPF_POSITIVE;

		case OP_SHIP_MANEUVER:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else if (argnum < 5)
				return OPF_NUMBER;
			else if (argnum == 5)
				return OPF_BOOL;
			else if (argnum < 9)
				return OPF_NUMBER;
			else if (argnum == 9)
				return OPF_BOOL;
			else
				return OPF_POSITIVE;

		case OP_SHIP_ROT_MANEUVER:
		case OP_SHIP_LAT_MANEUVER:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else if (argnum < 5)
				return OPF_NUMBER;
			else if (argnum == 5)
				return OPF_BOOL;
			else
				return OPF_POSITIVE;

		case OP_MODIFY_VARIABLE:
			if (argnum == 0) {
				return OPF_VARIABLE_NAME;
			} else {
				return OPF_AMBIGUOUS; 
			}

		case OP_MODIFY_VARIABLE_XSTR:
			if (argnum == 0) {
				return OPF_VARIABLE_NAME;
			} else if (argnum == 1) {
				return OPF_STRING;
			} else if (argnum == 2) {
				return OPF_NUMBER;
			} else {
				return OPF_NONE;
			}

		case OP_GET_VARIABLE_BY_INDEX:
		case OP_COPY_VARIABLE_BETWEEN_INDEXES:
			return OPF_POSITIVE;

		case OP_COPY_VARIABLE_FROM_INDEX:
			if (argnum == 0) {
				return OPF_POSITIVE;
			} else {
				return OPF_VARIABLE_NAME;
			}

		case OP_SET_VARIABLE_BY_INDEX:
			if (argnum == 0) {
				return OPF_POSITIVE;
			} else {
				return OPF_AMBIGUOUS;
			}

		case OP_CONTAINER_ADD_TO_LIST:
			if (argnum == 0) {
				return OPF_LIST_CONTAINER_NAME;
			} else if (argnum == 1) {
				return OPF_BOOL;
			} else {
				return OPF_CONTAINER_VALUE;
			}

		case OP_CONTAINER_REMOVE_FROM_LIST:
			if (argnum == 0) {
				return OPF_LIST_CONTAINER_NAME;
			} else {
				return OPF_CONTAINER_VALUE;
			}

		case OP_CONTAINER_ADD_TO_MAP:
		case OP_CONTAINER_REMOVE_FROM_MAP:
			if (argnum == 0) {
				return OPF_MAP_CONTAINER_NAME;
			} else {
				return OPF_CONTAINER_VALUE;
			}

		case OP_CONTAINER_GET_MAP_KEYS:
			if (argnum == 0) {
				return OPF_MAP_CONTAINER_NAME;
			} else if (argnum == 1) {
				return OPF_LIST_CONTAINER_NAME;
			} else if (argnum == 2) {
				return OPF_BOOL;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_CLEAR_CONTAINER:
			return OPF_CONTAINER_NAME;

		case OP_COPY_CONTAINER:
			if (argnum == 0 || argnum == 1) {
				return OPF_CONTAINER_NAME;
			} else if (argnum == 2) {
				return OPF_BOOL;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_APPLY_CONTAINER_FILTER:
			if (argnum == 0) {
				return OPF_CONTAINER_NAME;
			} else if (argnum == 1) {
				return OPF_LIST_CONTAINER_NAME;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_HAS_DOCKED:
		case OP_HAS_UNDOCKED:
		case OP_HAS_DOCKED_DELAY:
		case OP_HAS_UNDOCKED_DELAY:
		case OP_TIME_DOCKED:
		case OP_TIME_UNDOCKED:
			if ( argnum < 2 )
				return OPF_SHIP;
			else
				return OPF_POSITIVE;

		case OP_TIME_WING_DESTROYED:
		case OP_TIME_WING_ARRIVED:
		case OP_TIME_WING_DEPARTED:
		case OP_CLEAR_WING_GOALS:
			return OPF_WING;

		case OP_SET_SCANNED:
		case OP_SET_UNSCANNED:
		case OP_IS_SUBSYSTEM_DESTROYED:
			if (!argnum)
				return OPF_SHIP;
			else
				return OPF_SUBSYSTEM;
			
		case OP_HITS_LEFT_SUBSYSTEM:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1) 
				return OPF_SUBSYSTEM;
			else 
				return OPF_BOOL;

		case OP_HITS_LEFT_SUBSYSTEM_GENERIC:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_SUBSYSTEM_TYPE;

		case OP_HITS_LEFT_SUBSYSTEM_SPECIFIC:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_SUBSYSTEM;
			else
				// This shouldn't happen
				return OPF_NONE;

		case OP_DISTANCE_CENTER_SUBSYSTEM:
		case OP_DISTANCE_BBOX_SUBSYSTEM:
			if (argnum == 0)
				return OPF_SHIP_WING_SHIPONTEAM_POINT;
			else if (argnum == 1)
				return OPF_SHIP;
			else if (argnum == 2)
				return OPF_SUBSYSTEM;
			else
				// This shouldn't happen
				return OPF_NONE;

		case OP_NUM_WITHIN_BOX:
			if(argnum < 3)
				return OPF_NUMBER;
			else if(argnum < 6)
				return OPF_POSITIVE;
			else
				return OPF_SHIP_WING;

		case OP_IS_IN_BOX:
			if (argnum == 0) // First arg is a ship/wing/point
				return OPF_SHIP_WING_POINT;
			else if (argnum <= 6) // Next 6 args are coordinates
				return OPF_NUMBER;
			else // Next arg is a ship
				return OPF_SHIP;

		case OP_IS_IN_MISSION:
			return OPF_STRING;

		case OP_HAS_ARMOR_TYPE:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_ARMOR_TYPE;
			else
				return OPF_SUBSYSTEM;

		case OP_IS_DOCKED:
			return OPF_SHIP;

		// Sesquipedalian
		case OP_MISSILE_LOCKED:
			if (argnum == 0)
				return OPF_POSITIVE;
			else if (argnum == 1)
				return OPF_SHIP;
			else
				return OPF_SUBSYSTEM;

		case OP_TARGETED:
			if (!argnum)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_SUBSYSTEM;

		case OP_NODE_TARGETED:
			if (!argnum)
				return OPF_JUMP_NODE_NAME;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_NONE;

		case OP_IS_SUBSYSTEM_DESTROYED_DELAY:
			if ( argnum == 0 )
				return OPF_SHIP;
			else if ( argnum == 1 )
				return OPF_SUBSYSTEM;
			else
				return OPF_POSITIVE;

		case OP_IS_IFF:
			if (!argnum)
				return OPF_IFF;
			else
				return OPF_SHIP_WING;

		case OP_CHANGE_IFF:
			if (!argnum)
				return OPF_IFF;
			else
				return OPF_SHIP_WING_WHOLETEAM;

		case OP_IS_SPECIES:
			if (!argnum)
				return OPF_SPECIES;
			else
				return OPF_SHIP_WING;

		case OP_ADD_SHIP_GOAL:
			if (!argnum)
				return OPF_SHIP;
			else
				return OPF_AI_GOAL;

		case OP_ADD_WING_GOAL:
			if (!argnum)
				return OPF_WING;
			else
				return OPF_AI_GOAL;

		case OP_ADD_GOAL:
			if ( argnum == 0 )
				return OPF_SHIP_WING;
			else
				return OPF_AI_GOAL;

		case OP_REMOVE_GOAL:
			if ( argnum == 0 )
				return OPF_SHIP_WING;
			else if ( argnum == 1 )
				return OPF_AI_GOAL;
			else
				return OPF_BOOL;

		case OP_COND:
		case OP_WHEN:
		case OP_EVERY_TIME:
		case OP_IF_THEN_ELSE:
		case OP_PERFORM_ACTIONS_BOOL_FIRST:
		case OP_PERFORM_ACTIONS_BOOL_LAST:
			if (!argnum)
				return OPF_BOOL;
			else
				return OPF_NULL;

		case OP_SWITCH:
			if (!argnum)
				return OPF_NUMBER;
			else
				return OPF_NULL;

		case OP_WHEN_ARGUMENT:
		case OP_EVERY_TIME_ARGUMENT:
			if (argnum == 0)
				return OPF_FLEXIBLE_ARGUMENT;
			else if (argnum == 1)
				return OPF_BOOL;
			else
				return OPF_NULL;
			
		case OP_DO_FOR_VALID_ARGUMENTS:
		case OP_ON_MISSION_SKIP:
			return OPF_NULL;

		case OP_RANDOM_OF:
		case OP_IN_SEQUENCE:
			return OPF_ANYTHING;

		case OP_ANY_OF:
		case OP_EVERY_OF:
		case OP_RANDOM_MULTIPLE_OF:
			return OPF_DATA_OR_STR_CONTAINER;

		case OP_NUMBER_OF:
		case OP_FIRST_OF:
			if (argnum == 0)
				return OPF_POSITIVE;
			else
				return OPF_DATA_OR_STR_CONTAINER;

		case OP_FOR_COUNTER:
			return OPF_NUMBER;

		case OP_FOR_SHIP_CLASS:
			return OPF_SHIP_CLASS_NAME;

		case OP_FOR_SHIP_TYPE:
			return OPF_SHIP_TYPE;

		case OP_FOR_SHIP_TEAM:
			return OPF_IFF;

		case OP_FOR_SHIP_SPECIES:
			return OPF_SPECIES;

		case OP_FOR_SUBSYSTEMS:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_SUBSYSTEM_TYPE;

		case OP_FOR_CONTAINER_DATA:
			return OPF_CONTAINER_NAME;

		case OP_FOR_MAP_CONTAINER_KEYS:
			return OPF_MAP_CONTAINER_NAME;

		case OP_INVALIDATE_ARGUMENT:
		case OP_VALIDATE_ARGUMENT:
			return OPF_ANYTHING;

		case OP_FUNCTIONAL_IF_THEN_ELSE:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_NUMBER;

		case OP_FUNCTIONAL_SWITCH:
			return OPF_NUMBER;

		case OP_FUNCTIONAL_WHEN:
			if (argnum == 0 || argnum == 2)
				return OPF_BOOL;
			else if (argnum == 1)
				return OPF_FUNCTIONAL_WHEN_EVAL_TYPE;
			else
				return OPF_NULL;

		case OP_AI_DISABLE_SHIP:
		case OP_AI_DISABLE_SHIP_TACTICAL:
		case OP_AI_DISARM_SHIP:
		case OP_AI_DISARM_SHIP_TACTICAL:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_EVADE_SHIP:
		case OP_AI_IGNORE:
		case OP_AI_IGNORE_NEW:
		case OP_AI_REARM_REPAIR:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_FLY_TO_SHIP:
		case OP_AI_STAY_NEAR_SHIP:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1 || argnum == 2)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_CHASE:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_CHASE_WING:
			if (argnum == 0)
				return OPF_WING;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_CHASE_SHIP_CLASS:
			if (argnum == 0)
				return OPF_SHIP_CLASS_NAME;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_CHASE_SHIP_TYPE:
			if (argnum == 0)
				return OPF_SHIP_TYPE;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_GUARD:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_GUARD_WING:
			if (argnum == 0)
				return OPF_WING;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_KEEP_SAFE_DISTANCE:
			return OPF_POSITIVE;

		case OP_AI_DOCK:
			if (!argnum)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_DOCKER_POINT;
			else if (argnum == 2)
				return OPF_DOCKEE_POINT;
			else if(argnum == 3)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_UNDOCK:
			if (argnum == 0)
				return OPF_POSITIVE;
			else
				return OPF_SHIP;

		case OP_AI_DESTROY_SUBSYS:
			if (!argnum)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_SUBSYSTEM;
			else if (argnum == 2)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;
			
		case OP_GOALS_ID:
			return OPF_AI_GOAL;

		case OP_SET_CARGO:
		case OP_IS_CARGO:
			if (argnum == 0)
				return OPF_CARGO;
			else if (argnum == 1)
				return OPF_SHIP;
			else
				return OPF_SUBSYSTEM;

		case OP_CHANGE_AI_CLASS:
		case OP_IS_AI_CLASS:
			if (argnum == 0)
				return OPF_AI_CLASS;
			else if (argnum == 1)
				return OPF_SHIP;
			else
				return OPF_SUBSYS_OR_GENERIC;

		case OP_IS_SHIP_TYPE:
			if (argnum == 0)
				return OPF_SHIP_TYPE;
			else
				return OPF_SHIP;

		case OP_IS_SHIP_CLASS:
			if (argnum == 0)
				return OPF_SHIP_CLASS_NAME;
			else
				return OPF_SHIP;

		case OP_CHANGE_SOUNDTRACK:
			return OPF_SOUNDTRACK_NAME;

		case OP_PLAY_SOUND_FROM_TABLE:
			if (argnum == 3)
				return OPF_GAME_SND;
			else
				return OPF_NUMBER;

		case OP_PLAY_SOUND_FROM_FILE:
			if (argnum == 0)
				return OPF_STRING;
			else if (argnum == 3)
				return OPF_VARIABLE_NAME;
			else
				return OPF_NUMBER;

		case OP_CLOSE_SOUND_FROM_FILE:
		case OP_PAUSE_SOUND_FROM_FILE:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_VARIABLE_NAME;

		case OP_SET_FRIENDLY_DAMAGE_CAPS:
			return OPF_NUMBER;
		case OP_ALLOW_TREASON:
		case OP_END_MISSION:
		case OP_SET_DEBRIEFING_TOGGLED:
			return OPF_BOOL;

		case OP_SET_DEBRIEFING_PERSONA:
			return OPF_POSITIVE;

		case OP_SET_TRAITOR_OVERRIDE:
			return OPF_TRAITOR_OVERRIDE;

		case OP_SET_PLAYER_ORDERS:
		case OP_SET_ORDER_ALLOWED_TARGET:
			if (argnum==0)
				return OPF_SHIP;
			if (argnum==1)
				return OPF_BOOL;
			else 
				return OPF_AI_ORDER;

		case OP_ENABLE_GENERAL_ORDERS:
		case OP_VALIDATE_GENERAL_ORDERS:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_LUA_GENERAL_ORDER;

		case OP_SET_SOUND_ENVIRONMENT:
			if (argnum == 0)
				return OPF_SOUND_ENVIRONMENT;

			// fall through
			argnum--;
			FALLTHROUGH;

		case OP_UPDATE_SOUND_ENVIRONMENT:
		{
			// every two, the value repeats
			int a_mod = argnum % 2;

			if (a_mod == 0)
				return OPF_SOUND_ENVIRONMENT_OPTION;
			else
				return OPF_POSITIVE;
		}

		case OP_ADJUST_AUDIO_VOLUME:
		{
			if (argnum == 0)
				return OPF_AUDIO_VOLUME_OPTION;
			else
				return OPF_POSITIVE;
		}

		case OP_SET_EXPLOSION_OPTION:
		{
			// editing a ship
			if (argnum == 0)
				return OPF_SHIP;

			// every two, the value repeats
			int a_mod = (argnum - 1) % 2;

			if (a_mod == 0)
				return OPF_EXPLOSION_OPTION;
			else
				return OPF_POSITIVE;
		}

		case OP_HUD_DISABLE:
		case OP_HUD_DISABLE_EXCEPT_MESSAGES:
			return OPF_POSITIVE;

		case OP_HUD_SET_TEXT:
			if (argnum == 0)
				return OPF_CUSTOM_HUD_GAUGE;
			else
				return OPF_STRING;

		case OP_HUD_SET_XSTR:
			if (argnum == 0)
				return OPF_CUSTOM_HUD_GAUGE;
			if (argnum == 1)
				return OPF_STRING;
			else
				return OPF_NUMBER;

		case OP_HUD_SET_MESSAGE:
			if (argnum == 0)
				return OPF_CUSTOM_HUD_GAUGE;
			else
				return OPF_MESSAGE;

		case OP_HUD_SET_TEXT_NUM:
			if (argnum == 0)
				return OPF_CUSTOM_HUD_GAUGE;
			else
				return OPF_POSITIVE;

		case OP_HUD_SET_COORDS:
		case OP_HUD_SET_FRAME:
		case OP_HUD_SET_COLOR:
			if (argnum == 0)
				return OPF_ANY_HUD_GAUGE;
			else
				return OPF_POSITIVE;

		case OP_HUD_RESET_COLOR:
			return OPF_ANY_HUD_GAUGE;

		case OP_HUD_CLEAR_MESSAGES:
			return OPF_NONE;

		case OP_HUD_FORCE_SENSOR_STATIC:
			return OPF_BOOL;

		case OP_HUD_FORCE_EMP_EFFECT:
			if (argnum < 2)
				return OPF_NUMBER;
			else
				return OPF_MESSAGE_OR_STRING;

		case OP_SET_SQUADRON_WINGS:
			return OPF_WING;

		case OP_PLAYER_USE_AI:
		case OP_PLAYER_NOT_USE_AI:
			return OPF_NONE;

		case OP_SET_PLAYER_TARGET:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_SUBSYSTEM_OR_NONE;

		case OP_CREATE_BOLT:
			if (argnum == 0)
				return OPF_BOLT_TYPE;
			else if (argnum == 7)
				return OPF_BOOL;
			else
				return OPF_NUMBER;

		case OP_EXPLOSION_EFFECT:
			if (argnum <= 2)
				return OPF_NUMBER;
			else if (argnum == 9)
				return OPF_FIREBALL;
			else if (argnum == 10)
				return OPF_GAME_SND;
			else
				return OPF_POSITIVE;

		case OP_WARP_EFFECT:
			if (argnum <= 5)
				return OPF_NUMBER;
			else if (argnum == 8 || argnum == 9)
				return OPF_GAME_SND;
			else if (argnum == 10)
				return OPF_FIREBALL;
			else
				return OPF_POSITIVE;

		case OP_SEND_MESSAGE:
		case OP_SEND_RANDOM_MESSAGE:
			if ( argnum == 0 )
				return OPF_WHO_FROM;
			else if ( argnum == 1 )
				return OPF_PRIORITY;
			else
				return OPF_MESSAGE;

		case OP_SEND_BUILTIN_MESSAGE:
			switch (argnum) {
				case 0 : return OPF_MESSAGE_TYPE;
				case 1 : return OPF_SHIP_OR_NONE;
				case 2 : return OPF_BOOL;
				default: return OPF_WHO_FROM;
			}

		case OP_SEND_MESSAGE_LIST:
		case OP_SEND_MESSAGE_CHAIN:
		{
			// chain has one extra argument but is otherwise the same
			if (op_const == OP_SEND_MESSAGE_CHAIN)
			{
				if (argnum == 0)
					return OPF_EVENT_NAME;
				argnum--;
			}

			// every four, the value repeats
			int a_mod = argnum % 4;

			// who from
			if(a_mod == 0)
				return OPF_WHO_FROM;
			else if(a_mod == 1)
				return OPF_PRIORITY;
			else if(a_mod == 2)
				return OPF_MESSAGE;
			else if(a_mod == 3)
				return OPF_POSITIVE;
			else
				// This can't happen
				return OPF_NONE;
		}

		case OP_TRAINING_MSG:
			if (argnum < 2)
				return OPF_MESSAGE;
			else
				return OPF_POSITIVE;

		// Karajorma
		case OP_ENABLE_BUILTIN_MESSAGES:
		case OP_DISABLE_BUILTIN_MESSAGES:
				return OPF_WHO_FROM;

		case OP_SET_PERSONA:
			if (argnum == 0) 
				return OPF_PERSONA;
			else
				return OPF_SHIP;

		case OP_SET_MISSION_MOOD:
			return OPF_MISSION_MOOD;

		case OP_CHANGE_TEAM_COLOR:
			if (argnum == 0)
				return OPF_TEAM_COLOR;
			else if (argnum == 1)
				return OPF_NUMBER;
			else
				return OPF_SHIP;

		case OP_CALL_SSM_STRIKE:
			if (argnum == 0)
				return OPF_SSM_CLASS;
			else if (argnum == 1)
				return OPF_IFF;
			else
				return OPF_SHIP;

		case OP_SELF_DESTRUCT:
			return OPF_SHIP;

		case OP_NEXT_MISSION:
			return OPF_MISSION_NAME;

		case OP_END_CAMPAIGN:
			return OPF_BOOL;

		case OP_END_OF_CAMPAIGN:
			return OPF_NONE;

		case OP_PREVIOUS_GOAL_TRUE:
		case OP_PREVIOUS_GOAL_FALSE:
			if ( argnum == 0 )
				return OPF_MISSION_NAME;
			else if (argnum == 1 )
				return OPF_GOAL_NAME;
			else
				return OPF_BOOL;

		case OP_PREVIOUS_GOAL_INCOMPLETE:
			return OPF_GOAL_NAME;

		case OP_PREVIOUS_EVENT_TRUE:
		case OP_PREVIOUS_EVENT_FALSE:
		case OP_PREVIOUS_EVENT_INCOMPLETE:
			if (!argnum)
				return OPF_MISSION_NAME;
			else if ( argnum == 1 )
				return OPF_EVENT_NAME;
			else
				return OPF_BOOL;

		case OP_SABOTAGE_SUBSYSTEM:
			if (!argnum)
				return OPF_SHIP;		// changed from OPF_SHIP_NOT_PLAYER by Goober5000: now it can be the player ship also
			else if (argnum == 1 )
				return OPF_SUBSYS_OR_GENERIC;
			else
				return OPF_POSITIVE;

		case OP_REPAIR_SUBSYSTEM:
		case OP_SET_SUBSYSTEM_STRNGTH:
			if (!argnum)
				return OPF_SHIP;		// changed from OPF_SHIP_NOT_PLAYER by Goober5000: now it can be the player ship also
			else if (argnum == 1 )
				return OPF_SUBSYS_OR_GENERIC;
			else if (argnum == 2)
				return OPF_POSITIVE;
			else 
				return OPF_BOOL;

		case OP_DESTROY_SUBSYS_INSTANTLY:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_SUBSYS_OR_GENERIC;

		case OP_WAYPOINTS_DONE:
			if ( argnum == 0 )
				return OPF_SHIP_WING;
			else
				return OPF_WAYPOINT_PATH;

		case OP_WAYPOINTS_DONE_DELAY:
			if ( argnum == 0 )
				return OPF_SHIP_WING;
			else if ( argnum == 1 )
				return OPF_WAYPOINT_PATH;
			else
				return OPF_POSITIVE;

		case OP_INVALIDATE_GOAL:
		case OP_VALIDATE_GOAL:
			return OPF_GOAL_NAME;

		case OP_SHIP_TYPE_DESTROYED:
			if ( argnum == 0 )
				return OPF_POSITIVE;
			else
				return OPF_SHIP_TYPE;

		case OP_KEY_PRESSED:
			if (!argnum)
				return OPF_KEYPRESS;
			else
				return OPF_POSITIVE;

		case OP_KEY_RESET:
		case OP_KEY_RESET_MULTIPLE:
			return OPF_KEYPRESS;

		case OP_EVENT_TRUE:
		case OP_EVENT_FALSE:
			return OPF_EVENT_NAME;

		case OP_EVENT_INCOMPLETE:
		case OP_EVENT_TRUE_DELAY:
		case OP_EVENT_FALSE_DELAY:
		case OP_EVENT_TRUE_MSECS_DELAY:
		case OP_EVENT_FALSE_MSECS_DELAY:
			if (argnum == 0)
				return OPF_EVENT_NAME;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else if (argnum == 2)
				return OPF_BOOL;
			else
				return OPF_NONE;

		case OP_GOAL_INCOMPLETE:
		case OP_GOAL_TRUE_DELAY:
		case OP_GOAL_FALSE_DELAY:
			if (!argnum)
				return OPF_GOAL_NAME;
			else
				return OPF_POSITIVE;

		case OP_RESET_EVENT:
			return OPF_EVENT_NAME;

		case OP_RESET_GOAL:
			return OPF_GOAL_NAME;

		case OP_AI_CHASE_ANY:
			if (!argnum)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_PLAY_DEAD:
		case OP_AI_PLAY_DEAD_PERSISTENT:
			return OPF_POSITIVE;

		case OP_AI_STAY_STILL:
			if (argnum == 0)
				return OPF_SHIP_POINT;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_AI_FORM_ON_WING:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_GOOD_REARM_TIME:
		case OP_BAD_REARM_TIME:
			if ( argnum == 0 )
				return OPF_IFF;
			else
				return OPF_POSITIVE;

		case OP_NUM_PLAYERS:
			return OPF_NONE;

		case OP_SKILL_LEVEL_AT_LEAST:
			return OPF_SKILL_LEVEL;

		case OP_GRANT_MEDAL:
		case OP_WAS_MEDAL_GRANTED:
			return OPF_MEDAL_NAME;

		case OP_IS_CARGO_KNOWN:
			return OPF_SHIP;

		case OP_CARGO_KNOWN_DELAY:
			if ( argnum == 0 )
				return OPF_POSITIVE;
			else
				return OPF_SHIP;

		case OP_HAS_BEEN_TAGGED_DELAY:
			if ( argnum == 0 ) {
				return OPF_POSITIVE;
			} else {
				return OPF_SHIP;
			}

		case OP_ARE_SHIP_FLAGS_SET:
			if (argnum == 0) {
				return OPF_SHIP;
			} else {
				return OPF_SHIP_FLAG;
			}

		case OP_ARE_WING_FLAGS_SET:
			if (argnum == 0) {
				return OPF_WING;
			} else {
				return OPF_WING_FLAG;
			}

		case OP_IS_SHIP_EMP_ACTIVE:
			return OPF_SHIP;

		case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY:
			if ( argnum == 0 ) {
				return OPF_POSITIVE;
			} else if ( argnum == 1 ) {
				return OPF_SHIP;
			} else {
				return OPF_SUBSYSTEM;
			}

		case OP_ALLOW_SHIP:
		case OP_TECH_ADD_SHIP:
			return OPF_SHIP_CLASS_NAME;

		case OP_ALLOW_WEAPON:
		case OP_TECH_ADD_WEAPON:
			return OPF_WEAPON_NAME;

		case OP_TECH_ADD_INTEL:
		case OP_TECH_REMOVE_INTEL:
			return OPF_INTEL_NAME;

		case OP_TECH_ADD_INTEL_XSTR:
		case OP_TECH_REMOVE_INTEL_XSTR:
			return !(argnum % 2) ? OPF_INTEL_NAME : OPF_NUMBER;

		case OP_TECH_RESET_TO_DEFAULT:
			return OPF_NONE;

		case OP_CHANGE_PLAYER_SCORE:
			if (argnum == 0)
				return OPF_NUMBER;
			else 
				return OPF_SHIP;

		case OP_CHANGE_TEAM_SCORE:
			return OPF_NUMBER;

		case OP_SHIP_VAPORIZE:
		case OP_SHIP_NO_VAPORIZE:
			return OPF_SHIP;

		case OP_DONT_COLLIDE_INVISIBLE:
		case OP_COLLIDE_INVISIBLE:
			return OPF_SHIP;

		case OP_SET_MOBILE:
		case OP_SET_IMMOBILE:
			return OPF_SHIP;

		case OP_IGNORE_KEY:
			if (argnum == 0) 
				return OPF_NUMBER;
			else 
				return OPF_KEYPRESS;


		case OP_WARP_BROKEN:
		case OP_WARP_NOT_BROKEN:
		case OP_WARP_NEVER:
		case OP_WARP_ALLOWED:
			return OPF_SHIP;

		case OP_SET_SUBSPACE_DRIVE:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_SHIP;

		case OP_FLASH_HUD_GAUGE:
			return OPF_BUILTIN_HUD_GAUGE;

		case OP_GOOD_PRIMARY_TIME:
			if (argnum == 0 || argnum == 2) {
				return OPF_SHIP_WING_WHOLETEAM;
			} else if (argnum == 1) {
				return OPF_WEAPON_NAME;
			} else
				return OPF_BOOL;

		case OP_GOOD_SECONDARY_TIME:
			if ( argnum == 0 )
				return OPF_IFF;
			else if ( argnum == 1 )
				return OPF_POSITIVE;
			else if ( argnum == 2 )
				return OPF_HUGE_WEAPON;
			else
				return OPF_SHIP;

		case OP_PERCENT_SHIPS_ARRIVED:
		case OP_PERCENT_SHIPS_DEPARTED:
		case OP_PERCENT_SHIPS_DESTROYED:
			if ( argnum == 0 ){
				return OPF_POSITIVE;
			} else {
				return OPF_SHIP_WING;
			}
			break;

		case OP_PERCENT_SHIPS_DISARMED:
		case OP_PERCENT_SHIPS_DISABLED:
		case OP_PERCENT_SHIPS_SCANNED:
			if ( argnum == 0 ){
				return OPF_POSITIVE;
			} else {
				return OPF_SHIP;
			}
			break;

		case OP_DEPART_NODE_DELAY:	
			if ( argnum == 0 ){
				return OPF_POSITIVE;
			} else if ( argnum == 1 ){
				return OPF_JUMP_NODE_NAME;
			} else {
				return OPF_SHIP;
			}

		case OP_DESTROYED_DEPARTED_DELAY:
			if ( argnum == 0 ){
				return OPF_POSITIVE;
			} else {
				return OPF_SHIP_WING;
			}

		case OP_JETTISON_CARGO_DELAY:
		case OP_JETTISON_CARGO_NEW:
			if(argnum == 1){
				return OPF_POSITIVE;
			} else {
				return OPF_SHIP;
			}

		case OP_SET_DOCKED:
			if (argnum == 0) {
				return OPF_SHIP;
			} else if (argnum == 1) {
				return OPF_DOCKER_POINT;
			} else if (argnum == 2) {
				return OPF_SHIP;
			} else {
				return OPF_DOCKEE_POINT;
			}

		case OP_CARGO_NO_DEPLETE:
			if (argnum == 0) {
				return OPF_SHIP;
			} else {
				return OPF_NUMBER;
			}

		case OP_BEAM_FIRE:
			switch(argnum) {
				case 0:
					return OPF_SHIP;
				case 1:
					return OPF_SUBSYSTEM;
				case 2:
					return OPF_SHIP;
				case 3:
					return OPF_SUBSYSTEM;
				case 4:
					return OPF_BOOL;
				default:
					UNREACHABLE("Invalid argnum %d detected!", argnum);
					return OPF_NULL;
			}

		case OP_BEAM_FIRE_COORDS:
			switch(argnum) {
				case 0:
					return OPF_SHIP;
				case 1:
					return OPF_SUBSYSTEM;
				case 5:
					return OPF_BOOL;
				default:
					return OPF_NUMBER;
			}

		case OP_BEAM_FLOATING_FIRE:
			switch(argnum) {
				case 0:
					return OPF_WEAPON_NAME;
				case 1:
				case 6:
					return OPF_SHIP_OR_NONE;
				case 2:
					return OPF_IFF;
				case 7:
					return OPF_SUBSYSTEM_OR_NONE;
				default:
					return OPF_NUMBER;
			}

		case OP_IS_TAGGED:
			return OPF_SHIP;

		case OP_IS_PLAYER:
			if (argnum == 0) {
				return OPF_BOOL;
			} else {
				return OPF_SHIP;
			}

		case OP_NUM_KILLS:
		case OP_NUM_ASSISTS:
		case OP_SHIP_SCORE:
		case OP_SHIP_DEATHS: 
		case OP_RESPAWNS_LEFT:
			return OPF_SHIP;

		case OP_SET_RESPAWNS:
			if (argnum == 0 ) {
				return OPF_POSITIVE;
			}
			else {
				return OPF_SHIP;
			}

		case OP_ADD_REMOVE_HOTKEY:
			if (argnum == 0)
				return OPF_BOOL;
			if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_SHIP_WING;

		case OP_NUM_TYPE_KILLS:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_SHIP_TYPE;
			}

		case OP_NUM_CLASS_KILLS:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_SHIP_CLASS_NAME;
			}

		case OP_BEAM_FREE:
		case OP_BEAM_LOCK:
		case OP_TURRET_FREE:
		case OP_TURRET_LOCK:
		case OP_TURRET_TAGGED_SPECIFIC:
		case OP_TURRET_TAGGED_CLEAR_SPECIFIC:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_SUBSYS_TARGET_DISABLE:
		case OP_TURRET_SUBSYS_TARGET_ENABLE:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_SUBSYS_OR_GENERIC;
			}
		
		case OP_TURRET_CHANGE_WEAPON:
			if(argnum == 0) {
				return OPF_SHIP;
			} else if(argnum == 1) {
				return OPF_SUBSYSTEM;
			} else if(argnum == 2) {
				return OPF_WEAPON_NAME;
			} else if(argnum > 2) {
				return OPF_POSITIVE;
			} else {
				return OPF_NONE;
			}

		case OP_TURRET_SET_DIRECTION_PREFERENCE:
			if(argnum == 0) {
				return OPF_SHIP;
			} else if(argnum == 1) {
				return OPF_NUMBER;
			} else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_SET_RATE_OF_FIRE:
			if(argnum == 0) {
				return OPF_SHIP;
			} else if(argnum == 1) {
				return OPF_NUMBER;
			} else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_SET_OPTIMUM_RANGE:
			if(argnum == 0) {
				return OPF_SHIP;
			} else if(argnum == 1) {
				return OPF_NUMBER;
			} else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_SET_FORCED_TARGET:
			if (argnum == 0) {
				return OPF_SHIP;
			}
			else if (argnum == 1) {
				return OPF_SHIP;
			}
			else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_SET_FORCED_SUBSYS_TARGET:
			if (argnum == 0) {
				return OPF_SHIP;
			}
			else if (argnum == 1) {
				return OPF_SUBSYSTEM;
			}
			else if (argnum == 2) {
				return OPF_SHIP;
			}
			else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_CLEAR_FORCED_TARGET:
			if (argnum == 0) {
				return OPF_SHIP;
			}
			else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_SET_INACCURACY:
			if (argnum == 0) {
				return OPF_SHIP;
			}
			else if (argnum == 1) {
				return OPF_NUMBER;
			}
			else {
				return OPF_SUBSYSTEM;
			}

		case OP_TURRET_SET_TARGET_PRIORITIES:
			if(argnum == 0) {
				return OPF_SHIP;
			} else if(argnum == 1) {
				return OPF_SUBSYSTEM;
			} else if(argnum == 2) {
				return OPF_BOOL;
			} else {
				return OPF_TARGET_PRIORITIES;
			}

		case OP_SET_ARMOR_TYPE:
			if(argnum == 0) {
				return OPF_SHIP;
			} else if(argnum == 1) {
				return OPF_BOOL;
			} else if(argnum == 2) {
				return OPF_ARMOR_TYPE;
			} else {
				return OPF_SUBSYS_OR_GENERIC;
			}

		case OP_WEAPON_SET_DAMAGE_TYPE:
			if(argnum == 0) {
				return OPF_BOOL;
			} else if(argnum == 1) {
				return OPF_DAMAGE_TYPE;
			} else if(argnum == 2) {
				return OPF_BOOL;
			} else {
				return OPF_WEAPON_NAME;
			}

		case OP_SHIP_SET_DAMAGE_TYPE:
			if(argnum == 0) {
				return OPF_BOOL;
			} else if(argnum == 1) {
				return OPF_DAMAGE_TYPE;
			} else if(argnum == 2) {
				return OPF_BOOL;
			} else {
				return OPF_SHIP;
			}

		case OP_SHIP_SHOCKWAVE_SET_DAMAGE_TYPE:
			if(argnum == 0) {
				return OPF_DAMAGE_TYPE;
			} else if(argnum == 1) {
				return OPF_BOOL;
			} else {
				return OPF_SHIP_CLASS_NAME;
			}

		case OP_FIELD_SET_DAMAGE_TYPE:
			if(argnum == 0) {
				return OPF_DAMAGE_TYPE;
			} else {
				return OPF_BOOL;
			}

		case OP_TURRET_SET_TARGET_ORDER:
			if(argnum == 0) {
				return OPF_SHIP;
			} else if(argnum == 1) {
				return OPF_SUBSYSTEM;
			} else {
				return OPF_TURRET_TARGET_ORDER;
			}

		case OP_SHIP_TURRET_TARGET_ORDER:
			if(argnum == 0) {
				return OPF_SHIP;
			} else {
				return OPF_TURRET_TARGET_ORDER;
			}

		case OP_LOCK_ROTATING_SUBSYSTEM:
		case OP_FREE_ROTATING_SUBSYSTEM:
		case OP_REVERSE_ROTATING_SUBSYSTEM:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_ROTATING_SUBSYSTEM;

		case OP_LOCK_TRANSLATING_SUBSYSTEM:
		case OP_FREE_TRANSLATING_SUBSYSTEM:
		case OP_REVERSE_TRANSLATING_SUBSYSTEM:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_TRANSLATING_SUBSYSTEM;

		case OP_ROTATING_SUBSYS_SET_TURN_TIME:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_ROTATING_SUBSYSTEM;
			else if (argnum == 2)
				return OPF_NUMBER;
			else
				return OPF_POSITIVE;

		case OP_TRANSLATING_SUBSYS_SET_SPEED:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_TRANSLATING_SUBSYSTEM;
			else if (argnum == 2)
				return OPF_NUMBER;
			else
				return OPF_POSITIVE;

		case OP_TRIGGER_SUBMODEL_ANIMATION:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_ANIMATION_TYPE;
			else if (argnum == 2 || argnum == 3)
				return OPF_NUMBER;
			else if (argnum == 4)
				return OPF_BOOL;
			else if (argnum == 5)
				return OPF_SUBSYSTEM;
			else
				return OPF_NONE;

		case OP_BEAM_FREE_ALL:
		case OP_BEAM_LOCK_ALL:
		case OP_TURRET_FREE_ALL:
		case OP_TURRET_LOCK_ALL:
		case OP_TURRET_TAGGED_ONLY_ALL:
		case OP_TURRET_TAGGED_CLEAR_ALL:
			return OPF_SHIP;

		case OP_ADD_REMOVE_ESCORT:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_NUMBER;
			}

		case OP_AWACS_SET_RADIUS:
			if(argnum == 0){
				return OPF_SHIP;
			} else if(argnum == 1){
				return OPF_AWACS_SUBSYSTEM;
			} else {
				return OPF_NUMBER;
			}

		case OP_PRIMITIVE_SENSORS_SET_RANGE:
			if (!argnum)
				return OPF_SHIP;
			else
				return OPF_NUMBER;

		case OP_CAP_WAYPOINT_SPEED:
			if (argnum == 0) {
				return OPF_SHIP;
			} else {
				return OPF_NUMBER;
			}

		case OP_SET_WING_FORMATION:
			if (argnum == 0)
				return OPF_WING_FORMATION;
			else if (argnum == 1)
				return OPF_NUMBER;
			else
				return OPF_WING;

		case OP_SUBSYS_SET_RANDOM:
			if (argnum == 0) {
				return OPF_SHIP;
			} else if (argnum == 1 || argnum == 2) {
				return OPF_NUMBER;
			} else {
				return OPF_SUBSYSTEM;
			}

		case OP_SUPERNOVA_START:
			return OPF_POSITIVE;

		case OP_SHIELD_RECHARGE_PCT:
		case OP_WEAPON_RECHARGE_PCT:
		case OP_ENGINE_RECHARGE_PCT:
			return OPF_SHIP;			

		case OP_GET_ETS_VALUE:
			if (argnum == 0) {
				return OPF_STRING;
			} else {
				return OPF_SHIP;
			}

		case OP_GET_POWER_OUTPUT:
			return OPF_SHIP;

		case OP_SET_ETS_VALUES:
			if (argnum < 3) {
				return OPF_POSITIVE;
			} else {
				return OPF_SHIP;
			}

		case OP_SHIELD_QUAD_LOW:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_NUMBER;
			}

		case OP_PRIMARY_AMMO_PCT:
		case OP_SECONDARY_AMMO_PCT:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_NUMBER;
			}
			
		// Karajorma
		case OP_GET_PRIMARY_AMMO:
		case OP_SET_PRIMARY_AMMO:
		case OP_GET_SECONDARY_AMMO:
		case OP_SET_SECONDARY_AMMO:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_NUMBER;
			}
		
		// Karajorma
		case OP_SET_PRIMARY_WEAPON:
		case OP_SET_SECONDARY_WEAPON:
			if(argnum == 0)
			{
				return OPF_SHIP;
			}
				
			else if (argnum == 2)
			{
				return OPF_WEAPON_NAME;
			}
			else 
			{
				return OPF_NUMBER;
			}

		// DahBlount
		case OP_TURRET_GET_PRIMARY_AMMO:
		case OP_TURRET_GET_SECONDARY_AMMO:
		case OP_TURRET_SET_PRIMARY_AMMO:
		case OP_TURRET_SET_SECONDARY_AMMO:
			if (argnum == 0){
				return OPF_SHIP;
			} else if (argnum == 1){
				return OPF_SUBSYSTEM;
			} else {
				return OPF_NUMBER;
			}

		// Goober5000
		case OP_IS_IN_TURRET_FOV:
			if (argnum == 0 || argnum == 1) {
				return OPF_SHIP;
			} else if (argnum == 2) {
				return OPF_SUBSYSTEM;
			} else {
				return OPF_POSITIVE;
			}

		case OP_GET_NUM_COUNTERMEASURES:
			return OPF_SHIP;

		case OP_SET_NUM_COUNTERMEASURES:
			if (argnum == 0)
				return OPF_SHIP;
			else 
				return OPF_POSITIVE;

		// Karajorma	
		case OP_LOCK_PRIMARY_WEAPON:
		case OP_UNLOCK_PRIMARY_WEAPON:
		case OP_LOCK_SECONDARY_WEAPON:
		case OP_UNLOCK_SECONDARY_WEAPON:
		// KeldorKatarn		
		case OP_LOCK_AFTERBURNER:
		case OP_UNLOCK_AFTERBURNER:
			return OPF_SHIP;

		
		case OP_SET_AFTERBURNER_ENERGY: 
		case OP_SET_WEAPON_ENERGY:
		case OP_SET_SHIELD_ENERGY:
			if (argnum == 0) {
				return OPF_POSITIVE;
			}
			else {
				return OPF_SHIP;
			}

		case OP_SET_AMBIENT_LIGHT:
			return OPF_POSITIVE;
			
		case OP_SET_POST_EFFECT:
			if (argnum == 0)
				return OPF_POST_EFFECT;
			else
				return OPF_POSITIVE;

		case OP_CHANGE_SUBSYSTEM_NAME:
			if (argnum == 0) {
				return OPF_SHIP;
			}
			else if (argnum == 1) {
				return OPF_STRING;
			}
			else {
				return OPF_SUBSYSTEM;
			}
		
		case OP_IS_SECONDARY_SELECTED:
		case OP_IS_PRIMARY_SELECTED:
			if(argnum == 0){
				return OPF_SHIP;
			} else {
				return OPF_NUMBER;
			}

		case OP_DAMAGED_ESCORT_LIST:
			if (argnum < 2)
			{
				return OPF_NUMBER;
			}
			else
			{
				return OPF_SHIP;
			}

		case OP_DAMAGED_ESCORT_LIST_ALL:
			return OPF_POSITIVE;

		case OP_CHANGE_SHIP_CLASS:
			if (!argnum)
				return OPF_SHIP_CLASS_NAME;
			else
				return OPF_SHIP;

		case OP_SHIP_COPY_DAMAGE:
			return OPF_SHIP;

		case OP_DEACTIVATE_GLOW_POINTS:	//-Bobboau
		case OP_ACTIVATE_GLOW_POINTS:	//-Bobboau
		case OP_DEACTIVATE_GLOW_MAPS:	//-Bobboau
		case OP_ACTIVATE_GLOW_MAPS:		//-Bobboau
			return OPF_SHIP;	//a list of ships that are to be activated/deactivated

		case OP_DEACTIVATE_GLOW_POINT_BANK:
		case OP_ACTIVATE_GLOW_POINT_BANK:
			if (!argnum)
				return OPF_SHIP;		//the ship
			else
				return OPF_POSITIVE;		//the glow bank to disable

		// taylor
		case OP_SET_SKYBOX_MODEL:
			if (argnum == 0)
				return OPF_SKYBOX_MODEL_NAME;
			else if (argnum == 1)
				return OPF_BOOL;
			else
				return OPF_SKYBOX_FLAGS;

		case OP_SET_SKYBOX_ORIENT:
			return OPF_NUMBER;

		case OP_SET_SKYBOX_ALPHA:
			return OPF_NUMBER;

		// Goober5000 - this is complicated :)
		case OP_SET_SUPPORT_SHIP:
			if ((argnum == 0) || (argnum == 2))
				return OPF_DEPARTURE_LOCATION;	// use this for both because we don't want Near Ship or In Front of Ship
			if ((argnum == 1) || (argnum == 3))
				return OPF_SHIP_WITH_BAY;		// same - we don't want to anchor around anything without a docking bay
			if (argnum == 4)
				return OPF_SUPPORT_SHIP_CLASS;
			if (argnum == 5)
				return OPF_NUMBER;
			if (argnum == 6)
				return OPF_NUMBER;
			break;

		// Goober5000
		case OP_SET_ARRIVAL_INFO:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_ARRIVAL_LOCATION;
			else if (argnum == 2)
				return OPF_ARRIVAL_ANCHOR_ALL;
			else if (argnum == 3)
				return OPF_NUMBER;
			else if (argnum == 4 || argnum == 5)
				return OPF_POSITIVE;
			else if (argnum == 6)
				return OPF_BOOL;
			break;

		// Goober5000
		case OP_SET_DEPARTURE_INFO:
			if (argnum == 0)
				return OPF_SHIP_WING;
			else if (argnum == 1)
				return OPF_DEPARTURE_LOCATION;
			else if (argnum == 2)
				return OPF_SHIP_WITH_BAY;
			else if (argnum == 3 || argnum == 4)
				return OPF_NUMBER;
			else if (argnum == 5)
				return OPF_BOOL;
			break;

		case OP_KAMIKAZE:
			if (argnum==0)
				return OPF_POSITIVE;
			else
				return OPF_SHIP_WING;

		case OP_NUM_SHIPS_IN_BATTLE:	//phreak modified by FUBAR
			return OPF_SHIP_WING_WHOLETEAM;

		case OP_NUM_SHIPS_IN_WING:	// Karajorma
			return OPF_WING;

		case OP_CURRENT_SPEED:
			return OPF_SHIP_WING;

		case OP_TURRET_FIRED_SINCE:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_SUBSYSTEM;
			else
				return OPF_POSITIVE;
			
		case OP_PRIMARY_FIRED_SINCE:
		case OP_SECONDARY_FIRED_SINCE:
			if (argnum == 0) 
				return OPF_SHIP;
			else 
				return OPF_POSITIVE;

		case OP_HAS_PRIMARY_WEAPON:
		case OP_HAS_SECONDARY_WEAPON:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1) 
				return OPF_WEAPON_BANK_NUMBER;
			else 
				return OPF_WEAPON_NAME;

		case OP_TURRET_HAS_PRIMARY_WEAPON:
		case OP_TURRET_HAS_SECONDARY_WEAPON:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_SUBSYSTEM;
			else if (argnum == 2)
				return OPF_WEAPON_BANK_NUMBER;
			else
				return OPF_WEAPON_NAME;
			
		case OP_DIRECTIVE_VALUE:
			if (argnum == 0)
				return OPF_NUMBER;
			else 
				return OPF_BOOL;			

		case OP_GET_HOTKEY:
			return OPF_SHIP_WING;

		case OP_NAV_IS_VISITED:		//Kazan
		case OP_NAV_DISTANCE:		//kazan
		case OP_NAV_DEL:			//kazan
		case OP_NAV_HIDE:			//kazan
		case OP_NAV_RESTRICT:		//kazan
		case OP_NAV_UNHIDE:			//kazan
		case OP_NAV_UNRESTRICT:		//kazan
		case OP_NAV_SET_VISITED:	//kazan
		case OP_NAV_UNSET_VISITED:	//kazan
		case OP_NAV_SELECT:			//Talon1024
			return OPF_STRING;
		
		case OP_NAV_SET_CARRY:		//kazan
		case OP_NAV_UNSET_CARRY:	//kazan
			return OPF_SHIP_WING;

		case OP_NAV_SET_NEEDSLINK: // kazan
		case OP_NAV_UNSET_NEEDSLINK:
		case OP_NAV_ISLINKED:
				return OPF_SHIP;

		case OP_NAV_USECINEMATICS:
		case OP_NAV_USEAP:
			return OPF_BOOL;

		case OP_NAV_ADD_WAYPOINT:	//kazan
			if (argnum==0)
				return OPF_STRING;
			else if (argnum==1)
				return OPF_WAYPOINT_PATH;
			else if (argnum==2)
				return OPF_POSITIVE;
			else 
				return OPF_SHIP_WING_WHOLETEAM;

		case OP_NAV_ADD_SHIP:		//kazan
			if (argnum==0)
				return OPF_STRING;
			else
				return OPF_SHIP;

		case OP_NAV_SET_COLOR:
		case OP_NAV_SET_VISITED_COLOR:
			if (argnum >= 0 && argnum <= 2)
				return OPF_POSITIVE;
			else
				return OPF_STRING;

		//<Cutscenes>
		case OP_SCRAMBLE_MESSAGES:
		case OP_UNSCRAMBLE_MESSAGES:
			return OPF_SHIP;

		case OP_CUTSCENES_GET_FOV:
			return OPF_NONE;

		case OP_CUTSCENES_SET_CUTSCENE_BARS:
		case OP_CUTSCENES_UNSET_CUTSCENE_BARS:
		case OP_CUTSCENES_FADE_IN:
		case OP_CUTSCENES_FADE_OUT:
		case OP_CUTSCENES_SET_TIME_COMPRESSION:
			return OPF_POSITIVE;

		case OP_CUTSCENES_SET_FOV:
			return OPF_NUMBER;

		case OP_CUTSCENES_SET_CAMERA:
			return OPF_STRING;
		
		case OP_CUTSCENES_SET_CAMERA_POSITION:
		case OP_CUTSCENES_SET_CAMERA_FACING:
		case OP_CUTSCENES_SET_CAMERA_ROTATION:
			if(argnum < 3)
				return OPF_NUMBER;
			else
				return OPF_POSITIVE;

		case OP_CUTSCENES_SET_CAMERA_FACING_OBJECT:
			if(argnum < 1)
				return OPF_SHIP_WING_POINT;
			else
				return OPF_POSITIVE;

		case OP_CUTSCENES_SET_CAMERA_FOV:
			return OPF_POSITIVE;
		
		case OP_CUTSCENES_SET_CAMERA_HOST:
		case OP_CUTSCENES_SET_CAMERA_TARGET:
			if(argnum < 1)
				return OPF_SHIP_WING_POINT_OR_NONE;
			else
				return OPF_SUBSYSTEM_OR_NONE;

		case OP_CUTSCENES_RESET_CAMERA:
			return OPF_BOOL;

		case OP_CUTSCENES_RESET_FOV:
		case OP_CUTSCENES_RESET_TIME_COMPRESSION:
			return OPF_NONE;

		case OP_CUTSCENES_FORCE_PERSPECTIVE:
			if (argnum == 1)
				return OPF_NUMBER;
			else
				return OPF_BOOL;

		case OP_ALLOW_PHOTO_MODE:
			return OPF_BOOL;

		case OP_SET_CAMERA_SHUDDER:
			if (argnum == 0 || argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_CUTSCENES_SHOW_SUBTITLE:
			if (argnum < 2)
				return OPF_NUMBER;
			else if (argnum == 2)
				return OPF_STRING;
			else if (argnum == 3)
				return OPF_POSITIVE;
			else if (argnum == 4)
				return OPF_STRING;
			else if (argnum == 5)
				return OPF_POSITIVE;
			else if (argnum < 8)
				return OPF_BOOL;
			else if (argnum < 12)
				return OPF_POSITIVE;
			else if (argnum == 12)
				return OPF_BOOL;
			else if (argnum == 13)
				return OPF_NUMBER;
			else
				return OPF_NONE;

		case OP_CUTSCENES_SHOW_SUBTITLE_TEXT:
			if (argnum == 0)
				return OPF_MESSAGE_OR_STRING;
			else if (argnum == 1 || argnum == 2)
				return OPF_NUMBER;
			else if (argnum == 3 || argnum == 4)
				return OPF_BOOL;
			else if (argnum >= 5 && argnum <= 10)
				return OPF_POSITIVE;
			else if (argnum == 11)
				return OPF_FONT;
			else if (argnum == 12)
				return OPF_BOOL;
			else if (argnum == 13)
				return OPF_NUMBER;
			else if (argnum == 14)
				return OPF_BOOL;
			else
				return OPF_NONE;

		case OP_CUTSCENES_SHOW_SUBTITLE_IMAGE:
			if (argnum == 0)
				return OPF_STRING;
			else if (argnum == 1 || argnum == 2)
				return OPF_NUMBER;
			else if (argnum == 3 || argnum == 4)
				return OPF_BOOL;
			else if (argnum >= 5 && argnum <= 8)
				return OPF_POSITIVE;
			else if (argnum == 9 || argnum == 10)
				return OPF_BOOL;
			else
				return OPF_NONE;

		//</Cutscenes>

		case OP_JUMP_NODE_SET_JUMPNODE_NAME: //CommanderDJ
		case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME:
			if(argnum==0)
				return OPF_JUMP_NODE_NAME;
			else if (argnum==1)
				return OPF_STRING;
			else
				return OPF_NONE;

		case OP_JUMP_NODE_SET_JUMPNODE_COLOR:
			if(argnum==0)
				return OPF_JUMP_NODE_NAME;
			else
				return OPF_POSITIVE;

		case OP_JUMP_NODE_SET_JUMPNODE_MODEL:
			if(argnum==0)
				return OPF_JUMP_NODE_NAME;
			else if (argnum == 1)
				return OPF_STRING;
			else
				return OPF_BOOL;

		case OP_JUMP_NODE_SHOW_JUMPNODE:
		case OP_JUMP_NODE_HIDE_JUMPNODE:
				return OPF_JUMP_NODE_NAME;

		case OP_CHANGE_BACKGROUND:
			return OPF_POSITIVE;

		case OP_ADD_BACKGROUND_BITMAP:
		case OP_ADD_BACKGROUND_BITMAP_NEW:
			if (argnum == 0)
				return OPF_BACKGROUND_BITMAP;
			else if (argnum == 8)
				return OPF_VARIABLE_NAME;
			else
				return OPF_POSITIVE;

		case OP_REMOVE_BACKGROUND_BITMAP:
			return OPF_POSITIVE;

		case OP_ADD_SUN_BITMAP:
		case OP_ADD_SUN_BITMAP_NEW:
			if (argnum == 0)
				return OPF_SUN_BITMAP;
			else if (argnum == 5)
				return OPF_VARIABLE_NAME;
			else
				return OPF_POSITIVE;

		case OP_REMOVE_SUN_BITMAP:
			return OPF_POSITIVE;

		case OP_NEBULA_CHANGE_STORM:
			return OPF_NEBULA_STORM_TYPE;

		case OP_NEBULA_CHANGE_PATTERN:
			return OPF_NEBULA_PATTERN;

		case OP_NEBULA_TOGGLE_POOF:
			if (!argnum)
				return OPF_NEBULA_POOF;
			else
				return OPF_BOOL;

		case OP_NEBULA_SET_POOFS:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_NEBULA_POOF;

		case OP_NEBULA_FADE_POOF:
			if (argnum == 0)
				return OPF_NEBULA_POOF;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_BOOL;

		case OP_NEBULA_FADE_POOFS:
			if (argnum == 0)
				return OPF_BOOL;
			else if (argnum == 1)
				return OPF_POSITIVE;
			else
				return OPF_NEBULA_POOF;

		case OP_NEBULA_CHANGE_FOG_COLOR:
		case OP_NEBULA_SET_RANGE:
			return OPF_POSITIVE;

		case OP_VOLUMETRICS_TOGGLE:
			return OPF_BOOL;

		case OP_TOGGLE_ASTEROID_FIELD:
			return OPF_BOOL;

		case OP_SET_ASTEROID_FIELD:
			if (argnum <= 2)
				return OPF_POSITIVE;
			else if (argnum <= 5)
				return OPF_BOOL;
			else if (argnum <= 11)
				return OPF_NUMBER;
			else if (argnum == 12)
				return OPF_BOOL;
			else if (argnum <= 18)
				return OPF_NUMBER;
			else
				return OPF_SHIP;

		case OP_SET_DEBRIS_FIELD:
			if (argnum <= 1)
				return OPF_POSITIVE;
			else if (argnum <= 4)
				return OPF_DEBRIS_TYPES;
			else if (argnum <= 10)
				return OPF_NUMBER;
			else
				return OPF_BOOL;

		case OP_CONFIG_ASTEROID_FIELD:
			if (argnum <= 2)
				return OPF_POSITIVE;
			else if (argnum <= 8)
				return OPF_NUMBER;
			else if (argnum == 9)
				return OPF_BOOL;
			else if (argnum <= 15)
				return OPF_NUMBER;
			else
				return OPF_ASTEROID_TYPES;

		case OP_CONFIG_DEBRIS_FIELD:
			if (argnum <= 1)
				return OPF_POSITIVE;
			else if (argnum <= 7)
				return OPF_NUMBER;
			else if (argnum <= 8)
				return OPF_BOOL;
			else
				return OPF_DEBRIS_TYPES;

		case OP_CONFIG_FIELD_TARGETS:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_SHIP;

		case OP_SET_MOTION_DEBRIS:
			return OPF_MOTION_DEBRIS;

		case OP_SCRIPT_EVAL_BOOL:
		case OP_SCRIPT_EVAL_NUM:
		case OP_SCRIPT_EVAL_BLOCK:
		case OP_SCRIPT_EVAL:
			return OPF_STRING;

		case OP_SCRIPT_EVAL_STRING:
			if (!argnum)
				return OPF_STRING;
			else
				return OPF_VARIABLE_NAME;

		case OP_SCRIPT_EVAL_MULTI:
			if (argnum == 0) 
				return OPF_STRING;
			else if (argnum == 1) 
				return OPF_BOOL;
			else 
				return OPF_SHIP;

		case OP_CHANGE_IFF_COLOR:
			if ((argnum == 0) || (argnum == 1))
				return OPF_IFF;
			else if ((argnum >= 2) && (argnum <=4))
				return OPF_POSITIVE;
			else return OPF_SHIP_WING;

		case OP_HUD_DISPLAY_GAUGE:
			if ( argnum == 0 ) {
				return OPF_POSITIVE;
			} else {
				return OPF_HUD_ELEMENT;
			}

		case OP_DISABLE_ETS:
		case OP_ENABLE_ETS:
				return OPF_SHIP;

		case OP_IS_FACING:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_SHIP_POINT;
			else
				return OPF_POSITIVE;

		case OP_FORCE_GLIDE:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_BOOL;

		case OP_HUD_SET_DIRECTIVE:
			if (argnum == 0)
				return OPF_CUSTOM_HUD_GAUGE;
			else
				return OPF_STRING;

		case OP_HUD_GAUGE_SET_ACTIVE:
			if (argnum == 0)
				return OPF_ANY_HUD_GAUGE;
			else
				return OPF_BOOL;

		case OP_HUD_ACTIVATE_GAUGE_TYPE:
			if (argnum == 0)
				return OPF_BUILTIN_HUD_GAUGE;
			else
				return OPF_BOOL;

		case OP_HUD_SET_CUSTOM_GAUGE_ACTIVE:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_CUSTOM_HUD_GAUGE;

		case OP_HUD_SET_BUILTIN_GAUGE_ACTIVE:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_BUILTIN_HUD_GAUGE;

		case OP_GET_COLGROUP_ID:
			return OPF_SHIP_PROP;

		case OP_ADD_TO_COLGROUP:
		case OP_REMOVE_FROM_COLGROUP:
			if (argnum == 0)
				return OPF_SHIP_PROP;
			else
				return OPF_POSITIVE;

		case OP_ADD_TO_COLGROUP_NEW:
		case OP_REMOVE_FROM_COLGROUP_NEW:
			if (argnum == 0)
				return OPF_POSITIVE;
			else
				return OPF_SHIP_PROP;

		case OP_SHIP_EFFECT:
			if (argnum == 0)
				return OPF_SHIP_EFFECT;
			else if (argnum == 1)
				return OPF_NUMBER;
			else
				return OPF_SHIP_WING;

		case OP_CLEAR_SUBTITLES:
			return OPF_NONE;

		case OP_SET_THRUSTERS:
			if (argnum == 0)
				return OPF_BOOL;
			else
				return OPF_SHIP;

		case OP_OVERRIDE_MOTION_DEBRIS:
			return OPF_BOOL;

		case OP_REPLACE_TEXTURE:
			if (argnum == 0 || argnum == 1)
				return OPF_STRING;
			else
				return OPF_SHIP_WING;

		case OP_REPLACE_TEXTURE_SKYBOX:
			return OPF_STRING;
      
		case OP_SET_ALPHA_MULT:
			if (argnum == 0)
				return OPF_POSITIVE;
			else
				return OPF_SHIP_WING;

		case OP_IS_LANGUAGE:
			return OPF_LANGUAGE;

		case OP_USED_CHEAT:
			return OPF_STRING;

		case OP_TRIGGER_ANIMATION_NEW:
		case OP_STOP_LOOPING_ANIMATION:
			if (argnum == 0)
				return OPF_SHIP;
			else if (argnum == 1)
				return OPF_ANIMATION_TYPE;
			else if (argnum == 2)
				return OPF_ANIMATION_NAME;
			else
				return OPF_BOOL;

		case OP_UPDATE_MOVEABLE:
			if (argnum == 0)
				return OPF_SHIP;
			else if(argnum == 1)
				return OPF_ANIMATION_NAME;
			else
				return OPF_NUMBER;

		case OP_ADVANCE_MOVEABLE:
			if (argnum == 0)
				return OPF_SHIP;
			else
				return OPF_ANIMATION_NAME;

		case OP_IS_CONTAINER_EMPTY:
		case OP_GET_CONTAINER_SIZE:
			if (argnum == 0) {
				return OPF_CONTAINER_NAME;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_LIST_HAS_DATA:
			if (argnum == 0) {
				return OPF_LIST_CONTAINER_NAME;
			} else {
				return OPF_CONTAINER_VALUE;
			}

		case OP_LIST_DATA_INDEX:
			if (argnum == 0) {
				return OPF_LIST_CONTAINER_NAME;
			} else if (argnum == 1) {
				return OPF_CONTAINER_VALUE;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_MAP_HAS_KEY:
			if (argnum == 0) {
				return OPF_MAP_CONTAINER_NAME;
			} else {
				return OPF_CONTAINER_VALUE;
			}

		case OP_MAP_HAS_DATA_ITEM:
			if (argnum == 0) {
				return OPF_MAP_CONTAINER_NAME;
			} else if (argnum == 1) {
				return OPF_CONTAINER_VALUE;
			} else if (argnum == 2) {
				return OPF_VARIABLE_NAME;
			} else {
				// This shouldn't happen
				return OPF_NONE;
			}

		case OP_SET_GRAVITY_ACCEL:
			return OPF_POSITIVE;

		default: {
			auto dynamicSEXP = sexp::get_dynamic_sexp(op_const);
			if (dynamicSEXP != nullptr) {
				return dynamicSEXP->getArgumentType(argnum);
			}

			Assertion(false, "query_operator_argument_type(%d, %d) called for unsupported operator type!", op_const, argnum);
		}
	}

	return 0;
}

// DA: 1/7/99  Used to rename ships and waypoints, not variables
// Strictly used in FRED
void update_sexp_references(const char *old_name, const char *new_name)
{
	int i;

	Assert(strlen(new_name) < TOKEN_LENGTH);
	for (i = 0; i < Num_sexp_nodes; i++)
	{
		if ((SEXP_NODE_TYPE(i) == SEXP_ATOM) && (Sexp_nodes[i].subtype == SEXP_ATOM_STRING))
			if (!stricmp(CTEXT(i), old_name))
				strcpy(Sexp_nodes[i].text, new_name);
	}
}

// DA: 1/7/99  Used to rename event names, goal names, not variables
// Strictly used in FRED
void update_sexp_references(const char *old_name, const char *new_name, int format)
{
	int i;
	if (!strcmp(old_name, new_name)) {
		return;
	}

	Assert(strlen(new_name) < TOKEN_LENGTH);
	for (i = 0; i < Num_sexp_nodes; i++)
	{
		if (is_sexp_top_level(i))
			update_sexp_references(old_name, new_name, format, i);
	}
}

// DA: 1/7/99  Used to rename event names, goal names, not variables
// recursive function to update references to a certain type of data
void update_sexp_references(const char *old_name, const char *new_name, int format, int node)
{
	int i, n, op;

	if (node < 0)
		return;

	if ((SEXP_NODE_TYPE(node) == SEXP_LIST) && (Sexp_nodes[node].subtype == SEXP_ATOM_LIST))
	{
		if (Sexp_nodes[node].first)
			update_sexp_references(old_name, new_name, format, Sexp_nodes[node].first);

		if (Sexp_nodes[node].rest)
			update_sexp_references(old_name, new_name, format, Sexp_nodes[node].rest);

		return;
	}

	if (SEXP_NODE_TYPE(node) != SEXP_ATOM)
		return;

	if (Sexp_nodes[node].subtype != SEXP_ATOM_OPERATOR)
		return;

	op = get_operator_index(node);
	Assert(Sexp_nodes[node].first < 0);
	n = Sexp_nodes[node].rest;
	i = 0;
	while (n >= 0)
	{
		if (SEXP_NODE_TYPE(n) == SEXP_LIST)
		{
			update_sexp_references(old_name, new_name, format, Sexp_nodes[n].first);
		}
		else
		{
			Assert((SEXP_NODE_TYPE(n) == SEXP_ATOM) &&
				   ((Sexp_nodes[n].subtype == SEXP_ATOM_NUMBER) || (Sexp_nodes[n].subtype == SEXP_ATOM_STRING) ||
					   (Sexp_nodes[n].subtype == SEXP_ATOM_CONTAINER_NAME) ||
					   (Sexp_nodes[n].subtype == SEXP_ATOM_CONTAINER_DATA)));

			if (query_operator_argument_type(op, i) == format)
			{
				if (!stricmp(CTEXT(n), old_name))
					strcpy(Sexp_nodes[n].text, new_name);
			}
		}

		n = Sexp_nodes[n].rest;
		i++;
	}
}

std::pair<int, sexp_src> query_referenced_in_sexp(sexp_ref_type  /*type*/, const char *name, int &node)
{
	int i, n, j;

	for (n=0; n<Num_sexp_nodes; n++){
		if ((SEXP_NODE_TYPE(n) == SEXP_ATOM) && (Sexp_nodes[n].subtype == SEXP_ATOM_STRING)){
			if (!stricmp(CTEXT(n), name)){
				break;
			}
		}
	}

	if (n == Num_sexp_nodes){
		return std::make_pair(-1, sexp_src::NONE);
	}

	node = n;

	// so we know it's being used somewhere..  Time to find out where..
	for (i=0; i<MAX_SHIPS; i++)
		if (Ships[i].objnum >= 0) {
			if (query_node_in_sexp(n, Ships[i].arrival_cue)){
				return std::make_pair(i, sexp_src::SHIP_ARRIVAL);
			}
			if (query_node_in_sexp(n, Ships[i].departure_cue)){
				return std::make_pair(i, sexp_src::SHIP_DEPARTURE);
			}
		}

	for (i=0; i<MAX_WINGS; i++){
		if (Wings[i].wave_count) {
			if (query_node_in_sexp(n, Wings[i].arrival_cue)){
				return std::make_pair(i, sexp_src::WING_ARRIVAL);
			}
			if (query_node_in_sexp(n, Wings[i].departure_cue)){
				return std::make_pair(i, sexp_src::WING_DEPARTURE);
			}
		}
	}

	for (i=0; i<(int)Mission_events.size(); i++){
		if (query_node_in_sexp(n, Mission_events[i].formula)){
			return std::make_pair(i, sexp_src::EVENT);
		}
	}

	for (i=0; i<(int)Mission_goals.size(); i++){
		if (query_node_in_sexp(n, Mission_goals[i].formula)){
			return std::make_pair(i, sexp_src::MISSION_GOAL);
		}
	}

	for (i = 0; i < static_cast<int>(The_mission.cutscenes.size()); i++) {
		if (query_node_in_sexp(n, The_mission.cutscenes[i].formula)) {
			return std::make_pair(i, sexp_src::MISSION_CUTSCENE);
		}
	}

	for (j=0; j<Num_teams; j++) {
		for (i=0; i<Debriefings[j].num_stages; i++) {
			if (query_node_in_sexp(n, Debriefings[j].stages[i].formula)){
				return std::make_pair(i, sexp_src::DEBRIEFING);
			}
		}
	}

	for (j=0; j<Num_teams; j++) {
		for (i=0; i<Briefings[j].num_stages; i++) {
			if (query_node_in_sexp(n, Briefings[j].stages[i].formula)){
				return std::make_pair(i, sexp_src::BRIEFING);
			}
		}
	}

	return std::make_pair(-1, sexp_src::UNKNOWN);
}

void skip_white(const char **str)
{
	if ((**str == ' ') || (**str == '\t')) {
		(*str)++;
	}
}

int validate_float(const char **str)
{
	int count = 0, dot = 0;

	while (isdigit(**str) || **str == '.') {
		if (**str == '.') {
			if (dot) {
				return -1;
			}

			dot = 1;
		}

		(*str)++;
		count++;
	}

	if (!count) {
		return -1;
	}

	return 0;
}

int verify_vector(const char *text)
{
	const char *str;

	if (text == nullptr)
		return -1;

	auto len = strlen(text);
	if (text[0] != '(' || text[len - 1] != ')'){
		return -1;
	}

	str = &text[0];
	skip_white(&str);
	if (*str != '('){
		return -1;
	}

	str++;
	skip_white(&str);
	if (validate_float(&str)){
		return -1;
	}

	skip_white(&str);
	if (validate_float(&str)){
		return -1;
	}

	skip_white(&str);
	if (validate_float(&str)){
		return -1;
	}

	skip_white(&str);
	if (*str != ')'){
		return -1;
	}

	str++;
	skip_white(&str);
	if (*str){
		return -1;
	}

	return 0;
}

/**
 * Check if operator return type opr is a valid match for operator argument type opf
 */
bool sexp_query_type_match(int opf, int opr)
{
	switch (opf) {
		case OPF_NUMBER:
			return ((opr == OPR_NUMBER) || (opr == OPR_POSITIVE));

		case OPF_POSITIVE:
			// Goober5000's number hack
			return ((opr == OPR_POSITIVE) || (opr == OPR_NUMBER));

		case OPF_BOOL:
			return (opr == OPR_BOOL);

		case OPF_NULL:
			return (opr == OPR_NULL);

		// Goober5000
		case OPF_FLEXIBLE_ARGUMENT:
			return (opr == OPR_FLEXIBLE_ARGUMENT);

		// Goober5000
		case OPF_ANYTHING:
		case OPF_CONTAINER_VALUE: // jg18
		case OPF_DATA_OR_STR_CONTAINER: // jg18
			// don't match any operators, only data
			return false;

		case OPF_AI_GOAL:
			return (opr == OPR_AI_GOAL);
	}

	return false;
}

/**
 * Finds the operator that is the best textual match for the input string, given the required OPF type.  For equal matches,
 * the alphabetically earliest operator is returned.
 * 
 * Note: Returns the operator index, not the operator value.
 */
int sexp_match_closest_operator(const SCP_string &str, int opf)
{
	int best = -1;
	size_t min = SCP_string::npos;

	for (int op_index : Sorted_operator_indexes)
	{
		const auto &op_text = Operators[op_index].text;
		auto opr = query_operator_return_type(Operators[op_index].value);

		if (sexp_query_type_match(opf, opr))
		{
			size_t cost = stringcost(op_text, str, Max_operator_length, stringcost_tolower_equal);
			if (best < 0 || cost < min)
			{
				min = cost;
				best = op_index;
			}
		}
	}

	return best;
}

bool sexp_recoverable_error(int num)
{
	switch (num)
	{
		// These two errors may cause mysterious bugs in FSO,
		// but the mission will run without crashing.
		case SEXP_CHECK_AMBIGUOUS_EVENT_NAME:
		case SEXP_CHECK_AMBIGUOUS_GOAL_NAME:

		// Having an invalid gauge in FSO won't hurt,
		// as all places which call hud_get_gauge() or hud_get_custom_gauge() check its return value for NULL.
		case SEXP_CHECK_INVALID_CUSTOM_HUD_GAUGE:
		case SEXP_CHECK_INVALID_ANY_HUD_GAUGE:

		// Trying to set an invalid sound environment has no effect, and all sound enviroments are invalid if EFX is disabled.
		// Invalid sound environment options are simiarly harmless.
		case SEXP_CHECK_INVALID_SOUND_ENVIRONMENT:
		case SEXP_CHECK_INVALID_SOUND_ENVIRONMENT_OPTION:
			return true;

		// most errors will halt mission loading
		default:
			return false;
	}
}

const char *sexp_error_message(int num)
{
	switch (num) {
		case SEXP_CHECK_NONOP_ARGS:
			return "Data shouldn't have arguments";

		case SEXP_CHECK_OP_EXPECTED:
			return "Operator expected instead of data";

		case SEXP_CHECK_UNKNOWN_OP:
			return "Unrecognized operator";

		case SEXP_CHECK_TYPE_MISMATCH:
			return "Argument type mismatch";

		case SEXP_CHECK_BAD_ARG_COUNT:
			return "Argument count is illegal";

		case SEXP_CHECK_UNKNOWN_TYPE:
			return "Unknown operator argument type";

		case SEXP_CHECK_INVALID_NUM:
			return "Not a number";

		case SEXP_CHECK_MUST_BE_INTEGER:
			return "Number must be an integer";

		case SEXP_CHECK_NEGATIVE_NUM:
			return "Negative number not allowed";

		case SEXP_CHECK_NUM_RANGE_INVALID:
			return "Number is out of range";

		case SEXP_CHECK_INVALID_SHIP:
			return "Invalid ship name";

		case SEXP_CHECK_INVALID_PROP:
			return "Invalid prop name";

		case SEXP_CHECK_INVALID_WING:
			return "Invalid wing name";

		case SEXP_CHECK_INVALID_SUBSYS:
			return "Invalid subsystem name";

		case SEXP_CHECK_INVALID_AWACS_SUBSYS:
			return "This must be an AWACS subsystem";

		case SEXP_CHECK_INVALID_ROTATING_SUBSYS:
			return "This must be a rotating subsystem";

		case SEXP_CHECK_INVALID_TRANSLATING_SUBSYS:
			return "This must be a translating subsystem";

		case SEXP_CHECK_INVALID_SUBSYS_TYPE:
			return "Invalid subsystem type";

		case SEXP_CHECK_INVALID_IFF:
			return "Invalid team name";

		case SEXP_CHECK_INVALID_AI_CLASS:
			return "Invalid AI class name";

		case SEXP_CHECK_INVALID_POINT:
			return "Invalid waypoint";

		case SEXP_CHECK_INVALID_SHIP_WING:
			return "Invalid ship or wing name";

		case SEXP_CHECK_INVALID_SHIP_PROP:
			return "Invalid ship or prop name";

		case SEXP_CHECK_INVALID_SHIP_TYPE:
			return "Invalid ship type";

		case SEXP_CHECK_UNKNOWN_MESSAGE:
			return "Invalid message name";

		case SEXP_CHECK_INVALID_PRIORITY:
			return "Invalid priority";

		case SEXP_CHECK_INVALID_MISSION_NAME:
			return "Invalid mission filename";

		case SEXP_CHECK_INVALID_GOAL_NAME:
			return "Invalid goal name";

		case SEXP_CHECK_INVALID_LEVEL:
			return "Mission level too low in tree";

		case SEXP_CHECK_INVALID_MSG_SOURCE:
			return "Invalid message source";

		case SEXP_CHECK_INVALID_DOCKER_POINT:
			return "Invalid docker point";

		case SEXP_CHECK_INVALID_DOCKEE_POINT:
			return "Invalid dockee point";

		case SEXP_CHECK_ORDER_NOT_ALLOWED:
			return "Ship not allowed to have this order";

		case SEXP_CHECK_DOCKING_NOT_ALLOWED:
			return "Ship can't dock with target ship";

		case SEXP_CHECK_INVALID_EVENT_NAME:
			return "Event name is invalid (not known)";

		case SEXP_CHECK_INVALID_SKILL_LEVEL:
			return "Skill level name is invalid (not known)";

		case SEXP_CHECK_INVALID_MEDAL_NAME:
			return "Invalid medal name";

		case SEXP_CHECK_INVALID_WEAPON_NAME:
			return "Invalid weapon name";

		case SEXP_CHECK_INVALID_INTEL_NAME:
			return "Invalid intel name";

		case SEXP_CHECK_INVALID_SHIP_CLASS_NAME:
			return "Invalid ship class name";

		case SEXP_CHECK_INVALID_SKYBOX_NAME:
			return "Invalid skybox name";

		case SEXP_CHECK_INVALID_SKYBOX_FLAG:
			return "Invalid skybox flag";

		case SEXP_CHECK_INVALID_JUMP_NODE:
			return "Invalid jump node";

		case SEXP_CHECK_UNKNOWN_ERROR:
			return "Unknown error";

		case SEXP_CHECK_INVALID_SUPPORT_SHIP_CLASS:
			return "Invalid support ship class";

		case SEXP_CHECK_INVALID_SHIP_WITH_BAY:
			return "Ship does not have a hangar bay";

		case SEXP_CHECK_INVALID_ARRIVAL_LOCATION:
			return "Invalid arrival location";

		case SEXP_CHECK_INVALID_DEPARTURE_LOCATION:
			return "Invalid departure location";

		case SEXP_CHECK_INVALID_ARRIVAL_ANCHOR_ALL:
			return "Invalid universal arrival anchor";

		case SEXP_CHECK_INVALID_SOUNDTRACK_NAME:
			return "Invalid soundtrack name";

		case SEXP_CHECK_INVALID_PERSONA_NAME:
			return "Invalid persona name";

		case SEXP_CHECK_INVALID_VARIABLE:
			return "Invalid variable name"; 

		case SEXP_CHECK_INVALID_VARIABLE_TYPE:
			return "Invalid variable type";

		case SEXP_CHECK_INVALID_FONT:
			return "Invalid font name";

		case SEXP_CHECK_INVALID_HUD_ELEMENT:
			return "Invalid HUD element magic name";

		case SEXP_CHECK_INVALID_SOUND_ENVIRONMENT:
			return "Invalid sound environment";

		case SEXP_CHECK_INVALID_SOUND_ENVIRONMENT_OPTION:
			return "Invalid sound environment option";

		case SEXP_CHECK_INVALID_AUDIO_VOLUME_OPTION:
			return "Invalid audio volume option";

		case SEXP_CHECK_INVALID_EXPLOSION_OPTION:
			return "Invalid explosion option";

		case SEXP_CHECK_INVALID_SHIP_EFFECT:
			return "Invalid ship effect name";

		case SEXP_CHECK_INVALID_TURRET_TARGET_ORDER:
			return "Invalid turret target order";

		case SEXP_CHECK_INVALID_TURRET_TYPE:
			return "Invalid turret type";

		case SEXP_CHECK_INVALID_ARMOR_TYPE:
			return "Invalid armor type";

		case SEXP_CHECK_INVALID_DAMAGE_TYPE:
			return "Invalid damage type";

		case SEXP_CHECK_INVALID_BUILTIN_HUD_GAUGE:
			return "Invalid builtin HUD gauge";

		case SEXP_CHECK_INVALID_CUSTOM_HUD_GAUGE:
			return "Invalid custom HUD gauge";

		case SEXP_CHECK_INVALID_ANY_HUD_GAUGE:
			return "Invalid custom or builtin HUD gauge";

		case SEXP_CHECK_INVALID_TARGET_PRIORITIES:
			return "Invalid target priorities";
			
		case SEXP_CHECK_INVALID_ANIMATION_TYPE:
			return "Invalid animation type";

		case SEXP_CHECK_INVALID_MISSION_MOOD:
			return "Invalid mission mood";

		case SEXP_CHECK_INVALID_SHIP_FLAG:
			return "Invalid ship flag";

		case SEXP_CHECK_INVALID_WING_FLAG:
			return "Invalid wing flag";

		case SEXP_CHECK_INVALID_TEAM_COLOR:
			return "Not a valid Team Color setting";

		case SEXP_CHECK_INVALID_GAME_SND:
			return "Invalid game sound";

		case SEXP_CHECK_INVALID_SSM_CLASS:
			return "Invalid SSM class";

		case SEXP_CHECK_INVALID_FIREBALL:
			return "Invalid fireball";

		case SEXP_CHECK_INVALID_SPECIES:
			return "Invalid species name";

		case SEXP_CHECK_INVALID_FUNCTIONAL_WHEN_EVAL_TYPE:
			return "Invalid functional-when evaluation type";

		case SEXP_CHECK_MISPLACED_SPECIAL_ARGUMENT:
			return "Found " SEXP_ARGUMENT_STRING " without an argument handler higher in the sexp node tree";

		case SEXP_CHECK_AMBIGUOUS_GOAL_NAME:
			return "Ambiguous goal name (more than one goal with the same name)";

		case SEXP_CHECK_AMBIGUOUS_EVENT_NAME:
			return "Ambiguous event name (more than one event with the same name)";

		case SEXP_CHECK_INVALID_CONTAINER:
			return "Invalid container name";

		case SEXP_CHECK_MISSING_CONTAINER_MODIFIER:
			return "Missing container modifier";

		case SEXP_CHECK_INVALID_LIST_MODIFIER:
			return "Invalid list modifier";

		case SEXP_CHECK_WRONG_MAP_KEY_TYPE:
			return "Wrong map key type";

		case SEXP_CHECK_WRONG_CONTAINER_TYPE:
			return "Wrong container type";
			
		case SEXP_CHECK_INVALID_ANIMATION:
			return "Invalid animation specifier";

		case SEXP_CHECK_WRONG_CONTAINER_DATA_TYPE:
			return "Wrong container data type";

		case SEXP_CHECK_INVALID_SPECIAL_ARG_TYPE:
			return "Invalid special argument type";

		case SEXP_CHECK_INVALID_ASTEROID:
			return "Invalid asteroid debris type";

		case SEXP_CHECK_INVALID_WING_FORMATION:
			return "Invalid wing formation";

		case SEXP_CHECK_INVALID_MOTION_DEBRIS:
			return "Invalid motion debris type";

		case SEXP_CHECK_INVALID_BOLT_TYPE:
			return "Invalid lightning bolt type";

		case SEXP_CHECK_INVALID_TRAITOR_OVERRIDE:
			return "Invalid traitor override";

		case SEXP_CHECK_INVALID_LUA_GENERAL_ORDER:
			return "Invalid Lua general order";

		case SEXP_CHECK_INVALID_SHIP_POINT:
			return "Invalid ship or waypoint name";

		case SEXP_CHECK_INVALID_SHIP_WING_SHIPONTEAM_POINT:
			return "Invalid ship, wing, ship on team, or waypoint name";

		case SEXP_CHECK_INVALID_SHIP_WING_POINT:
			return "Invalid ship, wing, or waypoint name";

		case SEXP_CHECK_INVALID_ORDER_RECIPIENT:
			return "Invalid order recipient";

		case SEXP_CHECK_INVALID_SHIP_WING_WHOLETEAM:
			return "Invalid ship, wing, or team name";

		case SEXP_CHECK_INVALID_CUSTOM_STRING:
			return "Invalid custom string name";

		case SEXP_CHECK_INVALID_MESSAGE_TYPE:
			return "Invalid message type";

		case SEXP_CHECK_POTENTIAL_ISSUE:
			return "This particular SEXP_CHECK_ code is handled differently from the others.  You shouldn't actually see this message; if you do, report it to a SCP coder.";

		default:
			Warning(LOCATION, "Unhandled sexp error code %d!", num);
			return "Unhandled sexp error code!";
	}
}

int query_sexp_ai_goal_valid(int sexp_ai_goal, int ship_num)
{
	int i, op;

	for (op=0; op<(int)Operators.size(); op++)
		if (Operators[op].value == sexp_ai_goal)
			break;

	Assert(op < (int)Operators.size());
	for (i=0; i<Num_sexp_ai_goal_links; i++)
		if (Sexp_ai_goal_links[i].op_code == sexp_ai_goal)
			break;

	if (i >= Num_sexp_ai_goal_links) {
		//is a luaAI?
		Assert(ai_lua_has_mode(sexp_ai_goal));

		return true;
	}


	return ai_query_goal_valid(ship_num, Sexp_ai_goal_links[i].ai_goal);
}

// Goober5000
// This function specifically checks the sexp node text that can be referenced
// by a special argument.  In this situation the variable could appear as an
// undecorated variable_name or as the full @variable_name[variable_contents].
// There is also no need to copy the characters since the string is a small
// token rather than an entire file.  See also check_string_for_sexp_variable().
int check_sexp_node_text_for_sexp_variable(const char *text)
{
	// if the text is a formatted variable name, get the variable index
	if (text[0] == SEXP_VARIABLE_CHAR)
	{
		char variable_name[TOKEN_LENGTH];

		if (!get_unformatted_sexp_variable_name(variable_name, text))
			return -1;

		return get_index_sexp_variable_name(variable_name);
	}

	// try a straight lookup
	return get_index_sexp_variable_name(text);
}

/**
 * Wrapper around Sexp_node[xx].text for normal and variable
 */
const char *CTEXT(int n)
{
	int sexp_variable_index = -1;

	Assertion(n >= 0 && n < Num_sexp_nodes, "Passed an out-of-range node index (%d) to CTEXT!", n);
	if ( n < 0 || n >= Num_sexp_nodes ) {
		return "!INVALID CTEXT NODE INDEX!";
	}

	// Goober5000 - MWAHAHAHAHAHAHAHA!  Thank you, Volition programmers!  Without
	// the CTEXT wrapper, when-argument would probably be infeasibly difficult to code.
	if (Sexp_nodes[n].flags & SNF_SPECIAL_ARG_IN_NODE)
	{
		if (Fred_running)
		{
			// CTEXT is used when writing sexps to savefiles, so don't translate the argument
			return Sexp_nodes[n].text;
		}
		else
		{
			// make sure we have an argument to replace it with
			if (Sexp_replacement_arguments.empty())
				return Sexp_nodes[n].text;
		}

		auto current_argument = Sexp_replacement_arguments.back();
		auto text = current_argument.first;

		// check referenced node for a variable
		if (current_argument.second >= 0)
		{
			int arg_n = current_argument.second;

			if (!(Sexp_nodes[arg_n].flags & SNF_CHECKED_ARG_FOR_VAR))
			{
				// nodes that have an officially formatted variable will store the variable index in the token
				if (Sexp_nodes[arg_n].type & SEXP_FLAG_VARIABLE)
					Sexp_nodes[arg_n].cached_variable_index = atoi(text);
				else
					Sexp_nodes[arg_n].cached_variable_index = check_sexp_node_text_for_sexp_variable(text);

				Sexp_nodes[arg_n].flags |= SNF_CHECKED_ARG_FOR_VAR;
			}

			sexp_variable_index = Sexp_nodes[arg_n].cached_variable_index;
		}

		// if we have a variable, return the variable value, else return the regular argument
		if (sexp_variable_index >= 0)
			return Sexp_variables[sexp_variable_index].text;
		else
			return text;
	}

	// Goober5000 - if not special argument, proceed as normal
	if (Sexp_nodes[n].type & SEXP_FLAG_VARIABLE)
	{
		if (Fred_running)
		{
			sexp_variable_index = get_index_sexp_variable_name(Sexp_nodes[n].text);
		}
		else
		{
			sexp_variable_index = sexp_get_variable_index(n);
		}
		// Reference a Sexp_variable
		// string format -- "Sexp_variables[xx]=number" or "Sexp_variables[xx]=string", where xx is the index

		// if variable not found, just return the node text
		if (sexp_variable_index < 0)
			return Sexp_nodes[n].text;

		Assert( !(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_NOT_USED) );
		Assert(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_SET);

		if (Log_event) {
			Current_event_log_variable_buffer->push_back(Sexp_variables[sexp_variable_index].text); 
			Current_event_log_variable_buffer->push_back(Sexp_variables[sexp_variable_index].variable_name); 
		}

		return Sexp_variables[sexp_variable_index].text;
	}
	else if (Sexp_nodes[n].subtype == SEXP_ATOM_CONTAINER_DATA)
	{
		return sexp_container_CTEXT(n);
	}
	else
	{
		return Sexp_nodes[n].text;
	}
}


/**
 * Set all Sexp_variables to type uninitialized
 */
void init_sexp_vars()
{
	for (int i=0; i<MAX_SEXP_VARIABLES; i++) {
		Sexp_variables[i].type = SEXP_VARIABLE_NOT_USED;
		Block_variables[i].type = SEXP_VARIABLE_NOT_USED;
	}
}

/**
 * Add a variable to the block variable array rather than the Sexp_variables array
 */
void add_block_variable(const char *text, const char *var_name, int type, int index)
{
	Assert( (index >= 0) && (index < MAX_SEXP_VARIABLES) );

	strcpy_s(Block_variables[index].text, text);
	strcpy_s(Block_variables[index].variable_name, var_name);
	Block_variables[index].type &= ~SEXP_VARIABLE_NOT_USED;
	Block_variables[index].type = (type | SEXP_VARIABLE_SET);

}

/**
 * Add a Sexp_variable to be used in a mission.
 *
 * This should be called from within mission parse.
 */
int sexp_add_variable(const char *text, const char *var_name, int type, int index)
{
	// if index == -1, find next open slot
	if (index == -1) {
		for (int i=0; i<MAX_SEXP_VARIABLES; i++) {
			if (Sexp_variables[i].type == SEXP_VARIABLE_NOT_USED) {
				index = i;
				break;
			}
		}
	} else {
		Assert( (index >= 0) && (index < MAX_SEXP_VARIABLES) );
	}

	if (index >= 0) {
		strcpy_s(Sexp_variables[index].text, text);
		strcpy_s(Sexp_variables[index].variable_name, var_name);
		Sexp_variables[index].type &= ~SEXP_VARIABLE_NOT_USED;
		Sexp_variables[index].type = (type | SEXP_VARIABLE_SET);
	}

	return index;
}

// Goober5000 - minor variant of the above that is now required for variable arrays
void sexp_add_array_block_variable(int index, bool is_numeric)
{
	Assert(index >= 0 && index < MAX_SEXP_VARIABLES);

	strcpy_s(Sexp_variables[index].text, "");
	strcpy_s(Sexp_variables[index].variable_name, "variable array block");

	if (is_numeric)
		Sexp_variables[index].type = SEXP_VARIABLE_NUMBER | SEXP_VARIABLE_SET;
	else
		Sexp_variables[index].type = SEXP_VARIABLE_STRING | SEXP_VARIABLE_SET;
}

/**
 * Modify a Sexp_variable to be used in a mission
 *
 * This should be called in mission when an sexp_variable is to be modified
 */
void sexp_modify_variable(const char *text, int index, bool sexp_callback)
{
	Assert(index >= 0 && index < MAX_SEXP_VARIABLES);
	Assert(Sexp_variables[index].type & SEXP_VARIABLE_SET);
	Assert( !MULTIPLAYER_CLIENT );
	const size_t maxCopyLen = TOKEN_LENGTH - 1;

	if (strchr(text, '$') != nullptr || strchr(text, sexp_container::DELIM) != nullptr)
	{
		// we want to use the same text substitution that's in messages etc.
		SCP_string temp_text = text;
		sexp_replace_variable_names_with_values(temp_text);
		sexp_container_replace_refs_with_values(temp_text);
		message_translate_tokens(temp_text);

		if (temp_text.length() > maxCopyLen)
			Warning(LOCATION, "String too long.  Only " SIZE_T_ARG " characters will be assigned to %s.\n\nOriginal string:\n%s", maxCopyLen, Sexp_variables[index].variable_name, temp_text.c_str());

		// copy to original buffer
		auto len = temp_text.copy(Sexp_variables[index].text, maxCopyLen);
		Sexp_variables[index].text[len] = 0;
	}
	else
	{
		if (strlen(text) > maxCopyLen)
			Warning(LOCATION, "String too long.  Only " SIZE_T_ARG " characters will be assigned to %s.\n\nOriginal string:\n%s", maxCopyLen, Sexp_variables[index].variable_name, text);

		// no variables, so no substitution
		strncpy(Sexp_variables[index].text, text, maxCopyLen);
		Sexp_variables[index].text[maxCopyLen] = 0;
	}
	Sexp_variables[index].type |= SEXP_VARIABLE_MODIFIED;

	// do multi_callback_here
	// if we're called from the sexp code send a SEXP packet (more efficient) 
	if( MULTIPLAYER_MASTER && (Sexp_variables[index].type & SEXP_VARIABLE_NETWORK) && sexp_callback) {
		 Current_sexp_network_packet.start_callback();
		 Current_sexp_network_packet.send_int(index);
		Current_sexp_network_packet.send_string(Sexp_variables[index].text);
		 Current_sexp_network_packet.end_callback();
	}
	// otherwise send a SEXP variable packet
	else if ( (Game_mode & GM_MULTIPLAYER) && (Sexp_variables[index].type & SEXP_VARIABLE_NETWORK) ) {
		send_variable_update_packet(index, Sexp_variables[index].text);
	}
}

void multi_sexp_modify_variable()
{
	char value[TOKEN_LENGTH];
	int variable_index = -1;

	// get the data
	Current_sexp_network_packet.get_int(variable_index);
	if (!Current_sexp_network_packet.get_string(value)) {
		return;
	}

	// set the sexp_variable
	if ( (variable_index >= 0) && (variable_index < MAX_SEXP_VARIABLES) ) {
		// maybe create it first
		if (!(Sexp_variables[variable_index].type & SEXP_VARIABLE_SET)) {
			mprintf(("Warning; received multi packet for variable index which is not set!  Assuming this should be an array block variable...\n"));
			sexp_add_array_block_variable(variable_index, can_construe_as_integer(value));
		}

		strcpy_s(Sexp_variables[variable_index].text, value);
	}	
}

// copy an argument node's value(s) to Sexp_replacement_arguments for the *-of/in-sequence SEXPs
// returns the number of argument strings copied
int copy_node_to_replacement_args(int node, int container_value_index)
{
	int num_args = 0;

	if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_NAME) {
		const char *container_name = Sexp_nodes[node].text;
		const auto *p_container = get_sexp_container(container_name);

		Assertion(p_container, "Special argument SEXP given nonexistent container %s. Please report!", container_name);
		const auto &container = *p_container;
		Assertion(container.is_of_string_type(),
			"Attempt to use for-* SEXP with ineligible container argument %s. Please report!",
			container_name);

		if (container_value_index != -1) {
			const SCP_string &container_value = container.get_value_at_index(container_value_index);
			Sexp_replacement_arguments.emplace_back(container_value.c_str(), node);
			num_args = 1;
		} else {
			if (container.is_list()) {
				// populate in reverse order, so that the LIFO processing that callers use will process them in the correct order
				for (auto rev_it = container.list_data.crbegin(), end = container.list_data.crend(); rev_it != end; ++rev_it) {
					// use -1 because these strings are not associated with an individual node
					Sexp_replacement_arguments.emplace_back(rev_it->c_str(), -1);
				}
			} else if (container.is_map()) {
				for (const auto& kv_pair : container.map_data) {
					// use -1 because these strings are not associated with an individual node
					Sexp_replacement_arguments.emplace_back(kv_pair.first.c_str(), -1);
				}
			} else {
				UNREACHABLE("Container %s has invalid type (%d). Please report!", container_name, (int)container.type);
			}

			num_args = container.size();
		}
	} else {
		Assertion(container_value_index == -1,
			"Attempt to copy replacement argument string with unexpected index %d. Please report!",
			container_value_index);
		Sexp_replacement_arguments.emplace_back(Sexp_nodes[node].text, node);
		num_args = 1;
	}

	return num_args;
}

bool is_node_value_dynamic(int node)
{
	Assertion(node >= 0, "Attempt to check whether a non-existent node has a dynamic value. Please report!");

	return (Sexp_nodes[node].type & SEXP_FLAG_VARIABLE) || (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE) ||
		   (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_DATA);
}

int check_dynamic_value_node_type(int node, bool is_string, bool is_number)
{
	Assertion(is_node_value_dynamic(node),
		"Attempt to check dynamic value type of a node that isn't a dynamic value. Please report!");

	if (Sexp_nodes[node].type & SEXP_FLAG_VARIABLE) {
		const int var_index = sexp_get_variable_index(node);
		if (var_index < 0)
			return SEXP_CHECK_INVALID_VARIABLE;
		const int var_type = Sexp_variables[var_index].type;
		if (((var_type & SEXP_VARIABLE_STRING) && !is_string) || ((var_type & SEXP_VARIABLE_NUMBER) && !is_number)) {
			return SEXP_CHECK_INVALID_VARIABLE_TYPE;
		}
	} else if (Sexp_nodes[node].flags & SNF_SPECIAL_ARG_IN_NODE) {
		if (((Sexp_nodes[node].subtype == SEXP_ATOM_STRING) && !is_string) ||
			((Sexp_nodes[node].subtype == SEXP_ATOM_NUMBER) && !is_number)) {
			return SEXP_CHECK_INVALID_SPECIAL_ARG_TYPE;
		}
	} else if (Sexp_nodes[node].subtype == SEXP_ATOM_CONTAINER_DATA) {
		const auto *p_container = get_sexp_container(Sexp_nodes[node].text);
		if (!p_container)
			return SEXP_CHECK_INVALID_CONTAINER;
		if (!check_container_data_sexp_arg_type(p_container->type, is_string, is_number)) {
			return SEXP_CHECK_WRONG_CONTAINER_DATA_TYPE;
		}
	} else {
		UNREACHABLE("Unhandled dynamic value node %s. Please report!", Sexp_nodes[node].text);
	}

	return 0;
}

void sexp_modify_variable(int n)
{
	int sexp_variable_index;
	int new_number;
	char number_as_str[TOKEN_LENGTH];

	Assert(n >= 0);

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	// get sexp_variable index
	sexp_variable_index = sexp_get_variable_index(n);

	if (sexp_variable_index < 0)
	{
		Warning(LOCATION, "modify-variable: Variable %s does not exist!", Sexp_nodes[n].text);
	}
	else if (Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_NUMBER)
	{
		// get new numerical value
		new_number = eval_sexp(CDR(n));
		sprintf(number_as_str, "%d", new_number);

		// assign to variable
		sexp_modify_variable(number_as_str, sexp_variable_index);
	}
	else if (Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING)
	{
		// get new string
		auto new_text = CTEXT(CDR(n));

		// assign to variable
		sexp_modify_variable(new_text, sexp_variable_index);
	}
	else
	{
		Warning(LOCATION, "modify-variable: Variable %s has an unknown type!", Sexp_variables[sexp_variable_index].variable_name);
	}
}

bool is_sexp_node_numeric(int node)
{
	Assert(node >= 0);

	// make the common case fast: if the node has a CAR node, that means it uses an operator;
	// and operators cannot currently return strings
	if (Sexp_nodes[node].first >= 0)
		return true;
	
	// if the node text is numeric, the node is too
	if (can_construe_as_integer(Sexp_nodes[node].text))
		return true;

	// otherwise it's gotta be text
	return false;
}

// By Goober5000. Very similar to sexp_modify_variable(). Even uses the same multi code! :)	
void sexp_set_variable_by_index(int node)
{
	int sexp_variable_index, new_number;
	bool is_nan, is_nan_forever;
	char number_as_str[TOKEN_LENGTH];

	Assert(node >= 0);

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	// get sexp_variable index
	sexp_variable_index = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	// check range
	if (sexp_variable_index < 0 || sexp_variable_index >= MAX_SEXP_VARIABLES)
	{
		Warning(LOCATION, "set-variable-by-index: sexp variable index %d out of range!  min is 0; max is %d", sexp_variable_index, MAX_SEXP_VARIABLES - 1);
		return;
	}

	// verify variable set
	if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_SET))
	{
		// well phooey.  go ahead and create it
		sexp_add_array_block_variable(sexp_variable_index, is_sexp_node_numeric(CDR(node)));
	}

	if (Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_NUMBER)
	{
		// get new numerical value
		new_number = eval_num(CDR(node), is_nan, is_nan_forever);
		if (is_nan || is_nan_forever)
			strcpy_s(number_as_str, "NaN");
		else
			sprintf(number_as_str, "%d", new_number);

		// assign to variable
		sexp_modify_variable(number_as_str, sexp_variable_index);
	}
	else if (Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING)
	{
		// get new string
		auto new_text = CTEXT(CDR(node));

		// assign to variable
		sexp_modify_variable(new_text, sexp_variable_index);
	}
	else
	{
		Error(LOCATION, "Invalid variable type.\n");
	}
}

// Goober5000
int sexp_get_variable_by_index(int node)
{
	int sexp_variable_index;
	bool is_nan, is_nan_forever;

	Assert(node >= 0);

	// get sexp_variable index
	sexp_variable_index = eval_num(node, is_nan, is_nan_forever);
	if (is_nan)
		return SEXP_NAN;
	if (is_nan_forever)
		return SEXP_NAN_FOREVER;

	// check range
	if (sexp_variable_index < 0 || sexp_variable_index >= MAX_SEXP_VARIABLES)
	{
		Warning(LOCATION, "get-variable-by-index: sexp variable index %d out of range!  min is 0; max is %d", sexp_variable_index, MAX_SEXP_VARIABLES - 1);
		return 0;
	}

	if (Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_NOT_USED)
	{
		mprintf(("warning: retrieving a value from a sexp variable which is not in use!\n"));
	}
	else if (!(Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_SET))
	{
		mprintf(("warning: retrieving a value from a sexp variable which is not set!\n"));
	}

	if (Sexp_variables[sexp_variable_index].type & SEXP_VARIABLE_STRING)
	{
		mprintf(("warning: variable %d is a string but it is not possible to return a string value through a sexp!\n", sexp_variable_index));
		return SEXP_NAN_FOREVER;
	}

	return atoi(Sexp_variables[sexp_variable_index].text);
}

// Goober5000
// (yes, this reuses a lot of code, but it's a major pain to consolidate it)
void sexp_copy_variable_from_index(int node)
{
	int from_index, to_index;
	bool is_nan, is_nan_forever;

	Assert(node >= 0);

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	// get sexp_variable index
	from_index = eval_num(node, is_nan, is_nan_forever);
	if (is_nan || is_nan_forever)
		return;

	// check range
	if (from_index < 0 || from_index >= MAX_SEXP_VARIABLES)
	{
		Warning(LOCATION, "copy-variable-from-index: sexp variable index %d out of range!  min is 0; max is %d", from_index, MAX_SEXP_VARIABLES - 1);
		return;
	}

	if (Sexp_variables[from_index].type & SEXP_VARIABLE_NOT_USED)
	{
		mprintf(("warning: retrieving a value from a sexp variable which is not in use!\n"));
	}
	else if (!(Sexp_variables[from_index].type & SEXP_VARIABLE_SET))
	{
		mprintf(("warning: retrieving a value from a sexp variable which is not set!\n"));
	}

	// now get the variable we are modifying
	to_index = sexp_get_variable_index(CDR(node));

	if (to_index < 0)
	{
		Warning(LOCATION, "copy-variable-from-index: Variable %s does not exist!", Sexp_nodes[CDR(node)].text);
		return;
	}

	// verify matching types
	if ( ((Sexp_variables[from_index].type & SEXP_VARIABLE_NUMBER) && !(Sexp_variables[to_index].type & SEXP_VARIABLE_NUMBER))
		|| ((Sexp_variables[from_index].type & SEXP_VARIABLE_STRING) && !(Sexp_variables[to_index].type & SEXP_VARIABLE_STRING)) )
	{
		Warning(LOCATION, "copy-variable-from-index: cannot copy variables of different types!  source = '%s', destination = '%s'", Sexp_variables[from_index].variable_name, Sexp_variables[to_index].variable_name);
		return;
	}

	// assign to variable
	sexp_modify_variable(Sexp_variables[from_index].text, to_index);
}

// Goober5000
// (yes, this reuses a lot of code, but it's a major pain to consolidate it)
// (and yes, that reused a comment :p)
void sexp_copy_variable_between_indexes(int node)
{
	int from_index, to_index;
	bool is_nan, is_nan_forever;

	Assert(node >= 0);

	// Only do single player or multi host
	if ( MULTIPLAYER_CLIENT )
		return;

	// get sexp_variable indexes
	eval_nums(node, is_nan, is_nan_forever, from_index, to_index);
	if (is_nan || is_nan_forever)
		return;

	// check ranges
	if (from_index < 0 || from_index >= MAX_SEXP_VARIABLES)
	{
		Warning(LOCATION, "copy-variable-between-indexes: sexp variable index %d out of range!  min is 0; max is %d", from_index, MAX_SEXP_VARIABLES - 1);
		return;
	}
	if (to_index < 0 || to_index >= MAX_SEXP_VARIABLES)
	{
		Warning(LOCATION, "copy-variable-between-indexes: sexp variable index %d out of range!  min is 0; max is %d", to_index, MAX_SEXP_VARIABLES - 1);
		return;
	}

	if (Sexp_variables[from_index].type & SEXP_VARIABLE_NOT_USED)
	{
		mprintf(("warning: retrieving a value from a sexp variable which is not in use!\n"));
	}
	else if (!(Sexp_variables[from_index].type & SEXP_VARIABLE_SET))
	{
		mprintf(("warning: retrieving a value from a sexp variable which is not set!\n"));
	}

	if (!(Sexp_variables[to_index].type & SEXP_VARIABLE_SET))
	{
		// well phooey.  go ahead and create it
		sexp_add_array_block_variable(to_index, (Sexp_variables[from_index].type & SEXP_VARIABLE_NUMBER) != 0);
	}

	// verify matching types
	if ( ((Sexp_variables[from_index].type & SEXP_VARIABLE_NUMBER) && !(Sexp_variables[to_index].type & SEXP_VARIABLE_NUMBER))
		|| ((Sexp_variables[from_index].type & SEXP_VARIABLE_STRING) && !(Sexp_variables[to_index].type & SEXP_VARIABLE_STRING)) )
	{
		Warning(LOCATION, "copy-variable-between-indexes: cannot copy variables of different types!  source = '%s', destination = '%s'", Sexp_variables[from_index].variable_name, Sexp_variables[to_index].variable_name);
		return;
	}

	// assign to variable
	sexp_modify_variable(Sexp_variables[from_index].text, to_index);
}

// Different type needed for Fred (1) allow modification of type (2) no callback required
void sexp_fred_modify_variable(const char *text, const char *var_name, int index, int type)
{
	Assert(index >= 0 && index < MAX_SEXP_VARIABLES);
	Assert(Sexp_variables[index].type & SEXP_VARIABLE_SET);
	Assert( (type & SEXP_VARIABLE_NUMBER) || (type & SEXP_VARIABLE_STRING) );

	strcpy_s(Sexp_variables[index].text, text);
	strcpy_s(Sexp_variables[index].variable_name, var_name);
	Sexp_variables[index].type = (SEXP_VARIABLE_SET | SEXP_VARIABLE_MODIFIED | type);
}

/**
 * Return index of sexp_variable_name, -1 if not found
 */
int get_index_sexp_variable_name(const char *text)
{
	for (int i=0; i<MAX_SEXP_VARIABLES; i++) {
		if (Sexp_variables[i].type & SEXP_VARIABLE_SET) {
			// check case sensitive
			if ( !strcmp(Sexp_variables[i].variable_name, text) ) {
				return i;
			}
		}
	}

	// not found
	return -1;
}

/**
 * Return index of sexp_variable_name, -1 if not found
 */
int get_index_sexp_variable_name(SCP_string &text)
{
	for (int i=0; i<MAX_SEXP_VARIABLES; i++) {
		if (Sexp_variables[i].type & SEXP_VARIABLE_SET) {
			// check case sensitive
			if ( text == Sexp_variables[i].variable_name ) {
				return i;
			}
		}
	}

	// not found
	return -1;
}

// Goober5000 - tests whether a variable name starts here
// return index of sexp_variable_name, -1 if not found
int get_index_sexp_variable_name_special(const char *startpos)
{
	for (int i = MAX_SEXP_VARIABLES - 1; i >= 0; i--) {
		if (Sexp_variables[i].type & SEXP_VARIABLE_SET) {
			// check case sensitive
			// check number of chars in variable name
			if ( !strncmp(startpos, Sexp_variables[i].variable_name, strlen(Sexp_variables[i].variable_name)) ) {
				return i;
			}
		}
	}

	// not found
	return -1;
}

// Goober5000 - tests whether a variable name starts here
// return index of sexp_variable_name, -1 if not found
int get_index_sexp_variable_name_special(SCP_string &text, size_t startpos)
{
	for (int i = MAX_SEXP_VARIABLES - 1; i >= 0; i--) {
		if (Sexp_variables[i].type & SEXP_VARIABLE_SET) {
			// check case sensitive
			// check that the variable name starts here, as opposed to farther down the string
			size_t pos = text.find(Sexp_variables[i].variable_name, startpos);
			if (pos != SCP_string::npos && pos == startpos) {
				return i;
			}
		}
	}

	// not found
	return -1;
}

// Goober5000
// Note that the parameter here is max *length*, not max buffer size.  Leave room for the null-terminator!
bool sexp_replace_variable_names_with_values(char *text, int max_len)
{
	Assert(text != nullptr);
	Assert(max_len >= 0);

	bool replaced_anything = false;
	char *pos = text;
	do {
		// look for the meta-character
		pos = strchr(pos, '$');

		// found?
		if (pos != nullptr)
		{
			// see if a variable starts at the next char
			int var_index = get_index_sexp_variable_name_special(pos+1);
			if (var_index >= 0)
			{
				// get the replacement string ($variable)
				char what_to_replace[TOKEN_LENGTH+1];
				memset(what_to_replace, 0, TOKEN_LENGTH+1);
				strncpy(what_to_replace, pos, strlen(Sexp_variables[var_index].variable_name) + 1);

				// replace it
				auto diff = replace_one(text, what_to_replace, Sexp_variables[var_index].text, max_len);
				Assertion(diff >= 0, "Variable replacement should have succeeded!");

				pos = text + diff;
				replaced_anything = true;
			}
			// no match... so keep iterating along the string
			else
			{
				pos++;
			}
		}
	} while (pos != nullptr);

	return replaced_anything;
}

// Goober5000
bool sexp_replace_variable_names_with_values(SCP_string &text)
{
	bool replaced_anything = false;

	size_t lookHere = 0;
	size_t foundHere;

	do {
		// look for the meta-character
		foundHere = text.find('$', lookHere);

		// found?
		if (foundHere != SCP_string::npos)
		{
			// see if a variable starts at the next char
			int var_index = get_index_sexp_variable_name_special(text, foundHere+1);
			if (var_index >= 0)
			{
				// replace $variable with the value
				text.replace(foundHere, strlen(Sexp_variables[var_index].variable_name)+1, Sexp_variables[var_index].text);
				replaced_anything = true;

				lookHere = foundHere + strlen(Sexp_variables[var_index].text);
			}
			// no match... so keep iterating along the string
			else
			{
				lookHere = foundHere + 1;
			}
		}
	} while (foundHere != SCP_string::npos);

	return replaced_anything;
}

// returns the index of the nth variable of the type given or -1 if there aren't that many
// NOTE : Not 0th order. If you want the 4th string variable call it as get_nth_variable_index(4, SEXP_VARIABLE_STRING)
int get_nth_variable_index(int nth, int variable_type)
{
	// Loop through Sexp_variables until we have found the one corresponding to the argument
	Assert ((nth > 0) && (nth < MAX_SEXP_VARIABLES));
	for (int i=0; i < MAX_SEXP_VARIABLES; i++)	{
		if ((Sexp_variables[i].type & variable_type)) {
			nth--; 
		}

		if (!nth) {
			return i; 
		}
	}
	return -1;
}

/**
 * Count number of sexp_variables that are set
 */
int sexp_variable_count()
{
	int count = 0;

	for (int i=0; i<MAX_SEXP_VARIABLES; i++) {
		if ( Sexp_variables[i].type & SEXP_VARIABLE_SET) {
			count++;
		}
	}

	return count;
}

/**
 * Given an index in Sexp_variables, returns the number variables of a type in the array until this point
 */
int sexp_variable_typed_count(int sexp_variables_index, int variable_type)
{
	Assert ((sexp_variables_index >= 0) && (sexp_variables_index < MAX_SEXP_VARIABLES));
	// Loop through Sexp_variables until we have found the one corresponding to the argument
	int count = 0;
	for (int i=0; i < MAX_SEXP_VARIABLES; i++)	{
		if (!(Sexp_variables[i].type & variable_type)) {
			continue; 
		}

		if (i == sexp_variables_index) {
			return count ; 
		}
		count++;
	}
	// shouldn't ever get here
	Assert(false);
	return -1;
}

/**
 * Delete sexp_variable from active
 */
void sexp_variable_delete(int index)
{
	Assert(Sexp_variables[index].type & SEXP_VARIABLE_SET);

	Sexp_variables[index].type = SEXP_VARIABLE_NOT_USED;
}

int sexp_var_compare(const sexp_variable *sexp_var1, const sexp_variable *sexp_var2)
{
	int set1, set2;

	set1 = sexp_var1->type & SEXP_VARIABLE_SET;
	set2 = sexp_var2->type & SEXP_VARIABLE_SET;

	if (!set1 && !set2) {
		return 0;
	} else if (set1 && !set2) {
		return -1;
	} else if (!set1 && set2) {
		return 1;
	} else {
		return stricmp( sexp_var1->variable_name, sexp_var2->variable_name);
	}
}

/**
 * Sort sexp_variable list lexigraphically, with set before unset
 */
void sexp_variable_sort()
{
	insertion_sort(Sexp_variables, MAX_SEXP_VARIABLES, sexp_var_compare);
}

// Goober5000
const char *get_category_name(int category_id)
{
	for (auto &menu_item : op_menu)
	{
		if (menu_item.id == category_id)
			return menu_item.name.c_str();
	}

	return "<unknown>";
}

// Goober5000
int category_of_subcategory(int subcategory_id)
{
	switch (subcategory_id)
	{
		case CHANGE_SUBCATEGORY_MESSAGING:
		case CHANGE_SUBCATEGORY_AI_CONTROL:
		case CHANGE_SUBCATEGORY_SHIP_STATUS:
		case CHANGE_SUBCATEGORY_SHIELDS_ENGINES_AND_WEAPONS:
		case CHANGE_SUBCATEGORY_SUBSYSTEMS:
		case CHANGE_SUBCATEGORY_CARGO:
		case CHANGE_SUBCATEGORY_ARMOR_AND_DAMAGE_TYPES:
		case CHANGE_SUBCATEGORY_BEAMS_AND_TURRETS:
		case CHANGE_SUBCATEGORY_MODELS_AND_TEXTURES:
		case CHANGE_SUBCATEGORY_COORDINATE_MANIPULATION:
		case CHANGE_SUBCATEGORY_MISSION_AND_CAMPAIGN:
		case CHANGE_SUBCATEGORY_MUSIC_AND_SOUND:
		case CHANGE_SUBCATEGORY_HUD:
		case CHANGE_SUBCATEGORY_NAV:
		case CHANGE_SUBCATEGORY_CUTSCENES:
		case CHANGE_SUBCATEGORY_BACKGROUND_AND_NEBULA:
		case CHANGE_SUBCATEGORY_JUMP_NODES:
		case CHANGE_SUBCATEGORY_SPECIAL_EFFECTS:
		case CHANGE_SUBCATEGORY_VARIABLES:
		case CHANGE_SUBCATEGORY_CONTAINERS:
		case CHANGE_SUBCATEGORY_OTHER:
			return OP_CATEGORY_CHANGE;

		case STATUS_SUBCATEGORY_MISSION:
		case STATUS_SUBCATEGORY_CUTSCENES:
		case STATUS_SUBCATEGORY_PLAYER:
		case STATUS_SUBCATEGORY_MULTIPLAYER:
		case STATUS_SUBCATEGORY_SHIP_STATUS:
		case STATUS_SUBCATEGORY_SHIELDS_ENGINES_AND_WEAPONS:
		case STATUS_SUBCATEGORY_CARGO:
		case STATUS_SUBCATEGORY_DAMAGE:
		case STATUS_SUBCATEGORY_DISTANCE_AND_COORDINATES:
		case STATUS_SUBCATEGORY_VARIABLES:
		case STATUS_SUBCATEGORY_CONTAINERS:
		case STATUS_SUBCATEGORY_OTHER:
			return OP_CATEGORY_STATUS;

		default:
			// we might have a dynamically added subcategory
			return sexp::get_category_of_subcategory(subcategory_id);
	}
}

// Goober5000
int get_category(int op_id)
{
	switch (op_id)
	{
		case OP_PLUS:
		case OP_MINUS:
		case OP_MOD:
		case OP_MUL:
		case OP_DIV:
		case OP_RAND:
		case OP_ABS:
		case OP_MIN:
		case OP_MAX:
		case OP_AVG:
		case OP_RAND_MULTIPLE:
		case OP_POW:
		case OP_BITWISE_AND:
		case OP_BITWISE_OR:
		case OP_BITWISE_NOT:
		case OP_BITWISE_XOR:
		case OP_SET_BIT:
		case OP_UNSET_BIT:
		case OP_IS_BIT_SET:
		case OP_SIGNUM:
		case OP_IS_NAN:
		case OP_NAN_TO_NUMBER:
		case OP_ANGLE_VECTORS:
			return OP_CATEGORY_ARITHMETIC;

		case OP_TRUE:
		case OP_FALSE:
		case OP_AND:
		case OP_AND_IN_SEQUENCE:
		case OP_OR:
		case OP_EQUALS:
		case OP_GREATER_THAN:
		case OP_LESS_THAN:
		case OP_HAS_TIME_ELAPSED:
		case OP_HAS_TIME_ELAPSED_MSECS:
		case OP_NOT:
		case OP_STRING_EQUALS:
		case OP_STRING_GREATER_THAN:
		case OP_STRING_LESS_THAN:
		case OP_NOT_EQUAL:
		case OP_GREATER_OR_EQUAL:
		case OP_LESS_OR_EQUAL:
		case OP_XOR:
		case OP_PERFORM_ACTIONS_BOOL_FIRST:
		case OP_PERFORM_ACTIONS_BOOL_LAST:
		case OP_IS_TRUE_FOR_DURATION:
			return OP_CATEGORY_LOGICAL;

		case OP_GOAL_INCOMPLETE:
		case OP_GOAL_TRUE_DELAY:
		case OP_GOAL_FALSE_DELAY:
		case OP_EVENT_INCOMPLETE:
		case OP_EVENT_TRUE_DELAY:
		case OP_EVENT_FALSE_DELAY:
		case OP_PREVIOUS_EVENT_TRUE:
		case OP_PREVIOUS_EVENT_FALSE:
		case OP_PREVIOUS_GOAL_TRUE:
		case OP_PREVIOUS_GOAL_FALSE:
		case OP_EVENT_TRUE_MSECS_DELAY:
		case OP_EVENT_FALSE_MSECS_DELAY:
		case OP_RESET_EVENT:
		case OP_RESET_GOAL:
			return OP_CATEGORY_GOAL_EVENT;

		case OP_IS_DESTROYED_DELAY:
		case OP_IS_SUBSYSTEM_DESTROYED_DELAY:
		case OP_IS_DISABLED_DELAY:
		case OP_IS_DISARMED_DELAY:
		case OP_HAS_DOCKED_DELAY:
		case OP_HAS_UNDOCKED_DELAY:
		case OP_HAS_ARRIVED_DELAY:
		case OP_HAS_DEPARTED_DELAY:
		case OP_WAYPOINTS_DONE_DELAY:
		case OP_SHIP_TYPE_DESTROYED:
		case OP_PERCENT_SHIPS_DEPARTED:
		case OP_PERCENT_SHIPS_DESTROYED:
		case OP_DEPART_NODE_DELAY:
		case OP_DESTROYED_DEPARTED_DELAY:
		case OP_PERCENT_SHIPS_DISARMED:
		case OP_PERCENT_SHIPS_DISABLED:
		case OP_PERCENT_SHIPS_ARRIVED:
		case OP_PERCENT_SHIPS_SCANNED:
		case OP_NAV_IS_VISITED:
		case OP_WAS_DESTROYED_BY_DELAY:
			return OP_CATEGORY_OBJECTIVE;

		case OP_TIME_SHIP_DESTROYED:
		case OP_TIME_SHIP_ARRIVED:
		case OP_TIME_SHIP_DEPARTED:
		case OP_TIME_WING_DESTROYED:
		case OP_TIME_WING_ARRIVED:
		case OP_TIME_WING_DEPARTED:
		case OP_MISSION_TIME:
		case OP_MISSION_TIME_MSECS:
		case OP_TIME_DOCKED:
		case OP_TIME_UNDOCKED:
		case OP_TIME_TO_GOAL:
		case OP_SET_HUD_TIME_PAD:
			return OP_CATEGORY_TIME;

		case OP_SHIELDS_LEFT:
		case OP_HITS_LEFT:
		case OP_HITS_LEFT_SUBSYSTEM:
		case OP_SIM_HITS_LEFT:
		case OP_DISTANCE:
		case OP_DISTANCE_CENTER_SUBSYSTEM:
		case OP_LAST_ORDER_TIME:
		case OP_NUM_PLAYERS:
		case OP_SKILL_LEVEL_AT_LEAST:
		case OP_WAS_PROMOTION_GRANTED:
		case OP_WAS_MEDAL_GRANTED:
		case OP_CARGO_KNOWN_DELAY:
		case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY:
		case OP_HAS_BEEN_TAGGED_DELAY:
		case OP_IS_TAGGED:
		case OP_NUM_KILLS:
		case OP_NUM_TYPE_KILLS:
		case OP_NUM_CLASS_KILLS:
		case OP_SHIELD_RECHARGE_PCT:
		case OP_ENGINE_RECHARGE_PCT:
		case OP_WEAPON_RECHARGE_PCT:
		case OP_SHIELD_QUAD_LOW:
		case OP_SECONDARY_AMMO_PCT:
		case OP_IS_SECONDARY_SELECTED:
		case OP_IS_PRIMARY_SELECTED:
		case OP_SPECIAL_WARP_DISTANCE:
		case OP_IS_SHIP_VISIBLE:
		case OP_TEAM_SCORE:
		case OP_PRIMARY_AMMO_PCT:
		case OP_IS_SHIP_STEALTHY:
		case OP_IS_CARGO:
		case OP_IS_FRIENDLY_STEALTH_VISIBLE:
		case OP_GET_OBJECT_X:
		case OP_GET_OBJECT_Y:
		case OP_GET_OBJECT_Z:
		case OP_IS_AI_CLASS:
		case OP_IS_SHIP_TYPE:
		case OP_IS_SHIP_CLASS:
		case OP_NUM_SHIPS_IN_BATTLE:
		case OP_CURRENT_SPEED:
		case OP_IS_IFF:
		case OP_IS_SPECIES:
		case OP_NUM_WITHIN_BOX:
		case OP_SCRIPT_EVAL_NUM:
		case OP_NUM_SHIPS_IN_WING:
		case OP_GET_PRIMARY_AMMO:
		case OP_GET_SECONDARY_AMMO:
		case OP_NUM_ASSISTS:
		case OP_SHIP_SCORE:
		case OP_SHIP_DEATHS:
		case OP_RESPAWNS_LEFT:
		case OP_IS_PLAYER:
		case OP_GET_DAMAGE_CAUSED:
		case OP_AFTERBURNER_LEFT:
		case OP_WEAPON_ENERGY_LEFT:
		case OP_GET_ETS_VALUE:
		case OP_TURRET_FIRED_SINCE:
		case OP_PRIMARY_FIRED_SINCE:
		case OP_SECONDARY_FIRED_SINCE:
		case OP_GET_THROTTLE_SPEED:
		case OP_HITS_LEFT_SUBSYSTEM_GENERIC:
		case OP_HITS_LEFT_SUBSYSTEM_SPECIFIC:
		case OP_GET_OBJECT_PITCH:
		case OP_GET_OBJECT_BANK:
		case OP_GET_OBJECT_HEADING:
		case OP_HAS_PRIMARY_WEAPON:
		case OP_HAS_SECONDARY_WEAPON:
		case OP_STRING_TO_INT:
		case OP_STRING_GET_LENGTH:
		case OP_GET_OBJECT_SPEED_X:
		case OP_GET_OBJECT_SPEED_Y:
		case OP_GET_OBJECT_SPEED_Z:
		case OP_NAV_DISTANCE:
		case OP_NAV_ISLINKED:
		case OP_IS_FACING:
		case OP_DIRECTIVE_VALUE:
		case OP_GET_NUM_COUNTERMEASURES:
		case OP_IS_IN_BOX:
		case OP_IS_IN_MISSION:
		case OP_HAS_ARMOR_TYPE:
		case OP_ARE_SHIP_FLAGS_SET:
		case OP_TURRET_GET_PRIMARY_AMMO:
		case OP_TURRET_GET_SECONDARY_AMMO:
		case OP_TURRET_HAS_PRIMARY_WEAPON:
		case OP_TURRET_HAS_SECONDARY_WEAPON:
		case OP_IS_DOCKED:
		case OP_IS_IN_TURRET_FOV:
		case OP_GET_HOTKEY:
		case OP_DISTANCE_CENTER:
		case OP_DISTANCE_BBOX:
		case OP_DISTANCE_BBOX_SUBSYSTEM:
		case OP_IS_LANGUAGE:
		case OP_SCRIPT_EVAL_BOOL:
		case OP_IS_CONTAINER_EMPTY:
		case OP_GET_CONTAINER_SIZE:
		case OP_LIST_HAS_DATA:
		case OP_LIST_DATA_INDEX:
		case OP_MAP_HAS_KEY:
		case OP_MAP_HAS_DATA_ITEM:
		case OP_ANGLE_FVEC_TARGET:
		case OP_ARE_WING_FLAGS_SET:
		case OP_IS_SHIP_EMP_ACTIVE:
		case OP_PLAYER_IS_CHEATING_BASTARD:
		case OP_USED_CHEAT:
		case OP_CUTSCENES_GET_FOV:
		case OP_GET_SUPERNOVA_STAGE:
			return OP_CATEGORY_STATUS;

		case OP_WHEN:
		case OP_WHEN_ARGUMENT:
		case OP_EVERY_TIME:
		case OP_EVERY_TIME_ARGUMENT:
		case OP_ON_MISSION_SKIP:
		case OP_ANY_OF:
		case OP_EVERY_OF:
		case OP_RANDOM_OF:
		case OP_NUMBER_OF:
		case OP_INVALIDATE_ARGUMENT:
		case OP_RANDOM_MULTIPLE_OF:
		case OP_IN_SEQUENCE:
		case OP_VALIDATE_ARGUMENT:
		case OP_DO_FOR_VALID_ARGUMENTS:
		case OP_INVALIDATE_ALL_ARGUMENTS:
		case OP_VALIDATE_ALL_ARGUMENTS:
		case OP_FOR_COUNTER:
		case OP_IF_THEN_ELSE:
		case OP_NUM_VALID_ARGUMENTS:
		case OP_FUNCTIONAL_IF_THEN_ELSE:
		case OP_FOR_SHIP_CLASS:
		case OP_FOR_SHIP_TYPE:
		case OP_FOR_SHIP_TEAM:
		case OP_FOR_SHIP_SPECIES:
		case OP_FOR_PLAYERS:
		case OP_FOR_SUBSYSTEMS:
		case OP_FIRST_OF:
		case OP_SWITCH:
		case OP_FUNCTIONAL_SWITCH:
		case OP_FUNCTIONAL_WHEN:
		case OP_FOR_CONTAINER_DATA:
		case OP_FOR_MAP_CONTAINER_KEYS:
			return OP_CATEGORY_CONDITIONAL;

		case OP_CHANGE_IFF:
		case OP_REPAIR_SUBSYSTEM:
		case OP_SABOTAGE_SUBSYSTEM:
		case OP_SET_SUBSYSTEM_STRNGTH:
		case OP_PROTECT_SHIP:
		case OP_SEND_MESSAGE:
		case OP_SEND_BUILTIN_MESSAGE:
		case OP_SELF_DESTRUCT:
		case OP_CLEAR_GOALS:
		case OP_ADD_GOAL:
		case OP_REMOVE_GOAL:
		case OP_INVALIDATE_GOAL:
		case OP_VALIDATE_GOAL:
		case OP_SEND_RANDOM_MESSAGE:
		case OP_TRANSFER_CARGO:
		case OP_EXCHANGE_CARGO:
		case OP_UNPROTECT_SHIP:
		case OP_GOOD_REARM_TIME:
		case OP_BAD_REARM_TIME:
		case OP_GRANT_PROMOTION:
		case OP_GRANT_MEDAL:
		case OP_ALLOW_SHIP:
		case OP_ALLOW_WEAPON:
		case OP_GOOD_PRIMARY_TIME:
		case OP_GOOD_SECONDARY_TIME:
		case OP_WARP_BROKEN:
		case OP_WARP_NOT_BROKEN:
		case OP_WARP_NEVER:
		case OP_WARP_ALLOWED:
		case OP_SHIP_INVISIBLE:
		case OP_SHIP_VISIBLE:
		case OP_SHIP_INVULNERABLE:
		case OP_SHIP_VULNERABLE:
		case OP_RED_ALERT:
		case OP_TECH_ADD_SHIP:
		case OP_TECH_ADD_WEAPON:
		case OP_END_CAMPAIGN:
		case OP_JETTISON_CARGO_DELAY:
		case OP_MODIFY_VARIABLE:
		case OP_NOP:
		case OP_BEAM_FIRE:
		case OP_BEAM_FREE:
		case OP_BEAM_FREE_ALL:
		case OP_BEAM_LOCK:
		case OP_BEAM_LOCK_ALL:
		case OP_BEAM_PROTECT_SHIP:
		case OP_BEAM_UNPROTECT_SHIP:
		case OP_TURRET_FREE:
		case OP_TURRET_FREE_ALL:
		case OP_TURRET_LOCK:
		case OP_TURRET_LOCK_ALL:
		case OP_ADD_REMOVE_ESCORT:
		case OP_AWACS_SET_RADIUS:
		case OP_SEND_MESSAGE_LIST:
		case OP_CAP_WAYPOINT_SPEED:
		case OP_SHIP_GUARDIAN:
		case OP_SHIP_NO_GUARDIAN:
		case OP_TURRET_TAGGED_ONLY_ALL:
		case OP_TURRET_TAGGED_CLEAR_ALL:
		case OP_SUBSYS_SET_RANDOM:
		case OP_SUPERNOVA_START:
		case OP_CARGO_NO_DEPLETE:
		case OP_SET_SPECIAL_WARPOUT_NAME:
		case OP_SHIP_VANISH:
		case OP_PROP_VANISH:
		case OP_SHIELDS_ON:
		case OP_SHIELDS_OFF:
		case OP_CHANGE_AI_LEVEL:
		case OP_END_MISSION:
		case OP_SET_SCANNED:
		case OP_SET_UNSCANNED:
		case OP_SHIP_STEALTHY:
		case OP_SHIP_UNSTEALTHY:
		case OP_SET_CARGO:
		case OP_CHANGE_AI_CLASS:
		case OP_FRIENDLY_STEALTH_INVISIBLE:
		case OP_FRIENDLY_STEALTH_VISIBLE:
		case OP_DAMAGED_ESCORT_LIST:
		case OP_DAMAGED_ESCORT_LIST_ALL:
		case OP_SHIP_VAPORIZE:
		case OP_SHIP_NO_VAPORIZE:
		case OP_COLLIDE_INVISIBLE:
		case OP_DONT_COLLIDE_INVISIBLE:
		case OP_PRIMITIVE_SENSORS_SET_RANGE:
		case OP_CHANGE_SHIP_CLASS:
		case OP_SCRIPT_EVAL:
		case OP_SET_SUPPORT_SHIP:
		case OP_DEACTIVATE_GLOW_POINTS:
		case OP_ACTIVATE_GLOW_POINTS:
		case OP_DEACTIVATE_GLOW_MAPS:
		case OP_ACTIVATE_GLOW_MAPS:
		case OP_DEACTIVATE_GLOW_POINT_BANK:
		case OP_ACTIVATE_GLOW_POINT_BANK:
		case OP_CHANGE_SOUNDTRACK:
		case OP_TECH_ADD_INTEL:
		case OP_TECH_RESET_TO_DEFAULT:
		case OP_CREATE_BOLT:
		case OP_EXPLOSION_EFFECT:
		case OP_WARP_EFFECT:
		case OP_SET_OBJECT_FACING:
		case OP_SET_OBJECT_FACING_OBJECT:
		case OP_SET_OBJECT_POSITION:
		case OP_PLAY_SOUND_FROM_TABLE:
		case OP_PLAY_SOUND_FROM_FILE:
		case OP_CLOSE_SOUND_FROM_FILE:
		case OP_HUD_DISABLE:
		case OP_KAMIKAZE:
		case OP_MISSION_SET_SUBSPACE:
		case OP_TURRET_TAGGED_SPECIFIC:
		case OP_TURRET_TAGGED_CLEAR_SPECIFIC:
		case OP_LOCK_ROTATING_SUBSYSTEM:
		case OP_FREE_ROTATING_SUBSYSTEM:
		case OP_REVERSE_ROTATING_SUBSYSTEM:
		case OP_ROTATING_SUBSYS_SET_TURN_TIME:
		case OP_PLAYER_USE_AI:
		case OP_PLAYER_NOT_USE_AI:
		case OP_SET_PLAYER_TARGET:
		case OP_CLEAR_PLAYER_TARGET:
		case OP_HUD_DISABLE_EXCEPT_MESSAGES:
		case OP_FORCE_JUMP:
		case OP_HUD_SET_TEXT:
		case OP_HUD_SET_XSTR:
		case OP_HUD_SET_TEXT_NUM:
		case OP_HUD_SET_COORDS:
		case OP_HUD_SET_FRAME:
		case OP_HUD_SET_COLOR:
		case OP_HUD_RESET_COLOR:
		case OP_HUD_SET_MAX_TARGETING_RANGE:
		case OP_SHIP_TAG:
		case OP_SHIP_UNTAG:
		case OP_SHIP_CHANGE_ALT_NAME:
		case OP_SHIP_CHANGE_DISPLAY_NAME:
		case OP_SCRAMBLE_MESSAGES:
		case OP_UNSCRAMBLE_MESSAGES:
		case OP_CUTSCENES_SET_CUTSCENE_BARS:
		case OP_CUTSCENES_UNSET_CUTSCENE_BARS:
		case OP_CUTSCENES_FADE_IN:
		case OP_CUTSCENES_FADE_OUT:
		case OP_CUTSCENES_SET_CAMERA_POSITION:
		case OP_CUTSCENES_SET_CAMERA_FACING:
		case OP_CUTSCENES_SET_CAMERA_FACING_OBJECT:
		case OP_CUTSCENES_SET_CAMERA_ROTATION:
		case OP_CUTSCENES_SET_FOV:
		case OP_CUTSCENES_RESET_FOV:
		case OP_CUTSCENES_RESET_CAMERA:
		case OP_CUTSCENES_SHOW_SUBTITLE:
		case OP_CUTSCENES_SET_TIME_COMPRESSION:
		case OP_CUTSCENES_RESET_TIME_COMPRESSION:
		case OP_CUTSCENES_FORCE_PERSPECTIVE:
		case OP_ALLOW_PHOTO_MODE:
		case OP_JUMP_NODE_SET_JUMPNODE_NAME:
		case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME:
		case OP_JUMP_NODE_SET_JUMPNODE_COLOR:
		case OP_JUMP_NODE_SET_JUMPNODE_MODEL:
		case OP_JUMP_NODE_SHOW_JUMPNODE:
		case OP_JUMP_NODE_HIDE_JUMPNODE:
		case OP_SHIP_GUARDIAN_THRESHOLD:
		case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD:
		case OP_SET_GUARD_RANGE:
		case OP_SET_SKYBOX_MODEL:
		case OP_SHIP_CREATE:
		case OP_PROP_CREATE:
		case OP_WEAPON_CREATE:
		case OP_SET_OBJECT_SPEED_X:
		case OP_SET_OBJECT_SPEED_Y:
		case OP_SET_OBJECT_SPEED_Z:
		case OP_MISSION_SET_NEBULA:
		case OP_ADD_BACKGROUND_BITMAP:
		case OP_REMOVE_BACKGROUND_BITMAP:
		case OP_ADD_SUN_BITMAP:
		case OP_REMOVE_SUN_BITMAP:
		case OP_NEBULA_CHANGE_STORM:
		case OP_NEBULA_TOGGLE_POOF:
		case OP_NEBULA_SET_POOFS:
		case OP_NEBULA_FADE_POOF:
		case OP_NEBULA_FADE_POOFS:
		case OP_NEBULA_CHANGE_PATTERN:
		case OP_NEBULA_CHANGE_FOG_COLOR:
		case OP_NEBULA_SET_RANGE:
		case OP_VOLUMETRICS_TOGGLE:
		case OP_TURRET_CHANGE_WEAPON:
		case OP_TURRET_SET_TARGET_ORDER:
		case OP_SHIP_TURRET_TARGET_ORDER:
		case OP_SET_PRIMARY_AMMO:
		case OP_SET_SECONDARY_AMMO:
		case OP_SHIP_BOMB_TARGETABLE:
		case OP_SHIP_BOMB_UNTARGETABLE:
		case OP_SHIP_SUBSYS_TARGETABLE:
		case OP_SHIP_SUBSYS_UNTARGETABLE:
		case OP_SET_DEATH_MESSAGE:
		case OP_SET_PRIMARY_WEAPON:
		case OP_SET_SECONDARY_WEAPON:
		case OP_DISABLE_BUILTIN_MESSAGES:
		case OP_ENABLE_BUILTIN_MESSAGES:
		case OP_LOCK_PRIMARY_WEAPON:
		case OP_UNLOCK_PRIMARY_WEAPON:
		case OP_LOCK_SECONDARY_WEAPON:
		case OP_UNLOCK_SECONDARY_WEAPON:
		case OP_SET_CAMERA_SHUDDER:
		case OP_SET_FRIENDLY_DAMAGE_CAPS:
		case OP_ALLOW_TREASON:
		case OP_SHIP_COPY_DAMAGE:
		case OP_CHANGE_SUBSYSTEM_NAME:
		case OP_SET_PERSONA:
		case OP_CHANGE_PLAYER_SCORE:
		case OP_CHANGE_TEAM_SCORE:
		case OP_CUTSCENES_SET_CAMERA_FOV:
		case OP_CUTSCENES_SET_CAMERA:
		case OP_CUTSCENES_SET_CAMERA_HOST:
		case OP_CUTSCENES_SET_CAMERA_TARGET:
		case OP_LOCK_AFTERBURNER:
		case OP_UNLOCK_AFTERBURNER:
		case OP_SHIP_CHANGE_CALLSIGN:
		case OP_SET_RESPAWNS:
		case OP_SET_AFTERBURNER_ENERGY:
		case OP_SET_WEAPON_ENERGY:
		case OP_SET_SHIELD_ENERGY:
		case OP_SET_AMBIENT_LIGHT:
		case OP_CHANGE_IFF_COLOR:
		case OP_TURRET_SUBSYS_TARGET_DISABLE:
		case OP_TURRET_SUBSYS_TARGET_ENABLE:
		case OP_CLEAR_WEAPONS:
		case OP_SHIP_MANEUVER:
		case OP_SHIP_ROT_MANEUVER:
		case OP_SHIP_LAT_MANEUVER:
		case OP_GET_VARIABLE_BY_INDEX:
		case OP_SET_VARIABLE_BY_INDEX:
		case OP_SET_POST_EFFECT:
		case OP_TURRET_SET_OPTIMUM_RANGE:
		case OP_TURRET_SET_DIRECTION_PREFERENCE:
		case OP_TURRET_SET_TARGET_PRIORITIES:
		case OP_SET_ARMOR_TYPE:
		case OP_CUTSCENES_SHOW_SUBTITLE_TEXT:
		case OP_CUTSCENES_SHOW_SUBTITLE_IMAGE:
		case OP_HUD_DISPLAY_GAUGE:
		case OP_SET_SOUND_ENVIRONMENT:
		case OP_UPDATE_SOUND_ENVIRONMENT:
		case OP_SET_EXPLOSION_OPTION:
		case OP_ADJUST_AUDIO_VOLUME:
		case OP_FORCE_GLIDE:
		case OP_TURRET_SET_RATE_OF_FIRE:
		case OP_HUD_SET_MESSAGE:
		case OP_SHIP_SUBSYS_NO_REPLACE:
		case OP_SET_IMMOBILE:
		case OP_SET_MOBILE:
		case OP_SHIP_SUBSYS_NO_LIVE_DEBRIS:
		case OP_SHIP_SUBSYS_VANISHED:
		case OP_SHIP_SUBSYS_IGNORE_IF_DEAD:
		case OP_HUD_SET_DIRECTIVE:
		case OP_HUD_GAUGE_SET_ACTIVE:
		case OP_HUD_ACTIVATE_GAUGE_TYPE:
		case OP_SET_OBJECT_ORIENTATION:
		case OP_STRING_CONCATENATE:
		case OP_INT_TO_STRING:
		case OP_WEAPON_SET_DAMAGE_TYPE:
		case OP_SHIP_SET_DAMAGE_TYPE:
		case OP_SHIP_SHOCKWAVE_SET_DAMAGE_TYPE:
		case OP_FIELD_SET_DAMAGE_TYPE:
		case OP_TURRET_PROTECT_SHIP:
		case OP_TURRET_UNPROTECT_SHIP:
		case OP_DISABLE_ETS:
		case OP_ENABLE_ETS:
		case OP_NAV_ADD_WAYPOINT:
		case OP_NAV_ADD_SHIP:
		case OP_NAV_DEL:
		case OP_NAV_HIDE:
		case OP_NAV_RESTRICT:
		case OP_NAV_UNHIDE:
		case OP_NAV_UNRESTRICT:
		case OP_NAV_SET_VISITED:
		case OP_NAV_SET_CARRY:
		case OP_NAV_UNSET_CARRY:
		case OP_NAV_UNSET_VISITED:
		case OP_NAV_SET_NEEDSLINK:
		case OP_NAV_UNSET_NEEDSLINK:
		case OP_NAV_USECINEMATICS:
		case OP_NAV_USEAP:
		case OP_STRING_GET_SUBSTRING:
		case OP_STRING_SET_SUBSTRING:
		case OP_SET_NUM_COUNTERMEASURES:
		case OP_ADD_TO_COLGROUP:
		case OP_REMOVE_FROM_COLGROUP:
		case OP_GET_COLGROUP_ID:
		case OP_SHIP_EFFECT:
		case OP_CLEAR_SUBTITLES:
		case OP_BEAM_FIRE_COORDS:
		case OP_SET_DOCKED:
		case OP_SET_THRUSTERS:
		case OP_TRIGGER_SUBMODEL_ANIMATION:
		case OP_HUD_CLEAR_MESSAGES:
		case OP_SET_PLAYER_ORDERS:
		case OP_SUPERNOVA_STOP:
		case OP_SET_PLAYER_THROTTLE_SPEED:
		case OP_SET_DEBRIEFING_TOGGLED:
		case OP_SET_SUBSPACE_DRIVE:
		case OP_SET_ARRIVAL_INFO:
		case OP_SET_DEPARTURE_INFO:
		case OP_SET_SKYBOX_ORIENT:
		case OP_SET_SKYBOX_ALPHA:
		case OP_DESTROY_INSTANTLY:
		case OP_DESTROY_SUBSYS_INSTANTLY:
		case OP_DEBUG:
		case OP_SET_MISSION_MOOD:
		case OP_NAV_SELECT:
		case OP_NAV_UNSELECT:
		case OP_ALTER_SHIP_FLAG:
		case OP_CHANGE_TEAM_COLOR:
		case OP_TECH_ADD_INTEL_XSTR:
		case OP_COPY_VARIABLE_FROM_INDEX:
		case OP_COPY_VARIABLE_BETWEEN_INDEXES:
		case OP_SET_ETS_VALUES:
		case OP_CALL_SSM_STRIKE:
		case OP_OVERRIDE_MOTION_DEBRIS:
		case OP_HUD_SET_CUSTOM_GAUGE_ACTIVE:
		case OP_HUD_SET_BUILTIN_GAUGE_ACTIVE:
		case OP_SCRIPT_EVAL_STRING:
		case OP_SCRIPT_EVAL_MULTI:
		case OP_PAUSE_SOUND_FROM_FILE:
		case OP_SCRIPT_EVAL_BLOCK:
		case OP_BEAM_FLOATING_FIRE:
		case OP_TURRET_SET_PRIMARY_AMMO:
		case OP_TURRET_SET_SECONDARY_AMMO:
		case OP_JETTISON_CARGO_NEW:
		case OP_STRING_CONCATENATE_BLOCK:
		case OP_MODIFY_VARIABLE_XSTR:
		case OP_RESET_POST_EFFECTS:
		case OP_ADD_REMOVE_HOTKEY:
		case OP_TECH_REMOVE_INTEL_XSTR:
		case OP_TECH_REMOVE_INTEL:
		case OP_CHANGE_BACKGROUND:
		case OP_CLEAR_DEBRIS:
		case OP_SET_DEBRIEFING_PERSONA:
		case OP_SET_TRAITOR_OVERRIDE:
		case OP_ADD_TO_COLGROUP_NEW:
		case OP_REMOVE_FROM_COLGROUP_NEW:
		case OP_GET_POWER_OUTPUT:
		case OP_TURRET_SET_FORCED_TARGET:
		case OP_TURRET_SET_FORCED_SUBSYS_TARGET:
		case OP_TURRET_CLEAR_FORCED_TARGET:
		case OP_SEND_MESSAGE_CHAIN:
		case OP_TURRET_SET_INACCURACY:
		case OP_REPLACE_TEXTURE:
		case OP_REPLACE_TEXTURE_SKYBOX:
		case OP_SET_ALPHA_MULT:
		case OP_DESTROY_INSTANTLY_WITH_DEBRIS:
		case OP_TRIGGER_ANIMATION_NEW:
		case OP_UPDATE_MOVEABLE:
		case OP_ADVANCE_MOVEABLE:
		case OP_NAV_SET_COLOR:
		case OP_NAV_SET_VISITED_COLOR:
		case OP_CONTAINER_ADD_TO_LIST:
		case OP_CONTAINER_REMOVE_FROM_LIST:
		case OP_CONTAINER_ADD_TO_MAP:
		case OP_CONTAINER_REMOVE_FROM_MAP:
		case OP_CONTAINER_GET_MAP_KEYS:
		case OP_CLEAR_CONTAINER:
		case OP_ADD_BACKGROUND_BITMAP_NEW:
		case OP_ADD_SUN_BITMAP_NEW:
		case OP_CANCEL_FUTURE_WAVES:
		case OP_COPY_CONTAINER:
		case OP_APPLY_CONTAINER_FILTER:
		case OP_STOP_LOOPING_ANIMATION:
		case OP_LOCK_TRANSLATING_SUBSYSTEM:
		case OP_FREE_TRANSLATING_SUBSYSTEM:
		case OP_REVERSE_TRANSLATING_SUBSYSTEM:
		case OP_TRANSLATING_SUBSYS_SET_SPEED:
		case OP_ALTER_WING_FLAG:
		case OP_TOGGLE_ASTEROID_FIELD:
		case OP_HUD_FORCE_SENSOR_STATIC:
		case OP_HUD_FORCE_EMP_EFFECT:
		case OP_SET_SQUADRON_WINGS:
		case OP_SET_GRAVITY_ACCEL:
		case OP_FORCE_REARM:
		case OP_ABORT_REARM:
		case OP_SET_ORDER_ALLOWED_TARGET:
		case OP_ENABLE_GENERAL_ORDERS:
		case OP_VALIDATE_GENERAL_ORDERS:
		case OP_SET_ASTEROID_FIELD:
		case OP_SET_DEBRIS_FIELD:
		case OP_CONFIG_ASTEROID_FIELD:
		case OP_CONFIG_DEBRIS_FIELD:
		case OP_CONFIG_FIELD_TARGETS:
		case OP_SET_WING_FORMATION:
		case OP_SET_MOTION_DEBRIS:
			return OP_CATEGORY_CHANGE;

		case OP_AI_CHASE:
		case OP_AI_DOCK:
		case OP_AI_UNDOCK:
		case OP_AI_WARP_OUT:
		case OP_AI_WAYPOINTS:
		case OP_AI_WAYPOINTS_ONCE:
		case OP_AI_DESTROY_SUBSYS:
		case OP_AI_DISABLE_SHIP:
		case OP_AI_DISABLE_SHIP_TACTICAL:
		case OP_AI_DISARM_SHIP:
		case OP_AI_DISARM_SHIP_TACTICAL:
		case OP_AI_GUARD:
		case OP_AI_CHASE_ANY:
		case OP_AI_EVADE_SHIP:
		case OP_AI_STAY_NEAR_SHIP:
		case OP_AI_KEEP_SAFE_DISTANCE:
		case OP_AI_IGNORE:
		case OP_AI_STAY_STILL:
		case OP_AI_PLAY_DEAD:
		case OP_AI_IGNORE_NEW:
		case OP_AI_FORM_ON_WING:
		case OP_AI_CHASE_SHIP_CLASS:
		case OP_AI_CHASE_SHIP_TYPE:
		case OP_AI_PLAY_DEAD_PERSISTENT:
		case OP_AI_FLY_TO_SHIP:
		case OP_AI_REARM_REPAIR:
			return OP_CATEGORY_AI;

		case OP_GOALS_ID:
		case OP_NEXT_MISSION:
		case OP_IS_DESTROYED:
		case OP_IS_SUBSYSTEM_DESTROYED:
		case OP_IS_DISABLED:
		case OP_IS_DISARMED:
		case OP_HAS_DOCKED:
		case OP_HAS_UNDOCKED:
		case OP_HAS_ARRIVED:
		case OP_HAS_DEPARTED:
		case OP_WAYPOINTS_DONE:
		case OP_ADD_SHIP_GOAL:
		case OP_CLEAR_SHIP_GOALS:
		case OP_ADD_WING_GOAL:
		case OP_CLEAR_WING_GOALS:
		case OP_AI_CHASE_WING:
		case OP_AI_GUARD_WING:
		case OP_EVENT_TRUE:
		case OP_EVENT_FALSE:
		case OP_PREVIOUS_GOAL_INCOMPLETE:
		case OP_PREVIOUS_EVENT_INCOMPLETE:
		case OP_AI_WARP:
		case OP_IS_CARGO_KNOWN:
		case OP_COND:
		case OP_END_OF_CAMPAIGN:
			return OP_CATEGORY_UNLISTED;

		case OP_KEY_PRESSED:
		case OP_KEY_RESET:
		case OP_TARGETED:
		case OP_SPEED:
		case OP_FACING:
		case OP_ORDER:
		case OP_WAYPOINT_MISSED:
		case OP_PATH_FLOWN:
		case OP_WAYPOINT_TWICE:
		case OP_TRAINING_MSG:
		case OP_FLASH_HUD_GAUGE:
		case OP_SPECIAL_CHECK:
		case OP_SECONDARIES_DEPLETED:
		case OP_FACING2:
		case OP_PRIMARIES_DEPLETED:
		case OP_MISSILE_LOCKED:
		case OP_SET_TRAINING_CONTEXT_FLY_PATH:
		case OP_SET_TRAINING_CONTEXT_SPEED:
		case OP_KEY_RESET_MULTIPLE:
		case OP_RESET_ORDERS:
		case OP_QUERY_ORDERS:
		case OP_NODE_TARGETED:
		case OP_IGNORE_KEY:
			return OP_CATEGORY_TRAINING;

		default:
		{
			// Check if we have a dynamic SEXP with this operator and if there is, execute that
			auto dynamicSEXP = sexp::get_dynamic_sexp(op_id);
			if (dynamicSEXP != nullptr)
				return dynamicSEXP->getCategory();
			else
				return OP_CATEGORY_NONE;		// sexp doesn't have a category
		}
	}
}

// Goober5000 - for FRED2 menu subcategories
int get_subcategory(int op_id)
{
	switch(op_id)
	{
		case OP_SEND_MESSAGE_LIST:
		case OP_SEND_MESSAGE_CHAIN:
		case OP_SEND_MESSAGE:
		case OP_SEND_BUILTIN_MESSAGE:
		case OP_SEND_RANDOM_MESSAGE:
		case OP_SCRAMBLE_MESSAGES:
		case OP_UNSCRAMBLE_MESSAGES:
		case OP_ENABLE_BUILTIN_MESSAGES:
		case OP_DISABLE_BUILTIN_MESSAGES:
		case OP_SET_DEATH_MESSAGE:
		case OP_SET_PERSONA:
		case OP_SET_MISSION_MOOD:
			return CHANGE_SUBCATEGORY_MESSAGING;

		case OP_ADD_GOAL:
		case OP_REMOVE_GOAL:
		case OP_CLEAR_GOALS:
		case OP_GOOD_REARM_TIME:
		case OP_BAD_REARM_TIME:
		case OP_GOOD_PRIMARY_TIME:
		case OP_GOOD_SECONDARY_TIME:
		case OP_CHANGE_AI_CLASS:
		case OP_PLAYER_USE_AI:
		case OP_PLAYER_NOT_USE_AI:
		case OP_SET_PLAYER_TARGET:
		case OP_CLEAR_PLAYER_TARGET:
		case OP_SET_PLAYER_ORDERS:
		case OP_CAP_WAYPOINT_SPEED:
		case OP_SET_WING_FORMATION:
		case OP_SET_ORDER_ALLOWED_TARGET:
		case OP_ENABLE_GENERAL_ORDERS:
		case OP_VALIDATE_GENERAL_ORDERS:
			return CHANGE_SUBCATEGORY_AI_CONTROL;

		case OP_ALTER_SHIP_FLAG:
		case OP_ALTER_WING_FLAG:
		case OP_SET_GUARD_RANGE:
		case OP_PROTECT_SHIP:
		case OP_UNPROTECT_SHIP:
		case OP_BEAM_PROTECT_SHIP:
		case OP_BEAM_UNPROTECT_SHIP:
		case OP_TURRET_PROTECT_SHIP:
		case OP_TURRET_UNPROTECT_SHIP:
		case OP_SHIP_INVISIBLE:
		case OP_SHIP_VISIBLE:
		case OP_SHIP_STEALTHY:
		case OP_SHIP_UNSTEALTHY:
		case OP_FRIENDLY_STEALTH_INVISIBLE:
		case OP_FRIENDLY_STEALTH_VISIBLE:
		case OP_PRIMITIVE_SENSORS_SET_RANGE:
		case OP_SHIP_BOMB_TARGETABLE:
		case OP_SHIP_BOMB_UNTARGETABLE:
		case OP_KAMIKAZE:
		case OP_CHANGE_IFF:
		case OP_CHANGE_IFF_COLOR:
		case OP_ADD_REMOVE_ESCORT:
		case OP_SHIP_CHANGE_ALT_NAME:
		case OP_SHIP_CHANGE_CALLSIGN:
		case OP_SHIP_CHANGE_DISPLAY_NAME:
		case OP_SHIP_TAG:
		case OP_SHIP_UNTAG:
		case OP_SET_ARRIVAL_INFO:
		case OP_SET_DEPARTURE_INFO:
		case OP_CANCEL_FUTURE_WAVES:
			return CHANGE_SUBCATEGORY_SHIP_STATUS;

		case OP_SET_WEAPON_ENERGY:
		case OP_SET_SHIELD_ENERGY:
		case OP_SET_PLAYER_THROTTLE_SPEED:
		case OP_SET_AFTERBURNER_ENERGY:
		case OP_SET_SUBSPACE_DRIVE:
		case OP_SET_SPECIAL_WARPOUT_NAME:
		case OP_SET_PRIMARY_WEAPON:		// Karajorma
		case OP_SET_SECONDARY_WEAPON:	// Karajorma
		case OP_SET_PRIMARY_AMMO:		// Karajorma
		case OP_SET_SECONDARY_AMMO:		// Karajorma
		case OP_SET_NUM_COUNTERMEASURES: // Karajorma
		case OP_LOCK_PRIMARY_WEAPON:
		case OP_UNLOCK_PRIMARY_WEAPON:
		case OP_LOCK_SECONDARY_WEAPON:
		case OP_UNLOCK_SECONDARY_WEAPON:
		case OP_LOCK_AFTERBURNER:	// KeldorKatarn
		case OP_UNLOCK_AFTERBURNER:	// KeldorKatarn
		case OP_SHIELDS_ON:
		case OP_SHIELDS_OFF:
		case OP_FORCE_GLIDE:
		case OP_DISABLE_ETS:
		case OP_ENABLE_ETS:
		case OP_WARP_BROKEN:
		case OP_WARP_NOT_BROKEN:
		case OP_WARP_NEVER:
		case OP_WARP_ALLOWED:
		case OP_SET_ETS_VALUES:
		case OP_GET_POWER_OUTPUT:
			return CHANGE_SUBCATEGORY_SHIELDS_ENGINES_AND_WEAPONS;

		case OP_SHIP_INVULNERABLE:
		case OP_SHIP_VULNERABLE:
		case OP_SHIP_GUARDIAN:
		case OP_SHIP_NO_GUARDIAN:
		case OP_SHIP_GUARDIAN_THRESHOLD:
		case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD:
		case OP_SELF_DESTRUCT:
		case OP_DESTROY_INSTANTLY:
		case OP_DESTROY_INSTANTLY_WITH_DEBRIS:
		case OP_DESTROY_SUBSYS_INSTANTLY:
		case OP_SABOTAGE_SUBSYSTEM:
		case OP_REPAIR_SUBSYSTEM:
		case OP_SHIP_COPY_DAMAGE:
		case OP_SET_SUBSYSTEM_STRNGTH:
		case OP_SUBSYS_SET_RANDOM:
		case OP_LOCK_ROTATING_SUBSYSTEM:
		case OP_FREE_ROTATING_SUBSYSTEM:
		case OP_REVERSE_ROTATING_SUBSYSTEM:
		case OP_ROTATING_SUBSYS_SET_TURN_TIME:
		case OP_LOCK_TRANSLATING_SUBSYSTEM:
		case OP_FREE_TRANSLATING_SUBSYSTEM:
		case OP_REVERSE_TRANSLATING_SUBSYSTEM:
		case OP_TRANSLATING_SUBSYS_SET_SPEED:
		case OP_TRIGGER_SUBMODEL_ANIMATION:
		case OP_CHANGE_SUBSYSTEM_NAME:
		case OP_SHIP_SUBSYS_TARGETABLE:
		case OP_SHIP_SUBSYS_UNTARGETABLE:
		case OP_SHIP_SUBSYS_NO_REPLACE:
		case OP_SHIP_SUBSYS_NO_LIVE_DEBRIS:
		case OP_SHIP_SUBSYS_VANISHED:
		case OP_SHIP_SUBSYS_IGNORE_IF_DEAD:
		case OP_AWACS_SET_RADIUS:
			return CHANGE_SUBCATEGORY_SUBSYSTEMS;

		case OP_TRANSFER_CARGO:
		case OP_EXCHANGE_CARGO:
		case OP_SET_CARGO:
		case OP_JETTISON_CARGO_DELAY:
		case OP_JETTISON_CARGO_NEW:
		case OP_SET_DOCKED:
		case OP_CARGO_NO_DEPLETE:
		case OP_SET_SCANNED:
		case OP_SET_UNSCANNED:
			return CHANGE_SUBCATEGORY_CARGO;

		case OP_SET_ARMOR_TYPE:
		case OP_WEAPON_SET_DAMAGE_TYPE:
		case OP_SHIP_SET_DAMAGE_TYPE:
		case OP_SHIP_SHOCKWAVE_SET_DAMAGE_TYPE:
		case OP_FIELD_SET_DAMAGE_TYPE:
		case OP_SET_FRIENDLY_DAMAGE_CAPS:
			return CHANGE_SUBCATEGORY_ARMOR_AND_DAMAGE_TYPES;

		case OP_BEAM_FIRE:
		case OP_BEAM_FIRE_COORDS:
		case OP_BEAM_FLOATING_FIRE:
		case OP_BEAM_FREE:
		case OP_BEAM_FREE_ALL:
		case OP_BEAM_LOCK:
		case OP_BEAM_LOCK_ALL:
		case OP_TURRET_FREE:
		case OP_TURRET_FREE_ALL:
		case OP_TURRET_LOCK:
		case OP_TURRET_LOCK_ALL:
		case OP_TURRET_TAGGED_ONLY_ALL:
		case OP_TURRET_TAGGED_CLEAR_ALL:
		case OP_TURRET_TAGGED_SPECIFIC:
		case OP_TURRET_TAGGED_CLEAR_SPECIFIC:
		case OP_TURRET_CHANGE_WEAPON:
		case OP_TURRET_SET_DIRECTION_PREFERENCE:
		case OP_TURRET_SET_RATE_OF_FIRE:
		case OP_TURRET_SET_OPTIMUM_RANGE:
		case OP_TURRET_SET_FORCED_TARGET:
		case OP_TURRET_SET_FORCED_SUBSYS_TARGET:
		case OP_TURRET_CLEAR_FORCED_TARGET:
		case OP_TURRET_SET_TARGET_PRIORITIES:
		case OP_TURRET_SET_TARGET_ORDER:
		case OP_TURRET_SET_INACCURACY:
		case OP_SHIP_TURRET_TARGET_ORDER:
		case OP_TURRET_SUBSYS_TARGET_DISABLE:
		case OP_TURRET_SUBSYS_TARGET_ENABLE:
		case OP_TURRET_SET_PRIMARY_AMMO:
		case OP_TURRET_SET_SECONDARY_AMMO:
			return CHANGE_SUBCATEGORY_BEAMS_AND_TURRETS;

		case OP_CHANGE_SHIP_CLASS:
		case OP_DEACTIVATE_GLOW_MAPS:
		case OP_ACTIVATE_GLOW_MAPS:
		case OP_DEACTIVATE_GLOW_POINTS:
		case OP_ACTIVATE_GLOW_POINTS:
		case OP_DEACTIVATE_GLOW_POINT_BANK:
		case OP_ACTIVATE_GLOW_POINT_BANK:
		case OP_SET_THRUSTERS:
		case OP_DONT_COLLIDE_INVISIBLE:
		case OP_COLLIDE_INVISIBLE:
		case OP_ADD_TO_COLGROUP:
		case OP_REMOVE_FROM_COLGROUP:
		case OP_ADD_TO_COLGROUP_NEW:
		case OP_REMOVE_FROM_COLGROUP_NEW:
		case OP_GET_COLGROUP_ID:
		case OP_CHANGE_TEAM_COLOR:
		case OP_REPLACE_TEXTURE:
		case OP_REPLACE_TEXTURE_SKYBOX:
		case OP_SET_ALPHA_MULT:
		case OP_TRIGGER_ANIMATION_NEW:
		case OP_UPDATE_MOVEABLE:
		case OP_ADVANCE_MOVEABLE:
		case OP_STOP_LOOPING_ANIMATION:
			return CHANGE_SUBCATEGORY_MODELS_AND_TEXTURES;

		case OP_SET_OBJECT_POSITION:
		case OP_SET_OBJECT_ORIENTATION:
		case OP_SET_OBJECT_FACING:
		case OP_SET_OBJECT_FACING_OBJECT:
		case OP_SET_OBJECT_SPEED_X:
		case OP_SET_OBJECT_SPEED_Y:
		case OP_SET_OBJECT_SPEED_Z:
		case OP_SHIP_MANEUVER:
		case OP_SHIP_ROT_MANEUVER:
		case OP_SHIP_LAT_MANEUVER:
		case OP_SET_MOBILE:
		case OP_SET_IMMOBILE:
			return CHANGE_SUBCATEGORY_COORDINATE_MANIPULATION;

		case OP_INVALIDATE_GOAL:
		case OP_VALIDATE_GOAL:
		case OP_RED_ALERT:
		case OP_END_MISSION:
		case OP_FORCE_JUMP:
		case OP_END_CAMPAIGN:
		case OP_SET_DEBRIEFING_TOGGLED:
		case OP_SET_DEBRIEFING_PERSONA:
		case OP_SET_TRAITOR_OVERRIDE:
		case OP_ALLOW_TREASON:
		case OP_GRANT_PROMOTION:
		case OP_GRANT_MEDAL:
		case OP_ALLOW_SHIP:
		case OP_ALLOW_WEAPON:
		case OP_TECH_ADD_SHIP:
		case OP_TECH_ADD_WEAPON:
		case OP_TECH_ADD_INTEL:
		case OP_TECH_REMOVE_INTEL:
		case OP_TECH_ADD_INTEL_XSTR:
		case OP_TECH_REMOVE_INTEL_XSTR:
		case OP_TECH_RESET_TO_DEFAULT:
		case OP_CHANGE_PLAYER_SCORE:
		case OP_CHANGE_TEAM_SCORE:
		case OP_SET_RESPAWNS:
		case OP_ADD_REMOVE_HOTKEY:
			return CHANGE_SUBCATEGORY_MISSION_AND_CAMPAIGN;

		case OP_CHANGE_SOUNDTRACK:
		case OP_PLAY_SOUND_FROM_TABLE:
		case OP_PLAY_SOUND_FROM_FILE:
		case OP_CLOSE_SOUND_FROM_FILE:
		case OP_PAUSE_SOUND_FROM_FILE:
		case OP_SET_SOUND_ENVIRONMENT:
		case OP_UPDATE_SOUND_ENVIRONMENT:
		case OP_ADJUST_AUDIO_VOLUME:
			return CHANGE_SUBCATEGORY_MUSIC_AND_SOUND;

		case OP_HUD_DISABLE:
		case OP_HUD_DISABLE_EXCEPT_MESSAGES:
		case OP_HUD_SET_CUSTOM_GAUGE_ACTIVE:
		case OP_HUD_SET_BUILTIN_GAUGE_ACTIVE:
		case OP_HUD_SET_TEXT:
		case OP_HUD_SET_XSTR:
		case OP_HUD_SET_TEXT_NUM:
		case OP_HUD_SET_MESSAGE:
		case OP_HUD_SET_DIRECTIVE:
		case OP_HUD_SET_FRAME:
		case OP_HUD_SET_COLOR:
		case OP_HUD_RESET_COLOR:
		case OP_HUD_SET_COORDS:
		case OP_HUD_DISPLAY_GAUGE:
		case OP_HUD_GAUGE_SET_ACTIVE:
		case OP_HUD_ACTIVATE_GAUGE_TYPE:
		case OP_HUD_CLEAR_MESSAGES:
		case OP_HUD_SET_MAX_TARGETING_RANGE:
		case OP_HUD_FORCE_SENSOR_STATIC:
		case OP_HUD_FORCE_EMP_EFFECT:
		case OP_SET_SQUADRON_WINGS:
			return CHANGE_SUBCATEGORY_HUD;

		case OP_NAV_ADD_WAYPOINT:
		case OP_NAV_ADD_SHIP:
		case OP_NAV_DEL:
		case OP_NAV_HIDE:
		case OP_NAV_RESTRICT:
		case OP_NAV_UNHIDE:
		case OP_NAV_UNRESTRICT:
		case OP_NAV_SET_VISITED:
		case OP_NAV_SET_CARRY:
		case OP_NAV_UNSET_CARRY:
		case OP_NAV_UNSET_VISITED:
		case OP_NAV_SET_NEEDSLINK:
		case OP_NAV_UNSET_NEEDSLINK:
		case OP_NAV_USECINEMATICS:
		case OP_NAV_USEAP:
		case OP_NAV_SELECT:
		case OP_NAV_UNSELECT:
		case OP_NAV_SET_COLOR:
		case OP_NAV_SET_VISITED_COLOR:
			return CHANGE_SUBCATEGORY_NAV;

		case OP_CUTSCENES_SET_CUTSCENE_BARS:
		case OP_CUTSCENES_UNSET_CUTSCENE_BARS:
		case OP_CUTSCENES_FADE_IN:
		case OP_CUTSCENES_FADE_OUT:
		case OP_CUTSCENES_SET_CAMERA:
		case OP_CUTSCENES_SET_CAMERA_POSITION:
		case OP_CUTSCENES_SET_CAMERA_FACING:
		case OP_CUTSCENES_SET_CAMERA_FACING_OBJECT:
		case OP_CUTSCENES_SET_CAMERA_ROTATION:
		case OP_CUTSCENES_SET_CAMERA_HOST:
		case OP_CUTSCENES_SET_CAMERA_TARGET:
		case OP_CUTSCENES_SET_CAMERA_FOV:
		case OP_CUTSCENES_SET_FOV:
		case OP_CUTSCENES_RESET_FOV:
		case OP_CUTSCENES_RESET_CAMERA:
		case OP_CUTSCENES_SHOW_SUBTITLE:
		case OP_CUTSCENES_SHOW_SUBTITLE_TEXT:
		case OP_CUTSCENES_SHOW_SUBTITLE_IMAGE:
		case OP_CLEAR_SUBTITLES:
		case OP_CUTSCENES_FORCE_PERSPECTIVE:
		case OP_ALLOW_PHOTO_MODE:
		case OP_SET_CAMERA_SHUDDER:
		case OP_SUPERNOVA_START:
		case OP_SUPERNOVA_STOP:
		case OP_OVERRIDE_MOTION_DEBRIS:
			return CHANGE_SUBCATEGORY_CUTSCENES;

		case OP_SET_SKYBOX_MODEL:
		case OP_SET_SKYBOX_ORIENT:
		case OP_SET_SKYBOX_ALPHA:
		case OP_MISSION_SET_NEBULA:
		case OP_MISSION_SET_SUBSPACE:
		case OP_CHANGE_BACKGROUND:
		case OP_ADD_BACKGROUND_BITMAP:
		case OP_ADD_BACKGROUND_BITMAP_NEW:
		case OP_REMOVE_BACKGROUND_BITMAP:
		case OP_ADD_SUN_BITMAP:
		case OP_ADD_SUN_BITMAP_NEW:
		case OP_REMOVE_SUN_BITMAP:
		case OP_NEBULA_CHANGE_STORM:
		case OP_NEBULA_TOGGLE_POOF:
		case OP_NEBULA_SET_POOFS:
		case OP_NEBULA_FADE_POOF:
		case OP_NEBULA_FADE_POOFS:
		case OP_NEBULA_CHANGE_PATTERN:
		case OP_NEBULA_CHANGE_FOG_COLOR:
		case OP_NEBULA_SET_RANGE:
		case OP_VOLUMETRICS_TOGGLE:
		case OP_SET_AMBIENT_LIGHT:
		case OP_TOGGLE_ASTEROID_FIELD:
		case OP_SET_ASTEROID_FIELD:
		case OP_SET_DEBRIS_FIELD:
		case OP_CONFIG_ASTEROID_FIELD:
		case OP_CONFIG_DEBRIS_FIELD:
		case OP_CONFIG_FIELD_TARGETS:
		case OP_SET_MOTION_DEBRIS:
			return CHANGE_SUBCATEGORY_BACKGROUND_AND_NEBULA;

		case OP_JUMP_NODE_SET_JUMPNODE_NAME:
		case OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME:
		case OP_JUMP_NODE_SET_JUMPNODE_COLOR:
		case OP_JUMP_NODE_SET_JUMPNODE_MODEL:
		case OP_JUMP_NODE_SHOW_JUMPNODE:
		case OP_JUMP_NODE_HIDE_JUMPNODE:
			return CHANGE_SUBCATEGORY_JUMP_NODES;

		case OP_SET_POST_EFFECT:
		case OP_RESET_POST_EFFECTS:
		case OP_SHIP_EFFECT:
		case OP_SHIP_CREATE:
		case OP_PROP_CREATE:
		case OP_WEAPON_CREATE:
		case OP_SHIP_VANISH:
		case OP_PROP_VANISH:
		case OP_SHIP_VAPORIZE:
		case OP_SHIP_NO_VAPORIZE:
		case OP_SET_EXPLOSION_OPTION:
		case OP_CREATE_BOLT:
		case OP_EXPLOSION_EFFECT:
		case OP_WARP_EFFECT:
		case OP_CLEAR_WEAPONS:
		case OP_CLEAR_DEBRIS:
		case OP_CUTSCENES_SET_TIME_COMPRESSION:
		case OP_CUTSCENES_RESET_TIME_COMPRESSION:
		case OP_CALL_SSM_STRIKE:
		case OP_SET_GRAVITY_ACCEL:
		case OP_FORCE_REARM:
		case OP_ABORT_REARM:
			return CHANGE_SUBCATEGORY_SPECIAL_EFFECTS;

		case OP_MODIFY_VARIABLE:
		case OP_GET_VARIABLE_BY_INDEX:
		case OP_SET_VARIABLE_BY_INDEX:
		case OP_COPY_VARIABLE_FROM_INDEX:
		case OP_COPY_VARIABLE_BETWEEN_INDEXES:
		case OP_INT_TO_STRING:
		case OP_STRING_CONCATENATE:
		case OP_STRING_CONCATENATE_BLOCK:
		case OP_STRING_GET_SUBSTRING:
		case OP_STRING_SET_SUBSTRING:
		case OP_MODIFY_VARIABLE_XSTR:
			return CHANGE_SUBCATEGORY_VARIABLES;

		case OP_CONTAINER_ADD_TO_LIST:
		case OP_CONTAINER_REMOVE_FROM_LIST:
		case OP_CONTAINER_ADD_TO_MAP:
		case OP_CONTAINER_REMOVE_FROM_MAP:
		case OP_CONTAINER_GET_MAP_KEYS:
		case OP_CLEAR_CONTAINER:
		case OP_COPY_CONTAINER:
		case OP_APPLY_CONTAINER_FILTER:
			return CHANGE_SUBCATEGORY_CONTAINERS;

		case OP_DAMAGED_ESCORT_LIST:
		case OP_DAMAGED_ESCORT_LIST_ALL:
		case OP_SET_SUPPORT_SHIP:
		case OP_SCRIPT_EVAL_STRING:
		case OP_SCRIPT_EVAL_BLOCK:
		case OP_SCRIPT_EVAL:
		case OP_SCRIPT_EVAL_MULTI:
			return CHANGE_SUBCATEGORY_OTHER;

		case OP_NUM_SHIPS_IN_BATTLE:
		case OP_NUM_SHIPS_IN_WING:
		case OP_DIRECTIVE_VALUE:
		case OP_GET_HOTKEY:
			return STATUS_SUBCATEGORY_MISSION;

		case OP_CUTSCENES_GET_FOV:
		case OP_GET_SUPERNOVA_STAGE:
			return STATUS_SUBCATEGORY_CUTSCENES;

		case OP_WAS_PROMOTION_GRANTED:
		case OP_WAS_MEDAL_GRANTED:
		case OP_SKILL_LEVEL_AT_LEAST:
		case OP_NUM_KILLS:
		case OP_NUM_ASSISTS:
		case OP_NUM_TYPE_KILLS:
		case OP_NUM_CLASS_KILLS:
		case OP_SHIP_SCORE:
		case OP_LAST_ORDER_TIME:
		case OP_PLAYER_IS_CHEATING_BASTARD:
		case OP_IS_LANGUAGE:
		case OP_USED_CHEAT:
			return STATUS_SUBCATEGORY_PLAYER;

		case OP_NUM_PLAYERS:
		case OP_TEAM_SCORE:
		case OP_SHIP_DEATHS:
		case OP_RESPAWNS_LEFT:
		case OP_IS_PLAYER:
			return STATUS_SUBCATEGORY_MULTIPLAYER;

		case OP_HAS_BEEN_TAGGED_DELAY:
		case OP_IS_TAGGED:
		case OP_IS_SHIP_VISIBLE:
		case OP_IS_SHIP_STEALTHY:
		case OP_IS_FRIENDLY_STEALTH_VISIBLE:
		case OP_IS_IFF:
		case OP_IS_SPECIES:
		case OP_IS_AI_CLASS:
		case OP_IS_SHIP_CLASS:
		case OP_IS_SHIP_TYPE:
		case OP_CURRENT_SPEED:
		case OP_GET_THROTTLE_SPEED:
		case OP_IS_FACING:
		case OP_IS_IN_MISSION:
		case OP_HAS_ARMOR_TYPE:
		case OP_IS_DOCKED:
		case OP_NAV_ISLINKED:
		case OP_ARE_SHIP_FLAGS_SET:
		case OP_ARE_WING_FLAGS_SET:
		case OP_IS_SHIP_EMP_ACTIVE:
			return STATUS_SUBCATEGORY_SHIP_STATUS;

		case OP_SHIELD_RECHARGE_PCT:
		case OP_ENGINE_RECHARGE_PCT:
		case OP_WEAPON_RECHARGE_PCT:
		case OP_SHIELD_QUAD_LOW:
		case OP_PRIMARY_AMMO_PCT:
		case OP_SECONDARY_AMMO_PCT:
		case OP_IS_PRIMARY_SELECTED:
		case OP_IS_SECONDARY_SELECTED:
		case OP_GET_PRIMARY_AMMO:
		case OP_GET_SECONDARY_AMMO:
		case OP_TURRET_GET_PRIMARY_AMMO:
		case OP_TURRET_GET_SECONDARY_AMMO:
		case OP_TURRET_HAS_PRIMARY_WEAPON:
		case OP_TURRET_HAS_SECONDARY_WEAPON:
		case OP_GET_NUM_COUNTERMEASURES:
		case OP_AFTERBURNER_LEFT:
		case OP_WEAPON_ENERGY_LEFT:
		case OP_PRIMARY_FIRED_SINCE:
		case OP_SECONDARY_FIRED_SINCE:
		case OP_HAS_PRIMARY_WEAPON:
		case OP_HAS_SECONDARY_WEAPON:
		case OP_GET_ETS_VALUE:
		case OP_IS_IN_TURRET_FOV:
		case OP_TURRET_FIRED_SINCE:
			return STATUS_SUBCATEGORY_SHIELDS_ENGINES_AND_WEAPONS;
			
		case OP_CARGO_KNOWN_DELAY:
		case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY:
		case OP_IS_CARGO:
			return STATUS_SUBCATEGORY_CARGO;
			
		case OP_SHIELDS_LEFT:
		case OP_HITS_LEFT:
		case OP_HITS_LEFT_SUBSYSTEM:
		case OP_HITS_LEFT_SUBSYSTEM_GENERIC:
		case OP_HITS_LEFT_SUBSYSTEM_SPECIFIC:
		case OP_SIM_HITS_LEFT:
		case OP_GET_DAMAGE_CAUSED:
			return STATUS_SUBCATEGORY_DAMAGE;
		
		case OP_DISTANCE:
		case OP_DISTANCE_CENTER:
		case OP_DISTANCE_BBOX:
		case OP_DISTANCE_CENTER_SUBSYSTEM:
		case OP_DISTANCE_BBOX_SUBSYSTEM:
		case OP_NAV_DISTANCE:
		case OP_GET_OBJECT_X:
		case OP_GET_OBJECT_Y:
		case OP_GET_OBJECT_Z:
		case OP_GET_OBJECT_PITCH:
		case OP_GET_OBJECT_BANK:
		case OP_GET_OBJECT_HEADING:
		case OP_GET_OBJECT_SPEED_X:
		case OP_GET_OBJECT_SPEED_Y:
		case OP_GET_OBJECT_SPEED_Z:
		case OP_NUM_WITHIN_BOX:
		case OP_SPECIAL_WARP_DISTANCE:
		case OP_IS_IN_BOX:
		case OP_ANGLE_FVEC_TARGET:
			return STATUS_SUBCATEGORY_DISTANCE_AND_COORDINATES;

		case OP_STRING_TO_INT:
		case OP_STRING_GET_LENGTH:
			return STATUS_SUBCATEGORY_VARIABLES;

		case OP_IS_CONTAINER_EMPTY:
		case OP_GET_CONTAINER_SIZE:
		case OP_LIST_HAS_DATA:
		case OP_LIST_DATA_INDEX:
		case OP_MAP_HAS_KEY:
		case OP_MAP_HAS_DATA_ITEM:
			return STATUS_SUBCATEGORY_CONTAINERS;

		case OP_SCRIPT_EVAL_BOOL:
		case OP_SCRIPT_EVAL_NUM:
			return STATUS_SUBCATEGORY_OTHER;

		default:
		{
			// Check if we have a dynamic SEXP with this operator and if there is, execute that
			auto dynamicSEXP = sexp::get_dynamic_sexp(op_id);
			if (dynamicSEXP != nullptr)
				return dynamicSEXP->getSubcategory();
			else
				return OP_SUBCATEGORY_NONE;		// sexp doesn't have a subcategory
		}
	}
}

bool usable_in_campaign(int op_id)
{
	// For now, all dynamic SEXPS are only valid in missions
	if (op_id >= First_available_operator_id)
		return false;

	// exceptions to the general-by-category check below
	switch (op_id)
	{
		case OP_PREVIOUS_EVENT_TRUE:
		case OP_PREVIOUS_EVENT_FALSE:
		case OP_PREVIOUS_GOAL_TRUE:
		case OP_PREVIOUS_GOAL_FALSE:
		case OP_IS_LANGUAGE:
			return true;

		case OP_HAS_TIME_ELAPSED:
		case OP_HAS_TIME_ELAPSED_MSECS:
		case OP_PERFORM_ACTIONS_BOOL_FIRST:
		case OP_PERFORM_ACTIONS_BOOL_LAST:
		case OP_IS_TRUE_FOR_DURATION:
		case OP_EVERY_TIME:
		case OP_EVERY_TIME_ARGUMENT:
			return false;

		default:
			break;
	}

	// generally operations are allowed by category
	switch (get_category(op_id))
	{
		case OP_CATEGORY_ARITHMETIC:
		case OP_CATEGORY_LOGICAL:
		case OP_CATEGORY_CONDITIONAL:
		case OP_CATEGORY_UNLISTED:
		case OP_CATEGORY_TRAINING:
			return true;

		default:
			return false;
	}
}

// For tokenizing in SEXP help
#define MAX_SEXP_VARIABLES_1		249
#define TOKEN_LENGTH_1				31
#if (MAX_SEXP_VARIABLES_1) != (MAX_SEXP_VARIABLES - 1)
#error MAX_SEXP_VARIABLES_1 must be equal to MAX_SEXP_VARIABLES - 1!
#endif
#if (TOKEN_LENGTH_1) != (TOKEN_LENGTH - 1)
#error TOKEN_LENGTH_1 must be equal to TOKEN_LENGTH - 1!
#endif

// clang-format off
SCP_vector<sexp_help_struct> Sexp_help = {
	{ OP_PLUS, "Plus (Arithmetic operator)\r\n"
		"\tAdds numbers and returns results.\r\n\r\n"
		"Returns a number.  Takes 2 or more numeric arguments." },

	{ OP_MINUS, "Minus (Arithmetic operator)\r\n"
		"\tSubtracts numbers and returns results.\r\n\r\n"
		"Returns a number.  Takes 2 or more numeric arguments." },

	{ OP_MOD, "Mod (Arithmetic operator)\r\n"
		"\tDivides numbers and returns the remainer.\r\n\r\n"
		"Returns a number.  Takes 2 or more numeric arguments." },

	{ OP_MUL, "Multiply (Arithmetic operator)\r\n"
		"\tMultiplies numbers and returns results.\r\n\r\n"
		"Returns a number.  Takes 2 or more numeric arguments." },

	{ OP_DIV, "Divide (Arithmetic operator)\r\n"
		"\tDivides numbers and returns results.\r\n\r\n"
		"Returns a number.  Takes 2 or more numeric arguments." },

	{ OP_RAND, "Rand (Arithmetic operator)\r\n"
		"\tGets a random number.  This number will not change on successive calls to this sexp.\r\n\r\n"
		"Returns a number.  Takes 2 or 3 numeric arguments...\r\n"
		"\t1:\tLower bound, inclusive, of the random number.\r\n"
		"\t2:\tUpper bound, inclusive, of the random number.\r\n"
		"\t3:\t(optional) A seed to use when generating numbers. (Setting this to 0 is the same as having no seed at all)" },

	// Goober5000
	{ OP_RAND_MULTIPLE, "Rand-multiple (Arithmetic operator)\r\n"
		"\tGets a random number.  This number can and will change between successive calls to this sexp.\r\n\r\n"
		"Returns a number.  Takes 2 or 3 numeric arguments...\r\n"
		"\t1:\tLower bound, inclusive, of the random number.\r\n"
		"\t2:\tUpper bound, inclusive, of the random number.\r\n"
		"\t3:\t(optional) A seed to use when generating numbers. (Setting this to 0 is the same as having no seed at all)" },

	// -------------------------- Nav Points --- Kazan -------------------------- 
	{ OP_NAV_IS_VISITED, "is-nav-visited\r\n"
		"Returns true when the player has visited the given navpoint (Has closed to within 1000m of it). Takes 1 argument...\r\n"
		"\t1:\tThe name of the navpoint" },

	{ OP_NAV_DISTANCE, "distance-to-nav\r\n"
		"Returns the distance from the center of the player ship to a nav point. Takes 1 argument...\r\n"
		"\t1:\tThe name of the navpoint" },

	{ OP_NAV_ADD_WAYPOINT, "add-nav-waypoint\r\n"
		"Adds a Navpoint to a navpoint path. Takes 3 or 4 Arguments...\r\n"
		"\t1:\tName of the new navpoint.\r\n"
		"\t2:\tName of the navpoint path the new navpoint should be added to.\r\n"
		"\t3:\tPosition where the new navpoint will be inserted. Note: This is 1-indexed, so the first waypoint in a path has position 1.\r\n"
		"\t4:\t(Optional Argument) Controls the visibility of the new navpoint. Only entities belonging to the chosen category will be able to see it.\r\n" },

	{ OP_NAV_ADD_SHIP, "add-nav-ship\r\n"
		"Binds the named navpoint to the named ship - when the ship moves, the waypoint moves with it. Takes 2 Arguments...\r\n"
		"\t1:\tThe NavPoint's Name\r\n"
		"\t2:\tThe Ship's Name (ship must be in-mission)\r\n" },

	{ OP_NAV_DEL, "del-nav\r\n"
		"Deletes a nav point. Takes 1 Argument...\r\n"
		"\t1:\tNavPoint Name" },

	{ OP_NAV_HIDE, "hide-nav\r\n"
		"Hides a nav point. Takes 1 Argument...\r\n"
		"\t1:\tNavPoint Name, it then 'hides' that Nav Point\r\n" },

	{ OP_NAV_RESTRICT, "restrict-nav\r\n"
		"This causes the nav point to be unselectable. Takes 1 Argument...\r\n"
		"\t1:\tThe Navpoint name\r\n" },

	{ OP_NAV_UNHIDE, "unhide-nav\r\n"
		"Restores a hidden navpoint. Takes 1 Argument...\r\n"
		"\t1:\tThe Navpoint Name\r\n" },

	{ OP_NAV_UNRESTRICT, "unrestrict-nav\r\n"
		"Removes restrictions from a Navpoint. Takes 1 Argument...\r\n"	
		"\t1:\tThe Navpoint Name\r\n" },

	{ OP_NAV_SET_VISITED, "set-nav-visited\r\n"
		"Sets the status of the given Navpoint to \"visited\". Takes 1 Argument...\r\n"
		"\t1:\tThe Navpoint Name\r\n" },

	{ OP_NAV_UNSET_VISITED, "unset-nav-visited\r\n"
		"Removes the \"visited\" status from a Navpoint. Takes 1 Argument...\r\n"	
		"\t1:\tThe Navpoint Name\r\n" },

	{ OP_NAV_SET_CARRY, "nav-set-carry\r\n"
		"Sets the Nav Carry flag for all listed ships. Vessels with this flag will follow the player into and out of autopilot.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1:\tShips (must be in-mission) and Wings (do not need to be in-mission) that should receive the Nav Carry flag.\r\n" },

	{ OP_NAV_UNSET_CARRY, "unset-nav-carry\r\n"
		"Removes the Nav Carry flag from all listed ships and wings. Takes 1 or more arguments...\r\n"
		"\t1:\tShips (must be in-mission) and Wings (do not need to be in-mission) to remove the Nav Carry flag from\r\n" },

	{ OP_NAV_SET_NEEDSLINK, "set-nav-needslink\r\n"
		"Marks all listed ships as needing to link up before entering autopilot.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1:\tShips to mark (ships must be in-mission)\r\n" },

	{ OP_NAV_UNSET_NEEDSLINK, "unset-nav-needslink\r\n"
		"Removes the requirement for the listed ships to link up before entering autopilot.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1:\tShips to mark (ships must be in-mission)\r\n" },

	{ OP_NAV_ISLINKED, "is-nav-linked\r\n"
		"Determines if a ship is linked for autopilot (\"set-nav-carry\" or \"set-nav-needslink\" + linked).\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tShip to check (evaluation returns NAN until ship is in-mission)\r\n"},

	{ OP_NAV_USECINEMATICS, "use-nav-cinematics\r\n"
		"Controls the use of the cinematic autopilot camera. Takes 1 Argument...\r\n"
		"\t1:\tSet to true to enable automatic cinematics, set to false to disable automatic cinematics." },

	{ OP_NAV_USEAP, "use-autopilot\r\n"
		"Takes 1 boolean argument.\r\n"
		"\t1:\tSet to true to enable autopilot, set to false to disable autopilot." },

	{ OP_NAV_SELECT, "nav-select (Action operator)\r\n"
		"\tSelects a nav point.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of the nav point." },

	{ OP_NAV_UNSELECT, "nav-deselect (Action operator)\r\n"
		"\tDeselects any navpoint selected.\r\n\r\n"
		"Takes no arguments..." },

	{ OP_NAV_SET_COLOR, "nav-set-color (Action operator)\r\n"
		"\tSets the color used by this navpoint on the HUD.\r\n\r\n"
		"Takes 4 or more arguments...\r\n"
		"\t1:\tRed value\r\n"
		"\t2:\tGreen value\r\n"
		"\t3:\tBlue value\r\n"
		"\tRest:\tNavpoints that should use this color\r\n"
	},

	{ OP_NAV_SET_VISITED_COLOR, "nav-set-visited-color (Action operator)\r\n"
		"\tSets the color used by this navpoint on the HUD after it has been visited.\r\n\r\n"
		"Takes 4 or more arguments...\r\n"
		"\t1:\tRed value\r\n"
		"\t2:\tGreen value\r\n"
		"\t3:\tBlue value\r\n"
		"\tRest:\tNavpoints that should use this color\r\n"
	},

	// -------------------------- -------------------------- -------------------------- 

	// Goober5000
	{ OP_ABS, "Absolute value (Arithmetic operator)\r\n"
		"\tReturns the absolute value of a number.  Takes 1 numeric argument.\r\n" },

	// Goober5000
	{ OP_MIN, "Minimum value (Arithmetic operator)\r\n"
		"\tReturns the minimum of a set of numbers.  Takes 1 or more numeric arguments.\r\n" },

	// Goober5000
	{ OP_MAX, "Maximum value (Arithmetic operator)\r\n"
		"\tReturns the maximum of a set of numbers.  Takes 1 or more numeric arguments.\r\n" },

	// Goober5000
	{ OP_AVG, "Average value (Arithmetic operator)\r\n"
		"\tReturns the average (rounded to the nearest integer) of a set of numbers.  Takes 1 or more numeric arguments.\r\n" },

	// Goober5000
	{ OP_POW, "Power (Arithmetic operator)\r\n"
		"\tRaises one number to the power of the next number.  If the result will be larger than INT_MAX or smaller than INT_MIN, the appropriate limit will be returned.  Takes 2 numeric arguments.\r\n" },

	// Goober5000
	{ OP_SIGNUM, "Signum (Arithmetic operator)\r\n"
		"\tReturns the sign of a number: -1 if it is negative, 0 if it is 0, and 1 if it is positive.  Takes one argument.\r\n" },

	// Goober5000
	{ OP_IS_NAN, "Is-NaN (Arithmetic operator)\r\n"
		"\tReturns true if the argument is NaN (not-a-number).  This can happen e.g. when checking statistics for ships that have departed.  Takes one argument.\r\n" },

	// Goober5000
	{ OP_NAN_TO_NUMBER, "NaN-to-Number (Arithmetic operator)\r\n"
		"\tUsed to safely filter NaN values from arithmetic operations, which would otherwise pass NaN up the sexp tree.  If the argument is a number, it is returned.  If the argument is NaN, a zero is returned.  Takes one argument.\r\n" },

	// Goober5000
	{ OP_SET_BIT, "set-bit\r\n"
		"\tTurns on (sets to 1) a certain bit in the provided number, returning the result.  This allows numbers to store up to 32 boolean flags, from 2^0 to 2^31.  Takes 2 numeric arguments...\r\n"
		"\t1: The number whose bit should be set\r\n"
		"\t2: The index of the bit to set.  Valid indexes are between 0 and 31, inclusive.\r\n" },

	// Goober5000
	{ OP_UNSET_BIT, "unset-bit\r\n"
		"\tTurns off (sets to 0) a certain bit in the provided number, returning the result.  This allows numbers to store up to 32 boolean flags, from 2^0 to 2^31.  Takes 2 numeric arguments...\r\n"
		"\t1: The number whose bit should be unset\r\n"
		"\t2: The index of the bit to unset.  Valid indexes are between 0 and 31, inclusive.\r\n" },

	// Goober5000
	{ OP_IS_BIT_SET, "is-bit-set\r\n"
		"\tReturns true if the specified bit is set (equal to 1) in the provided number.  Takes 2 numeric arguments...\r\n"
		"\t1: The number whose bit should be tested\r\n"
		"\t2: The index of the bit to test.  Valid indexes are between 0 and 31, inclusive.\r\n" },

	// Goober5000
	{ OP_BITWISE_AND, "bitwise-and\r\n"
		"\tPerforms the bitwise AND operator on its arguments.  This is the same as if the logical AND operator was performed on each successive bit.  Takes 2 or more numeric arguments.\r\n" },

	// Goober5000
	{ OP_BITWISE_OR, "bitwise-or\r\n"
		"\tPerforms the bitwise OR operator on its arguments.  This is the same as if the logical OR operator was performed on each successive bit.  Takes 2 or more numeric arguments.\r\n" },

	// Goober5000
	{ OP_BITWISE_NOT, "bitwise-not\r\n"
		"\tPerforms the bitwise NOT operator on its argument.  This is the same as if the logical NOT operator was performed on each successive bit.\r\n\r\n"
		"Note that the operation is performed as if on an unsigned integer whose maximum value is INT_MAX.  In other words, there is no need to worry about the sign bit.\r\n\r\n"
		"Takes only 1 argument.\r\n" },

	// Goober5000
	{ OP_BITWISE_XOR, "bitwise-xor\r\n"
		"\tPerforms the bitwise XOR operator on its arguments.  This is the same as if the logical XOR operator was performed on each successive bit.  Takes 2 or more numeric arguments.\r\n" },

	{ OP_ANGLE_VECTORS, "angle-vectors\r\n"
		"\tCalculates the angle between two vectors.  "
		"Takes 6 arguments...\r\n"
		"\t1: The x component of the first vector.\r\n"
		"\t2: The y component of the first vector.\r\n"
		"\t3: The z component of the first vector.\r\n"
		"\t4: The x component of the second vector.\r\n"
		"\t5: The y component of the second vector.\r\n"
		"\t6: The z component of the second vector.\r\n"},

	{ OP_SET_OBJECT_SPEED_X, "set-object-speed-x (deprecated in favor of ship-maneuver)\r\n"
		"\tSets the X speed of a ship or wing (ship/wing must be in-mission).  "
		"Takes 2 or 3 arguments...\r\n"
		"\t1: The name of the object.\r\n"
		"\t2: The speed to set.\r\n"
		"\t3: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" },

	{ OP_SET_OBJECT_SPEED_Y, "set-object-speed-y (deprecated in favor of ship-maneuver)\r\n"
		"\tSets the Y speed of a ship or wing (ship/wing must be in-mission).  "
		"Takes 2 or 3 arguments...\r\n"
		"\t1: The name of the object.\r\n"
		"\t2: The speed to set.\r\n"
		"\t3: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" },

	{ OP_SET_OBJECT_SPEED_Z, "set-object-speed-z (deprecated in favor of ship-maneuver)\r\n"
		"\tSets the Z speed of a ship or wing (ship/wing must be in-mission).  "
		"Takes 2 or 3 arguments...\r\n"
		"\t1: The name of the object.\r\n"
		"\t2: The speed to set.\r\n"
		"\t3: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" },

	{ OP_GET_OBJECT_SPEED_X, "get-object-speed-x\r\n"
		"\tReturns the X speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission).  "
		"Takes 2 or 3 arguments...\r\n"
		"\t1: The name of the object.\r\n"
		"\t2: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" },

	{ OP_GET_OBJECT_SPEED_Y, "get-object-speed-y\r\n"
		"\tReturns the Y speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission).  "
		"Takes 2 or 3 arguments...\r\n"
		"\t1: The name of the object.\r\n"
		"\t2: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" },

	{ OP_GET_OBJECT_SPEED_Z, "get-object-speed-z\r\n"
		"\tReturns the Z speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission).  "
		"Takes 2 or 3 arguments...\r\n"
		"\t1: The name of the object.\r\n"
		"\t2: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" },

	// Goober5000
	{ OP_GET_OBJECT_X, "get-object-x\r\n"
		"\tReturns the absolute X coordinate of a set of coordinates relative to a particular object (or object's "
		"subsystem).  The input coordinates are the coordinates relative to the object's position and orientation.  "
		"If no input coordinates are specified, the coordinate returned is the coordinate of the object (or object's "
		"subsystem) itself.  Takes 1 to 5 arguments...\r\n"
		"\t1: The name of a ship, wing, or waypoint (evaluation returns NAN until object is in-mission).\r\n"
		"\t2: A ship subsystem (or \"" SEXP_NONE_STRING "\" if the first argument is not a ship - optional).\r\n"
		"\t3: The relative X coordinate (optional).\r\n"
		"\t4: The relative Y coordinate (optional).\r\n"
		"\t5: The relative Z coordinate (optional).\r\n" },

	// Goober5000
	{ OP_GET_OBJECT_Y, "get-object-y\r\n"
		"\tReturns the absolute Y coordinate of a set of coordinates relative to a particular object (or object's "
		"subsystem).  The input coordinates are the coordinates relative to the object's position and orientation.  "
		"If no input coordinates are specified, the coordinate returned is the coordinate of the object (or object's "
		"subsystem) itself.  Takes 1 to 5 arguments...\r\n"
		"\t1: The name of a ship, wing, or waypoint (evaluation returns NAN until object is in-mission).\r\n"
		"\t2: A ship subsystem (or \"" SEXP_NONE_STRING "\" if the first argument is not a ship - optional).\r\n"
		"\t3: The relative X coordinate (optional).\r\n"
		"\t4: The relative Y coordinate (optional).\r\n"
		"\t5: The relative Z coordinate (optional).\r\n" },

	// Goober5000
	{ OP_GET_OBJECT_Z, "get-object-z\r\n"
		"\tReturns the absolute Z coordinate of a set of coordinates relative to a particular object (or object's "
		"subsystem).  The input coordinates are the coordinates relative to the object's position and orientation.  "
		"If no input coordinates are specified, the coordinate returned is the coordinate of the object (or object's "
		"subsystem) itself.  Takes 1 to 5 arguments...\r\n"
		"\t1: The name of a ship, wing, or waypoint (evaluation returns NAN until object is in-mission).\r\n"
		"\t2: A ship subsystem (or \"" SEXP_NONE_STRING "\" if the first argument is not a ship - optional).\r\n"
		"\t3: The relative X coordinate (optional).\r\n"
		"\t4: The relative Y coordinate (optional).\r\n"
		"\t5: The relative Z coordinate (optional).\r\n" },

	// Goober5000
	{ OP_SET_OBJECT_POSITION, "set-object-position\r\n"
		"\tInstantaneously sets an object's spatial coordinates.  "
		"Takes 4 arguments...\r\n"
		"\t1: The name of a ship, wing, or waypoint (object does not need to be in-mission).\r\n"
		"\t2: The new X coordinate.\r\n"
		"\t3: The new Y coordinate.\r\n"
		"\t4: The new Z coordinate." },

	// Goober5000
	{ OP_GET_OBJECT_PITCH, "get-object-pitch\r\n"
		"\tReturns the pitch angle, in degrees, of a particular object.  The returned value will be between 0 and 360.  Takes 1 argument...\r\n"
		"\t1: The name of a ship or wing (evaluation returns NAN until ship/wing is in-mission).\r\n" },

	// Goober5000
	{ OP_GET_OBJECT_BANK, "get-object-bank\r\n"
		"\tReturns the bank angle, in degrees, of a particular object.  The returned value will be between 0 and 360.  Takes 1 argument...\r\n"
		"\t1: The name of a ship or wing (evaluation returns NAN until ship/wing is in-mission).\r\n" },

	// Goober5000
	{ OP_GET_OBJECT_HEADING, "get-object-heading\r\n"
		"\tReturns the heading angle, in degrees, of a particular object.  The returned value will be between 0 and 360.  Takes 1 argument...\r\n"
		"\t1: The name of a ship or wing (evaluation returns NAN until ship/wing is in-mission).\r\n" },

	// Goober5000
	{ OP_SET_OBJECT_ORIENTATION, "set-object-orientation\r\n"
		"\tInstantaneously sets an object's spatial orientation.  "
		"Takes 4 arguments...\r\n"
		"\t1: The name of a ship or wing (ship/wing does not need to be in-mission).\r\n"
		"\t2: The new pitch angle, in degrees.  The angle can be any number; it does not have to be between 0 and 360.\r\n"
		"\t3: The new bank angle, in degrees.  The angle can be any can be any number; it does not have to be between 0 and 360.\r\n"
		"\t4: The new heading angle, in degrees.  The angle can be any number; it does not have to be between 0 and 360." },

	{ OP_TRUE, "True (Boolean operator)\r\n"
		"\tA true boolean state\r\n\r\n"
		"Returns a boolean value." },

	{ OP_FALSE, "False (Boolean operator)\r\n"
		"\tA false boolean state\r\n\r\n"
		"Returns a boolean value." },

	{ OP_AND, "And (Boolean operator)\r\n"
		"\tAnd is true if all of its arguments are true.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more boolean arguments." },

	{ OP_OR, "Or (Boolean operator)\r\n"
		"\tOr is true if any of its arguments are true.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more boolean arguments." },

	{ OP_XOR, "Xor (Boolean operator)\r\n"
		"\tXor is true if exactly one of its arguments is true.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more boolean arguments." },

	{ OP_EQUALS, "Equals (Boolean operator)\r\n"
		"\tIs true if all of its arguments are equal.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more numeric arguments." },

	{ OP_GREATER_THAN, "Greater Than (Boolean operator)\r\n"
		"\tTrue if the first argument is greater than the subsequent argument(s).\r\n\r\n"
		"Returns a boolean value.  Takes 2 numeric arguments." },

	{ OP_LESS_THAN, "Less Than (Boolean operator)\r\n"
		"\tTrue if the first argument is less than the subsequent argument(s).\r\n\r\n"
		"Returns a boolean value.  Takes 2 numeric arguments." },

	{ OP_NOT_EQUAL, "Not Equal To (Boolean operator)\r\n"
		"\tIs true if the first argument is not equal to any of the subsequent arguments.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more numeric arguments." },

	{ OP_GREATER_OR_EQUAL, "Greater Than Or Equal To (Boolean operator)\r\n"
		"\tTrue if the first argument is greater than or equal to the subsequent argument(s).\r\n\r\n"
		"Returns a boolean value.  Takes 2 numeric arguments." },

	{ OP_LESS_OR_EQUAL, "Less Than Or Equal To (Boolean operator)\r\n"
		"\tTrue if the first argument is less than or equal to the subsequent argument(s).\r\n\r\n"
		"Returns a boolean value.  Takes 2 numeric arguments." },

	// Goober5000
	{ OP_PERFORM_ACTIONS_BOOL_FIRST, "perform-actions-bool-first\r\n"
		"\tThis sexp allows actions to be performed as part of a conditional test.  It is most useful for assigning variables or performing some sort of pre-test action within the conditional part of \"when\", etc.  "
		"It works well as the first branch of an \"and\" sexp, provided it returns true so as to not affect the return value of the \"and\".\r\n\r\n"
		"Returns a boolean value (note well the distinction between perform-actions-bool-first and perform-actions-bool-last).  Takes 2 or more arguments.\r\n"
		"\t1:\tA boolean value to return after all successive actions have been performed.  NOTE: Even though this value is not returned until the actions are performed, it is evaluated BEFORE any of the actions.\r\n"
		"\tRest:\tActions to perform, which would normally appear in a \"when\" sexp.\r\n" },

	// Goober5000
	{ OP_PERFORM_ACTIONS_BOOL_LAST, "perform-actions-bool-last\r\n"
		"\tThis sexp allows actions to be performed as part of a conditional test.  It is most useful for assigning variables or performing some sort of pre-test action within the conditional part of \"when\", etc.  "
		"It works well as the first branch of an \"and\" sexp, provided it returns true so as to not affect the return value of the \"and\".\r\n\r\n"
		"Returns a boolean value (note well the distinction between perform-actions-bool-first and perform-actions-bool-last).  Takes 2 or more arguments.\r\n"
		"\t1:\tA boolean value to return after all successive actions have been performed.  NOTE: This parameter is evaluated AFTER all of the actions have been performed.\r\n"
		"\tRest:\tActions to perform, which would normally appear in a \"when\" sexp.\r\n" },

	// Goober5000
	{ OP_IS_TRUE_FOR_DURATION, "is-true-for-duration\r\n"
		"\tTrue as long as all of the boolean arguments are true for at least the specified duration.  That is, as soon as all of the boolean arguments are true simultaneously, the "
		"clock starts running.  If all of the boolean arguments remain true for the entire duration, this sexp also becomes true; but if any boolean argument becomes false, "
		"the clock resets and the sexp becomes false.  This sexp may switch between true and false multiple times per mission if multiple durations satisfy the conditions.\r\n\r\n"
		"NOTE: This sexp will not work correctly with any special-argument sexp, as it relies on state data that must persist from frame to frame to keep track of the timing.\r\n\r\n"
		"Takes 2 or more arguments:\r\n"
		"\t1:\tThe duration, in milliseconds\r\n"
		"\tRest:\tOne or more boolean conditions\r\n" },

	// Goober5000
	{ OP_STRING_EQUALS, "String Equals (Boolean operator)\r\n"
		"\tIs true if all of its arguments are equal.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more string arguments." },

	// Goober5000
	{ OP_STRING_GREATER_THAN, "String Greater Than (Boolean operator)\r\n"
		"\tTrue if the first argument is greater than all of the other arguments.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more string arguments." },

	// Goober5000
	{ OP_STRING_LESS_THAN, "String Less Than (Boolean operator)\r\n"
		"\tTrue if the first argument is less than all of the other arguments.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more string arguments." },

	// Goober5000 - added wing capability
	{ OP_IS_IFF, "Is IFF (Boolean operator)\r\n"
		"\tTrue if ship(s) or wing(s) are all of the specified team/IFF.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tTeam (e.g. \"friendly\", \"hostile\", \"neutral\", \"unknown\").\r\n"
		"\tRest:\tName of ship or wing to check (ship/wing does not need to be in-mission)." },

	// Goober5000
	{ OP_IS_SPECIES, "Is Species (Boolean operator)\r\n"
		"\tTrue if ship(s) or wing(s) are all of the specified species.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tSpecies (e.g. \"Terran\", \"Vasudan\", \"Shivan\").\r\n"
		"\tRest:\tName of ship or wing to check (ship/wing does not need to be in-mission)." },

	// Goober5000
	{ OP_IS_AI_CLASS, "Is AI Class (Boolean operator)\r\n"
		"\tTrue if ship or ship subsystem(s) is/are all of the specified AI class.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tAI class (\"None\", \"Coward\", \"Lieutenant\", etc.)\r\n"
		"\t2:\tName of ship to check (evaluation returns NAN until ship is in-mission).\r\n"
		"\tRest:\tName of ship subsystem(s) to check (optional)" },

	// Goober5000
	{ OP_IS_SHIP_TYPE, "Is Ship Type (Boolean operator)\r\n"
		"\tTrue if ship or ships is/are all of the specified ship type.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tShip type (\"fighter\", \"bomber\", etc.)\r\n"
		"\t2:\tName of ship to check (ship/wing does not need to be in-mission) .\r\n" },

	// Goober5000
	{ OP_IS_SHIP_CLASS, "Is Ship Class (Boolean operator)\r\n"
		"\tTrue if ship or ships is/are all of the specified ship class.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tShip class\r\n"
		"\t2:\tName of ship to check (ship/wing does not need to be in-mission) .\r\n" },

	{ OP_HAS_TIME_ELAPSED, "Has time elapsed (Boolean operator)\r\n"
		"\tBecomes true when the specified amount of time has elapsed (Mission time "
		"becomes greater than or equal to the specified time).\r\n"
		"Returns a boolean value.  Takes 1 numeric argument...\r\n"
		"\t1:\tThe amount of time in seconds." },

	// Goober5000
	{ OP_HAS_TIME_ELAPSED_MSECS, "Has time elapsed, in milliseconds (Boolean operator)\r\n"
		"\tBecomes true when the specified amount of time has elapsed (Mission time "
		"becomes greater than or equal to the specified time).\r\n"
		"Returns a boolean value.  Takes 1 numeric argument...\r\n"
		"\t1:\tThe amount of time in milliseconds." },

	{ OP_NOT, "Not (Boolean operator)\r\n"
		"\tReturns opposite boolean value of argument (True becomes false, and "
		"false becomes true).\r\n\r\n"
		"Returns a boolean value.  Takes 1 boolean argument." },

	{ OP_PREVIOUS_GOAL_TRUE, "Previous Mission Goal True (Boolean operator)\r\n"
		"\tReturns true if the specified goal in the specified mission is true "
		"(or succeeded).  It returns false otherwise.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the mission.\r\n"
		"\t2:\tName of the goal in the mission.\r\n"
		"\t3:\t(Optional) True/False which signifies what this sexpression should return when "
		"this mission is played as a single mission, or is played as a campaign mission and skipped." },

	{ OP_PREVIOUS_GOAL_FALSE, "Previous Mission Goal False (Boolean operator)\r\n"
		"\tReturns true if the specified goal in the specified mission "
		"is false (or failed).  It returns false otherwise.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the mission.\r\n"
		"\t2:\tName of the goal in the mission.\r\n"
		"\t3:\t(Optional) True/False which signifies what this sexpression should return when "
		"this mission is played as a single mission, or is played as a campaign mission and skipped." },

	{ OP_PREVIOUS_GOAL_INCOMPLETE, "Previous Mission Goal Incomplete (Boolean operator)\r\n"
		"\tReturns true if the specified goal in the specified mission "
		"is incomplete (not true or false).  It returns false otherwise.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the mission.\r\n"
		"\t2:\tName of the goal in the mission.\r\n"
		"\t3:\t(Optional) True/False which signifies what this sexpression should return when "
		"this mission is played as a single mission, or is played as a campaign mission and skipped." },

	{ OP_PREVIOUS_EVENT_TRUE, "Previous Mission Event True (Boolean operator)\r\n"
		"\tReturns true if the specified event in the specified mission is true "
		"(or succeeded).  It returns false otherwise.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the mission.\r\n"
		"\t2:\tName of the event in the mission.\r\n"
		"\t3:\t(Optional) True/False which signifies what this sexpression should return when "
		"this mission is played as a single mission, or is played as a campaign mission and skipped." },

	{ OP_PREVIOUS_EVENT_FALSE, "Previous Mission Event False (Boolean operator)\r\n"
		"\tReturns true if the specified event in the specified mission "
		"is false (or failed).  It returns false otherwise.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the mission.\r\n"
		"\t2:\tName of the event in the mission.\r\n"
		"\t3:\t(Optional) True/False which signifies what this sexpression should return when "
		"this mission is played as a single mission, or is played as a campaign mission and skipped." },

	{ OP_PREVIOUS_EVENT_INCOMPLETE, "Previous Mission Event Incomplete (Boolean operator)\r\n"
		"\tReturns true if the specified event in the specified mission "
		"is incomplete (not true or false).  It returns false otherwise.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the mission.\r\n"
		"\t2:\tName of the event in the mission.\r\n"
		"\t3:\t(Optional) True/False which signifies what this sexpression should return when "
		"this mission is played as a single mission, or is played as a campaign mission and skipped." },

	{ OP_GOAL_TRUE_DELAY, "Mission Goal True (Boolean operator)\r\n"
		"\tReturns true N seconds after the specified goal in the this mission is true (or succeeded).  It returns false otherwise.\r\n\r\n"
		"\tThis operator works by checking the mission log.  Since goal status is evaluated after event status, a delay of 0 will cause an event to become true on the frame after the goal becomes true.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the event in the mission.\r\n"
		"\t2:\tNumber of seconds to delay before returning true."},

	{ OP_GOAL_FALSE_DELAY, "Mission Goal False (Boolean operator)\r\n"
		"\tReturns true N seconds after the specified goal in the this mission is false (or failed).  It returns false otherwise.\r\n\r\n"
		"\tThis operator works by checking the mission log.  Since goal status is evaluated after event status, a delay of 0 will cause an event to become true on the frame after the goal becomes false.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the event in the mission.\r\n"
		"\t2:\tNumber of seconds to delay before returning true."},

	{ OP_GOAL_INCOMPLETE, "Mission Goal Incomplete (Boolean operator)\r\n"
		"\tReturns true if the specified goal in the this mission is incomplete.  This "
		"sexpression will only be useful in conjunction with another sexpression like "
		"has-time-elapsed.  Used alone, it will return true upon mission startup.\r\n"
		"Returns a boolean value.  Takes 1 argument...\r\n"
		"\t1:\tName of the event in the mission."},

	{ OP_EVENT_TRUE_DELAY, "Mission Event True (Boolean operator)\r\n"
		"\tReturns true N seconds after the specified event in the this mission is true (or succeeded).  It returns false otherwise.\r\n\r\n"
		"\tNOTE: For a delay of 0 (if used in-mission), this operator will become true on the frame after the specified event becomes true.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the event in the mission.\r\n"
		"\t2:\tNumber of seconds to delay before returning true.\r\n"
		"\t3:\t(Optional) Defaults to False. When set to false, directives will only appear as soon as the specified event is true.\r\n"
		"\t\tWhen set to true, the event only affects whether the directive succeeds/fails, and has no effect on when it appears"},

	{ OP_EVENT_FALSE_DELAY, "Mission Event False (Boolean operator)\r\n"
		"\tReturns true N seconds after the specified event in the this mission is false (or failed).  It returns false otherwise.\r\n\r\n"
		"\tNOTE: For a delay of 0 (if used in-mission), this operator will become true on the frame after the specified event becomes false.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the event in the mission.\r\n"
		"\t2:\tNumber of seconds to delay before returning true.\r\n"
		"\t3:\t(Optional) Defaults to False. When set to false, directives will only appear as soon as the specified event is true.\r\n"
		"\t\tWhen set to true, the event only affects whether the directive succeeds/fails, and has no effect on when it appears"},

	{ OP_EVENT_TRUE_MSECS_DELAY, "Mission Event True (Boolean operator)\r\n"
		"\tReturns true N milliseconds after the specified event in the this mission is true (or succeeded).  It returns false otherwise.\r\n\r\n"
		"\tNOTE: For a delay of 0 (if used in-mission), this operator will become true on the frame after the specified event becomes true.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the event in the mission.\r\n"
		"\t2:\tNumber of milliseconds to delay before returning true.\r\n"
		"\t3:\t(Optional) Defaults to False. When set to false, directives will only appear as soon as the specified event is true.\r\n"
		"\t\tWhen set to true, the event only affects whether the directive succeeds/fails, and has no effect on when it appears"},

	{ OP_EVENT_FALSE_MSECS_DELAY, "Mission Event False (Boolean operator)\r\n"
		"\tReturns true N milliseconds after the specified event in the this mission is false (or failed).  It returns false otherwise.\r\n\r\n"
		"\tNOTE: For a delay of 0 (if used in-mission), this operator will become true on the frame after the specified event becomes false.\r\n\r\n"
		"Returns a boolean value.  Takes 2 required arguments and 1 optional argument...\r\n"
		"\t1:\tName of the event in the mission.\r\n"
		"\t2:\tNumber of milliseconds to delay before returning true.\r\n"
		"\t3:\t(Optional) Defaults to False. When set to false, directives will only appear as soon as the specified event is true.\r\n"
		"\t\tWhen set to true, the event only affects whether the directive succeeds/fails, and has no effect on when it appears"},

	{ OP_EVENT_INCOMPLETE, "Mission Event Incomplete (Boolean operator)\r\n"
		"\tReturns true if the specified event in the this mission is incomplete.  This "
		"sexpression will only be useful in conjunction with another sexpression like "
		"has-time-elapsed.  Used alone, it will return true upon mission startup.\r\n"
		"Returns a boolean value.  Takes 1 argument...\r\n"
		"\t1:\tName of the event in the mission."},

	{ OP_RESET_EVENT, "Reset-Event (Action operator)\r\n"
		"Clears all information associated with an event, resetting SEXP nodes and status flags so that it is as if the event had never been evaluated.  "
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of the event"
	},

	{ OP_RESET_GOAL, "Reset-Goal (Action operator)\r\n"
		"Clears all information associated with a goal, resetting SEXP nodes and status flags so that it is as if the goal had never been evaluated.  "
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of the goal"
	},

	{ OP_IS_DESTROYED_DELAY, "Is destroyed delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after all specified ships (including listed ships not in-mission) have been destroyed.\r\n"
		"\tWARNING: If multiple is-destroyed-delay SEXPs are used in a directive event, unexpected results may be "
		"observed. Instead, use a single is-destroyed-delay SEXP with multiple parameters.\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tTime delay in seconds (see above).\r\n"
		"\tRest:\tName of ship (or wing) to check status of." },

	{ OP_WAS_DESTROYED_BY_DELAY, "Was destroyed by delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after all specified ships (including listed ships not in-mission) have been destroyed by the specified first ship.\r\n\r\n"
		"Returns a boolean value.  Takes 3 or more arguments...\r\n"
		"\t1:\tTime delay in seconds (see above).\r\n"
		"\t2:\tShip that should have destroyed the other ships (see below).\r\n"
		"\tRest:\tName of ships to check status of." },

	{ OP_IS_SUBSYSTEM_DESTROYED_DELAY, "Is subsystem destroyed delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified subsystem of the specified "
		"ship is destroyed. This will remain true even if the subsystem is subsequently "
		"repaired. If the subsystem is destroyed multiple times, the delay is measured "
		"from the first time.\r\n\r\n"
		"Returns a boolean value.  Takes 3 arguments...\r\n"
		"\t1:\tName of ship the subsystem we are checking is on.\r\n"
		"\t2:\tThe name of the subsystem we are checking status of.\r\n"
		"\t3:\tTime delay in seconds (see above)." },

	{ OP_IS_DISABLED_DELAY, "Is disabled delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified ship(s) are disabled. A "
		"ship is disabled when all of its engine subsystems are destroyed. All "
		"ships must be disabled for this operator to return true. This will remain "
		"true even if an engine subsystem is subsequently repaired. If a ship becomes "
		"disabled multiple times, the delay is measured from the first time.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tTime delay is seconds (see above).\r\n"
		"\tRest:\tNames of ships to check disabled status of." },

	{ OP_IS_DISARMED_DELAY, "Is disarmed delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified ship(s) are disarmed. A "
		"ship is disarmed when all of its turret subsystems are destroyed. All "
		"ships must be disarmed for this function to return true. This will remain "
		"true even if a turret subsystem is subsequently repaired. If a ship becomes "
		"disarmed multiple times, the delay is measured from the first time.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tTime delay is seconds (see above).\r\n"
		"\tRest:\tNames of ships to check disarmed status of." },

	{ OP_HAS_DOCKED_DELAY, "Has docked delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified ships have docked the "
		"specified number of times.\r\n\r\n"
		"Returns a boolean value.  Takes 4 arguments...\r\n"
		"\t1:\tThe name of the docker ship\r\n"
		"\t2:\tThe name of the dockee ship\r\n"
		"\t3:\tThe number of times they have to have docked\r\n"
		"\t4:\tTime delay in seconds (see above)." },

	{ OP_HAS_UNDOCKED_DELAY, "Has undocked delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified ships have undocked the "
		"specified number of times.\r\n\r\n"
		"Returns a boolean value.  Takes 4 arguments...\r\n"
		"\t1:\tThe name of the docker ship\r\n"
		"\t2:\tThe name of the dockee ship\r\n"
		"\t3:\tThe number of times they have to have undocked\r\n"
		"\t4:\tTime delay in seconds (see above)." },

	{ OP_HAS_ARRIVED_DELAY, "Has arrived delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified ship(s) have arrived into the mission. "
		"Note that a ship which is present at mission start is NOT considered to have arrived.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tTime delay in seconds (see above).\r\n"
		"\tRest:\tName of ship (or wing) we want to check has arrived." },

	{ OP_HAS_DEPARTED_DELAY, "Has departed delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified ship(s) or wing(s) have departed "
		"from the mission by warping out.  If any ship was destroyed, this operator will "
		"never be true.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tTime delay in seconds (see above).\r\n"
		"\tRest:\tName of ship (or wing) we want to check has departed." },

	{ OP_WAYPOINTS_DONE_DELAY, "Waypoints done delay (Boolean operator)\r\n"
		"\tBecomes true <delay> seconds after the specified ship has completed flying the "
		"specified waypoint path.\r\n\r\n"
		"Returns a boolean value.  Takes 3 or 4 arguments...\r\n"
		"\t1:\tName of ship we are checking.\r\n"
		"\t2:\tWaypoint path we want to check if ship has flown.\r\n"
		"\t3:\tTime delay in seconds (see above).\r\n"
		"\t4:\tHow many times the ship has completed the waypoint path (optional)." },

	{ OP_SHIP_TYPE_DESTROYED, "Ship Type Destroyed (Boolean operator)\r\n"
		"\tBecomes true when the specified percentage of ship types in this mission (including ships not yet in-mission) have been destroyed.  The "
		"ship type is a generic type such as fighter, bomber, transport, etc.  Note that the percentage is updated when a ship finishes exploding, "
		"in contrast to the mission log and the *-destroyed-delay SEXPs which are updated when a ship starts exploding.  Note also that player ships "
		"and ships with the \"Ignore for counting goals\" flag are not included in the calculation.\r\n\r\n"
		"Returns a boolean value.  Takes 2 arguments...\r\n"
		"\t1:\tPercentage of ships that must be destroyed.\r\n"
		"\t2:\tShip type to check for." },

	{ OP_TIME_SHIP_DESTROYED, "Time ship destroyed (Time operator)\r\n"
		"\tReturns the time the specified ship was destroyed (evaluation returns NAN until then). "
		" Departed doesn't count as destroyed.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of ship we want to check." },

	{ OP_TIME_SHIP_ARRIVED, "Time ship arrived (Time operator)\r\n"
		"\tReturns the time the specified ship arrived into the mission.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of ship we want to check." },

	{ OP_TIME_SHIP_DEPARTED, "Time ship departed (Time operator)\r\n"
		"\tReturns the time the specified ship departed the mission "
		"(evaluation returns NAN until then).  Destroyed doesn't count as departed.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of ship we want to check." },

	{ OP_TIME_WING_DESTROYED, "Time wing destroyed (Time operator)\r\n"
		"\tReturns the time the specified wing was destroyed.  "
		"This will return NAN until all the ships in the wing have been destroyed. "
		"If any have departed, the wing can never be considered destroyed.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of wing we want to check." },

	{ OP_TIME_WING_ARRIVED, "Time wing arrived (Time operator)\r\n"
		"\tReturns the time the specified wing arrived into the mission.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of wing we want to check." },

	{ OP_TIME_WING_DEPARTED, "Time wing departed (Time operator)\r\n"
		"\tReturns the time the specified wing departed the mission.  "
		"This will return NAN until all the ships in the wing have departed.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of wing we want to check." },

	{ OP_MISSION_TIME, "Mission time (Time operator)\r\n"
		"\tReturns the current time into the mission.\r\n\r\n"
		"Returns a numeric value.  Takes no arguments." },

	{ OP_MISSION_TIME_MSECS, "Mission time, in milliseconds (Time operator)\r\n"
		"\tReturns the current time into the mission, in milliseconds.  Useful for more fine-grained timing than possible with normal second-based sexps.  (Tip: when an event occurs, assign the result of mission-time-msecs to a variable.  Then, in another event, wait until mission-time-msecs is greater than the value of that variable plus some delay amount.  This second event should be chained or coupled with additional conditions so that it doesn't accidentally fire on an uninitialized variable!)\r\n\r\n"
		"Returns a numeric value.  Takes no arguments." },

	{ OP_TIME_DOCKED, "Time docked (Time operator)\r\n"
		"\tReturns the time the specified ships docked.\r\n\r\n"
		"Returns a numeric value.  Takes 3 arguments...\r\n"
		"\t1:\tThe name of the docker ship.\r\n"
		"\t2:\tThe name of the dockee ship.\r\n"
		"\t3:\tThe number of times they must have docked to be true." },

	{ OP_TIME_UNDOCKED, "Time undocked (Time operator)\r\n"
		"\tReturns the time the specified ships undocked.\r\n\r\n"
		"Returns a numeric value.  Takes 3 arguments...\r\n"
		"\t1:\tThe name of the docker ship.\r\n"
		"\t2:\tThe name of the dockee ship.\r\n"
		"\t3:\tThe number of times they must have undocked to be true." },

	{ OP_TIME_TO_GOAL, "Time-to-goal (Time operator)\r\n"
		"\tReturns the number of seconds until a ship reaches its waypoint\r\n\r\n"
		"Returns a number value.  Takes 1 argument...\r\n"
		"\t1:\tName of ship to check waypoint time (evaluation returns NAN until ship is in-mission)." },

	// MjnMixael
	{ OP_SET_HUD_TIME_PAD, "Set HUD Timer Padding (Action operator)\r\n"
		"\tSets an additional padding that is added only to the visible clock on the HUD. "
		"It does not affect the actual mission time or the mission log! Time is an illusion "
		"and the illusion here exists only on the HUD.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tThe amount of time to add to the clock in seconds." },

	{ OP_AFTERBURNER_LEFT, "Afterburner left\r\n"
		"\tReturns a ship's current engine energy as a percentage.\r\n"
		"\t1: Ship name (evaluation returns NAN until ship is in-mission)\r\n" },

	{ OP_WEAPON_ENERGY_LEFT, "Weapon energy left\r\n"
		"\tReturns a ship's current weapon energy as a percentage.\r\n"
		"\t1: Ship name (evaluation returns NAN until ship is in-mission)\r\n" },

	{ OP_SHIELDS_LEFT, "Shields left (Status operator)\r\n"
		"\tReturns the current level of the specified ship's shields as a percentage.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of ship to check (evaluation returns NAN until ship is in-mission)." },

	{ OP_HITS_LEFT, "Hits left (Status operator)\r\n"
		"\tReturns the current level of the specified ship's hull as a percentage.  Note: for ships that have not arrived yet, or have departed, "
		" or have been destroyed (even if the ship is 'in-mission' but doing a death roll) this returns NaN.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of ship to check (evaluation returns NAN unless ship is in-mission and alive)." },

	{ OP_HITS_LEFT_SUBSYSTEM, "Hits left subsystem (status operator, deprecated)\r\n"
		"\tReturns the current level of the specified ship's subsystem integrity as a percentage of the damage done to *all "
		"subsystems of the same type*.  This operator provides the same functionality as the new hits-left-subsystem-generic "
		"operator, except that it gets the subsystem type in a very misleading way.  Common consensus among SCP programmers is "
		"that this operator was intended to behave like hits-left-subsystem-specific but was programmed incorrectly.  As such, "
		"this operator is deprecated.  Mission designers are strongly encouraged to use hits-left-subsystem-specific rather than "
		"the optional boolean parameter.\r\n\r\n"
		"Returns a numeric value.  Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of ship to check (evaluation returns NAN unless ship is in-mission and alive).\r\n"
		"\t2:\tName of subsystem on ship to check.\r\n"
		"\t3:\t(Optional) True/False. When set to true only the subsystem supplied will be tested; when set to false (the default), "
		"all subsystems of that type will be tested." },

	{ OP_HITS_LEFT_SUBSYSTEM_GENERIC, "hits-left-subsystem-generic (status operator)\r\n"
		"\tReturns the current level of integrity of a generic subsystem type, as a percentage.  A \"generic subsystem type\" "
		"is a subsystem *category*, (for example, Engines), that includes one or more *individual* subsystems (for example, engine01, "
		"engine02, and engine03) on a ship.\r\n\r\nThis is the way FreeSpace tests certain subsystem thresholds internally; for "
		"example, if the integrity of all engine subsystems (that is, the combined strength of all engines divided by the maximum "
		"total strength of all engines) is less than 30%, the player cannot warp out.\r\n\r\n"
		"Returns a numeric value.  Takes 2 arguments...\r\n"
		"\t1:\tName of ship to check (evaluation returns NAN unless ship is in-mission and alive)\r\n"
		"\t2:\tName of subsystem type to check\r\n" },

	{ OP_HITS_LEFT_SUBSYSTEM_SPECIFIC, "hits-left-subsystem-specific (status operator)\r\n"
		"\tReturns the current level of integrity of a specific subsystem, as a percentage.\r\n\r\n(If you were looking for the old "
		"hits-left-subsystem operator, this is almost certainly the operator you want.  The hits-left-subsystem operator "
		"suffers from a serious design flaw that causes it to behave like hits-left-subsystem-generic.  As such it has been deprecated "
		"and will not appear in the operator list; it can only be used if you type it in manually.  Old missions using hits-left-subsystem "
		"will still work, but mission designers are strongly encouraged to use the new operators instead.)\r\n\r\n"
		"Returns a numeric value.  Takes 2 arguments...\r\n"
		"\t1:\tName of ship to check (evaluation returns NAN unless ship is in-mission and alive)\r\n"
		"\t2:\tName of subsystem to check\r\n" },

	{ OP_SIM_HITS_LEFT, "Simulated Hits left (Status operator)\r\n"
		"\tReturns the current level of the specified ship's simulated hull as a percentage.  Note: for ships that have not arrived yet, or have departed, "
		" or have been destroyed (even if the ship is 'in-mission' but doing a death roll) this returns NaN.\r\n\r\n"
		"Returns a numeric value.  Takes 1 argument...\r\n"
		"\t1:\tName of ship to check (evaluation returns NAN unless ship is in-mission and alive)." },

	{ OP_DISTANCE, "Distance (Status operator)\r\n"
		"\tReturns the distance between two objects.  These can be ships, wings, or waypoints.  "
		"(Evaluation returns NAN until both objects are in-mission).\r\n"
		"When a wing or team is given (for either argument), the result will be the closest distance.\r\n\r\n"
		"NOTE: The standard retail SEXP measures between the center of one ship and the bounding box of another.  It is "
		"recommended to use center-distance or bbox-distance.\r\n\r\n"
		"Returns a numeric value.  Takes 2 arguments...\r\n"
		"\t1:\tThe name of one of the objects.\r\n"
		"\t2:\tThe name of the other object."
	},

	{ OP_DISTANCE_CENTER, "Distance-To-Center (Status operator)\r\n"
		"\tReturns the distance between the centers of two objects.  These can be ships, wings, or waypoints.  "
		"(Evaluation returns NAN until both objects are in-mission).\r\n"
		"When a wing or team is given (for either argument), the result will be the closest distance.\r\n\r\n"
		"Returns a numeric value.  Takes 2 arguments...\r\n"
		"\t1:\tThe name of one of the objects.\r\n"
		"\t2:\tThe name of the other object."
	},

	{ OP_DISTANCE_BBOX, "Distance-To-BBox (Status operator)\r\n"
		"\tReturns the distance between the bounding boxes of two objects.  These can be ships, wings, or waypoints.  "
		"(Evaluation returns NAN until both objects are in-mission).\r\n"
		"When a wing or team is given (for either argument), the result will be the closest distance.\r\n\r\n"
		"Returns a numeric value.  Takes 2 arguments...\r\n"
		"\t1:\tThe name of one of the objects.\r\n"
		"\t2:\tThe name of the other object."
	},

	{ OP_DISTANCE_CENTER_SUBSYSTEM, "Center distance from ship subsystem (Status operator)\r\n"
		"\tReturns the distance between the center of an object and the center of a ship subsystem.  "
		"The object can be a ship, wing, or waypoint.  (Evaluation returns NAN until both objects are in-mission).\r\n"
		"When a wing or team is given, the result will be the closest distance.\r\n\r\n"
		"Returns a numeric value.  Takes 3 arguments...\r\n"
		"\t1:\tThe name of the object.\r\n"
		"\t2:\tThe name of the ship which houses the subsystem.\r\n"
		"\t3:\tThe name of the subsystem." },

	{ OP_DISTANCE_BBOX_SUBSYSTEM, "BBox distance from ship subsystem (Status operator)\r\n"
		"\tReturns the distance between the bounding box of an object and the center of a ship subsystem.  "
		"The object can be a ship, wing, or waypoint.  (Evaluation returns NAN until both objects are in-mission).\r\n"
		"When a wing or team is given, the result will be the closest distance.\r\n\r\n"
		"Returns a numeric value.  Takes 3 arguments...\r\n"
		"\t1:\tThe name of the object.\r\n"
		"\t2:\tThe name of the ship which houses the subsystem.\r\n"
		"\t3:\tThe name of the subsystem." },

	{ OP_ANGLE_FVEC_TARGET, "angle-facing-object\r\n"
		"\tReturns the angle between the forward vector of a ship and the vector from the ship to another object.\r\n"
		"\tThis is effectively the FoV the ship would need to see the object.\r\n\r\n"
		"Returns a numeric value.  Takes 2 arguments...\r\n"
		"\t1:\tThe name of the ship from which to check.\r\n"
		"\t2:\tThe name of the object to check." },

	{ OP_NUM_WITHIN_BOX, "Number of specified objects in the box specified\r\n"
		"\t1: Box center (X)\r\n"
		"\t2: Box center (Y)\r\n"
		"\t3: Box center (Z)\r\n"
		"\t4: Box half-width (distance +/- away from center X)\r\n"
		"\t5: Box half-height (distance +/- away from center Y)\r\n"
		"\t6: Box half-depth (distance +/- away from center Z)\r\n"
		"\tRest:\tShips or wings to check" },

	{ OP_IS_IN_BOX, "Whether an object is in the box specified. If a second ship is specified, "
		"the box is relative to that ship's reference frame. \r\n"
		"\t1: Ships, wings, or points to check\r\n"
		"\t2: Min X\r\n"
		"\t3: Max X\r\n"
		"\t4: Min Y\r\n"
		"\t5: Max Y\r\n"
		"\t6: Min Z\r\n"
		"\t7: Max Z\r\n"
		"\t8: Ship to use as reference frame (optional)." },

	{ OP_IS_IN_MISSION, "Is-In-Mission (Status operator)\r\n"
		"\tChecks whether a given ship or wing is presently in the mission.  This sexp doesn't check the arrival list or exited status; it only tests to see if the "
		"ship is active or the wing has at least one ship present.  This means that internally the sexp only returns SEXP_TRUE or SEXP_FALSE and does not use any of "
		"the special shortcut values.  This is useful for ships created with ship-create, as those ships will not have used the conventional ship arrival list.\r\n\r\n"
		"Takes 1 or more string arguments, which are checked against the ship list and wing list.  (If more than 1 argument is specified, the sexp will only evaluate to true "
		"if all arguments are in the mission simultaneously.)" },

	{ OP_HAS_ARMOR_TYPE, "has-armor-type (Status operator)\r\n"
		"\tChecks if a given ship has a specific armor type.\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1:\tThe ship to check (evaluation returns NAN until ship is in-mission)\r\n"
		"\t2:\tThe armor type to check\r\n"
		"\t3:\tThe subsystem armor to check" },

	{ OP_IS_DOCKED, "Is-Docked (Status operator)\r\n"
		"\tChecks whether the specified ships are currently docked.  This sexp is different from has-docked-delay, which will return true if the ships have docked at "
		"any point in the past.  The has-docked-delay sexp checks the mission log, whereas the is-docked sexp examines the actual dockpoints.\r\n\r\n"
		"Takes 1 or more arguments.  (If more than 2 arguments are specified, the sexp will only evaluate to true if all ships are docked simultaneously.)\r\n"
		"If a single argument is supplied, the sexp will evaluate to true if anything is docked to the host ship.  (Evaluation returns NAN until all ships are in-mission). \r\n"
		"\t1:\tThe host ship.\r\n"
		"\tRest:\tShip to check as docked to the host ship." },

	{ OP_GET_DAMAGE_CAUSED, "Get damage caused (Status operator)\r\n"
		"\tReturns the amount of damage one or more ships have done to a ship.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tShip that has been damaged.\r\n"
		"\t2:\tName of ships that may have damaged it." },

	{ OP_LAST_ORDER_TIME, "Last order time (Status operator)\r\n"
		"\tReturns true if <count> seconds have elapsed since one or more ships have received "
		"a meaningful order from the player.  A meaningful order is currently any order that "
		"is not the warp out order.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tTime in seconds that must elapse.\r\n"
		"\tRest:\tName of ship (must be in-mission) or wing (does not need to be in-mission) to check for having received orders." },

	{ OP_WHEN, "When (Conditional operator)\r\n"
		"\tPerforms specified actions when a condition becomes true\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tBoolean expression that must be true for actions to take place.\r\n"
		"\tRest:\tActions to take when the boolean expression becomes true." },

	{ OP_COND, "Blah" },

	// Goober5000
	{ OP_WHEN_ARGUMENT, "When-argument (Conditional operator)\r\n"
		"\tPerforms specified actions when a condition, given a set of arguments, becomes true.\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tThe arguments to evaluate (see any-of, every-of, random-of, etc.).\r\n"
		"\t2:\tBoolean expression that must be true for actions to take place.\r\n"
		"\tRest:\tActions to take when the boolean expression becomes true." },

	// Goober5000
	{ OP_EVERY_TIME, "Every-time (Conditional operator)\r\n"
		"\tThis is a version of \"when\" that will always evaluate its arguments.  It's useful "
		"in situations where you need to repeatedly check things that may become true more than "
		"once.  Since this sexp will execute every time it's evaluated, you may need to use it as "
		"an argument to \"when\" if you want to impose restrictions on how it's called.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tBoolean expression that must be true for actions to take place.\r\n"
		"\tRest:\tActions to take when boolean expression is true." },

	// Goober5000
	{ OP_EVERY_TIME_ARGUMENT, "Every-time-argument (Conditional operator)\r\n"
		"\tThis is a version of \"when-argument\" that will always evaluate its arguments.  It's useful "
		"in situations where you need to repeatedly check things that may become true more than "
		"once.  Since this sexp will execute every time it's evaluated, you may need to use it as "
		"an argument to \"when\" (not \"when-argument\") if you want to impose restrictions on how it's called.\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tThe arguments to evaluate (see any-of, all-of, random-of, etc.).\r\n"
		"\t2:\tBoolean expression that must be true for actions to take place.\r\n"
		"\tRest:\tActions to take when the boolean expression becomes true." },

	// Goober5000
	{ OP_ON_MISSION_SKIP, "on-mission-skip (Conditional operator)\r\n"
		"\tThis is a special operator that performs its actions when, and only when, the player chooses to skip the current mission.  "
		"Since the player may do this in the debriefing, mission designers should limit these actions to campaign-specific things like "
		"adding entries to the tech room, allowing ships and weapons, and calling scripts.  Note that this operator must be the top-level "
		"operator in its event in order to work.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tActions to take if the player chooses to skip the current mission.\r\n" },

	// Goober5000
	{ OP_IF_THEN_ELSE, "If-then-else (Conditional operator)\r\n"
		"\tPerforms one action if a condition is true (like \"when\"), or another action (or set of actions) if the condition is false.  "
		"Note that this sexp only completes one of its branches once the condition has been determined; "
		"it does not come back later and evaluate the other branch if the condition happens to switch truth values.\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tBoolean expression to evaluate.\r\n"
		"\t2:\tActions to take if that expression becomes true.\r\n"
		"\tRest:\tActions to take if that expression becomes false.\r\n" },

	// Goober5000
	{ OP_SWITCH, "Switch (Conditional operator)\r\n"
		"\tPerforms one action according to the value of the first argument.  If the argument evaluates to 0, the first action is performed; if the argument is 1, the next action is performed, etc.  "
		"If the value is out of range, no action is performed at all.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tNumeric expression to evaluate.\r\n"
		"\tRest:\tList of actions, one of which (at most) will be taken according to the value of the expression.\r\n" },

	// Goober5000
	{ OP_FUNCTIONAL_IF_THEN_ELSE, "Functional If-then-else (Conditional operator)\r\n"
		"\tReturns the first number if the condition is true, and the second number otherwise.\r\n\r\n"
		"This sexp is very similar to the ?: ternary operator in C and other programming languages.  Unfortunately, "
		"due to limitations of the sexp system, it is not possible to create an equivalent sexp for string values.\r\n\r\n"
		"Note that all arguments will be evaluated, even though only one will be returned.\r\n\r\n"
		"Takes exactly 3 arguments...\r\n"
		"\t1:\tBoolean expression to evaluate.\r\n"
		"\t2:\tNumber to return if the expression is currently true.\r\n"
		"\t3:\tNumber to return if the expression is not currently true..\r\n" },

	// Goober5000
	{ OP_FUNCTIONAL_SWITCH, "Functional Switch (Conditional operator)\r\n"
		"\tReturns a number according to the value of the first argument.  If the argument evaluates to 0, the first of the following numbers is returned; if the argument is 1, the next number is performed, etc.  "
		"If the value is out of range, zero is returned.\r\n\r\n"
		"Note that all arguments will be evaluated, even though only one will be returned.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tNumeric expression to evaluate.\r\n"
		"\tRest:\tList of numeric expressions, one of which will be returned according to the current value of the first argument.\r\n" },

	// Goober5000
	{ OP_FUNCTIONAL_WHEN, "Functional When (Conditional operator)\r\n"
		"\tPerforms specified actions when a condition becomes true, then returns a boolean value.\r\n\r\n"
		"Takes 4 or more arguments...\r\n"
		"\t1:\tBoolean expression to be returned once the actions are taken\r\n"
		"\t2:\tWhether to evaluate the return value (the first boolean expression) before or after the actions are taken\r\n"
		"\t3:\tBoolean expression that must be true for actions to take place\r\n"
		"\tRest:\tActions to take when the condition (the second boolean expression) becomes true." },

	// Karajorma
	{ OP_DO_FOR_VALID_ARGUMENTS, "Do-for-valid-arguments (Conditional operator)\r\n"
		"\tPerforms specified actions once for each valid " SEXP_ARGUMENT_STRING " in the parent conditional. For example, a FREDder may use (do-for-valid-arguments (modify-variable @var (+ @var 1))) to count the number of valid arguments.\r\n\r\n"
		"\tThis operator must not be used to execute any SEXP that actually contains " SEXP_ARGUMENT_STRING ", as these are already being executed\r\n"
		"\tmultiple times without using Do-for-valid-arguments. Any use of "  SEXP_ARGUMENT_STRING " will \r\n" 
		"\tprevent execution of the entire SEXP unless it is nested inside another when(or every-time)-argument SEXP.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tActions to take." },

	// Karajorma
	{ OP_NUM_VALID_ARGUMENTS, "num-valid-arguments (Conditional operator)\r\n"
		"\tReturns the number of valid arguments in the argument list.\r\n\r\n"
		"Takes no arguments...\r\n"},

	// Goober5000
	{ OP_ANY_OF, "Any-of (Conditional operator)\r\n"
		"\tSupplies arguments for the " SEXP_ARGUMENT_STRING " special data item. The parent when-argument will fire if its condition holds for at least one of the given " SEXP_ARGUMENT_STRING " values; when it fires, all of its actions are evaluated for each " SEXP_ARGUMENT_STRING " for which the condition held.\r\n\r\n"
		"\tThis operator also accepts list containers with string data or map containers with string keys as arguments.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tAnything that could be used in place of " SEXP_ARGUMENT_STRING "." },

	// Goober5000
	{ OP_EVERY_OF, "Every-of (Conditional operator)\r\n"
		"\tSupplies arguments for the " SEXP_ARGUMENT_STRING " special data item. This operator behaves identically to any-of except that the parent when-argument will fire only when the condition holds for every argument, rather than at least one. Invalidated arguments are ignored.\r\n\r\n"
		"\tThis operator also accepts list containers with string data or map containers with string keys as arguments.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tAnything that could be used in place of " SEXP_ARGUMENT_STRING "." },

	// Goober5000
	{ OP_RANDOM_OF, "Random-of (Conditional operator)\r\n"
		"\tSupplies arguments for the " SEXP_ARGUMENT_STRING " special data item. One of the provided arguments is chosen at random and is used for every occurrence of " SEXP_ARGUMENT_STRING ". This choice is made only once, and the same " SEXP_ARGUMENT_STRING " is used every time. If you want it to change, use random-multiple-of. Note that invalidating this argument will cause the parent when-argument to never fire again.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tAnything that could be used in place of " SEXP_ARGUMENT_STRING "." },

	// Karajorma
	{ OP_RANDOM_MULTIPLE_OF, "Random-multiple-of (Conditional operator)\r\n"
		"\tSupplies arguments for the " SEXP_ARGUMENT_STRING " special data item. One of the provided arguments is chosen at random and is used for every occurrence of " SEXP_ARGUMENT_STRING ". Unlike random-of, the random selection is performed each time this SEXP is evaluated. It is safe to use invalidate-argument with this operator.\r\n"
		"\tThis operator also accepts list containers with string data or map containers with string keys as arguments.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tAnything that could be used in place of " SEXP_ARGUMENT_STRING "." },

	// Goober5000
	{ OP_NUMBER_OF, "Number-of (Conditional operator)\r\n"
		"\tSupplies arguments for the " SEXP_ARGUMENT_STRING " special data item.  Any [number] of the supplied arguments can satisfy the expression(s) "
		"in which " SEXP_ARGUMENT_STRING " is used.\r\n\r\n"
		"\tThis SEXP also accepts list containers with string data or map containers with string keys as arguments.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tNumber of arguments, as above\r\n"
		"\tRest:\tAnything that could be used in place of " SEXP_ARGUMENT_STRING "." },

	// Karajorma
	{ OP_IN_SEQUENCE, "In-Sequence (Conditional operator)\r\n"
		"\tSupplies arguments for the " SEXP_ARGUMENT_STRING " special data item.  The first argument in the list will be selected to satisfy the expression(s) "
		"in which " SEXP_ARGUMENT_STRING " is used. The same argument will be returned by all subsequent calls\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tAnything that could be used in place of " SEXP_ARGUMENT_STRING "." },

	// Goober5000
	{ OP_FOR_COUNTER, "For-Counter (Conditional operator)\r\n"
		"\tSupplies counter values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will count up from the start value to the stop value, and each value will be provided as an argument to the action operators.  "
		"The default increment is 1, but if the optional increment parameter is provided, the counter will increment by that number.  The stop value will be supplied if appropriate; e.g. counting from 0 to 10 by 2 will supply 0, 2, 4, 6, 8, and 10; "
		"but counting by 3 will supply 0, 3, 6, and 9.\r\n\r\n"
		"Note that the counter values are all treated as valid arguments, and it is impossible to invalidate a counter argument.  If you want to invalidate a counter value, use Any-of and list the values explicitly.\r\n\r\n"
		"This sexp will usually need to be accompanied by the string-to-int sexp, as the counter variables are provided in string format but are most useful in integer format.\r\n\r\n"
		"Takes 2 or 3 arguments...\r\n"
		"\t1:\tCounter start value\r\n"
		"\t2:\tCounter stop value\r\n"
		"\t3:\tCounter increment (optional)" },

	// Goober5000
	{ OP_FOR_SHIP_CLASS, "For-Ship-Class (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list all the ships of a certain class (or classes) in the mission, and each ship will be provided as an argument to the action operators.  "
		"Note that the ships are all treated as valid arguments, and it is impossible to invalidate a ship argument.  If you want to invalidate a ship, use Any-of and list the ships explicitly. Note that the parent when-argument will not fire if no matching ships are currently present, even if the conditions do not mention " SEXP_ARGUMENT_STRING ".\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tShip class from which to list ships\r\n" },

	// Goober5000
	{ OP_FOR_SHIP_TYPE, "For-Ship-Type (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list all the ships of a certain type (or types) in the mission, and each ship will be provided as an argument to the action operators.  "
		"Note that the ships are all treated as valid arguments, and it is impossible to invalidate a ship argument.  If you want to invalidate a ship, use Any-of and list the ships explicitly. Note that the parent when-argument will not fire if no matching ships are currently present, even if the conditions do not mention " SEXP_ARGUMENT_STRING ".\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tShip type from which to list ships\r\n" },

	// Goober5000
	{ OP_FOR_SHIP_TEAM, "For-Ship-Team (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list all the ships of a certain team (or teams) in the mission, and each ship will be provided as an argument to the action operators.  "
		"Note that the ships are all treated as valid arguments, and it is impossible to invalidate a ship argument.  If you want to invalidate a ship, use Any-of and list the ships explicitly. Note that the parent when-argument will not fire if no matching ships are currently present, even if the conditions do not mention " SEXP_ARGUMENT_STRING ".\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tTeam (IFF) from which to list ships\r\n" },

	// Goober5000
	{ OP_FOR_SHIP_SPECIES, "For-Ship-Species (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list all the ships of a certain species (or multiple species) in the mission, and each ship will be provided as an argument to the action operators.  "
		"Note that the ships are all treated as valid arguments, and it is impossible to invalidate a ship argument.  If you want to invalidate a ship, use Any-of and list the ships explicitly. Note that the parent when-argument will not fire if no matching ships are currently present, even if the conditions do not mention " SEXP_ARGUMENT_STRING ".\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tSpecies from which to list ships\r\n" },

	// Goober5000
	{ OP_FOR_PLAYERS, "For-Players (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list all the ships corresponding to valid players, and each ship will be provided as an argument to the action operators.  "
		"Note that the ships are all treated as valid arguments, and it is impossible to invalidate a ship argument.  If you want to invalidate a ship, use Any-of and list the ships explicitly. Note that the parent when-argument will not fire if no matching ships are currently present, even if the conditions do not mention " SEXP_ARGUMENT_STRING ".\r\n\r\n"
		"Takes no arguments.  Works in both single-player and multiplayer.\r\n" },

	// Goober5000
	{ OP_FOR_SUBSYSTEMS, "For-Subsystems (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list the subsystems on a specific ship, and each subsystem will be provided as an argument to the action operators.  "
		"If no subsystem type is specified, all subsystems are listed; otherwise the list will be restricted to subsystems that match any of the specified type(s).  "
		"Note that the subsystems are all treated as valid arguments, and it is impossible to invalidate a subsystem argument.  If you want to invalidate a subsystem, use Any-of and list the subsystems explicitly.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1:\tShip from which to list subsystems\r\n"
		"\tRest:\tSubsystem types\r\n" },

	// jg18
	{ OP_FOR_CONTAINER_DATA, "For-Container-Data (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list all data in a certain container (or multiple containers) in the mission, and each value will be provided as an argument to the action operators.  "
		"Note that the values are all treated as valid arguments, and it is impossible to invalidate a value argument.  If you want to invalidate a value, use Any-of and list the values explicitly.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tContainer whose data should be listed\r\n" },

	// jg18
	{ OP_FOR_MAP_CONTAINER_KEYS, "For-Map-Container-Keys (Conditional operator)\r\n"
		"\tSupplies values for the " SEXP_ARGUMENT_STRING " special data item.  This sexp will list all keys in a certain map container (or multiple map containers) in the mission, and each map key will be provided as an argument to the action operators.  "
		"Note that the map keys are all treated as valid arguments, and it is impossible to invalidate a key argument.  If you want to invalidate a key, use Any-of and list the keys explicitly.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tMap container whose keys should be listed\r\n" },

	// MageKing17
	{ OP_FIRST_OF, "First-of (Conditional operator)\r\n"
		"\tSupplies arguments for the " SEXP_ARGUMENT_STRING " special data item.  Up to [number] of the supplied arguments can satisfy the expression(s) "
		"in which " SEXP_ARGUMENT_STRING " is used. Essentially the same as any-of, but with a cap on the number of arguments that can be evaluated in a single call.\r\n\r\n"
		"\tThis SEXP also accepts list containers with string data or map containers with string keys as arguments.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tMaximum number of arguments that can be evaluated in a call.\r\n"
		"\tRest:\tAnything that could be used in place of " SEXP_ARGUMENT_STRING "." },

	// Goober5000
	{ OP_INVALIDATE_ARGUMENT, "Invalidate-argument (Conditional operator)\r\n"
		"\tRemoves an argument from future consideration as a " SEXP_ARGUMENT_STRING " special data item.\r\n"
		"\tFor argument-related SEXPs that accept containers as arguments, the retrieved arguments can't be individually invalidated.\r\n"
		"\tAdditionally, you can't invalidate individual container arguments.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tThe argument to remove from the preceding argument list." },

	// Karajorma
	{ OP_VALIDATE_ARGUMENT, "Validate-argument (Conditional operator)\r\n"
		"\tRestores an argument for future consideration as a " SEXP_ARGUMENT_STRING " special data item.\r\n"
		"\tIf the argument hasn't been previously invalidated, it will do nothing.\r\n"
		"\tFor argument-related SEXPs that accept containers as arguments, the retrieved arguments can't be individually validated.\r\n"
		"\tAdditionally, you can't validate individual container arguments.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tThe argument to restore to the preceding argument list." },

	// Karajorma
	{ OP_INVALIDATE_ALL_ARGUMENTS, "Invalidate-all-arguments (Conditional operator)\r\n"
		"\tRemoves all argument from future consideration as " SEXP_ARGUMENT_STRING " special data items.\r\n"
		"\tFor argument-related SEXPs that accept containers as arguments, container arguments will also be invalidated, but they can't be indvidiually validated.\r\n\r\n"
		"Takes no arguments." },

	// Karajorma
	{ OP_VALIDATE_ALL_ARGUMENTS, "Validate-all-arguments (Conditional operator)\r\n"
		"\tRestores all arguments for future consideration as " SEXP_ARGUMENT_STRING " special data items.\r\n"
		"\tIf the argument hasn't been previously invalidated, it will do nothing.\r\n"
		"\tFor argument-related SEXPs that accept containers as arguments, container arguments will also be validated, but they can't be indvidiually invalidated.\r\n\r\n"
		"Takes no arguments." },

	// Goober5000 - added wing capability
	{ OP_CHANGE_IFF, "Change IFF (Action operator)\r\n"
		"\tSets the specified ship(s) or wing(s) or all ships of a team to the specified team.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tTeam to change to (\"friendly\", \"hostile\" or \"unknown\").\r\n"
		"\tRest:\tName of ship, wing, or team to change team status of (ship/wing does not need to be in-mission)." },

	// Wanderer
	{ OP_CHANGE_IFF_COLOR, "Change IFF Color (Action operator)\r\n"
		"\tSets the specified ship(s) or wing(s) apparent color.\r\n"
		"Takes 6 or more arguments...\r\n"
		"\t1:\tName of the team from which target is observed from.\r\n"
		"\t2:\tName of the team of the observed target to receive the alternate color.\r\n"
		"\t3:\tRed color (value from 0 to 255).\r\n"
		"\t4:\tGreen color (value from 0 to 255).\r\n"
		"\t5:\tBlue color (value from 0 to 255).\r\n"
		"\tRest:\tName of ship or wing to change team status of (ship/wing does not need to be in-mission)." },

	// Goober5000
	{ OP_CHANGE_AI_CLASS, "Change AI Class (Action operator)\r\n"
		"\tSets the specified ship or ship subsystem(s) to the specified ai class.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tAI Class to change to (\"None\", \"Coward\", \"Lieutenant\", etc.)\r\n"
		"\t2:\tName of ship to change AI class of (ship must be in-mission)\r\n"
		"\tRest:\tName of subsystem(s) to change AI class of (optional)" },

	{ OP_MODIFY_VARIABLE, "Modify-variable (Misc. operator)\r\n"
		"\tModifies variable to specified value\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of Variable.\r\n"
		"\t2:\tValue to be set." },

	{ OP_GET_VARIABLE_BY_INDEX, "get-variable-by-index (originally variable-array-get)\r\n"
		"\tGets the value of the variable specified by the given index.  This is an alternate way "
		"to access variables rather than by their names, and it enables cool features such as "
		"arrays and pointers.\r\n\r\nPlease note that only numeric variables are supported.  Any "
		"attempt to access a string variable will result in a value of SEXP_NAN_FOREVER being returned.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tIndex of variable, from 0 to " SCP_TOKEN_TO_STR(MAX_SEXP_VARIABLES_1) "." },

	{ OP_SET_VARIABLE_BY_INDEX, "set-variable-by-index (originally variable-array-set)\r\n"
		"\tSets the value of the variable specified by the given index.  This is an alternate way "
		"to modify variables rather than by their names, and it enables cool features such as "
		"arrays and pointers.\r\n\r\nIn contrast to get-variable-by-index, note that this sexp "
		"*does* allow the modification of string variables.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tIndex of variable, from 0 to " SCP_TOKEN_TO_STR(MAX_SEXP_VARIABLES_1) ".\r\n"
		"\t2:\tValue to be set." },

	{ OP_COPY_VARIABLE_FROM_INDEX, "copy-variable-from-index\r\n"
		"\tRetrieves the value of the variable specified by the given index and stores it in another variable.  "
		"This is very similar to get-variable-by-index, except the result is stored in a new variable rather than "
		"being returned by value.  One important difference is that this sexp can be used to copy string variables as well as numeric variables.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tIndex of source variable, from 0 to " SCP_TOKEN_TO_STR(MAX_SEXP_VARIABLES_1) ".\r\n"
		"\t2:\tDestination variable.  The type of this variable must match the type of the variable referenced by the index." },

	{ OP_COPY_VARIABLE_BETWEEN_INDEXES, "copy-variable-between-indexes\r\n"
		"\tRetrieves the value of the variable specified by the first index and stores it in the variable specified by the second index.  The first variable is not modified.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tIndex of source variable, from 0 to " SCP_TOKEN_TO_STR(MAX_SEXP_VARIABLES_1) ".\r\n"
		"\t2:\tIndex of destination variable, from 0 to " SCP_TOKEN_TO_STR(MAX_SEXP_VARIABLES_1) ".  The types of both variables must match." },

	// Karajorma/jg18
	{ OP_CONTAINER_ADD_TO_LIST, "add-to-list\r\n"
		"\tAdds new entries to a list container.\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tName of the list container.\r\n"
		"\t2:\tIndicates whether to add the data to the start or the end of the list. True means at the end, false means at the start.\r\n"
		"\tRest:\tThe data to be added." },

	// Karajorma/jg18
	{ OP_CONTAINER_REMOVE_FROM_LIST, "remove-from-list\r\n"
		"\tRemoves entries from a list container.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the list container.\r\n"
		"\tRest:\tThe data to be removed." },

	// Karajorma/jg18
	{ OP_CONTAINER_ADD_TO_MAP, "add-to-map\r\n"
		"\tAdds new entries to a map container.\r\n"
		"\tIf the map container already has data associated with the provided key, the data is replaced.\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tName of the map container.\r\n"
		"\t2:\tKey that will be used to retrieve the data stored in the map.\r\n"
		"\t3:\tData that will be associated with the provided key.\r\n"
		"\tRest:\tAdditional pairs of arguments of a key and its associated data." },

	// Karajorma/jg18
	{ OP_CONTAINER_REMOVE_FROM_MAP, "remove-from-map\r\n"
		"\tRemoves entries by their keys from a map container.\r\n"
		"\tIgnores keys that are not associated with data in the map container.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the map container.\r\n"
		"\tRest:\tKey whose data will be removed." },

	// Karajorma/jg18
	{ OP_CONTAINER_GET_MAP_KEYS, "get-map-keys\r\n"
		"\tCopies a map container's keys in no particular order into a list container.\r\n"
		"\tThe list container's data type must match the map container's key type.\r\n"
		"\tBy default, the list container is cleared before the keys are copied.\r\n\r\n"
		"Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of the map container.\r\n"
		"\t2:\tName of the list container.\r\n"
		"\t3:\t(Optional) When true, the list container's current contents are deleted. When false, the keys are appended to the end of the list." },

	// Karajorma/jg18
	{ OP_CLEAR_CONTAINER, "clear-container\r\n"
		"\tDeletes the current contents of one or more containers.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of the container(s) to clear.\r\n" },

	// jg18
	{ OP_COPY_CONTAINER, "copy-container\r\n"
		"\tCopies a container's contents to another container.\r\n"
		"\tThe two containers' types must match, except their persistence rules can be different.\r\n"
		"\tBy default, the second container is cleared before copying takes place.\r\n\r\n"
		"Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of the container to copy from.\r\n"
		"\t2:\tName of the container to copy to.\r\n"
		"\t3:\t(Optional) When true, the second container's contents are deleted before copying." },

	// jg18
	{ OP_APPLY_CONTAINER_FILTER, "apply-container-filter\r\n"
		"\tRemoves values from a container using a list container as a filter.\r\n"
		"\tAll values in the 'filter' container will be removed from the first container. The 'filter' container is left unchanged.\r\n"
		"\tIf the first container is a map container, then the filter is applied to its keys and not its data.\r\n"
		"\tThe filter's data type must match the type of the values to be removed from the first container.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of the container to apply the filter to.\r\n"
		"\t2:\tName of the list container to be used as the filter." },

	{ OP_PROTECT_SHIP, "Protect ship (Action operator)\r\n"
		"\tProtects a ship from being attacked by any enemy ship.  Any ship "
		"that is protected will not come under enemy fire.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship(s) to protect (ship does not need to be in-mission)." },

	{ OP_UNPROTECT_SHIP, "Unprotect ship (Action operator)\r\n"
		"\tUnprotects a ship from being attacked by any enemy ship.  Any ship "
		"that is not protected can come under enemy fire.  This function is the opposite "
		"of protect-ship.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship(s) to unprotect (ship does not need to be in-mission)." },

	{ OP_BEAM_PROTECT_SHIP, "Beam Protect ship (Action operator)\r\n"
		"\tProtects a ship from being attacked with beam weapon.  Any ship "
		"that is beam protected will not come under enemy beam fire.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship(s) to protect (ship does not need to be in-mission)." },

	{ OP_BEAM_UNPROTECT_SHIP, "Beam Unprotect ship (Action operator)\r\n"
		"\tUnprotects a ship from being attacked with beam weapon.  Any ship "
		"that is not beam protected can come under enemy beam fire.  This function is the opposite "
		"of beam-protect-ship.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship(s) to unprotect (ship does not need to be in-mission)." },

	// Goober5000
	{ OP_TURRET_PROTECT_SHIP, "Turret Protect ship (Action operator)\r\n"
		"\tProtects a ship from being attacked with a turret weapon of a given type.  Any ship "
		"that is turret protected will not come under enemy fire from that type of turret, though it may come under fire by other turrets.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tType of turret (currently supported types are \"beam\", \"flak\", \"laser\", and \"missile\")\r\n"
		"\tRest:\tName of ship(s) to protect (ship does not need to be in-mission)." },

	// Goober5000
	{ OP_TURRET_UNPROTECT_SHIP, "Turret Unprotect ship (Action operator)\r\n"
		"\tUnprotects a ship from being attacked with a turret weapon of a given type.  Any ship "
		"that is not turret protected can come under enemy fire from that type of turret.  This function is the opposite "
		"of turret-protect-ship.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tType of turret (currently supported types are \"beam\", \"flak\", \"laser\", and \"missile\")\r\n"
		"\tRest:\tName of ship(s) to unprotect (ship does not need to be in-mission)." },

	{ OP_SEND_MESSAGE, "Send message (Action operator)\r\n"
		"\tSends a message to the player.  Can be sent by a ship, wing, or special "
		"source.  To send it from a special source, make the first character of the first "
		"argument a \"#\".\r\n\r\n"
		"\tHigh and Normal priorities are not interrupted by anything and take precedence over built-in messages.\r\n"
		"\tLow priority may be interrupted by built-in messages.\r\n"
		"\tIf the sender is destroyed, Normal and Low priority messages will not be sent.  High priority messages will be reassigned to Command and will be sent if one of the following is true:\r\n"
		"\t1. The message has no persona\r\n"
		"\t2. The message has a wingman persona, the message's filename starts with 1_ through 6_, and there exists a corresponding filename starting with TC_ that can be used instead\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1:\tName of who the message is from.\r\n"
		"\t2:\tPriority of message (\"Low\", \"Normal\" or \"High\").\r\n"
		"\t3:\tName of message (from message list)." },

	{ OP_SEND_BUILTIN_MESSAGE, "Send builtin message (Action operator)\r\n"
		"\tSends a builtin message (from messages.tbl) to the player. The message is sent exactly the same way "
		"as it would be if it were sent automatically; i.e. the sender's persona and species are considered, as "
		"is the mission mood and any message filters, and disable-builtin-messages is respected.\r\n"
		"Due to technical limitations, all #special sources are treated as #Command.\r\n"
		"It is legal to send a message from a source which can't normally send it, so long as appropriate messages "
		"are defined in messages.tbl. For example, this can be used to make a cruiser send a Help message.\r\n\r\n"
		"Takes 4 or more arguments...\r\n"
		"\t1:\tThe type of message to send.\r\n"
		"\t2:\tThe message's subject (used with message filters). If you don't know what this means, set it to <none>.\r\n"
		"\t3:\tPick a random sender? If this is false, the first available sender will be used.\r\n"
		"\tRest:\tWho should send the message - a ship, a wing, #Command, or <any wingman>.\r\n"
	},

	// Karajorma	
	{ OP_ENABLE_BUILTIN_MESSAGES, "Enable builtin messages (Action operator)\r\n"
		"\tTurns the built in messages sent by command or pilots on\r\n"
		"Takes 0 or more arguments...\r\n"
		"If no arguments are supplied any ships not given individual silence orders will be able\r\n"
		"to send built in messages. Command will also be unsilenced\r\n"
		"Using the Any Wingman option cancels radio silence for all ships in wings.\r\n"
		"\tAll:\tName of ship to allow to talk (ship does not need to be in-mission)." },
		
	// Karajorma
	{ OP_DISABLE_BUILTIN_MESSAGES, "Disable builtin messages (Action operator)\r\n"
		"\tTurns the built in messages sent by command or pilots off\r\n"
		"Takes 0 or more arguments....\r\n"
		"If no arguments are supplied all built in messages are disabled.\r\n"
		"Using the Any Wingman option silences for all ships in wings.\r\n"
		"\tAll:\tName of ship to be silenced (ship does not need to be in-mission)." },

	// Karajorma	
	{ OP_SET_MISSION_MOOD, "set Mission Mood (Action operator)\r\n"
		"\tSets the mood of the mission, this affects the choice of builtin messages sent by wingmen\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tMission mood (from messages.tbl) to use." },

	// Karajorma	
	{ OP_SET_PERSONA, "Set Persona (Action operator)\r\n"
		"\tSets the persona of the supplied ship to the persona supplied\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tPersona to use.\r\n"
		"\tRest:\tName of the ship (ship must be in-mission)." },

	{ OP_SELF_DESTRUCT, "Self destruct (Action operator)\r\n"
		"\tCauses the specified ship(s) to self destruct.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship to self destruct (ship must be in-mission)." },

	{ OP_NEXT_MISSION, "Next Mission (Action operator)\r\n"
		"\tThe next mission operator is used for campaign branching in the campaign editor.  "
		"It specifies which mission should played be next in the campaign.  This operator "
		"generally follows a 'when' or 'cond' statement in the campaign file.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of mission (filename) to proceed to." },

	{ OP_CLEAR_GOALS, "Clear goals (Action operator)\r\n"
		"\tClears the goals for the specified ships and/or wings.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship or wing (ship/wing must be in-mission)." },

	{ OP_ADD_GOAL, "Add goal (Action operator)\r\n"
		"\tAdds a goal to a ship or wing.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship or wing to add goal to (ship/wing must be in-mission).\r\n"
		"\t2:\tGoal to add." },

	// Goober5000
	{ OP_REMOVE_GOAL, "Remove goal (Action operator)\r\n"
		"\tRemoves a goal from a ship or wing.  Note that, by default, only the type of goal and the priority are matched.  This operator "
		"does not distinguish between, for example, two different waypoint goals.\r\n\r\n"
		"Takes 2 to 4 arguments...\r\n"
		"\t1:\tName of ship or wing to remove goal from (ship/wing must be in-mission).\r\n"
		"\t2:\tGoal to remove.\r\n"
		"\t3:\tWhether to remove all matching goals (optional; defaults to false; if false, only the first matching goal will be removed).\r\n"
		"\t4:\tWhether to ignore the priority when matching a goal (optional).\r\n"
	},

	{ OP_SABOTAGE_SUBSYSTEM, "Sabotage subystem (Action operator)\r\n"
		"\tReduces the specified subsystem integrity by the specified percentage.  "
		"If the percentage strength of the subsystem (after completion) is less than 0%, the "
		"subsystem strength is set to 0%.\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n"
		"\t2:\tName of subsystem to sabotage.\r\n"
		"\t3:\tPercentage to reduce subsystem integrity by." },

	{ OP_REPAIR_SUBSYSTEM, "Repair Subystem (Action operator)\r\n"
		"\tIncreases the specified subsystem integrity by the specified percentage.  "
		"If the percentage strength of the subsystem (after completion) is greater than 100%, the "
		"subsystem strength is set to 100%.\r\n\r\n"
		"Takes 3 to 5 arguments...\r\n"
		"\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n"
		"\t2:\tName of subsystem to repair.\r\n"
		"\t3:\tPercentage to increase subsystem integrity by.\r\n"
		"\t4:\tRepair submodel if it exists.  Optional argument that defaults to true.\r\n"
		"\t5:\tIf we are repairing submodels and an ancestor submodel was totally destroyed, repair that too.  Optional argument that defaults to true.\r\n" },

	{ OP_SET_SUBSYSTEM_STRNGTH, "Set Subsystem Strength (Action operator)\r\n"
		"\tSets the specified subsystem to the the specified percentage.  "
		"If the percentage specified is < 0, strength is set to 0.  If the percentage is "
		"greater than 100%, the subsystem strength is set to 100%.\r\n\r\n"
		"Takes 3 to 5 arguments...\r\n"
		"\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n"
		"\t2:\tName of subsystem to set strength.\r\n"
		"\t3:\tPercentage to set subsystem integrity to.\r\n" 
		"\t4:\tRepair submodel if it exists.  Optional argument that defaults to true.\r\n"
		"\t5:\tIf we are repairing submodels and an ancestor submodel was totally destroyed, repair that too.  Optional argument that defaults to true.\r\n" },

	{ OP_DESTROY_SUBSYS_INSTANTLY, "destroy-subsys-instantly\r\n"
		"\tDestroys the specified subsystems without effects.  "
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n"
		"\tRest:\tName of subsystem to destroy.\r\n"},

	{ OP_INVALIDATE_GOAL, "Invalidate goal (Action operator)\r\n"
		"\tMakes a mission goal invalid, which causes it to not show up on mission goals "
		"screen, or be evaluated.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of mission goal to invalidate." },

	{ OP_VALIDATE_GOAL, "Validate goal (Action operator)\r\n"
		"\tMakes a mission goal valid again, so it shows up on mission goals screen.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of mission goal to validate." },

	{ OP_SEND_RANDOM_MESSAGE, "Send random message (Action operator)\r\n"
		"\tSends a random message to the player from those supplied.  Can be sent by a "
		"ship, wing, or special source.  To send it from a special source, make the first "
		"character of the first argument a \"#\".\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tName of who the message is from.\r\n"
		"\t2:\tPriority of message (\"Low\", \"Normal\" or \"High\").\r\n"
		"\tRest:\tName of message (from message list)." },

	{ OP_TRANSFER_CARGO, "Transfer Cargo (Action operator)\r\n"
		"\tTransfers the cargo from one ship to another ship.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship that cargo is being transferred from (ship must be in-mission).\r\n"
		"\t2:\tName of ship that cargo is being transferred to (ship must be in-mission)." },

	{ OP_EXCHANGE_CARGO, "Exchange Cargo (Action operator)\r\n"
		"\tExchanges the cargos of two ships.  If one of the two ships contains no cargo, "
		"the cargo is transferred instead.\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of one of the ships (ship must be in-mission).\r\n"
		"\t2:\tName of the other ship (ship must be in-mission)." },

	// Goober5000
	{ OP_SET_CARGO, "Set Cargo (Action operator)\r\n"
		"\tSets the cargo on a ship or ship subsystem.  The cargo-no-deplete flag status is carried through to the new cargo.\r\n"
		"Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of the cargo\r\n"
		"\t2:\tName of the ship (does not need to be in-mission)\r\n"
		"\t3:\tName of the ship subsystem (optional)" },

	// Goober5000
	{ OP_IS_CARGO, "Is Cargo (Status operator)\r\n"
		"\tChecks whether the specified ship or ship subsystem contains a particular cargo.\r\n"
		"Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of the cargo\r\n"
		"\t2:\tName of the ship (ship does not need to be in-mission)\r\n"
		"\t3:\tName of the ship subsystem (Optional. Ship must be in-mission)" },

	// Goober5000
	{ OP_CHANGE_SOUNDTRACK, "change-soundtrack\r\n"
		"\tChanges the mission music.  Takes 1 argument...\r\n"
		"\t1: Name of the music selection (taken from music.tbl)" },

	// Goober5000
	{ OP_PLAY_SOUND_FROM_TABLE, "play-sound-from-table\r\n"
		"\tPlays a sound listed in the Game Sounds section of sounds.tbl.  Note that if the sound is a non-3D sound (if the min and max radius are 0, or unspecified), the sound will just play without being fixed at a particular position in space.  "
		"In this case, the origin coordinates will be ignored.  (A better design would have put the sound index first and made the origin arguments optional, but the difference between 2D and 3D sounds was not understood at the time.  C'est la vie.)\r\n\r\n"
		"Takes 4 arguments...\r\n"
		"\t1: Origin X\r\n"
		"\t2: Origin Y\r\n"
		"\t3: Origin Z\r\n"
		"\t4: Sound (index into sounds.tbl or name of the sound entry)" },

	// Goober5000
	{ OP_PLAY_SOUND_FROM_FILE, "play-sound-from-file\r\n"
		"\tPlays a sound, such as a music soundtrack, from a file.  Multiple sounds can be played with this sexp by assigning each sound to a music handle stored in a variable.\r\n"
		"Takes 1 to 4 arguments...\r\n"
		"\t1: Sound (file name)\r\n"
		"\t2: Enter a non-zero number to loop. default is off (optional).\r\n"
		"\t3: Enter a non-zero number to use environment effects. default is off (optional).\r\n"
		"\t4: Numeric variable in which to store the music handle (optional).  If no variable is specified, the 'default' handle is used.  "
		"Only one 'default' track can be played at a time, but multiple variable-managed tracks can be played.  NOTE: Handles are not globally unique.  If a sound "
		"finishes playing, its handle may be reused for another sound played later in the mission.\r\n"
	},

	// Goober5000
	{ OP_CLOSE_SOUND_FROM_FILE, "close-sound-from-file\r\n"
		"\tCloses the currently playing sound started by play-sound-from-file, if there is any.  Takes 0 to 2 arguments...\r\n"
		"\t1: Fade (optional; default is true)\r\n"
		"\t2: Numeric variable containing a music handle (optional).  If no variable is specified, the 'default' handle is used."
	},

	// Goober5000
	{ OP_PAUSE_SOUND_FROM_FILE, "pause-sound-from-file\r\n"
		"\tPauses or unpauses the currently playing sound started by play-sound-from-file, if there is any.  Takes 1 or 2 arguments...\r\n"
		"\t1: Boolean - True to pause, False to unpause\r\n"
		"\t2: Numeric variable containing a music handle (optional).  If no variable is specified, the 'default' handle is used."
	},

	// Taylor
	{ OP_SET_SOUND_ENVIRONMENT, "set-sound-environment\r\n"
		"Sets the EAX environment for all sound effects.  Optionally sets one or more parameters specific to the environment.  Takes 1 or more arguments...\r\n"
		"\t1:\tSound environment name (a value of \"" SEXP_NONE_STRING "\" will disable the effects)\r\n"
		"\t2:\tEnvironment option (optional)\r\n"
		"\t3:\tEnvironment value x 1000, e.g. 10 is 0.01 (optional)\r\n"
		"Use Add-Data to specify additional environment options in repeating option-value pairs, just like Send-Message-List can have additional messages in source-priority-message-delay groups.\r\n\r\n"
		"IMPORTANT: each additional option in the list MUST HAVE two entries; any option without the two proper fields will be ignored, as will any successive options." },

	// Taylor
	{ OP_UPDATE_SOUND_ENVIRONMENT, "update-sound-environment\r\n"
		"Updates the current EAX environment with new values.  Takes 2 or more arguments...\r\n"
		"\t1:\tEnvironment option\r\n"
		"\t2:\tEnvironment value x 1000, e.g. 10 is 0.01\r\n"
		"Use Add-Data to specify additional environment options in repeating option-value pairs, just like Send-Message-List can have additional messages in source-priority-message-delay groups.\r\n\r\n"
		"IMPORTANT: Each additional option in the list MUST HAVE two entries; any option without the two proper fields will be ignored, as will any successive options." },
	
	//The E
	{ OP_ADJUST_AUDIO_VOLUME, "adjust-audio-volume\r\n"
		"Adjusts the relative volume of one sound type. Takes 1 to 3 arguments....\r\n"
		"\t1:\tSound Type to adjust, either Music, Voice or Effects. Will act as a reset for the given category, if no other argument present\r\n"
		"\t2:\tPercentage of the users' settings to adjust to (optional), 0 will be silence, 100 means the maximum volume as set by the user\r\n"
		"\t3:\tFade time (optional), time in milliseconds to adjust the volume"},

	// Goober5000
	{ OP_SET_EXPLOSION_OPTION, "set-explosion-option\r\n"
		"Sets an explosion option on a particular ship.  Takes 3 or more arguments...\r\n"
		"\t1:\tShip name (ship must be in-mission)\r\n"
		"\t2:\tExplosion option\r\n"
		"\t3:\tExplosion value (for shockwave speed, 0 will produce no shockwave; for death roll time, 0 will use the default time)\r\n"
		"Use Add-Data to specify additional explosion options in repeating option-value pairs, just like Send-Message-List can have additional messages in source-priority-message-delay groups.\r\n\r\n"
		"IMPORTANT: Each additional option in the list MUST HAVE two entries; any option without the two proper fields will be ignored, as will any successive options." },

	// MjnMixael
	{ OP_CREATE_BOLT, "create-bolt\r\n"
		"\tCreates a lightning bolt at the specified location  "
		"Takes 8 arguments...\r\n"
		"\t1:  The bolt type to create\r\n"
		"\t2:  Origin X\r\n"
		"\t3:  Origin Y\r\n"
		"\t4:  Origin Z\r\n"
		"\t5:  Strike X\r\n"
		"\t6:  Strike Y\r\n"
		"\t7:  Strike Z\r\n"
		"\t8:  (Optional) Whether or not to play the default lightning sounds. Defaults to true\r\n" },
			
	// Goober5000
	{ OP_EXPLOSION_EFFECT, "explosion-effect\r\n"
		"\tCauses an explosion at a given origin, with the given parameters.  "
		"Takes 11 to 14 arguments...\r\n"
		"\t1:  Origin X\r\n"
		"\t2:  Origin Y\r\n"
		"\t3:  Origin Z\r\n"
		"\t4:  Damage\r\n"
		"\t5:  Blast force\r\n"
		"\t6:  Size of explosion (if 0, explosion will not be visible)\r\n"
		"\t7:  Inner radius to apply damage (if 0, explosion will not be visible)\r\n"
		"\t8:  Outer radius to apply damage (if 0, explosion will not be visible)\r\n"
		"\t9:  Shockwave speed (if 0, there will be no shockwave)\r\n"
		"\t10: Fireball Type (unique ID of a fireball entry, OR a number: 0 = medium explosion [default, 1st in table], 1 = large explosion 1 [4th in table], 2 = large explosion 2 [5th in table], 3 or greater the respective entry in fireball.tbl)\r\n"
		"\t11: Sound (index into sounds.tbl or name of the sound entry)\r\n"
		"\t12: EMP intensity (optional)\r\n"
		"\t13: EMP duration in seconds (optional)\r\n"
		"\t14: Whether to use the full EMP time for capship turrets (optional, defaults to false)" },

	// Goober5000
	{ OP_WARP_EFFECT, "warp-effect\r\n"
		"\tCauses a subspace warp effect at a given origin, facing toward a given location, with the given parameters.\r\n"
		"Takes 12 to 14 arguments...\r\n"
		"\t1:  Origin X\r\n"
		"\t2:  Origin Y\r\n"
		"\t3:  Origin Z\r\n"
		"\t4:  Location X\r\n"
		"\t5:  Location Y\r\n"
		"\t6:  Location Z\r\n"
		"\t7:  Radius\r\n"
		"\t8:  Duration in seconds (values smaller than 4 are ignored)\r\n"
		"\t9:  Warp opening sound (index into sounds.tbl or name of the sound entry)\r\n"
		"\t10: Warp closing sound (index into sounds.tbl or name of the sound entry)\r\n"
		"\t11: Fireball Type (unique ID of a fireball entry, OR a number: 0 for standard blue [default], 1 for Knossos green, 2 or greater the respective entry in fireball.tbl)\r\n"
		"\t12: Shape (0 for 2-D [default], 1 for 3-D)\r\n"
		"\t13: Duration of the opening effect, in milliseconds (and also the closing effect if the next argument is not specified)\r\n"
		"\t14: Duration of the closing effect, in milliseconds\r\n"	},

	// Goober5000
	{ OP_SET_OBJECT_FACING, "set-object-facing\r\n"
		"\tSets an object's orientation to face the specified coordinates.  "
		"Takes 4 arguments...\r\n"
		"\t1: The name of a ship or wing (ship/wing does not need to be in-mission).\r\n"
		"\t2: The X coordinate to face.\r\n"
		"\t3: The Y coordinate to face.\r\n"
		"\t4: The Z coordinate to face.\r\n"
		"\t5: Turn time in milliseconds (optional)\r\n"
		"\t6: Bank (optional). Enter a non-zero value to enable banking.\r\n"
		"Note that if Turn time is specified, this needs to be called once per frame, as the rotation is reset every frame.\r\n"
		"Note that Turn time is the time for the object to rotate a full revolution, *not* how long it will take to do this turn." },

	// Goober5000
	{ OP_SET_OBJECT_FACING_OBJECT, "set-object-facing-object\r\n"
		"\tSets an object's orientation to face the specified object.  "
		"Takes 2 arguments...\r\n"
		"\t1: The name of a ship or wing (ship/wing does not need to be in-mission).\r\n"
		"\t2: The object to face (object does not need to be in-mission).\r\n"
		"\t3: Turn time in milliseconds (optional)\r\n"
		"\t4: Bank (optional). Enter a non-zero value to enable banking.\r\n"
		"Note that if Turn time is specified, this needs to be called once per frame, as the rotation is reset every frame.\r\n"
		"Note that Turn time is the time for the object to rotate a full revolution, *not* how long it will take to do this turn." },

	// Wanderer
	{ OP_SHIP_MANEUVER, "ship-maneuver\r\n"
		"\tCombines the effects of the ship-rot-maneuver and ship-lat-maneuver sexps.  Takes 10 or 11 arguments:\r\n"
		"\t1: The name of a ship or wing (ship/wing must be in-mission).\r\n"
		"\t2: Duration of the maneuver, in milliseconds.  Specifying 0 or 1 indicates infinite duration.\r\n"
		"\t3: Heading movement velocity, as a percentage (-100 to 100) of the tabled maximum heading velocity, or 0 to not modify the ship's current value\r\n"
		"\t4: Pitch movement velocity, as a percentage (-100 to 100) of the tabled maximum pitch velocity, or 0 to not modify the ship's current value\r\n"
		"\t5: Bank movement velocity, as a percentage (-100 to 100) of the tabled maximum bank velocity, or 0 to not modify the ship's current value\r\n"
		"\t6: Whether to apply all of the rotational velocity values even if any of them are 0\r\n"
		"\t7: Vertical movement velocity, as a percentage (-100 to 100) of the tabled maximum vertical velocity, or 0 to not modify the ship's current value\r\n"
		"\t8: Sideways movement velocity, as a percentage (-100 to 100) of the tabled maximum sideways velocity, or 0 to not modify the ship's current value\r\n"
		"\t9: Forward movement velocity, as a percentage (-100 to 100) of the tabled maximum forward velocity, or 0 to not modify the ship's current value\r\n"
		"\t10: Whether to apply all of the lateral velocity values even if any of them are 0\r\n"
		"\t11: Maneuver flags (optional): a bitfield with any of the following options:\r\n"
		"\t\t1 or 2^0: don't bank when changing heading\r\n"
		"\t\t2 or 2^1: allow maneuvers exceeding tabled maximums (outside the -100 to 100 range)\r\n"
		"\t\t4 or 2^2: instantaneously jump to the goal velocity\r\n"
		"\t\t8 or 2^3: keeps old, but still active, maneuver values that were not overwritten\r\n"
	},

	// Wanderer
	{ OP_SHIP_ROT_MANEUVER, "ship-rot-maneuver\r\n"
		"\tCauses a ship to move in a rotational direction.  For the purposes of this sexp, this means the ship rotates along its own heading, pitch, or bank axis (or a combination of axes) without regard to normal ship rotation rules.  "
		"You may find it necessary to disable the ship AI (e.g. by issuing a play-dead order) before running this sexp.\r\n\r\n"
		"Takes 6 or 7 arguments:\r\n"
		"\t1: The name of a ship or wing (ship/wing must be in-mission).\r\n"
		"\t2: Duration of the maneuver, in milliseconds.  Specifying 0 or 1 indicates infinite duration.\r\n"
		"\t3: Heading movement velocity, as a percentage (-100 to 100) of the tabled maximum heading velocity, or 0 to not modify the ship's current value\r\n"
		"\t4: Pitch movement velocity, as a percentage (-100 to 100) of the tabled maximum pitch velocity, or 0 to not modify the ship's current value\r\n"
		"\t5: Bank movement velocity, as a percentage (-100 to 100) of the tabled maximum bank velocity, or 0 to not modify the ship's current value\r\n"
		"\t6: Whether to apply all of the above velocity values even if any of them are 0\r\n"
		"\t7: Maneuver flags (optional): a bitfield with any of the following options:\r\n"
		"\t\t1 or 2^0: don't bank when changing heading\r\n"
		"\t\t2 or 2^1: allow maneuvers exceeding tabled maximums (outside the -100 to 100 range)\r\n"
		"\t\t4 or 2^2: instantaneously jump to the goal velocity\r\n"
		"\t\t8 or 2^3: keeps old, but still active, maneuver values that were not overwritten\r\n"
	},
	
	// Wanderer
	{ OP_SHIP_LAT_MANEUVER, "ship-lat-maneuver\r\n"
		"\tCauses a ship to move in a lateral direction.  For the purposes of this sexp, this means the ship translates along its own X, Y, or Z axis (or a combination of axes) without regard to normal ship movement rules.  "
		"You may find it necessary to disable the ship AI (e.g. by issuing a play-dead order) before running this sexp.\r\n\r\n"
		"Takes 6 or 7 arguments:\r\n"
		"\t1: The name of a ship or wing (ship/wing must be in-mission).\r\n"
		"\t2: Duration of the maneuver, in milliseconds.  Specifying 0 or 1 indicates infinite duration.\r\n"
		"\t3: Vertical movement velocity, as a percentage (-100 to 100) of the tabled maximum vertical velocity, or 0 to not modify the ship's current value\r\n"
		"\t4: Sideways movement velocity, as a percentage (-100 to 100) of the tabled maximum sideways velocity, or 0 to not modify the ship's current value\r\n"
		"\t5: Forward movement velocity, as a percentage (-100 to 100) of the tabled maximum forward velocity, or 0 to not modify the ship's current value\r\n"
		"\t6: Whether to apply all of the above velocity values even if any of them are 0\r\n"
		"\t7: Maneuver flags (optional): a bitfield with any of the following options:\r\n"
		"\t\t1 or 2^0: don't bank when changing heading (which won't have any affect for lat-maneuver)\r\n"
		"\t\t2 or 2^1: allow maneuvers exceeding tabled maximums (outside the -100 to 100 range)\r\n"
		"\t\t4 or 2^2: instantaneously jump to the goal velocity\r\n"
		"\t\t8 or 2^3: keeps old, but still active, maneuver values that were not overwritten\r\n"
	},

	// Goober5000
	{ OP_SHIP_TAG, "ship-tag\r\n"
		"\tTags a ship.  Takes 3 or 8 arguments...\r\n"
		"\t1: The name of a ship (ship must be in-mission).\r\n"
		"\t2: The tag level (currently 1, 2, or 3).\r\n"
		"\t3: The tag time (in seconds).\r\n"
		"\t4: A SSM missile (optional - used only for TAG-C).\r\n"
		"\t5: The X origin of the SSM missile (optional - used only for TAG-C).\r\n"
		"\t6: The Y origin of the SSM missile (optional - used only for TAG-C).\r\n"
		"\t7: The Z origin of the SSM missile (optional - used only for TAG-C).\r\n" 
		"\t8: The team the SSM missile belongs to (optional - used only for TAG-C).\r\n" },

	{ OP_SHIP_UNTAG, "ship-untag\r\n"
		"\tUntags a ship.  Takes 1 argument...\r\n"
		"\t1: The name of a ship (ship must be in-mission).\r\n" },

/*	made obsolete by Goober5000 - it only works in debug builds anyway
	{ OP_INT3, "Error (Debug directive)\r\n"
		"Causes the game to halt with an error." },
*/

	{ OP_AI_CHASE, "Ai-chase (Ship goal)\r\n"
		"\tCauses the specified ship to chase and attack the specified target.\r\n\r\n"
		"Takes 2 to 4 arguments...\r\n"
		"\t1:\tName of ship to chase.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_CHASE_WING, "Ai-chase wing (Ship goal)\r\n"
		"\tCauses the specified ship to chase and attack the specified target.\r\n\r\n"
		"Takes 2 to 4 arguments...\r\n"
		"\t1:\tName of wing to chase.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_CHASE_SHIP_CLASS, "Ai-chase ship class (Ship goal)\r\n"
		"\tCauses the specified ship to chase and attack the specified target ship class.\r\n\r\n"
		"Takes 2 to 4 arguments...\r\n"
		"\t1:\tName of ship class to chase.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_CHASE_SHIP_TYPE, "Ai-chase ship type (Ship goal)\r\n"
		"\tCauses the specified ship to chase and attack a target ship type.\r\n\r\n"
		"Takes 2 to 4 arguments...\r\n"
		"\t1:\tName of ship type to chase.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_CHASE_ANY, "Ai-chase-any (Ship goal)\r\n"
		"\tCauses the specified ship to chase and attack any ship on the opposite team.\r\n\r\n"
		"Takes 1 or 2 arguments...\r\n"
		"\t1:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t2 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_DOCK, "Ai-dock (Ship goal)\r\n"
		"\tCauses one ship to dock with another ship.\r\n\r\n"
		"Takes 4 arguments...\r\n"
		"\t1:\tName of dockee ship (The ship that \"docker\" will dock with).\r\n"
		"\t2:\tDocker's docking point - Which dock point docker uses to dock.\r\n"
		"\t3:\tDockee's docking point - Which dock point on dockee docker will move to.\r\n"
		"\t4:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t5 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." },

	{ OP_AI_UNDOCK, "Ai-undock (Ship goal)\r\n"
		"\tCauses the specified ship to undock from who it is currently docked with.\r\n\r\n"
		"Takes 1 or 2 arguments...\r\n"
		"\t1:\tGoal priority (number between 0 and 89).\r\n"
		"\t2 (optional):\tShip to undock from.  If none is specified, the code will pick the first docked ship." },

	{ OP_AI_WARP_OUT, "Ai-warp-out (Ship/Wing Goal)\r\n"
		"\tCauses the specified ship/wing to immediately warp out of the mission, from its current location.  "
		"It will warp even if its departure cue is specified as a hangar bay.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of waypoint path to follow to warp out (not used).\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100)." },

	{ OP_AI_WAYPOINTS, "Ai-waypoints (Ship goal)\r\n"
		"\tCauses the specified ship to fly a waypoint path continuously.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of waypoint path to fly.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false.\r\n"
		"\t4 (optional):\tStarting index (1-n) of the waypoint path.\r\n"
		"\t5 (optional):\tWhether to fly the waypoint path in reverse.\r\n"
	},

	{ OP_AI_WAYPOINTS_ONCE, "Ai-waypoints once (Ship goal)\r\n"
		"\tCauses the specified ship to fly a waypoint path.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of waypoint path to fly.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false.\r\n"
		"\t4 (optional):\tStarting index (1-n) of the waypoint path.\r\n"
		"\t5 (optional):\tWhether to fly the waypoint path in reverse.\r\n"
	},

	{ OP_AI_DESTROY_SUBSYS, "Ai-destroy subsys (Ship goal)\r\n"
		"\tCauses the specified ship to attack and try and destroy the specified subsystem "
		"on the specified ship.\r\n\r\n"
		"Takes 3 or 4 arguments...\r\n"
		"\t1:\tName of ship subsystem is on.\r\n"
		"\t2:\tName of subsystem on the ship to attack and destroy.\r\n"
		"\t3:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t4 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t5 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

{ OP_AI_DISABLE_SHIP, "Ai-disable-ship (Ship/wing goal)\r\n"
		"\tThis AI goal causes a ship/wing to destroy all of the engine subsystems on "
		"the specified ship.  This goal is different than ai-destroy-subsystem since a ship "
		"may have multiple engine subsystems requiring the use of > 1 ai-destroy-subsystem "
		"goals.\r\n"
		"Please note that this goal will call \"protect-ship\" on the target "
		"to prevent overzealous AI ships from destroying it in the process of disabling it.  It will "
		"also clear attack goals from any other AI ships which have goals to attack the target.  "
		"If the ship must be destroyed later on, be sure to call an \"unprotect-ship\" sexp, or "
		"consider using ai-disable-ship-tactical.\r\n\r\n"
		"Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of ship whose engine subsystems should be destroyed\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_DISABLE_SHIP_TACTICAL, "Ai-disable-ship-tactical (Ship/wing goal)\r\n"
		"\tThis AI goal causes a ship/wing to destroy all of the engine subsystems on "
		"the specified ship.  This goal is different than ai-destroy-subsystem since a ship "
		"may have multiple engine subsystems requiring the use of > 1 ai-destroy-subsystem "
		"goals.\r\n"
		"This goal is specifically a \"tactical disable\" of its target, performing the specific task without "
		"implying anything else.  It does not protect the target ship, nor does it clear attack goals from other "
		"AI ships attacking the target ship.\r\n\r\n"
		"Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of ship whose engine subsystems should be destroyed\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_DISARM_SHIP, "Ai-disarm-ship (Ship/wing goal)\r\n"
		"\tThis AI goal causes a ship/wing to destroy all of the turret subsystems on "
		"the specified ship.  This goal is different than ai-destroy-subsystem since a ship "
		"may have multiple turret subsystems requiring the use of > 1 ai-destroy-subsystem "
		"goals.\r\n"
		"Please note that this goal may call \"protect-ship\" on the target "
		"to prevent overzealous AI ships from destroying it in the process of disarming it.  It will "
		"also clear attack goals from any other AI ships which have goals to attack the target.  "
		"If the ship must be destroyed later on, be sure to call an \"unprotect-ship\" sexp, or "
		"consider using ai-disarm-ship-tactical.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship whose turret subsystems should be destroyed\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_DISARM_SHIP_TACTICAL, "Ai-disarm-ship-tactical (Ship/wing goal)\r\n"
		"\tThis AI goal causes a ship/wing to destroy all of the turret subsystems on "
		"the specified ship.  This goal is different than ai-destroy-subsystem since a ship "
		"may have multiple turret subsystems requiring the use of > 1 ai-destroy-subsystem "
		"goals.\r\n"
		"This goal is specifically a \"tactical disarm\" of its target, performing the specific task without "
		"implying anything else.  It does not protect the target ship, nor does it clear attack goals from other "
		"AI ships attacking the target ship.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship whose turret subsystems should be destroyed\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false."
	},

	{ OP_AI_GUARD, "Ai-guard (Ship goal)\r\n"
		"\tCauses the specified ship to guard a ship from other ships not on the same team.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship to guard.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." },

	{ OP_AI_GUARD_WING, "Ai-guard wing (Ship goal)\r\n"
		"\tCauses the specified ship to guard a wing of ships from other ships not on the "
		"same team.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of wing to guard.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." },

	{ OP_AI_FLY_TO_SHIP, "Ai-fly-to-ship (Ship goal)\r\n"
		"\tCauses the specified ship to fly towards another ship's position.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship to fly towards.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n"
		"\t3:\tDistance from the target ship to fly to (optional; defaults to 300).\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false.\r\n"
		"\t5 (optional):\tWhether the ship should always stay in a particular location as if escorting the target; defaults to false." },

	{ OP_AI_REARM_REPAIR, "Ai-rearm-repair (Ship goal)\r\n"
		"\tCauses the specified ship to rearm and/or repair another ship.  Typically only works on support ships.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship to rearm or repair.\r\n"
		"\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100)." },

	{ OP_NOP, "Do-nothing (Action operator)\r\n"
		"\tDoes nothing.  This is used as the default for any required action arguments "
		"of an operator." },

	{ OP_KEY_PRESSED, "Key-pressed (Boolean training operator)\r\n"
		"\tBecomes true when the specified default key has been pressed.  Default key "
		"refers to the what the key normally is when not remapped.  FreeSpace will "
		"automatically account for any keys that have been remapped.  If the optional "
		"delay is specified, becomes true that many seconds after the key has been pressed.\r\n\r\n"
		"Returns a boolean value.  Takes 1 or 2 arguments...\r\n"
		"\t1:\tDefault key to check for.\r\n"
		"\t2:\tDelay before operator registers as true (optional).\r\n" },

	{ OP_KEY_RESET, "Key-reset (Training operator)\r\n"
		"\tMarks the specified default key as having not been pressed, so key-pressed will be false "
		"until the player presses it again.  See key-pressed help for more information about "
		"what a default key is.\r\n\r\n"
		"\tNote that this sexp will only fire once when used in a repeating event, which may not be what you want.  Use key-reset-multiple "
		"if a key should be reset each time the event repeats.\r\n\r\n"
		"Returns a boolean value.  Takes 1 or more arguments...\r\n"
		"\tAll:\tDefault key to reset." },

	// Goober5000
	{ OP_KEY_RESET_MULTIPLE, "Key-reset-multiple (Training operator)\r\n"
		"\tMarks the specified default key as having not been pressed, so key-pressed will be false "
		"until the player presses it again.  See key-pressed help for more information about "
		"what a default key is.\r\n\r\n"
		"\tNote that this sexp will fire every time an event repeats.  If you only want a key to be reset once regardless of how many times the event repeats, use key-reset.\r\n\r\n"
		"Returns a boolean value.  Takes 1 or more arguments...\r\n"
		"\tAll:\tDefault key to reset." },

	{ OP_TARGETED, "Targeted (Boolean training operator)\r\n"
		"\tIs true as long as the player has the specified ship (or ship's subsystem) targeted, "
		"or has been targeted for the specified amount of time.\r\n\r\n"
		"Returns a boolean value.  Takes 1 to 3 arguments (first required, rest optional):\r\n"
		"\t1:\tName of ship to check if targeted by player.\r\n"
		"\t2:\tLength of time target should have been kept for (optional).\r\n"
		"\t3:\tName of subsystem on ship to check if targeted (optional)." },

	{ OP_NODE_TARGETED, "Node-Targeted (Boolean training operator)\r\n"
		"\tIs true as long as the player has the specified jump node targeted, "
		"or has been targeted for the specified amount of time.\r\n\r\n"
		"Returns a boolean value.  Takes 1 to 2 arguments (first required, rest optional):\r\n"
		"\t1:\tName of Jump Node to check if targeted by player.\r\n"
		"\t2:\tLength of time target should have been kept for (optional)."},

	// Sesquipedalian
	{ OP_MISSILE_LOCKED, "Missile-locked (Boolean training operator)\r\n"
		"\tIs true as long as the player has had a missile lock for the specified amount of time. "
		"Optional arguments require lock to be maintained on a specified ship (or ship's subsystem).\r\n\r\n"
		"Returns a boolean value.  Takes 1 to 3 arguments (first required, rest optional):\r\n"
		"\t1:\tLength of time missile lock should have been kept for.\r\n"
		"\t2:\tName of ship to check if locked onto by player (optional).\r\n"
		"\t3:\tName of subsystem on ship to check if locked onto (optional)." },

	{ OP_SPEED, "Speed (Boolean training operator)\r\n"
		"\tBecomes true when the player has been within the specified speed range set by "
		"set-training-context-speed for the specified amount of time.\r\n\r\n"
		"Returns a boolean value.  Takes 1 argument...\r\n"
		"\t1:\tTime in seconds." },

	{ OP_GET_THROTTLE_SPEED, "Get-Throttle-Speed (Training operator)\r\n"
		"\tReturns the current throttle speed that the ship has been set to. Reverse speeds are returned as a negative value. "
		"Takes 1 argument...\r\n"
		"\t1:\tName of the player ship to check the throttle value for." },

	{ OP_FACING, "Facing (Boolean training operator)\r\n"
		"\tIs true as long as the specified ship is within the player's specified "
		"forward cone.  A forward cone is defined as any point that the angle between the "
		"vector of the point and the player, and the forward facing vector is within the "
		"given angle.\r\n\r\n"
		"Returns a boolean value.  Takes 2 argument...\r\n"
		"\t1:\tShip to check is within forward cone.\r\n"
		"\t2:\tAngle in degrees of the forward cone." },

	{ OP_IS_FACING, "Is Facing (Boolean training operator)\r\n"
		"\tIs true as long as the second object is within the first ship's specified "
		"forward cone.  A forward cone is defined as any point that the angle between the "
		"vector of the ship and point, and the forward facing vector is within the "
		"given angle. If the distance between the two is greater than the fourth "
		"parameter, this will return false.\r\n\r\n"
		"Returns a boolean value.  Takes 3 or 4 argument...\r\n"
		"\t1:\tShip to check from.\r\n"
		"\t2:\tObject to check is within forward cone.\r\n"
		"\t3:\tAngle in degrees of the forward cone.\r\n"
		"\t4:\tRange in meters (optional)."},

	{ OP_FACING2, "Facing Waypoint(Boolean training operator)\r\n"
		"\tIs true as long as the specified first waypoint is within the player's specified "
		"forward cone.  A forward cone is defined as any point that the angle between the "
		"vector of the point and the player, and the forward facing vector is within the "
		"given angle.\r\n\r\n"
		"Returns a boolean value.  Takes 2 argument...\r\n"
		"\t1:\tName of waypoint path whose first point is within forward cone.\r\n"
		"\t2:\tAngle in degrees of the forward cone." },

	// fixed by Goober5000 and then deprecated by Karajorma
	{ OP_ORDER, "Order (Boolean training operator)\r\n"
		"\tDeprecated - Use Query-Orders in any new mission.\r\n\r\n"
		"\tBecomes true when the player had given the specified ship or wing the specified order.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or 3 arguments...\r\n"
		"\t1:\tName of ship or wing to check if given order to.\r\n"
		"\t2:\tName of order to check if player has given.\r\n"
		"\t3:\tName of the target of the order (optional)." },

	{ OP_QUERY_ORDERS, "Query-Orders (Boolean training operator)\r\n"
		"\tBecomes true when the player had given the specified ship or wing the specified order.\r\n\r\n"
		"Returns a boolean value.  Takes 2 or more arguments...\r\n"
		"\t1:\tName of ship or wing to check if given order to.\r\n"
		"\t2:\tName of order to check if player has given.\r\n"
		"\t3:\tMaximum length of time since order was given. Use 0 for any time in the mission.\r\n"
		"\t4:\tName of the target of the order (optional).\r\n"
		"\t5:\tName of player ship giving the order(optional).\r\n"
		"\t6:\tName of the subsystem for Destroy Subsystem orders.(optional)" },

	// Karajorma
	{ OP_RESET_ORDERS, "Reset-Orders (Action training operator)\r\n"
		"\tResets the list of orders the player has given.\r\n"
		"Takes no arguments." },

	{ OP_WAYPOINT_MISSED, "Waypoint-missed (Boolean training operator)\r\n"
		"\tBecomes true when a waypoint is flown, but the waypoint is ahead of the one "
		"they are supposed to be flying.  The one they are supposed to be flying is the "
		"next one in sequence in the path after the last one they have hit.\r\n\r\n"
		"Returns a boolean value.  Takes no arguments." },

	{ OP_PATH_FLOWN, "Path-flown (Boolean training operator)\r\n"
		"\tBecomes true when all the waypoints in the path have been flown, in sequence.\r\n\r\n"
		"Returns a boolean value.  Takes no arguments." },

	{ OP_WAYPOINT_TWICE, "Waypoint-twice (Boolean training operator)\r\n"
		"\tBecomes true when a waypoint is hit that is before the last one hit, which "
		"indicates they have flown a waypoint twice.\r\n\r\n"
		"Returns a boolean value.  Takes no arguments." },

	{ OP_TRAINING_MSG, "Training-msg (Action training operator)\r\n"
		"\tSends the player a training message.  Uses the same messages as normal messages, "
		"only they get displayed differently using this operator.\r\n\r\n"
		"\tSecondary messages are only used when an event has a repeat count greater than 1. If "
		"a secondary message is specified, it is sent on the last repeat iteration, while the "
		"primary message is sent all other times.\r\n\r\n"
		"Takes 1-4 arguments...\r\n"
		"\t1:\tName of primary message to send.\r\n"
		"\t2:\tName of secondary message to send (optional).\r\n"
		"\t3:\tDelay (in seconds) to wait before sending message. (optional)\r\n"
		"\t4:\tAmount of Time (in seconds) to display message (optional)." },

	{ OP_SET_TRAINING_CONTEXT_FLY_PATH, "Set-training-context-fly-path (Training context operator)\r\n"
		"\tTells FreeSpace that the player is expected to fly a waypoint path.  This must be "
		"executed before waypoint-missed, waypoint-twice and path-flown operators become valid.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of waypoint path player should fly.\r\n"
		"\t2:\tDistance away a player needs to be from a waypoint for it to be registered as flown." },

	{ OP_SET_TRAINING_CONTEXT_SPEED, "Set-training-context-speed (Training context operator)\r\n"
		"\tTells FreeSpace that the player is expected to fly within a certain speed range.  Once "
		"this operator has been executed, you can measure how long they have been within this "
		"speed range with the speed operator.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tMinimum speed of range player is to fly between.\r\n"
		"\t2:\tMaximum speed of range player is to fly between." },

	// Karajorma
	{ OP_STRING_TO_INT, "string-to-int\r\n"
		"\tConverts a string into an integer.  All non-numeric characters (except for the negative sign) will be ignored, as will any fractional part of a decimal number.  This behavior is somewhat different than the atoi() function in C or C++, which will abort if it encounters any non-numeric character.  For a string like \"turret31\", this sexp will return 31, but atoi() will return 0.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tString to convert" },

	// Goober5000
	{ OP_STRING_GET_LENGTH, "string-get-length\r\n"
		"\tReturns the length of the specified string.  Takes 1 argument." },

	// Goober5000
	{ OP_INT_TO_STRING, "int-to-string\r\n"
		"\tConverts an integer into a string.  The destination must be a string variable.\r\n"
		"Takes 2 argument...\r\n"
		"\t1:\tInteger to convert\r\n"
		"\t2:\tString variable to contain the result\r\n" },

	// Goober5000
	{ OP_STRING_CONCATENATE, "string-concatenate (deprecated in favor of string-concatenate-block)\r\n"
		"\tConcatenates two strings, putting the result into a string variable.  If the length of the string will "
		"exceed the sexp variable token limit (" SCP_TOKEN_TO_STR(TOKEN_LENGTH_1) "), it will be truncated.\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1: First string\r\n"
		"\t2: Second string\r\n"
		"\t3: String variable to hold the result\r\n" },

	// Goober5000
	{ OP_STRING_CONCATENATE_BLOCK, "string-concatenate-block\r\n"
		"\tConcatenates two or more strings, putting the result into a string variable.  If the length of the string will "
		"exceed the sexp variable token limit (" SCP_TOKEN_TO_STR(TOKEN_LENGTH_1) "), it will be truncated.\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1: String variable to hold the result\r\n"
		"\tRest: Strings to concatenate.  At least two of these are required; the rest are optional.\r\n" },

	// Goober5000
	{ OP_STRING_GET_SUBSTRING, "string-get-substring\r\n"
		"\tExtracts a substring from a parent string, putting the result into a string variable.  If the length of the string will "
		"exceed the sexp variable token limit (" SCP_TOKEN_TO_STR(TOKEN_LENGTH_1) "), it will be truncated.\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1: Parent string\r\n"
		"\t2: Index at which the substring begins (0-based)\r\n"
		"\t3: Length of the substring\r\n"
		"\t4: String variable to hold the result\r\n" },

	// Goober5000
	{ OP_STRING_SET_SUBSTRING, "string-set-substring\r\n"
		"\tReplaces a substring from a parent string with a new string, putting the result into a string variable.  If the length of the string will "
		"exceed the sexp variable token limit (" SCP_TOKEN_TO_STR(TOKEN_LENGTH_1) "), it will be truncated.\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1: Parent string\r\n"
		"\t2: Index at which the substring begins (0-based)\r\n"
		"\t3: Length of the substring\r\n"
		"\t4: New substring (which can be a different length than the old substring)\r\n"
		"\t5: String variable to hold the result\r\n" },

	// Karajorma/jg18
	{ OP_IS_CONTAINER_EMPTY, "is-container-empty (Boolean operator)\r\n"
		"\tReturns true if the specified container has no elements in it.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of the container." },

	// Karajorma/jg18
	{ OP_GET_CONTAINER_SIZE, "get-container-size (Status operator)\r\n"
		"\tReturns the number of elements in the specified container, or 0 if empty.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of the container." },

	// Karajorma/jg18
	{ OP_LIST_HAS_DATA, "list-has-data (Boolean operator)\r\n"
		"\tReturns true if the specified list container has elements which match the supplied values.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the list container.\r\n"
		"\tRest:\tValue that might be in the list container" },

	// Karajorma/jg18
	{ OP_LIST_DATA_INDEX, "list-data-index\r\n"
		"\tReturns the index (starting at 0) for the supplied value in the list container.\r\n"
		"\tIf the supplied value is not in the container, returns -1.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of the list container.\r\n"
		"\t2:\tValue that might be in the list container" },

	// Karajorma/jg18
	{ OP_MAP_HAS_KEY, "map-has-key (Boolean operator)\r\n"
		"\tReturns true if the specified map container has keys that match the supplied values.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the map container.\r\n"
		"\tRest:\tValue that might be a key in the map container" },

	// Karajorma/jg18
	{ OP_MAP_HAS_DATA_ITEM, "map-has-data-item (Boolean operator)\r\n"
		"\tReturns true if the specified map container has a key whose data matches the supplied value.\r\n"
		"\tIf a variable is also supplied, three cases are possible:\r\n"
		"\t\t(1) If a single key has the supplied value as its data, the key is stored in the variable.\r\n"
		"\t\t(2) If multiple keys have the supplied value as their data, one of those keys is stored in the variable. The key may not be the same every time.\r\n"
		"\t\t(3) If there is no key that has the supplied value as its data, the variable is left unchanged.\r\n\r\n"
		"Takes either 2 or 3 arguments...\r\n"
		"\t1:\tName of the map container.\r\n"
		"\t2:\tValue that might be data associated with a key in the map container.\r\n"
		"\t3:\tVariable to hold a map key associated with the data, if one exists (optional). The variable type must match the key type." },

	// Karajorma
	{ OP_DEBUG, "debug\r\n"
		"\tPops up a warning on debug builds (and optionally on release builds)\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1:\tLimit warning to debug builds.  If false, display warning in release builds too. Defaults to true.\r\n"
		"\t2:\tA short string which will appear in the warning, or a message if the string matches a message name.\r\n"},

	{ OP_GRANT_PROMOTION, "Grant promotion (Action operator)\r\n"
		"\tIn a single player game, grants the player an automatic promotion to the "
		"next rank which they can obtain. If they are already at the highest rank, "
		"this operator has no effect. It takes no arguments." },

	{ OP_GRANT_MEDAL, "Grant medal (Action operator)\r\n"
		"\tIn single player missions, this function grants the given medal to the player.  "
		"Currently, only 1 medal will be allowed to be given per mission.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of medal to grant to player." },

	{ OP_GOOD_PRIMARY_TIME, "Set preferred primary weapon\r\n"
		"\tForces the given ship/wing/team to use the specified primary weapon against a certain ship/wing/team.  "
		"Use the fourth parameter to control overriding the normal AI weapon selection\r\n\r\n"
		"Takes 4 arguments...\r\n"
		"\t1:\tName of ship/wing/team which will prefer firing given weapon\r\n"
		"\t2:\tWeapon name\r\n"
		"\t3:\tTarget name\r\n"
		"\t4:\tActivate/deactivate" },

	{ OP_GOOD_SECONDARY_TIME, "Set preferred secondary weapons\r\n"
		"\tThis sexpression is used to inform the AI about preferred secondary weapons to "
		"fire during combat.  When this expression is evaluated, any AI ships of the given "
		"team prefer to fire the given weapon at the given ship. (Preferred over other "
		"secondary weapons)\r\n\r\n"
		"Takes 4 arguments...\r\n"
		"\t1:\tTeam name which will prefer firing given weapon\r\n"
		"\t2:\tMaximum number of this type of weapon above team can fire.\r\n"
		"\t3:\tWeapon name (list includes only the valid weapons for this expression)\r\n"
		"\t4:\tShip name at which the above named team should fire the above named weapon (ship must be in-mission)." },

	{ OP_AND_IN_SEQUENCE, "And in sequence (Boolean operator)\r\n"
		"\tReturns true if all of its arguments have become true in the order they are listed in.\r\n"
		"NOTE 1: This is more strict than might be apparent.  If argument 2 becomes true while argument 1 is still pending, then the operator will immediately "
		"return false because the arguments did not become true in the correct sequence.\r\n"
		"NOTE 2: This operator should not be used with when-argument or any -argument SEXPs because its method of caching arguments is not compatible.\r\n"
		"Returns a boolean value.  Takes 2 or more boolean arguments." },

	{ OP_SKILL_LEVEL_AT_LEAST, "Skill level at least (Boolean operator)\r\n"
		"\tReturns true if the player has selected the given skill level or higher.\r\n\r\n"
		"Returns a boolean value.  Takes 1 argument...\r\n"
		"\t1:\tName of the skill level to check." },

	{ OP_NUM_PLAYERS, "Num players (Status operator)\r\n"
		"\tReturns the current number of players (multiplayer) playing in the current mission.\r\n\r\n"
		"Returns a numeric value.  Takes no arguments." },

	{ OP_IS_CARGO_KNOWN, "Is cargo known (Boolean operator)\r\n"
		"\tReturns true if all of the specified objects' cargo is known by the player (i.e. they "
		"have scanned each one.\r\n\r\n"
		"Returns a boolean value.  Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship to check if its cargo is known." },

	{ OP_HAS_BEEN_TAGGED_DELAY, "Has ship been tagged (delay) (Boolean operator)\r\n"
		"\tReturns true if all of the specified ships have been tagged.\r\n\r\n"
		"Returns a boolean value after <delay> seconds when all ships have been tagged.  "
		"(Event evaluation is deferred until the ships are in-mission).  Takes 2 or more arguments...\r\n"
		"\t1:\tDelay in seconds after which sexpression will return true when all ships have been tagged.\r\n"
		"\tRest:\tNames of ships to check if tagged." },

	{ OP_ARE_SHIP_FLAGS_SET, "Are ship flags set (Boolean operator)\r\n"
		"\tReturns true if all of the specified flags have been set for this particular ship.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the ship (ship does not have to be in-mission).\r\n"
		"\tRest:\tShip, object, parse object, or ai flags which might be set for this ship." },

	{ OP_ARE_WING_FLAGS_SET, "Are wing flags set (Boolean operator)\r\n"
		"\tReturns true if all of the specified flags have been set for this particular wing.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the wing (wing does not have to be in-mission).\r\n"
		"\tRest:\tWing flags which might be set for this wing." },

	{ OP_IS_SHIP_EMP_ACTIVE, "Is ship emp active (Boolean operator)\r\n"
		"\tReturns true if all of the specified ships are currently experiencing EMP effects.\r\n\r\n"
		"Returns a boolean value if all ships are currently EMP'd.  "
		"(Evaluation is deferred until all ships are in-mission). Takes 1 or more arguments...\r\n"
		"\tAll:\tName of the ships to check." },

	{ OP_CAP_SUBSYS_CARGO_KNOWN_DELAY, "Is capital ship subsystem cargo known (delay) (Boolean operator)\r\n"
		"\tReturns true if all of the specified subsystem cargo is known by the player.\r\n"
		"\tNote: Cargo must be explicitly named.\r\n\r\n"
		"Returns a boolean value after <delay> seconds when all cargo is known.  Takes 3 or more arguments...\r\n"
		"\t1:\tDelay in seconds after which sexpression will return true when all cargo scanned.\r\n"
		"\t2:\tName of capital ship\r\n"
		"\tRest:\tNames of subsystems to check for cargo known.." },

	{ OP_CARGO_KNOWN_DELAY, "Is cargo known (delay) (Boolean operator)\r\n"
		"\tReturns true if all of the specified objects' cargo is known by the player (i.e. they "
		"have scanned each one.\r\n\r\n"
		"Returns a boolean value after <delay> seconds when all cargo is known.  Takes 2 or more arguments...\r\n"
		"\t1:\tDelay in seconds after which sexpression will return true when all cargo scanned.\r\n"
		"\tRest:\tNames of ships/cargo to check for cargo known." },

	{ OP_WAS_PROMOTION_GRANTED, "Was promotion granted (Boolean operator)\r\n"
		"\tReturns true if a promotion was granted via the 'Grant promotion' operator in the mission.\r\n\r\n"
		"Returns a boolean value.  Takes no arguments." },

	{ OP_WAS_MEDAL_GRANTED, "Was medal granted (Boolean operator)\r\n"
		"\tReturns true if a medal was granted via via the 'Grant medal' operator in the mission.  "
		"If you provide the optional argument to this operator, then true is only returned if the "
		"specified medal was granted.\r\n\r\n"
		"Returns a boolean value.  Takes 0 or 1 arguments...\r\n"
		"\t1:\tName of medal to specifically check for (optional)." },

	{ OP_GOOD_REARM_TIME, "Good rearm time (Action operator)\r\n"
		"\tInforms the game logic that right now is a good time for a given team to attempt to "
		"rearm their ships.  The time parameter specifies how long the \"good time\" will last.\r\n"
		"Can be used to prematurely end its counterpart, bad-rearm-time.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tTeam Name\r\n"
		"\t2:\tTime in seconds rearm window should last" },

	{ OP_BAD_REARM_TIME, "Bad rearm time (Action operator)\r\n"
		"\tInforms the game logic that right now is a bad time for a given team to attempt to "
		"rearm their ships.  AI ships will not choose to rearm. "
		"The time parameter specifies how long the \"bad time\" will last.\r\n"
		"Can be used to prematurely end its counterpart, good-rearm-time.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tTeam Name\r\n"
		"\t2:\tTime in seconds 'no rearm' window should last" },

	{ OP_ALLOW_SHIP, "Allow ship (Action operator)\r\n"
		"\tThis operator makes the given ship type available to the player team.  Players will be "
		"able to have ships of this type in their starting wings in all future missions of this "
		"campaign.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of ship type (or ship class) to allow." },

	{ OP_ALLOW_WEAPON, "Allow weapon (Action operator)\r\n"
		"\tThis operator makes the given weapon available to the player team.  Players will be "
		"able to equip ships with in all future missions of this campaign.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of weapon (primary or secondary) to allow." },

	{ OP_TECH_ADD_SHIP, "Tech add ship (Action operator)\r\n"
		"\tThis operator makes the given ship type available in the techroom database.  Players will "
		"then be able to view this ship's specs there.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ship type (or ship class) to add." },

	{ OP_TECH_ADD_WEAPON, "Tech add weapon (Action operator)\r\n"
		"\tThis operator makes the given weapon available in the techroom database.  Players will "
		"then be able to view this weapon's specs there.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of weapon (primary or secondary) to add." },

	{ OP_TECH_ADD_INTEL, "Tech add intel (Action operator, deprecated in favor of tech-add-intel-xstr)\r\n"
		"\tThis operator makes the given intel entry available in the techroom database.  Players will "
		"then be able to view this intel entry there.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of intel entry to add." },

	{ OP_TECH_REMOVE_INTEL, "Tech remove intel (Action operator, deprecated in favor of tech-remove-intel-xstr)\r\n"
		"\tThis operator removes the given intel entry in the techroom database.  Players will "
		"then not be able to view this intel entry there.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of intel entry to remove." },

	{ OP_TECH_ADD_INTEL_XSTR, "Tech add intel XSTR (Action operator)\r\n"
		"\tThis operator makes the given intel entry available in the techroom database.  Players will "
		"then be able to view this intel entry there.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of intel entry to add.\r\n"
		"\t2:\tXSTR ID of intel entry, or -1 if there is no XSTR entry.\r\n"
		"Use Add-Data for multiple entries.\r\n\r\n"
		"IMPORTANT: Each additional entry in the list MUST HAVE two fields; "
		"any entry without both fields will be ignored, as will any successive entries." },

	{ OP_TECH_REMOVE_INTEL_XSTR, "Tech remove intel XSTR (Action operator)\r\n"
		"\tThis operator removes the given intel entry, so it is no longer available in the techroom database.  Players will "
		"then not be able to view this intel entry there.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of intel entry to remove.\r\n"
		"\t2:\tXSTR ID of intel entry, or -1 if there is no XSTR entry.\r\n"
		"Use Add-Data for multiple entries.\r\n\r\n"
		"IMPORTANT: Each additional entry in the list MUST HAVE two fields; "
		"any entry without both fields will be ignored, as will any successive entries." },

	{ OP_TECH_RESET_TO_DEFAULT, "Tech reset to default (Action operator)\r\n"
		"\tThis operator resets the tech room to the default represented in the tables.  This is "
		"useful for starting new campaigns, so that the player will not see tech entries carried over "
		"from previous campaigns.\r\n\r\n"
		"Takes no arguments." },

	{ OP_CHANGE_PLAYER_SCORE, "Change Player Score (Action operator)\r\n"
		"\tThis operator allows direct alteration of the player's score for this mission.\r\n\r\n"
		"Takes 2 or more arguments...\r\n" 
		"\t1:\tAmount to alter the player's score by.\r\n"
		"\tRest:\tName of ship the player is flying."},

	{ OP_CHANGE_TEAM_SCORE, "Change Team Score (Action operator)\r\n"
		"\tThis operator allows direct alteration of the team's score for a TvT mission (Does nothing otherwise).\r\n\r\n"
		"Takes 2 arguments...\r\n" 
		"\t1:\tAmount to alter the team's score by.\r\n"
		"\t2:\tThe team to alter the score for. (0 will add the score to all teams!)"},

	{ OP_AI_EVADE_SHIP, "Ai-evade ship (Ship goal)\r\n"
		"\tCauses the specified ship to go into evade mode and run away like the "
		"coward it is.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of ship to evade from.\r\n"
		"\t2:\tGoal priority (number between 0 and 89)." },

	{ OP_AI_STAY_NEAR_SHIP, "Ai-stay near ship (Ship goal)\r\n"
		"\tCauses the specified ship to park itself near the given ship and move closer if the other ship moves too far "
		"away from it.\r\n\r\n"
		"Takes 3 to 5 arguments...\r\n"
		"\t1:\tName of ship to stay near.\r\n"
		"\t2:\tGoal priority (number between 0 and 89).\r\n"
		"\t3:\tDistance to stay within (optional; defaults to 300).\r\n"
		"\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false.\r\n"
		"\t5 (optional):\tWhether the ship should always stay in a particular location as if escorting the target; defaults to false." },

	{ OP_AI_KEEP_SAFE_DISTANCE, "Ai-keep safe distance (Ship goal)\r\n"
		"\tTells the specified ship to stay a safe distance away from any ship that isn't on the "
		"same team as it.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tGoal priority (number between 0 and 89)." },

	{ OP_AI_IGNORE, "Ai-ignore (Ship goal)\r\n"
		"\tTells the specified ship to ignore the given target and not consider it when picking something "
		"to attack, but ALSO sets the protect-ship flag on the target so that NO ship can attack it.  Only 1 target "
		"can be ignored at a time, and the protect-ship flag will remain set on the target unless some other sexp "
		"explicitly removes it or instructs a ship to attack the target.  In most cases mission designers will want to use "
		"ai-ignore-new instead.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of target to ignore.\r\n"
		"\t2:\tGoal priority (number between 0 and 89) - note, this does not imply any ranking of ignored targets." },

	// Goober5000
	{ OP_AI_IGNORE_NEW, "Ai-ignore-new (Ship goal)\r\n"
		"\tTells the specified ship to ignore the given target and not consider it when picking something "
		"to attack.  Up to " SCP_TOKEN_TO_STR(MAX_IGNORE_NEW_OBJECTS) " targets can be ignored at a time.  Targets ignored by "
		"any given ship do not affect targets ignored by any other ship.\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of target to ignore.\r\n"
		"\t2:\tGoal priority (number between 0 and 89) - note, this does not imply any ranking of ignored targets." },

	{ OP_AI_STAY_STILL, "Ai-stay-still (Ship goal)\r\n"
		"\tCauses the specified ship to stay still.  The ship will do nothing until attacked at "
		"which time the ship will come to life and defend itself.\r\n\r\n"
		"By default all goals on the ship's goal list will be cleared when this goal runs, which means that the ship forgets both "
		"this goal and all previous goals, and if it receives any other goal in any way, "
		"it will immediately begin a new behavior.  Use the optional sexp flag (argument 3) to prevent this from happening.\r\n\r\n"
		"Takes 2 to 3 arguments...\r\n"
		"\t1:\tShip or waypoint the ship staying still will directly face (currently not implemented)\r\n"
		"\t2:\tGoal priority (number between 0 and 89).\r\n"
		"\t3:\tWhether to clear all goals (optional).  If not specified this will be true, or the value defined in ai_profiles.\r\n"
	},

	{ OP_AI_PLAY_DEAD, "Ai-play-dead (Ship goal)\r\n"
		"\tCauses the specified ship to pretend that it is dead and not do anything.  This "
		"expression should be used to indicate that a ship has no pilot and cannot respond "
		"to any enemy threats.  A ship playing dead will not respond to any attack.\r\n\r\n"
		"By default all goals on the ship's goal list will be cleared when this goal runs, which means that the ship forgets both "
		"this goal and all previous goals, and if it receives any other goal in any way, "
		"it will immediately come back to life.  Use ai-play-dead-persistent to prevent this from happening.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tGoal priority (number between 0 and 89)." },

	{ OP_AI_PLAY_DEAD_PERSISTENT, "Ai-play-dead-persistent (Ship goal)\r\n"
		"\tCauses the specified ship to pretend that it is dead and not do anything.  This "
		"goal behaves like ai-play-dead, with the important differences that the goal 'persists' "
		"until it is removed, and the existing goal list is not cleared.  The ship will not "
		"come back to life whenever it is given an order or a new goal, so the only ways the "
		"ship will stop playing dead are if this goal is removed, or a new goal of a higher "
		"priority is assigned.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tGoal priority (number between 0 and 89)." },

	{ OP_AI_FORM_ON_WING, "Ai-form-on-wing (Ship Goal)\r\n"
		"\tCauses the ship to form on the specified ship's wing. This works analogous to the "
		"player order, and by default will cause all goals on the ship's goal list to be cleared when this goal runs, which means that the ship forgets both "
		"this goal and all previous goals, and if it receives any other goal in any way, "
		"it will immediately begin a new behavior.  By default it will also be given an internal override flag that will cause it to "
		"take priority over all other goals.\r\n\r\n"
		"Takes 1 to 5 arguments...\r\n"
		"\t1:\tShip to form on.\r\n"
		"\t2:\tGoal priority (number between 0 and 89, optional).  If not specified this will be 99, or the number defined in ai_profiles.\r\n"
		"\t3:\tWhether to clear all goals (optional).  If not specified this will be true, or the value defined in ai_profiles.\r\n"
		"\t4:\tWhether this goal should override all other goals (optional).  If not specified this will be true, or the value defined in ai_profiles.\r\n"
		"\t5:\tWhether this goal should be cleared if any other goal is assigned (optional).  If not specified this will be false.\r\n"
	},

	{ OP_FLASH_HUD_GAUGE, "Ai-flash hud gauge (Training goal)\r\n"
		"\tCauses the specified hud gauge to flash to draw the player's attention to it.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tName of hud gauge to flash." },
		
	{ OP_ALTER_SHIP_FLAG, "alter-ship-flag\r\n"
		"\tSets or unsets a ship's flag, object flag, parse flag, or AI flag.\r\n\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tShip flag name\r\n"
		"\t2:\tTrue if turning on, false if turning off\r\n"
		"\t3:\tTrue/False - Apply this flag to future waves of this wing. Apply to ship if not present\r\n"
		"\tRest:\t (optional) Name of ships, wings, or entire teams. If not supplied, will work on all ships in the mission\r\n\r\n"
	},

	{ OP_ALTER_WING_FLAG, "alter-wing-flag\r\n"
		"\tSets or unsets a wing's flag.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tWing flag name\r\n"
		"\t2:\tTrue if turning on, false if turning off\r\n"
		"\tRest:\t (optional) Name of wings. If not supplied, will work on all wings in the mission\r\n\r\n"
	},

	{ OP_CANCEL_FUTURE_WAVES, "cancel-future-waves\r\n"
		"\tCancel all waves of a wing which have not yet arrived. Waves that have arrived already are not affected. The is-destroyed-delay, "
		"ship-type-destroyed, and similar operators behave as though the cancelled waves do not exist.\r\n\r\nIf this operator is called on a wing "
		"which has not arrived, the wing will never arrive, exactly as if its arrival cue were set to false; and that wing is not marked as destroyed. "
		"Otherwise, wings will be marked as destroyed or departed in the same manner as they would have been if they were truly on the last wave.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tThe name of a wing to cancel." },

	{ OP_SHIP_VISIBLE, "ship-visible\r\n"
		"\tCauses the ships listed in this sexpression to be visible with player sensors.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make visible to sensors (ships do not need to be in-mission)." },

	{ OP_SHIP_INVISIBLE, "ship-invisible\r\n"
		"\tCauses the ships listed in this sexpression to be invisible to player sensors.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make invisible to sensors (ships do not need to be in-mission)." },

	{ OP_SHIP_VULNERABLE, "ship-vulnerable\r\n"
		"\tCauses the ship listed in this sexpression to be vulnerable to weapons.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make vulnerable to weapons (ships do not need to be in-mission)." },

	{ OP_SHIP_INVULNERABLE, "ship-invulnerable\r\n"
		"\tCauses the ships listed in this sexpression to be invulnerable to weapons.  Use with caution!!!!\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make invulnerable to weapons (ships do not need to be in-mission)." },

	{ OP_SHIP_BOMB_TARGETABLE, "ship-targetable-as-bomb\r\n"
		"\tCauses the ships listed in this sexpression to be targetable with bomb targeting key.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make targetable with bomb targeting key (ships do not need to be in-mission)." },

	{ OP_SHIP_BOMB_UNTARGETABLE, "ship-untargetable-as-bomb\r\n"
		"\tCauses the ships listed in this sexpression to not be targetable with bomb targeting key.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make nontargetable with bomb targeting key (ships do not need to be in-mission)." },

	{ OP_SHIELDS_ON, "shields-on\r\n" //-Sesquipedalian
		"\tCauses the ship listed in this sexpression to have their shields activated.\r\n"
		"If the ship had no-shields prior to the sexp being called, the ETS will be rebalanced to default.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to activate shields on (ships do not need to be in-mission)." },

	{ OP_SHIELDS_OFF, "shields-off\r\n" //-Sesquipedalian
		"\tCauses the ships listed in this sexpression to have their shields deactivated.  \r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to deactivate shields on (ships do not need to be in-mission)." },

	{ OP_SHIP_GUARDIAN, "ship-guardian\r\n"
		"\tCauses the ships listed in this sexpression to not take any damage below 1% of hull strength.  As such, the ship will not be killable by weapons.  Use with caution!!!!\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make unkillable (ships do not need to be in-mission)." },

	{ OP_SHIP_NO_GUARDIAN, "ship-no-guardian\r\n"
		"\tCauses the ships listed in this sexpression to be killable by weapons, if not invulnerable.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1+:\tName of ships to make killable (ships do not need to be in-mission)." },

	// Goober5000
 	{ OP_SHIP_GUARDIAN_THRESHOLD, "ship-guardian-threshold\r\n"
 		"\tSame as ship-guardian, except the lowest possible hull value (as a percentage) is specified by the sexp rather than defaulting to 1.\r\n"
 		"Call with a threshold of 0 (or use ship-no-guardian) to deactivate.\r\n\r\n"
 		"Takes 2 or more arguments...\r\n"
 		"\t1:\tThreshold value.\r\n"
 		"\t2+:\tName of ships to make unkillable (ships must be in-mission)." },
 
 	// Goober5000
	{ OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD, "ship-subsys-guardian-threshold\r\n"
		"\tSame as ship-guardian-threshold, but works on subsystems.\r\n"
		"Call with a threshold of 0 to deactivate.\r\n\r\n"
 		"Takes 3 or more arguments...\r\n"
		"\t1:\tThreshold value.\r\n"
		"\t2:\tShip housing the subsystem(s) (ships must be in-mission).\r\n"
		"\t3+:\tSubsystems to make unkillable." },

	// MjnMixael
	{ OP_SET_GUARD_RANGE, "set-guard-range\r\n"
		"\tSets the max range in meters at which any ships guarding this ship will engage with threats.\r\n"
		"This range will override the default dynamic range behavior for ships obeying a guard order.\r\n"
		"If the value is <= 0, regular dynamic guard range behavior will resume. Positive values are used as is with no size validation based on ship class.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tGuard range cap in meters (<= 0 disables cap).\r\n"
		"\t2+:\tShip(s) to apply the cap to (ships must be in-mission)." },

	// Goober5000
	{ OP_SHIP_STEALTHY, "ship-stealthy\r\n"
		"\tCauses the ships listed in this sexpression to become stealth ships (i.e. invisible to radar).\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ships to make stealthy (ships do not need to be in-mission)." },

	// Goober5000
	{ OP_SHIP_UNSTEALTHY, "ship-unstealthy\r\n"
		"\tCauses the ships listed in this sexpression to become non-stealth ships (i.e. visible to radar).\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ships to make non-stealthy (ships do not need to be in-mission)." },

	// Goober5000
	{ OP_FRIENDLY_STEALTH_INVISIBLE, "friendly-stealth-invisible\r\n"
		"\tCauses the friendly ships listed in this sexpression to be invisible to radar, just like hostile stealth ships.  "
		"It doesn't matter if the ship is friendly at the time this sexp executes: as long as it is a stealth ship, it will "
		"be invisible to radar both as hostile and as friendly.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ships (ships do not need to be in-mission)" },

	// Goober5000
	{ OP_FRIENDLY_STEALTH_VISIBLE, "friendly-stealth-visible\r\n"
		"\tCauses the friendly ships listed in this sexpression to resume their normal behavior of being visible to radar as "
		"stealth friendlies.  Does not affect their visibility as stealth hostiles.\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tName of ships (ships do not need to be in-mission)" },

	// Goober5000
	{ OP_SHIP_SUBSYS_TARGETABLE, "ship-subsys-targetable\r\n"
		"\tCauses the specified ship subsystem(s) to be targetable on radar.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\tRest: Name of the ship's subsystem(s)" },

	// Goober5000
	{ OP_SHIP_SUBSYS_UNTARGETABLE, "ship-subsys-untargetable\r\n"
		"\tCauses the specified ship subsystem(s) to not be targetable on radar.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\tRest: Name of the ship's subsystem(s)" },

	// FUBAR
	{ OP_SHIP_SUBSYS_NO_REPLACE, "ship-subsys-no-replace\r\n"
		"\tCauses the -destroyed version of specified ship subsystem(s) to not render when it's destroyed.\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\t2:\tTrue = Do not render or False = render if exists\r\n"
		"\tRest: Name of the ship's subsystem(s)\r\n" 
		"\tNote: If subsystem is already dead it will vanish or reappear out of thin air" },


	// FUBAR
	{ OP_SHIP_SUBSYS_NO_LIVE_DEBRIS, "ship-subsys-no-live-debris\r\n"
		"\tCauses the specified ship subsystem(s) to not render live debris when it's destroyed.\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\t2:\tTrue = Do not render or False = render if exists\r\n"
		"\tRest: Name of the ship's subsystem(s)" },

	// FUBAR
	{ OP_SHIP_SUBSYS_VANISHED, "ship-subsys-vanish\r\n"
		"\tCauses the subsystem to vanish without a trace - no fanfare, notification, or special effects.  See also ship-vanish.\r\n"
		"\tSingle Player Only!  Warning: This will cause subsystem destruction not to be logged, so 'has-departed', etc. will not work\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\t2:\tTrue = vanish or False = don't vanish\r\n"
		"\tRest: Name of the ship's subsystem(s)\r\n" 
		"\tNote: Useful for replacing subsystems with actual docked models." },

	// FUBAR
	{ OP_SHIP_SUBSYS_IGNORE_IF_DEAD, "ship-subsys-ignore-if-dead\r\n"
		"\tCauses secondary weapons to ignore dead ship subsystem(s)and home on hull instead.\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\t2:\tTrue = Ignore dead or False = don't ignore\r\n"
		"\tRest: Name of the ship's subsystem(s)" },

	// Goober5000
	{ OP_SHIP_CHANGE_ALT_NAME, "ship-change-alt-name\r\n"
		"\tChanges the alternate ship class name displayed in the HUD target window.  Takes 2 or more arguments...\r\n"
		"\t1:\tThe ship class name to display\r\n"
		"\tRest:\tThe ships to display the new class name (ships do not need to be in-mission)" },

	{ OP_SHIP_CHANGE_DISPLAY_NAME, "ship-change-display-name\r\n"
		"\tChanges the ship name displayed in the HUD target window.  Takes 2 arguments...\r\n"
		"\t1:\tThe ship name to display\r\n"
		"\tRest:\tThe ship to display the new name (ships do not need to be in-mission)" },

	// FUBAR
	{ OP_SHIP_CHANGE_CALLSIGN, "ship-change-callsign\r\n"
		"\tChanges the callsign of a ship.  Takes 2 or more arguments...\r\n"
		"\t1:\tThe callsign to display or empty to remove\r\n"
		"\tRest:\tThe ships to display the new callsign (ships do not need to be in-mission)" },

	// Goober5000
	{ OP_SET_DEATH_MESSAGE, "set-death-message\r\n"
		"\tSets the message displayed when the specified players are killed.  Takes 1 or more arguments...\r\n"
		"\t1:\tThe message\r\n"
		"\tRest:\tThe players for whom this message is displayed (optional) (currently not implemented)" },

	{ OP_PERCENT_SHIPS_ARRIVED, "percent-ships-arrived\r\n"
		"\tBoolean function which returns true if the percentage of ships in the listed ships and wings "
		"which have arrived is greater than or equal to the given percentage.  For wings, all ships of all waves "
		"are used for calculation for the total possible ships to arrive.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tPercentage of arrived ships at which this function will return true.\r\n"
		"\t2+:\tList of ships/wings whose arrival status should be determined." },

	{ OP_PERCENT_SHIPS_DEPARTED, "percent-ships-departed\r\n"
		"\tBoolean function which returns true if the percentage of ships in the listed ships and wings "
		"which have departed is greater than or equal to the given percentage.  For wings, all ships of all waves "
		"are used for calculation for the total possible ships to depart.  Listed ships and wings yet to arrive are "
		"included in the total from which the percentage is calculated.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tPercentage of departed ships at which this function will return true.\r\n"
		"\t2+:\tList of ships/wings whose departure status should be determined." },

	{ OP_PERCENT_SHIPS_DESTROYED, "percent-ships-destroyed\r\n"
		"\tBoolean function which returns true if the percentage of ships in the listed ships and wings "
		"which have been destroyed is greater than or equal to the given percentage.  For wings, all ships of all waves "
		"are used for calculation for the total possible ships to be destroyed.  Listed ships and wings yet to arrive are "
		"included in the total from which the percentage is calculated.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tPercentage of destroyed ships at which this function will return true.\r\n"
		"\t2+:\tList of ships/wings whose destroyed status should be determined." },

	// Goober5000
	{ OP_PERCENT_SHIPS_DISARMED, "percent-ships-disarmed\r\n"
		"\tBoolean function which returns true if the percentage of ships in the listed ships "
		"which have been disarmed is greater than or equal to the given percentage.  Wings cannot be checked with this function; "
		"any ships that are part of wings must be listed explicitly.  Listed ships yet to arrive are "
		"included in the total from which the percentage is calculated.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tPercentage of disarmed ships at which this function will return true.\r\n"
		"\t2+:\tList of ships whose disarmed status should be determined." },

	// Goober5000
	{ OP_PERCENT_SHIPS_DISABLED, "percent-ships-disabled\r\n"
		"\tBoolean function which returns true if the percentage of ships in the listed ships "
		"which have been disabled is greater than or equal to the given percentage.  Wings cannot be checked with this function; "
		"any ships that are part of wings must be listed explicitly.  Listed ships yet to arrive are "
		"included in the total from which the percentage is calculated.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tPercentage of disabled ships at which this function will return true.\r\n"
		"\t2+:\tList of ships whose disabled status should be determined." },

	// Goober5000
	{ OP_PERCENT_SHIPS_SCANNED, "percent-ships-scanned\r\n"
		"\tBoolean function which returns true if the percentage of ships in the listed ships "
		"which have been scanned (or cargo revealed) is greater than or equal to the given percentage.  Wings cannot be checked with this function; "
		"any ships that are part of wings must be listed explicitly.  Listed ships yet to arrive are "
		"included in the total from which the percentage is calculated.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tPercentage of scanned ships at which this function will return true.\r\n"
		"\t2+:\tList of ships whose scanned status should be determined." },

	{ OP_RED_ALERT, "red-alert\r\n"
		"\tCauses Red Alert status in a mission.  This function ends the current mission, and moves to "
		"the next mission in the campaign under red alert status.  There should only be one branch from "
		"a mission that uses this expression\r\n\r\n"
		"Takes no arguments."},

	{ OP_MISSION_SET_NEBULA, "mission-set-nebula\r\n" 
		"\tTurns nebula on/off\r\n"
		"\tTakes 1 or 2 arguments...\r\n"
		"\t1:\t0 for nebula off, 1 for nebula on\r\n"
		"\t2:\tNebula range (optional; defaults to 3000).  See also nebula-set-range.\r\n" },

	{ OP_MISSION_SET_SUBSPACE, "mission-set-subspace\r\n"
		"\tTurns subspace on/off\r\n"
		"\tTakes 1 argument...\r\n"
		"\r1:\t0 for subspace off, 1 for subspace on" },

	//-Sesquipedalian
	{ OP_END_MISSION, "end-mission\r\n" 
		"\tEnds the mission without the usual jump-out sequence. This immediately dumps the player back into a normal debriefing. Red-alert status is not invoked.\r\n"
		"\t1:\tEnd Mission even if the player is dead (optional; defaults to true)\r\n"
		"\t2:\tBoot the player out into the main hall instead of going to the debriefing (optional; defaults to false; not supported in multi)\r\n"
		"\t3:\tGo to the mainhall instead of starting the next mission (optional; defaults to false; not yet tested in multi)"
	},

	// Goober5000
	{ OP_SET_DEBRIEFING_TOGGLED, "set-debriefing-toggled\r\n"
		"\tSets or clears the \"toggle debriefing\" mission flag.  If set, the mission will have its debriefing turned off, unless it is a multiplayer dogfight mission, in which case its debriefing will be turned on.  Takes 1 argument.\r\n"
	},

	// Goober5000
	{ OP_SET_DEBRIEFING_PERSONA, "set-debriefing-persona\r\n"
		"\tSets the numeric prefix to be used for debriefing voice files in automatic debriefing stages (promotions, ace badges, traitor).  This is usually specified in the campaign editor, but it can be overridden mid-mission with this sexp.  Takes 1 argument.\r\n"
	},

	// MjnMixael
	{ OP_SET_TRAITOR_OVERRIDE, "set-traitor-override\r\n"
		"\tSets the override debriefing info for the traitor debrief. Must be tabled in traitor.tbl."
	},

	// Goober5000
	{ OP_FORCE_JUMP, "force-jump\r\n"
		"\tForces activation of the player's subspace drive, thus ending the mission.  Takes no arguments."
	},

	// Goober5000
	{ OP_SET_SCANNED, "set-scanned\r\n"
		"\tSets the cargo on the specified ship or ship subsystem as known or scanned.  Takes 1 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\tRest:\tName of a subsystem on that ship (optional)\r\n" },

	// Goober5000
	{ OP_SET_UNSCANNED, "set-unscanned\r\n"
		"\tSets the cargo on the specified ship or ship subsystem as unknown or unscanned.  Takes 1 or more arguments...\r\n"
		"\t1:\tName of a ship (ship must be in-mission)\r\n"
		"\tRest:\tName of a subsystem on that ship (optional)\r\n" },

	{ OP_DEPART_NODE_DELAY, "depart-node-delay\r\n"
		"\tReturns true N seconds after the listed ships depart, if those ships depart within the "
		"radius of the given jump node.  The delay value is given in seconds.\r\n\r\n"
		"Takes 3 or more arguments...r\n"
		"\t1:\tDelay in seconds after the last ship listed departed before this expression can return true.\r\n"
		"\t2:\tName of a jump node\r\n"
		"\t3+:\tList of ships to check for departure within radius of the jump node." },

	{ OP_DESTROYED_DEPARTED_DELAY, "destroyed-or-departed-delay\r\n"
		"\tReturns true N seconds after all the listed ships or wings have been destroyed or have "
		"departed.\r\n\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tDelay in seconds after the last ship/wing is destroyed or departed before this expression can return true.\r\n"
		"\t2+:\tName of a ship or wing" },

	// description added by Goober5000
	{ OP_SPECIAL_CHECK, "Special-check\r\n"
		"\tMike K.'s special training sexp.  Returns a boolean value.  Takes 1 argument as follows:\r\n"
		"\t0:    Ship \"Freighter 1\" is aspect locked by the player\r\n"
		"\t1:    Player has fired Interceptor#Weak at Freighter 1\r\n"
		"\t2:    Ship \"Freighter 1\", subsystem \"Weapons\" is aspect locked by the player\r\n"
		"\t3:    Apply 10 points of damage to player's forward shields (action operator)\r\n"
		"\t4:    Player's front shields are nearly gone\r\n"
		"\t5:    Quickly recharge player's front shields (action operator)\r\n"
		"\t6:    Reduce all shield quadrants except front to 0 (action operator)\r\n"
		"\t7:    Front shield quadrant is near maximum strength\r\n"
		"\t8:    Rear shield quadrant is near maximum strength\r\n"
		"\t9:    Reduce left and right shield quadrants to 0 (action operator)\r\n"
		"\t10:   Player has fewer than 8 missiles left\r\n"
		"\t11:   Player has 8 or more missiles left\r\n"
		"\t12:   Player has fewer than 4 missiles left\r\n"
		"\t13:   Reduce front shield quadrant to 0 (action operator)\r\n"
		"\t100:  Player is out of countermeasures\r\n"
		"\t2000: Training failed"
	},

	{ OP_END_CAMPAIGN, "end-campaign\r\n"
		"\tEnds the builtin campaign.  Should only be used by the main FreeSpace campaign\r\n"
		"\t1:\tEnd Campaign even if the player is dead (optional; defaults to true)\r\n" },

	{ OP_SHIP_VAPORIZE, "ship-vaporize\r\n"
		"\tSets the ship to vaporize when it is destroyed.  Does not actually destroy the ship - use self-destruct for that.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships on which to set the vaporize flag (ships do not need to be in-mission)" },

	{ OP_SHIP_NO_VAPORIZE, "ship-no-vaporize\r\n"
		"\tSets the ship to not vaporize when it is destroyed.  Does not actually destroy the ship - use self-destruct for that.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships on which to unset the vaporize flag (ships do not need to be in-mission)" },

	{ OP_DONT_COLLIDE_INVISIBLE, "don't-collide-invisible\r\n"
		"\tSets the \"don't collide invisible\" flag on a list of ships.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships on which to set the \"don't collide invisible\" flag (ships do not need to be in-mission)" },

	{ OP_COLLIDE_INVISIBLE, "collide-invisible\r\n"
		"\tUnsets the \"don't collide invisible\" flag on a list of ships.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships on which to unset the \"don't collide invisible\" flag (ships do not need to be in-mission)" },

	{ OP_SET_MOBILE, "set-mobile\r\n"
		"\tAllows the specified ship(s) to change position and orientation by clearing the \"immobile\" flag.  Opposite of set-immobile.  "
		"NOTE: This also clears the \"don't-change-position\" and \"don't-change-orientation\" flags to avoid any surprises or conflicts with the \"immobile\" flag.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships on which to modify flags (ships do not need to be in-mission)" },

	{ OP_SET_IMMOBILE, "set-immobile\r\n"
		"\tPrevents the specified ship(s) from changing position or orientation (at least until the death roll starts) by setting the \"immobile\" flag.  Opposite of set-mobile.  "
		"NOTE: This also clears the \"don't-change-position\" and \"don't-change-orientation\" flags to avoid any surprises or conflicts with the \"immobile\" flag.\r\n"
		"Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships on which to modify flags (ships do not need to be in-mission)" },

	{ OP_IGNORE_KEY, "ignore-key\r\n"
		"\tCauses the game to ignore (or stop ignoring) a certain key.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1: Number of times to ignore this key (-1 = forever, 0 = stop ignoring). \r\n"
		"\tRest: Which key(s) to ignore.\r\n"
	},

	{ OP_WARP_BROKEN, "break-warp\r\n"
		"\tBreak the warp drive on the specified ship.  A broken warp drive can be repaired by "
		"a repair ship.  Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships to break the warp drive on (ships do not need to be in-mission)" },

	{ OP_WARP_NOT_BROKEN, "fix-warp\r\n"
		"\tFixes a broken warp drive instantaneously.  This option applies to warp drives broken with "
		"the break-warp sepxression.  Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships whose warp drive should be fixed (ships do not need to be in-mission)"},

	{ OP_WARP_NEVER, "never-warp\r\n"
		"\tNever allows a ship to warp out.  When this sexpression is used, the given ships will "
		"never be able to warp out.  The warp drive cannot be repaired.  Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships whose are not allowed to warp out under any condition (ships do not need to be in-mission)"},

	{ OP_WARP_ALLOWED, "allow-warp\r\n"
		"\tAllows a ship which was previously not allowed to warp out to do so.  When this sexpression is "
		"used, the given ships will be able to warp out again.  Takes 1 or more arguments...\r\n"
		"\tAll:\tList of ships whose are allowed to warp out (ships do not need to be in-mission)"},

	{ OP_SET_SUBSPACE_DRIVE, "set-subspace-drive\r\n"
		"\tTurns on or off the subspace edrive for the given ships.  A ship with no "
		"subspace drive will act as though it doesn't even occur to it to depart "
		"via subspace, and if ordered to do so, it will look for a friendly ship with "
		"a hangar bay. If the ship is the player, pressing Alt-J will not not initiate "
		"a jump, nor give any indication that a jump failed. Takes 2 or more arguments...\r\n"
		"\t1:\tTrue if the ship should have a drive; false otherwise\r\n"
		"\tRest:\tList of ships (ships do not need to be in-mission)" },

	{ OP_JETTISON_CARGO_DELAY, "jettison-cargo-delay (deprecated)\r\n"
		"\tCauses a cargo carrying ship to jettison its cargo without the undocking procedure.  Takes 2 or more arguments...\r\n"
		"\t1: Ship to jettison cargo (ship must be in-mission)\r\n"
		"\t2: Delay after which to jettison cargo (note that this isn't actually used)\r\n"
		"\tRest (optional): Cargo to jettison.  If no optional arguments are specified, the ship jettisons all cargo.\r\n"
	},

	{ OP_JETTISON_CARGO_NEW, "jettison-cargo\r\n"
		"\tCauses a cargo carrying ship to jettison its cargo without the undocking procedure.  This is an upgrade of the old "
		"jettison-cargo-delay sexp which a) didn't use a delay, and b) botched the physics calculations.  Takes 1 or more arguments...\r\n"
		"\t1: Ship to jettison cargo (ship must be in-mission)\r\n"
		"\t2 (optional): Speed with which to jettison cargo (defaults to 25)\r\n"
		"\tRest (optional): Cargo to jettison.  If no cargo arguments are specified, the ship jettisons all cargo.\r\n"
	},

	{ OP_SET_DOCKED, "set-docked\r\n"
		"\tCauses one ship to become instantly docked to another at the specified docking ports (both ships must be in-mission).  Takes 4 arguments...\r\n"
		"\t1: Docker ship\r\n"
		"\t2: Docker point\r\n"
		"\t3: Dockee ship\r\n"
		"\t4: Dockee point\r\n"
	},

	{ OP_BEAM_FIRE, "fire-beam\r\n"
		"\tFire a beam weapon from a specified subsystem\r\n"
		"\t1:\tShip which will be firing (ship must be in-mission)\r\n"
		"\t2:\tTurret which will fire the beam (note, this turret must have at least 1 beam weapon on it)\r\n"
		"\t3:\tShip which will be targeted\r\n"
		"\t4:\tSubsystem to target (optional)\r\n"
		"\t5:\tWhether to force the beam to fire (disregarding FOV and subsystem status) (optional)\r\n" },

	{ OP_BEAM_FIRE_COORDS, "fire-beam-at-coordinates\r\n"
		"\tFire a beam weapon from a specified subsystem at a set of coordinates.  Not compatible with multiplayer.\r\n"
		"\t1:\tShip which will be firing (ship must be in-mission)\r\n"
		"\t2:\tTurret which will fire the beam (note, this turret must have at least 1 beam weapon on it)\r\n"
		"\t3:\tx coordinate to be targeted\r\n"
		"\t4:\ty coordinate to be targeted\r\n"
		"\t5:\tz coordinate to be targeted\r\n"
		"\t6:\t(Optional Operator) Whether to force the beam to fire (disregarding FOV and subsystem status). Defaults to False\r\n"
		"\t7:\tsecond x coordinate to be targeted (optional; only used for slash beams)\r\n"
		"\t8:\tsecond y coordinate to be targeted (optional; only used for slash beams)\r\n"
		"\t9:\tsecond z coordinate to be targeted (optional; only used for slash beams)\r\n" },

	{ OP_BEAM_FLOATING_FIRE, "beam-create\r\n"
		"\tFire a beam weapon from the specified coordinates to the specified target. Not compatible with multiplayer.\r\n"
		"\t1:\tBeam weapon to fire\r\n"
		"\t2:\tParent ship (for kill credit, if applicable, must be in-mission; or \"" SEXP_NONE_STRING "\")\r\n"
		"\t3:\tTeam for this beam to be on (related to difficulty-based damage)\r\n"
		"\t4:\tX coordinate to fire from\r\n"
		"\t5:\tY coordinate to fire from\r\n"
		"\t6:\tZ coordinate to fire from\r\n"
		"\t7:\tTarget ship (must be in-mission or \"" SEXP_NONE_STRING "\")\r\n"
		"\t8:\tTarget subsystem (optional; can be \"" SEXP_NONE_STRING "\")\r\n"
		"\t9:\tX coordinate to fire at (optional)\r\n"
		"\t10:\tY coordinate to fire at (optional)\r\n"
		"\t11:\tZ coordinate to fire at (optional)\r\n"
		"\t12:\tSecond X coordinate to fire at (optional; used for slash beams)\r\n"
		"\t13:\tSecond Y coordinate to fire at (optional; used for slash beams)\r\n"
		"\t14:\tSecond Z coordinate to fire at (optional; used for slash beams)\r\n" },

	{ OP_IS_TAGGED, "is-tagged\r\n"
		"\tReturns whether a given ship is tagged or not (evaluation returns NAN until ship is in-mission)\r\n"},

	{ OP_IS_PLAYER, "is-player\r\n"
		"\tReturns true if all the ships specified are currently under control of a player.\r\n"
		"\t1 \t(SinglePlayer) When true ships under AI control return false even if they are the player ship\r\n"
		"\t \t(Multiplayer) When true ships that are respawning return true if the player is still connected\r\n"
		"\tRest \tList of ships to test"},

	{ OP_NUM_KILLS, "num-kills\r\n"
		"\tReturns the # of kills a player has. The ship specified in the first field should be the ship the player is in.\r\n"
		"\tSo, for single player, this would be Alpha 1. For multiplayer, it can be any ship with a player in it. If, at any\r\n"
		"\ttime there is no player in a given ship, this sexpression will return 0"},

	{ OP_NUM_ASSISTS, "num-assists\r\n"
		"\tReturns the # of assists a player has. The ship specified in the first field should be the ship the player is in.\r\n"
		"\tSo, for single player, this would be Alpha 1. For multiplayer, it can be any ship with a player in it. If, at any\r\n"
		"\ttime there is no player in a given ship, this sexpression will return 0"},

	{ OP_SHIP_SCORE, "ship-score\r\n"
		"\tReturns the score a player has. The ship specified in the first field should be the ship the player is in.\r\n"
		"\tSo, for single player, this would be Alpha 1. For multiplayer, it can be any ship with a player in it. If, at any\r\n"
		"\ttime there is no player in a given ship, this sexpression will return 0"},

	{ OP_SHIP_DEATHS, "ship-deaths\r\n"
		"\tReturns the # times a ship that is a player start has died.\r\n"
		"\tThe ship specified in the first field should be the ship that could have a player in.it\r\n"
		"\tOnly really useful for multiplayer."},

	{ OP_RESPAWNS_LEFT, "respawns-left\r\n"
		"\tReturns the # respawns a player (or AI that could have been a player) has remaining.\r\n"
		"\tThe ship specified in the first field should be the player start.\r\n"
		"\tOnly really useful for multiplayer."},

	{ OP_CLEAR_WEAPONS, "clear-weapons\r\n"
		"\tRemoves all live weapons currently in the mission.\r\n"
		"\t1: (Optional) Remove only this specific class of weapon\r\n"},

	{ OP_CLEAR_DEBRIS, "clear-debris\r\n"
		"\tRemoves all ship debris currently in the mission.\r\n"
		"\t1: (Optional) Remove only debris from this specific class of ship\r\n"},

	{ OP_SET_RESPAWNS, "set-respawns\r\n"
		"\tSet the # respawns a player (or AI that could have been a player) has used.\r\n"
		"\t1: Number of respawns used up\r\n"
		"\tRest: The player start ship to operate on.\r\n"
		"\tOnly really useful for multiplayer."},

	{ OP_ADD_REMOVE_HOTKEY, "add-remove-hotkey\r\n"
		"\tAdd or remove hotkey for the specified ship(s) or wing(s)\r\n"
		"\tTakes 3 or more arguments\r\n"
		"\t1: Boolean. True adds hotkey and false removes hotkey. \r\n"
		"\t2: Integer of hotkey to add or remove. 0=F5, 1=F6, ... \r\n"
		"\t(rest): Name(s) of ship(s) or wing(s)"},

	{ OP_NUM_TYPE_KILLS, "num-type-kills\r\n"
		"\tReturns the # of kills a player has on a given ship type (fighter, bomber, cruiser, etc).\r\n"
		"The ship specified in the first field should be the ship the player is in.\r\n"
		"\tSo, for single player, this would be Alpha 1. For multiplayer, it can be any ship with a player in it. If, at any\r\n"
		"\ttime there is no player in a given ship, this sexpression will return 0"},

	{ OP_NUM_CLASS_KILLS, "num-class-kills\r\n"
		"\tReturns the # of kills a player has on a specific ship class (Ulysses, Hercules, etc).\r\n"
		"The ship specified in the first field should be the ship the player is in.\r\n"
		"\tSo, for single player, this would be Alpha 1. For multiplayer, it can be any ship with a player in it. If, at any\r\n"
		"\ttime there is no player in a given ship, this sexpression will return 0"},	

	{ OP_BEAM_FREE, "beam-free\r\n"
		"\tSets one or more turrets to allow beam weapons firing.  When a beam-equipped turret is activated, there will be a short random delay (between 50 and 4000 milliseconds) before firing.\r\n"
		"\t1: Ship to be operated on (ship must be in-mission)\r\n"
		"\t2, 3, etc : List of turrets to activate\r\n"},

	{ OP_BEAM_FREE_ALL, "beam-free-all\r\n"
		"\tSets all beam weapons on the specified ship to be active (ship must be in-mission).  For each beam-equipped turret, there will be a different short random delay (between 50 and 4000 milliseconds) before firing.\r\n"},

	{ OP_BEAM_LOCK, "beam-lock\r\n"
		"\tSets one or more turrets to NOT allow beam weapons firing.\r\n"
		"\t1: Ship to be operated on (ship must be in-mission)\r\n"
		"\t2, 3, etc : List of turrets to deactivate\r\n"},

	{ OP_BEAM_LOCK_ALL, "beam-lock-all\r\n"
		"\tSets all beam turrets on the specified ship to be deactivated (ship must be in-mission)\r\n"},

	{ OP_TURRET_FREE, "turret-free\r\n"
		"\tSets one or more turrets to allow standard primary or secondary weapons firing.  When a turret is activated, there will be a short random delay (between 50 and 4000 milliseconds) before firing.\r\n"
		"\t1: Ship to be operated on (ship must be in-mission)\r\n"
		"\t2, 3, etc : List of turrets to activate\r\n"},

	{ OP_TURRET_FREE_ALL, "turret-free-all\r\n"
		"\tSets all standard primary or secondary weapon turrets on the specified ship to be active (ship must be in-mission).  For each turret, there will be a different short random delay (between 50 and 4000 milliseconds) before firing.\r\n"},

	{ OP_TURRET_LOCK, "turret-lock\r\n"
		"\tSets one or more turrets to NOT allow standard primary or secondary weapons firing.\r\n"
		"\t1: Ship to be operated on (ship must be in-mission)\r\n"
		"\t2, 3, etc : List of turrets to deactivate\r\n"},

	{ OP_TURRET_LOCK_ALL, "turret-lock-all\r\n"
		"\tSets all turret weapons on the specified ship to be deactivated (ship must be in-mission)\r\n"},

	{ OP_TURRET_CHANGE_WEAPON, "turret-change-weapon\r\n"
		"\tSets a given turret weapon slot to the specified weapon\r\n"
		"\t1: Ship turret is on (ship must be in-mission)\r\n"
		"\t2: Turret\r\n"
		"\t3: Weapon to set slot to\r\n"
		"\t4: Primary slot (or 0 to use secondary)\r\n"
		"\t5: Secondary slot (or 0 to use primary)"},

	{ OP_TURRET_SET_DIRECTION_PREFERENCE, "turret-set-direction-preference\r\n"
		"\tSets specified ship turrets direction preference to the specified value\r\n"
		"\t1: Ship turrets are on (ship must be in-mission)\r\n"
		"\t2: Preference to set, 0 to disable, or negative to reset to default\r\n"
		"\trest: Turrets to set\r\n"},

	{ OP_TURRET_SET_RATE_OF_FIRE, "turret-set-rate-of-fire\r\n"
		"\tSets specified ship turrets rate of fire to the specified value\r\n"
		"\t1: Ship turrets are on (ship must be in-mission)\r\n"
		"\t2: Rate to set in percentage format (200 = 2x, 50 = .5x), 0 to set to number of fire points, or negative to reset to default\r\n"
		"\trest: Turrets to set\r\n"},

	{ OP_TURRET_SET_OPTIMUM_RANGE, "turret-set-optimum-range\r\n"
		"\tSets specified ship turrets optimum range to the specified value\r\n"
		"\t1: Ship turrets are on (ship must be in-mission)\r\n"
		"\t2: Priority to set, 0 to disable, or negative to reset to default\r\n"
		"\trest: Turrets to set\r\n"},

	{ OP_TURRET_SET_FORCED_TARGET, "turret-set-forced-target\r\n"
		"\tForces one or more turrets to target a specific ship. Will hang on to this target until dead or cleared by turret-clear-forced-target\r\n"
		"\tWill still not fire if the target is protected or otherwise an unsuitable target for the turret weapons\r\n"
		"\t1: Ship to target (ship must be in-mission)\r\n"
		"\t2: Ship turrets are on (ship must be in-mission)\r\n"
		"\trest: Turrets to set\r\n" },
	
	{ OP_TURRET_SET_FORCED_SUBSYS_TARGET, "turret-set-forced-subsys-target\r\n"
		"\tForces one or more turrets to target a specific subsystem on a particular ship. Will hang on to this subsystem target until destroyed or cleared by turret-clear-forced-target\r\n"
		"\tWill still not fire if the target is protected or otherwise an unsuitable target for the turret weapons\r\n"
		"\t1: Ship to target (ship must be in-mission)\r\n"
		"\t2: Subsystem to target\r\n"
		"\t3: Ship turrets are on (ship must be in-mission)\r\n"
		"\trest: Turrets to set\r\n" },

	{ OP_TURRET_CLEAR_FORCED_TARGET, "turret-clear-forced-target\r\n"
		"\tClears any targets set by turret-set-forced-target\r\n"
		"\t1: Ship turrets are on (ship must be in-mission)\r\n"
		"\trest: Turrets to have their targets cleared\r\n" },

	{ OP_TURRET_SET_TARGET_PRIORITIES, "turret-set-target-priorities\r\n"
		"\tSets target priorities for the specified ship turret\r\n"
		"\t1: Ship turret is on (ship must be in-mission)\r\n"
		"\t2: Turret to set\r\n"
		"\t3: True = Set new list, False = Reset to turret default\r\n"
		"\trest: Priorities to set (max 32) or blank for no priorities\r\n"},

	{ OP_TURRET_SET_INACCURACY, "turret-set-inaccuracy\r\n"
		"\tMakes the specified turrets more inaccurate by firing their shots in a cone, like field of fire.  "
		"This will only decrease their accuracy, it cannot make the weapons more accurate than normal.\r\n"
		"\tDoes not work on beams.\r\n"
		"\t1: Ship turret(s) are on (ship must be in-mission)\r\n"
		"\t2: TENTHS of a degree. (i.e. 100 = 10 degrees)\r\n"
		"\trest: (optional) Turrets to set. If omitted affects all turrets on the ship.\r\n" },

	{ OP_TURRET_GET_PRIMARY_AMMO, "turret-get-primary-ammo\r\n"
		"\tGets the turret's primary bank ammo, only works with ballistic weapons\r\n"
		"\t1: Ship turret is on (evaluation returns NAN until ship is in-mission)\r\n"
		"\t2: Turret the bank is on\r\n"
		"\t3: Bank to check ammo\r\n"},

	{ OP_TURRET_GET_SECONDARY_AMMO, "turret-get-secondary-ammo\r\n"
		"\tGets the turret's secondary bank ammo\r\n"
		"\t1: Ship turret is on (evaluation returns NAN until ship is in-mission)\r\n"
		"\t2: Turret to check ammo\r\n"
		"\t3: Bank to check ammo\r\n" },

	{ OP_TURRET_SET_PRIMARY_AMMO, "turret-set-primary-ammo\r\n"
		"\tSets the turret's primary bank ammo, only works with ballistic weapons\r\n"
		"\t1: Ship turret is on (ship must be in-mission)\r\n"
		"\t2: Turret the bank is on\r\n"
		"\t3: Bank to add ammo to\r\n" 
		"\t4: Amount to add" },

	{ OP_TURRET_SET_SECONDARY_AMMO, "turret-set-secondary-ammo\r\n"
		"\tSets the turret's secondary bank ammo\r\n"
		"\t1: Ship turret is on (ship must be in-mission)\r\n"
		"\t2: Turret the bank is on\r\n"
		"\t3: Bank to add ammo to\r\n"
		"\t4: Amount to add" },

	{ OP_IS_IN_TURRET_FOV, "is-in-turret-fov\r\n"
		"\tChecks whether the given craft is within the field of view of a turret, and optionally within a specified range.  Takes 3 to 4 arguments...\r\n"
		"\t1: Ship being targeted (false if ship not in-mission)\r\n"
		"\t2: Ship which carries a turret (false if ship not in-mission)\r\n"
		"\t3: Turret to check\r\n"
		"\t4: Range in meters (optional)\r\n" },

	{ OP_SET_ARMOR_TYPE, "set-armor-type\r\n"
		"\tSets the armor type for a ship or subsystem\r\n"
		"\t1: Ship subsystem is on (ship must be in-mission)\r\n"
		"\t2: Set = true/Reset to default = false\r\n"
		"\t3: Armor type to set or <none>\r\n"
		"\trest: Subsystems to set (hull for ship, shield for shields)\r\n"},

	{ OP_WEAPON_SET_DAMAGE_TYPE, "weapon-set-damage-type\r\n"
		"\tSets the damage type for weapons or their shockwaves\r\n"
		"\t1: True = set weapon, False = set shockwave\r\n"
		"\t2: damage type to set or <none>\r\n"
		"\t3: Set = true/Reset to default = false\r\n"
		"\trest: Weapons to set\r\n"},

	{ OP_SHIP_SET_DAMAGE_TYPE, "ship-set-damage-type\r\n"
		"\tSets the damage type for ships collision or debris\r\n"
		"\t1: true = set collision, False = set debris\r\n"
		"\t2: Damage type to set or <none>\r\n"
		"\t3: Set = true/Reset to default = false\r\n"
		"\trest: Ships to set (ship must be in-mission)\r\n"},

	{ OP_SHIP_SHOCKWAVE_SET_DAMAGE_TYPE, "ship-set-shockwave-damage-type\r\n"
		"\tSets the shockwave damage type for a class of ship.  All ships of that class are changed.\r\n"
		"\t1: Damage type to set or <none>\r\n"
		"\t2: Set = true/Reset to default = false\r\n"
		"\trest: Ship classes to set\r\n"},

	{ OP_FIELD_SET_DAMAGE_TYPE, "field-set-damage-type\r\n"
		"\tSets the damage type for asteroid/debris fields\r\n"
		"\t1: Damage type to set or <none>\r\n"
		"\t2: Set = true/Reset to default = false\r\n"},

	{ OP_TURRET_SET_TARGET_ORDER, "turret-set-target-order\r\n"
		"\tSets targeting order of a given turret\r\n"
		"\t1: Ship turret is on (ship must be in-mission)\r\n"
		"\t2: Turret\r\n"
		"\trest: Target order type (Bombs,ships,asteroids)"},

	{ OP_SHIP_TURRET_TARGET_ORDER, "ship-turret-target-order\r\n"
		"\tSets targeting order of all turrets on a given ship\r\n"
		"\t1: Ship turrets are on (ship must be in-mission)\r\n"
		"\trest: Target order type (Bombs,ships,asteroids)"},

	{ OP_TURRET_SUBSYS_TARGET_DISABLE, "turret-subsys-target-disable\r\n"
		"\tPrevents turrets from targeting only the subsystems when targeting large targets\r\n"
		"\t1: Ship to be operated on (ship must be in-mission)\r\n"
		"\trest: List of turrets that are affected\r\n"},

	{ OP_TURRET_SUBSYS_TARGET_ENABLE, "turret-subsys-target-enable\r\n"
		"\tSets turret to target the subsystems when targeting large targets\r\n"
		"\t1: Ship to be operated on (ship must be in-mission)\r\n"
		"\trest: List of turrets that are affected\r\n"},

	{ OP_ADD_REMOVE_ESCORT, "add-remove-escort\r\n"
		"\tAdds or removes a ship from an escort list.\r\n"
		"\t1: Ship to be added or removed (ship must be in-mission)\r\n"
		"\t2: 0 to remove from the list, any positive value will be used as the escort priority\r\n"
		"NOTE : it _IS_ safe to add a ship which may already be on the list or remove\r\n"
		"a ship which is not on the list\r\n"},

	{ OP_AWACS_SET_RADIUS, "awacs-set-radius\r\n"
		"\tSets the awacs radius for a given ship subsystem. NOTE : does not work properly in multiplayer\r\n"
		"\t1: Ship which has the awacs subsystem (ship must be in-mission)\r\n"
		"\t2: Awacs subsystem\r\n"
		"\t3: New radius\r\n"},

	// Goober5000
	{ OP_PRIMITIVE_SENSORS_SET_RANGE, "primitive-sensors-set-range\r\n"
		"\tSets the range of the primitive sensors on a ship.  Ships outside of this range will not appear on "
		"sensors.  Has no effect on ships that do not have the \"primitive-sensors\" flag2 set.  Takes 2 arguments...\r\n"
		"\t1: Ship on which to set range (ship must be in-mission)\r\n"
		"\t2: Range, in meters\r\n" },

	{ OP_SEND_MESSAGE_LIST, "send-message-list\r\n"
		"\tSends a series of delayed messages. All times are accumulated. Takes arguments in multiples of 4.\r\n"
		"\t1:\tName of who the message is from.\r\n"
		"\t2:\tPriority of message (\"Low\", \"Normal\" or \"High\").\r\n"
		"\t3:\tName of message (from message list).\r\n"
		"\t4:\tDelay from previous message in list (if any) in ms\r\n"
		"Use Add-Data for multiple messages.\r\n\r\n"
		"IMPORTANT: Each additional message in the list MUST HAVE four entries; "
		"any message without the four proper fields will be ignored, as will any "
		"successive messages.\r\n\r\n"
		"See send-message for the message priority descriptions.\r\n"
	},

	{ OP_SEND_MESSAGE_CHAIN, "send-message-chain\r\n"
		"\tThis works just like send-message-list, but if the event specified in the first argument becomes true, all un-played messages are cancelled.\r\n"
		"\t1:\tThe event that will cancel the message chain.\r\n"
		"\t2:\tName of who the message is from.\r\n"
		"\t3:\tPriority of message (\"Low\", \"Normal\" or \"High\").\r\n"
		"\t4:\tName of message (from message list).\r\n"
		"\t5:\tDelay from previous message in list (if any) in ms\r\n"
		"Use Add-Data for multiple messages.\r\n\r\n"
		"IMPORTANT: Each additional message in the list MUST HAVE four entries; "
		"any message without the four proper fields will be ignored, as will any "
		"successive messages.\r\n\r\n"
		"See send-message for the message priority descriptions.\r\n"
	},

	{ OP_CAP_WAYPOINT_SPEED, "cap-waypoint-speed\r\n"
		"\tSets the maximum speed of a ship while flying waypoints.\r\n"
		"\t1: Ship name (ship must be in-mission)\r\n"
		"\t2: Maximum speed while flying waypoints (must be greater than 0)\r\n"
		"\tNOTE: This will only work if the ship is already in the game\r\n"
		"\tNOTE: Set to -1 to reset\r\n"},

	{ OP_SET_WING_FORMATION, "set-wing-formation\r\n"
		"\tSets the formation used by one or more wings\r\n"
		"\t1: Formation name\r\n"
		"\t2: Formation multiplier, as a percentage (1x = 100)\r\n"
		"\tRest: Wing that should fly this formation (wing does not need to be in-mission)\r\n"
	},

	{ OP_TURRET_TAGGED_ONLY_ALL, "turret-tagged-only\r\n"
		"\tMakes turrets target and hence fire strictly at tagged objects\r\n"
		"\tAll: Ship name (ship must be in-mission)\r\n"
		"\tNOTE: Will not stop a turret already firing at an untagged ship\r\n"},

	{ OP_TURRET_TAGGED_CLEAR_ALL, "turret-tagged-clear\r\n"
		"\tRelaxes restriction on turrets targeting only tagged ships\r\n"
		"\tAll: Ship name (ship must be in-mission)\r\n"},

	{ OP_PRIMARIES_DEPLETED, "primaries-depleted\r\n"
		"\tReturns true if ship is out of primary weapons\r\n"
		"\t1: Ship name\r\n"},

	{ OP_SECONDARIES_DEPLETED, "secondaries-depleted\r\n"
		"\tReturns true if ship is out of secondary weapons\r\n"
		"\t1: Ship name\r\n"},

	{ OP_SUBSYS_SET_RANDOM, "subsys-set-random\r\n"
		"\tSets ship subsystem strength in a given range\r\n"
		"\t1: Ship name (ship must be in-mission)\r\n"
		"\t2: Lower bound, inclusive, of the subsystem strength\r\n"
		"\t3: Upper bound, inclusive, of the subsystem strength\r\n"
		"\t4: List of subsystem names not to be randomized\r\n"},

	{ OP_SUPERNOVA_START, "supernova-start\r\n"
		"\t1: Time in seconds that the supernova lasts.  Note that it will actually hit the player at " SCP_TOKEN_TO_STR(SUPERNOVA_HIT_TIME) " seconds.  If you want the HUD gauge to adjust for this, use the '$Supernova hits at zero' game_settings.tbl option.\r\n"},

	{ OP_SUPERNOVA_STOP, "supernova-stop\r\n"
		"\tStops a supernova in progress.  Note this only works if the camera hasn't cut to the player's death animation yet (is not yet at stage 3 as described in get-supernova-stage).\r\n"},

	{ OP_GET_SUPERNOVA_STAGE, "get-supernova-stage\r\n"
		"\tReturns the current stage of the active supernova, if any.  Takes no arguments.  Note that the game considers the player to be hit by the supernova at stage 3.\r\n"
		"\tPossible return values:\r\n"
		"\t0 = not active\r\n"
		"\t1 = player still in control. shockwave approaching.\r\n"
		"\t2 = shockwave still approaching, but very close. sound1 has started\r\n"
		"\t3 = camera cut. player controls locked. letterbox. sound2 has started. particles start\r\n"
		"\t4 = tooltime. lots of particles. camera stops moving\r\n"
		"\t5 = player is effectively dead. fade to white. stop simulation\r\n"
		"\t6 = dead popup\r\n"
	},

	{ OP_WEAPON_RECHARGE_PCT, "weapon-recharge-pct\r\n"
		"\tReturns a percentage from 0 to 100\r\n"
		"\t1: Ship name\r\n" },

	{ OP_SHIELD_RECHARGE_PCT, "shield-recharge-pct\r\n"
		"\tReturns a percentage from 0 to 100\r\n"
		"\t1: Ship name\r\n" },

	{ OP_ENGINE_RECHARGE_PCT, "engine-recharge-pct\r\n"
		"\tReturns a percentage from 0 to 100\r\n"
		"\t1: Ship name\r\n" },

	{ OP_GET_ETS_VALUE, "get-ets-value\r\n"
		"\tGets one ETS index for a ship\r\n"
		"\tIndex values are used in a hard-coded array to lookup recharge percentages\r\n"
		"\tEach index has a valid range of 0 to 12\r\n"
		"\t1: ETS index to get, Engine|Shield|Weapon\r\n"
		"\t2: Ship name\r\n"},

	{ OP_SET_ETS_VALUES, "set-ets-values\r\n"
		"\tSets ETS indexes for a ship\r\n"
		"\tIndex values are used in a hard-coded array to lookup recharge percentages\r\n"
		"\tEach index has a valid range of 0 to 12\r\n"
		"\tUse index values retrieved with get-ets-value\r\n"
		"\tOr if you use your own values, ensure they add up to 12 (e.g. 0,3,9)\r\n"
		"\t1: Engine index\r\n"
		"\t2: Shields index\r\n"
		"\t3: Weapons index\r\n"
		"\t4: Ship name\r\n"},

	{ OP_GET_POWER_OUTPUT, "get-power-output\r\n"
		"\tGets the power output rating of a ship\r\n"
		"\t1: Ship name"
	},

	{ OP_CARGO_NO_DEPLETE, "cargo-no-deplete\r\n"
		"\tCauses the named ship to have unlimited cargo.\r\n"
		"\tNote:  only applies to BIG or HUGE ships\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1:\tName of one of the ships (ship does not need to be in-mission).\r\n"
		"\t2:\toptional: 1 disallow depletion, 0 allow depletion." },

	{ OP_SHIELD_QUAD_LOW, "shield-quad-low\r\n"
		"\tReturns true if the specified ship has a shield quadrant below\r\n"
		"\tthe specified threshold percentage\r\n"
		"\t1: Ship name\r\n"
		"\t2: Percentage\r\n" },

	{ OP_PRIMARY_AMMO_PCT, "primary-ammo-pct\r\n"
		"\tReturns the percentage of ammo remaining in the specified ballistic primary bank (0 to 100).  Non-ballistic primary banks return as 100%.\r\n"
		"\t1: Ship name\r\n"
		"\t2: Bank to check (from 0 to N-1, where N is the number of primary banks in the ship; N or higher will return the cumulative percentage for all ballistic banks)" },

	// Karajorma
	{ OP_GET_PRIMARY_AMMO, "get-primary-ammo\r\n"
		"\tReturns the amount of ammo remaining in the specified ballistic primary bank.  Non-ballistic primary banks return as 0.\r\n"
		"\t1: Ship name\r\n"
		"\t2: Bank to check (from 0 to N-1, where N is the number of primary banks in the ship; N or higher will return the cumulative total for all ballistic banks)" },

	{ OP_SECONDARY_AMMO_PCT, "secondary-ammo-pct\r\n"
		"\tReturns the percentage of ammo remaining in the specified bank (0 to 100)\r\n"
		"\t1: Ship name\r\n"
		"\t2: Bank to check (from 0 to N-1, where N is the number of secondary banks in the ship; N or higher will return the cumulative percentage for all banks)" },

	// Karajorma
	{ OP_GET_SECONDARY_AMMO, "get-secondary-ammo\r\n"
		"\tReturns the amount of ammo remaining in the specified bank\r\n"
		"\t1: Ship name\r\n"
		"\t2: Bank to check (from 0 to N-1, where N is the number of secondary banks in the ship; N or higher will return the cumulative total for all banks)" },

	// Karajorma
	{ OP_GET_NUM_COUNTERMEASURES, "get-num-countermeasures\r\n"
		"\tReturns the amount of countermeasures remaining\r\n"
		"\t1: Ship name\r\n" },

	{ OP_IS_SECONDARY_SELECTED, "is-secondary-selected\r\n"
		"\tReturns true if the specified bank is selected\r\n"
		"\t1: Ship name\r\n"
		"\t2: Bank to check (This is a zero-based index. The first bank is numbered 0.)\r\n"},

	{ OP_IS_PRIMARY_SELECTED, "is-primary-selected\r\n"
		"\tReturns true if the specified bank is selected\r\n"
		"\t1: Ship name\r\n"
		"\t2: Bank to check (This is a zero-based index. The first bank is numbered 0.)\r\n"},

	{ OP_SPECIAL_WARP_DISTANCE, "special-warp-dist\r\n"
		"\tReturns distance to the plane of the knossos device in percent length of ship\r\n"
		"\t(ie, 100 means front of ship is 1 ship length from plane of knossos device)\r\n"
		"\t1: Ship name\r\n"}, 

	{ OP_SET_SPECIAL_WARPOUT_NAME, "special-warpout-name\r\n"
		"\tSets the name of the knossos device to be used for warpout\r\n"
		"\t1: Ship name to exit (ship must be in-mission)\r\n"
		"\t2: Name of knossos device (knossos device must be in-mission)\r\n"},

	{ OP_SHIP_VANISH, "ship-vanish\r\n"
		"\tMakes the named ship vanish (no log and vanish)\r\n"
		"\tSingle Player Only!  Warning: This will cause ship exit not to be logged, so 'has-departed', etc. will not work\r\n"
		"\t1: List of ship names to vanish (ship must be in-mission)\r\n"},

	{ OP_PROP_VANISH, "prop-vanish\r\n"
		"\tMakes the named prop vanish\r\n"
		"\tSingle Player Only!\r\n"
		"\t1: List of prop names to vanish (prop must be in-mission)\r\n"},

	{ OP_DESTROY_INSTANTLY, "destroy-instantly\r\n"
		"\tSelf-destructs the named ship without explosion, death roll, or debris.  That is, the ship is instantly gone from the mission and the only indication of what happened is a mission log entry.\r\n"
		"\tNon-player ship only!\r\n"
		"\tAll: List of ship names to destroy.\r\n"},

	{ OP_DESTROY_INSTANTLY_WITH_DEBRIS, "destroy-instantly-with-debris\r\n"
		"\tLike destroy-instantly, but only spawns debris. No explosions, no sound, no deathroll.\r\n"
		"\tNon-player ship only!\r\n"
		"\tAll: List of ship names to destroy.\r\n" },

	{ OP_SHIP_CREATE, "ship-create\r\n"
		"\tCreates a new ship\r\n"
		"\tTakes 5 to 10 arguments...\r\n"
		"\t1: Name of new ship (use \"" SEXP_NONE_STRING "\" for a default name)\r\n"
		"\t2: Class of new ship\r\n"
		"\t3: X position\r\n"
		"\t4: Y position\r\n"
		"\t5: Z position\r\n"
		"\t6: Pitch (optional)\r\n"
		"\t7: Bank (optional)\r\n"
		"\t8: Heading (optional)\r\n"
		"\t9: Team (optional; overrides ships.tbl default if set)\r\n"
		"\t10: Show in mission log (optional; defaults to true)\r\n"
	},

	{ OP_PROP_CREATE, "prop-create\r\n"
		"\tCreates a new prop\r\n"
		"\tTakes 5 to 8 arguments...\r\n"
		"\t1: Name of new prop (use \"" SEXP_NONE_STRING "\" for a default name)\r\n"
		"\t2: Class of new prop\r\n"
		"\t3: X position\r\n"
		"\t4: Y position\r\n"
		"\t5: Z position\r\n"
		"\t6: Pitch (optional)\r\n"
		"\t7: Bank (optional)\r\n"
		"\t8: Heading (optional)\r\n"
	},

	// Goober5000
	{ OP_WEAPON_CREATE, "weapon-create\r\n"
		"\tCreates a new weapon\r\n"
		"\tTakes 5 to 10 arguments...\r\n"
		"\t 1: Name of parent ship (or \"" SEXP_NONE_STRING "\" for no parent)\r\n"
		"\t 2: Class of new weapon\r\n"
		"\t 3: X position\r\n"
		"\t 4: Y position\r\n"
		"\t 5: Z position\r\n"
		"\t 6: Pitch (optional)\r\n"
		"\t 7: Bank (optional)\r\n"
		"\t 8: Heading (optional)\r\n"
		"\t 9: Targeted ship (optional)\r\n"
		"\t10: Targeted subsystem (optional)\r\n"
	},

	{ OP_IS_SHIP_VISIBLE, "is-ship-visible\r\n"
		"\tCheck whether ship is visible on a certain ship's radar.  Returns 0 - not visible, 1 - partially visible, 2 - fully visible.\r\n"
		"\tNote: In multiplayer, the second argument *must* be supplied or this SEXP will default to the first player.\r\n"
		"\tTakes 1 or 2 arguments...\r\n"
		"\t1: Name of ship to check\r\n"
		"\t2 (optional): Name of the viewing ship.  Defaults to the player's ship in single-player mode.  If this ship is not in-mission, the SEXP will return 0.\r\n" },

	// Goober5000
	{ OP_IS_SHIP_STEALTHY, "is-ship-stealthy\r\n"
		"\tCheck whether ship is currently stealthy.\r\n"
		"\tTrue if stealth flag set, false otherwise.  Takes 1 argument...\r\n"
		"\t1: Name of ship to check\r\n"},

	// Goober5000
	{ OP_IS_FRIENDLY_STEALTH_VISIBLE, "is-friendly-stealth-visible\r\n"
		"\tCheck whether ship will be visible to radar as a stealth friendly.\r\n"
		"\tTakes 1 argument...\r\n"
		"\t1: Name of ship to check\r\n"},

	{ OP_TEAM_SCORE, "team-score\r\n"
		"\tGet the score of a multi team vs team game.\r\n"
		"\t1: Team index (1 for team 1 and 2 for team 2)\r\n"},

	// phreak
	{ OP_DAMAGED_ESCORT_LIST, "damaged-escort-list\r\n"
		"\tSets the most damaged ship in <ship list> to <priority1>, sets the others to <priority2>.  Don't use this sexp in the same mission as damaged-escort-list-all, or strange results might occur.\r\n"
		"\t1: Priority1\r\n"
		"\t2: Priority2\r\n"
		"\tRest: <ship_list>\r\n\r\n"
	},

	// Goober5000
	{ OP_DAMAGED_ESCORT_LIST_ALL, "damaged-escort-list-all\r\n"
		"\tSets the most damaged ship in the entire existing escort list (even what's not shown onscreen) to <priority1>, the next most damaged to <priority2>, and so on.  "
		"If there are more ships than priorities, the least most damaged ships are all set to the last priority in the list.  Don't use this sexp in the same mission as damaged-escort-list, or strange results might occur.\r\n"
		"\tTakes between 1 and MAX_COMPLETE_ESCORT_LIST (currently 20) arguments...\r\n"
		"\t1: Priority 1\r\n"
		"\tRest: Priorities 2 through 20 (optional)\r\n\r\n"
	},

	// Goober5000
	{ OP_CHANGE_SHIP_CLASS, "change-ship-class\r\n"
		"\tCauses the listed ships' classes to be changed to the specified ship class.  Takes 2 or more arguments...\r\n"
		"\t1: The name of the new ship class\r\n"
		"\tRest: The list of ships to change the classes of (ships do not need to be in-mission)"
	},

	// Goober5000
	{ OP_SHIP_COPY_DAMAGE, "ship-copy-damage\r\n"
		"\tCopies the damage (hull, shields, and subsystems) from the first ship in the list to the rest.  The initial ship must be currently "
		"present in the mission, but the target ships may be either in-mission or on the arrival list.  Takes 2 or more arguments...\r\n"
		"\t1: The name of the ship that supplies the damage stats\r\n"
		"\tRest: The list of ships to be modified"
	},

	// Goober5000
	{ OP_SET_SUPPORT_SHIP, "set-support-ship\r\n"
		"\tSets information for all support ships in a mission.  This includes not only arrival/departure information, but also whether and how many times ships can rearm, and the characteristics of the support ship.  Takes 6 or 7 arguments...\r\n"
		"\t1: Arrival location\r\n"
		"\t2: Arrival anchor\r\n"
		"\t3: Departure location\r\n"
		"\t4: Departure anchor\r\n"
		"\t5: Support ship class\r\n"
		"\t6: Maximum number of support ships consecutively in this mission (use 0 to disallow support; use a negative number for infinity)\r\n"
		"\t7: Maximum number of support ships concurrently in this mission (optional, default 1)\r\n"
		"\r\n"
		"Note: The support ship will emerge from or depart into hyperspace if the location is set as hyperspace *or* the anchor is set as <no anchor>."
	},

	// Goober5000
	{ OP_SET_ARRIVAL_INFO, "set-arrival-info\r\n"
		"\tSets arrival information for a ship or wing.  Takes 2 to 8 arguments...\r\n"
		"\t1: Ship or wing name\r\n"
		"\t2: Arrival location\r\n"
		"\t3: Arrival anchor (optional; only required for certain locations)\r\n"
		"\t4: Arrival path mask (optional; this is a bitfield where the bits set to 1 correspond to the paths to use; defaults to 0 which is a special case that means all paths can be used)\r\n"
		"\t5: Arrival distance (optional; defaults to 0)\r\n"
		"\t6: Arrival delay (optional; defaults to 0)\r\n"
		"\t7: Whether to show a jump effect if arriving from subspace (optional; defaults to true)\r\n"
		"\t8: Whether to adjust the size of the jump effect if the ship is docked (optional; defaults to true)\r\n"
	},

	// Goober5000
	{ OP_SET_DEPARTURE_INFO, "set-departure-info\r\n"
		"\tSets departure information for a ship or wing.  Takes 2 to 7 arguments...\r\n"
		"\t1: Ship or wing name (ship/wing does not need to be in-mission)\r\n"
		"\t2: Departure location\r\n"
		"\t3: Departure anchor (optional; only required for certain locations)\r\n"
		"\t4: Departure path mask (optional; this is a bitfield where the bits set to 1 correspond to the paths to use; defaults to 0 which is a special case that means all paths can be used)\r\n"
		"\t5: Departure delay (optional; defaults to 0)\r\n"
		"\t6: Whether to show a jump effect if departing to subspace (optional; defaults to true)\r\n"
		"\t7: Whether to adjust the size of the jump effect if the ship is docked (optional; defaults to true)\r\n"
	},

	// Bobboau
	{ OP_DEACTIVATE_GLOW_POINTS, "deactivate-glow-points\r\n"
		"\tDeactivates all glow points on a ship.  Takes 1 or more arguments...\r\n"
		"\tAll: Name of ship on which to deactivate glow points (ship must be in-mission)\r\n"
	},

	// Bobboau
	{ OP_ACTIVATE_GLOW_POINTS, "activate-glow-points\r\n"
		"\tActivates all glow points on a ship.  Takes 1 or more arguments...\r\n"
		"\tAll: Name of ship on which to activate glow points (ship must be in-mission)\r\n"
	},

	// Bobboau
	{ OP_DEACTIVATE_GLOW_MAPS, "deactivate-glow-maps\r\n"
		"\tDeactivates the glow maps for a ship.  Takes 1 or more arguments...\r\n"
		"\tAll: Name of ship on which to deactivate glow maps (ship must be in-mission)\r\n"
	},

	// Bobboau
	{ OP_ACTIVATE_GLOW_MAPS, "activate-glow-maps\r\n"
		"\tActivates the glow maps for a ship.  Takes 1 or more arguments...\r\n"
		"\tAll: Name of ship on which to activate glow maps (ship must be in-mission)\r\n"
	},

	// Bobboau
	{ OP_DEACTIVATE_GLOW_POINT_BANK, "deactivate-glow-point-bank\r\n"
		"\tDeactivates one or more glow point bank(s) on a ship.  Takes 2 or more arguments...\r\n"
		"\t1: Name of ship on which to deactivate glow point bank(s) (ship must be in-mission)\r\n"
		"\tRest: Name of glow point bank to deactivate\r\n"
	},

	// Bobboau
	{ OP_ACTIVATE_GLOW_POINT_BANK, "activate-glow-point-bank\r\n"
		"\tActivates one or more glow point bank(s) on a ship.  Takes 2 or more arguments...\r\n"
		"\t1: Name of ship on which to activate glow point bank(s) (ship must be in-mission)\r\n"
		"\tRest: Name of glow point bank to activate\r\n"
	},

	//-Sesquipedalian
	{ OP_KAMIKAZE, "kamikaze\r\n"
		"\tTells ships to perform a kamikaze on its current target. Takes 2 or more arguments...\r\n"
		"\t1: Damage dealt when kamikaze is done\r\n"
		"\tRest: Names of ships to perform kamikaze (ships do not need to be in-mission)\r\n"
	},
	
	//phreak
	{ OP_TURRET_TAGGED_SPECIFIC, "turret-tagged-specific\r\n"
		"\tSpecific turrets on a ship only fire at tagged targets, as opposed to all turrets doing this using turret-tagged-only\r\n"
		"\tIt is safe to slave turrets already slaved\r\n"
		"\tTakes 2 or more arguments...\r\n"
		"\t1: Name of ship to slave some turrets to target only tagged ships (ship must be in-mission)\r\n"
		"\tRest: Turrets to slave\r\n"
	},

	//phreak
	{ OP_TURRET_TAGGED_CLEAR_SPECIFIC, "turret-tagged-clear-specific\r\n"
		"\tSpecific turrets on a ship are free to fire on untagged ships, as opposed to all turrets doing this using turret-tagged-clear\r\n"
		"\tIt is safe to unslave turrets already free\r\n"
		"\tTakes 2 or more arguments...\r\n"
		"\t1: Name of ship to unslave some turrets to target any hostile ship (ship must be in-mission)\r\n"
		"\tRest: Turrets to unslave\r\n"
	},

	// Goober5000
	{ OP_LOCK_ROTATING_SUBSYSTEM, "lock-rotating-subsystem\r\n"
		"\tInstantaneously locks a rotating subsystem so that it cannot rotate unless freed by free-rotating-subsystem.  "
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\tRest:\tName of the rotating subsystem to lock"
	},

	// Goober5000
	{ OP_FREE_ROTATING_SUBSYSTEM, "free-rotating-subsystem\r\n"
		"\tInstantaneously frees a rotating subsystem previously locked by lock-rotating-subsystem.  "
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\tRest:\tName of the rotating subsystem to free"
	},

	// Goober5000
	{ OP_REVERSE_ROTATING_SUBSYSTEM, "reverse-rotating-subsystem\r\n"
		"\tInstantaneously reverses the rotation direction of a rotating subsystem.  "
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\tRest:\tName of the rotating subsystem to reverse"
	},

	// Goober5000
	{ OP_ROTATING_SUBSYS_SET_TURN_TIME, "rotating-subsys-set-turn-time\r\n"
		"\tSets the turn time of a rotating subsystem.  "
		"Takes 3 or 4 arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\t2:\tName of the rotating subsystem to configure\r\n"
		"\t3:\tThe time for one complete rotation, in milliseconds - positive is counterclockwise, negative is clockwise\r\n"
		"\t4:\tThe acceleration (x1000, just as #3 is seconds x1000) to change from the current turn rate to the desired turn rate.  "
		"This is actually the time to complete one rotation that changes in one second, or the reciprocal of what you might expect, "
		"meaning that larger numbers cause slower acceleration.  (FS2 defaults to 2pi/0.5, or about 12.566, which would be 12566 in this sexp.)  "
		"The advantage of this method is so that this argument can be directly compared to the previous argument using a ratio, without worrying about pi.  "
		"Omit this argument if you want an instantaneous change."
	},

	// Goober5000
	{ OP_LOCK_TRANSLATING_SUBSYSTEM, "lock-translating-subsystem\r\n"
		"\tInstantaneously locks a translating subsystem so that it cannot translate unless freed by free-translating-subsystem.  "
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\tRest:\tName of the translating subsystem to lock"
	},

	// Goober5000
	{ OP_FREE_TRANSLATING_SUBSYSTEM, "free-translating-subsystem\r\n"
		"\tInstantaneously frees a translating subsystem previously locked by lock-translating-subsystem.  "
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\tRest:\tName of the translating subsystem to free"
	},

	// Goober5000
	{ OP_REVERSE_TRANSLATING_SUBSYSTEM, "reverse-translating-subsystem\r\n"
		"\tInstantaneously reverses the translation direction of a translating subsystem.  "
		"Takes 2 or more arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\tRest:\tName of the translating subsystem to reverse"
	},

	// Goober5000
	{ OP_TRANSLATING_SUBSYS_SET_SPEED, "translating-subsys-set-speed\r\n"
		"\tSets the speed (movement rate) of a translating subsystem.  "
		"Takes 3 or 4 arguments...\r\n"
		"\t1:\tName of the ship housing the subsystem (ship must be in-mission)\r\n"
		"\t2:\tName of the translating subsystem to configure\r\n"
		"\t3:\tThe movement rate, in millimeters per second (i.e. one thousand times the speed in m/s) - positive is along the vector of the translation axis\r\n"
		"\t4:\tThe acceleration (x1000, just as #3 is m/s x1000) to change from the current movement rate to the desired movement rate.  "
		"Omit this argument if you want an instantaneous change."
	},

	// Goober5000
	{ OP_TRIGGER_SUBMODEL_ANIMATION, "trigger-submodel-animation\r\n"
		"\tActivates a submodel animation trigger for a given ship.  Takes 4 to 6 arguments...\r\n"
		"\t1: The ship on which the animation should run (ship must be in-mission)\r\n"
		"\t2: The type of animation (named as one would see them in ships.tbl)\r\n"
		"\t3: The subtype of animation, which is type-dependent.  For docking animations this is the dock index.\r\n"
		"\t4: The animation direction: 1 for forward, or -1 for reverse\r\n"
		"\t5: (Optional) Whether the animation should instantly snap to its final position\r\n"
		"\t6: (Optional) A subsystem, if the animation should trigger on only a specific subsystem as opposed to all applicable subsystems\r\n"
	},

	// Karajorma
	{ OP_SET_PRIMARY_AMMO, "set-primary-ammo\r\n"
		"\tSets the amount of ammo for the specified ballistic bank\r\n"
		"\t1: Ship name (ship must be in-mission)\r\n"
		"\t2: Bank to check (0, 1, and 2 are legal banks)\r\n" 
		"\t3: Number to set this bank to (If this is larger than the maximum, bank will be set to maximum).\r\n"
		"\t4: Rearm Limit. Support ships will only supply this number of weapons (If this is larger than the maximum, bank will be set to maximum)"
	},

	// Karajorma
	{ OP_SET_SECONDARY_AMMO, "set-secondary-ammo\r\n"
		"\tSets the amount of ammo for the specified bank\r\n"
		"\t1: Ship name (ship must be in-mission)\r\n"
		"\t2: Bank to check (0, 1, 2 and 3 are legal banks)\r\n" 
		"\t3: Number to set this bank to (If this is larger than the maximum, bank will be set to maximum).\r\n"
		"\t4: Rearm Limit. Support ships will only supply this number of weapons (If this is larger than the maximum, bank will be set to maximum)"
	},
	
	// Karajorma
	{ OP_SET_PRIMARY_WEAPON, "set-primary-weapon\r\n"
		"\tSets the weapon for the specified bank\r\n"
		"\t1: Ship name (ship must be in-mission)\r\n"
		"\t2: Bank to check (0, 1 and 2 are legal banks)\r\n" 
		"\t3: Name of the primary weapon \r\n"
		"\t4: Number to set ammo of this bank to (If this is larger than the maximum, bank will be set to maximum)\r\n"
		"\t5: Rearm Limit. Support ships will only supply this number of weapons (If this is larger than the maximum, bank will be set to maximum)"
	},
	
	// Karajorma
	{ OP_SET_SECONDARY_WEAPON, "set-secondary-weapon\r\n"
		"\tSets the weapon for the specified bank\r\n"
		"\t1: Ship name (ship must be in-mission)\r\n"
		"\t2: Bank to check (0, 1, 2 and 3 are legal banks)\r\n" 
		"\t3: Name of the secondary weapon \r\n"
		"\t4: Number to set ammo of this bank to (If this is larger than the maximum, bank will be set to maximum)\r\n"
		"\t5: Rearm Limit. Support ships will only supply this number of weapons (If this is larger than the maximum, bank will be set to maximum)"
	},


	
	// Karajorma
	{ OP_SET_NUM_COUNTERMEASURES, "set-num-countermeasures\r\n"
		"\tSets the number of countermeasures the ship has\r\n"
		"\tValues greater than the maximum a ship can carry are set to the maximum\r\n"
		"\t1: Ship name (ship must be in-mission)\r\n"
		"\t2: Number to set"
	},
	
	// Karajorma
	{ OP_LOCK_PRIMARY_WEAPON, "lock-primary-weapon\r\n"
		"\tLocks the primary banks for the specified ship(s)\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\t(all): Name(s) of ship(s) to lock (ships do not need to be in-mission)"
	},

	// Karajorma
	{ OP_UNLOCK_PRIMARY_WEAPON, "unlock-primary-weapon\r\n"
		"\tUnlocks the primary banks for the specified ship(s)\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\t(all): Name(s) of ship(s) to lock (ships do not need to be in-mission)"
	},
	
	// Karajorma
	{ OP_LOCK_SECONDARY_WEAPON, "lock-secondary-weapon\r\n"
		"\tLocks the secondary banks for the specified ship(s)\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\t(all): Name(s) of ship(s) to lock (ships do not need to be in-mission)"
	},

	// Karajorma
	{ OP_UNLOCK_SECONDARY_WEAPON, "unlock-secondary-weapon\r\n"
		"\tUnlocks the secondary banks for the specified ship(s)\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\t(all): Name(s) of ship(s) to lock (ships do not need to be in-mission)"
	},
	
	// KeldorKatarn
	{ OP_LOCK_AFTERBURNER, "lock-afterburner\r\n"
		"\tLocks the afterburners on the specified ship(s)\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\t(all): Name(s) of ship(s) to lock (ships do not need to be in-mission)"
	},

	// KeldorKatarn
	{ OP_UNLOCK_AFTERBURNER, "unlock-afterburner\r\n"
		"\tUnlocks the afterburners on the specified ship(s)\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\t(all): Name(s) of ship(s) to lock (ships do not need to be in-mission)"
	},

	// Karajorma
	{ OP_SET_AFTERBURNER_ENERGY, "set-afterburner-energy\r\n"
		"\tSets the afterburner energy for the specified ship(s)\r\n"
		"\tTakes 2 or more arguments\r\n"
		"\t1: percentage of maximum afterburner energy.\r\n"
		"\t(rest): Name(s) of ship(s) (ship must be in-mission)"
	},

	{ OP_SET_WEAPON_ENERGY, "set-weapon-energy\r\n"
		"\tSets the weapon energy for the specified ship(s)\r\n"
		"\tTakes 2 or more arguments\r\n"
		"\t1: percentage of maximum weapon energy.\r\n"
		"\t(rest): Name(s) of ship(s) (ship must be in-mission)"
	},

	{ OP_SET_SHIELD_ENERGY, "set-shield-energy\r\n"
		"\tSets the shield energy for the specified ship(s)\r\n"
		"\tTakes 2 or more arguments\r\n"
		"\t1: percentage of maximum shield energy.\r\n"
		"\t(rest): Name(s) of ship(s) (ship must be in-mission)"
	},

	{ OP_SET_AMBIENT_LIGHT, "set-ambient-light\r\n"
		"\tSets the ambient light level for the mission\r\n"
		"\tTakes 3 arguments\r\n"
		"\t1: Red (0 - 255).\r\n"
		"\t2: Green (0 - 255).\r\n"
		"\t3: Blue (0 - 255)."
	},

	{ OP_SET_GRAVITY_ACCEL, "set-gravity-accel\r\n"
		"\tSets the gravity acceleration rate in units of 0.01 m/s^2\r\n"
		"\te.g. '981' would be earth gravity, 9.81 m/s^2.\r\n"
		"\t1: Acceleration rate"
	},

	{ OP_FORCE_REARM, "force-rearm\r\n"
		"\tForces a ship to instantly begin a rearm sequence as if docked to a support ship\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\tALL: The ship to rearm\r\n"
	},

	{ OP_ABORT_REARM, "abort-rearm\r\n"
		"\tForces a ship to instantly abort a rearm sequence\r\n"
		"\tTakes 1 or more arguments\r\n"
		"\tALL: The ship to abort rearm\r\n"
	},

	{ OP_SET_POST_EFFECT, "set-post-effect\r\n"
		"\tConfigures a post-processing effect.  Takes 2 arguments...\r\n"
		"\t1: Effect type\r\n"
		"\t2: Effect intensity (0 - 100)\r\n"
		"\t3: (Optional) Red (0 - 255)\r\n"
		"\t4: (Optional) Green (0 - 255)\r\n"
		"\t5: (Optional) Blue (0 - 255)\r\n"
	},

	{ OP_RESET_POST_EFFECTS, "reset-post-effects\r\n"
		"\tResets all post-processing effects to their default values.  Takes no arguments.\r\n"
	},

	{ OP_CHANGE_SUBSYSTEM_NAME, "change-subsystem-name\r\n"
		"\tChanges the name of the specified subsystem on the specified ship\r\n"
		"\tTakes 3 or more arguments\r\n"
		"\t1: Name of the ship.\r\n"
		"\t2: New name for the subsystem (names larger than the maximum display size will be truncated\r\n"
		"\t3: Name(s) of subsystem(s) to rename (ship must be in-mission)\r\n"
	},

	//phreak
	{ OP_NUM_SHIPS_IN_BATTLE, "num-ships-in-battle\r\n"
		"\tReturns the number of ships in battle or the number of ships in battle out of a list of teams, wings, and ships.  Takes 0 or more arguments...\r\n"
		"\t(all):\tTeams, Wings, and Ships to query (optional)"
	},

	// Karajorma
	{ OP_NUM_SHIPS_IN_WING, "num-ships-in-wing\r\n"
		"\tReturns the number of ships in battle which belong to a given wing.  Takes 1 or more arguments...\r\n"
		"\t(all):\tName of wing(s) to check"
	},

	// Goober5000
	{ OP_HUD_DISABLE, "hud-disable\r\n"
		"\tSets whether the hud is disabled.  Takes 1 argument...\r\n"
		"\t1: Flag (1 to disable, 0 to re-enable)"
	},
	
	// Goober5000
	{ OP_HUD_DISABLE_EXCEPT_MESSAGES, "hud-disable-except-messages\r\n"
		"\tSets whether the hud (except for messages) is disabled.  Takes 1 argument...\r\n"
		"\t1: Flag (1 to disable, 0 to re-enable)"
	},

	// Goober5000
	{ OP_HUD_SET_MAX_TARGETING_RANGE, "hud-set-max-targeting-range\r\n"
		"\tSets the farthest distance at which an object can be targeted.  Takes 1 argument...\r\n"
		"\1: Maximum targeting distance (0 for infinite)\r\n"
	},

	{ OP_HUD_DISPLAY_GAUGE, "hud-display-gauge <milliseconds> <gauge>\r\n"
		"\tCauses specified hud gauge to appear or disappear for so many milliseconds.  Takes 1 argument...\r\n"
		"\t1: Number of milliseconds that the warpout gauge should appear on the HUD.  Zero "
		"will immediately cause the gauge to disappear.\r\n"
		"\t2: Name of HUD element.  Must be one of:\r\n"
		"\t\t" SEXP_HUD_GAUGE_WARPOUT " - the \"Subspace drive active\" box that appears above the viewscreen.\r\n"
	},

	// Goober5000
	{ OP_PLAYER_USE_AI, "player-use-ai\r\n"
		"\tCauses the player's ship to be controlled by the FreeSpace AI.  Takes 0 arguments.\r\n"
	},

	// Goober5000
	{ OP_PLAYER_NOT_USE_AI, "player-not-use-ai\r\n"
		"\tCauses the player's ship to not be controlled by the FreeSpace AI.  Takes 0 arguments.\r\n"
	},

	// LuytenKy
	{ OP_SET_PLAYER_TARGET, "set-player-target\r\n"
		"\tSets the player's current target to the specified ship, and optionally a subsystem on that ship.\r\n"
		"\tIf the subsystem is destroyed, the ship will still be targeted. Takes 1 to 2 arguments.\r\n"
		"\t1:\tThe ship to target.\r\n"
		"\t2:\t(Optional) The subsystem to target. Use <none> to target no subsystem.\r\n"
	},
	{ OP_CLEAR_PLAYER_TARGET, "clear-player-target\r\n"
		"\tClears the player's current target and subsystem target. Takes 0 arguments.\r\n"
	},

	// Kestrellius
	{ OP_SET_FRIENDLY_DAMAGE_CAPS, "set-friendly-damage-caps\r\n"
		"\tSets limits on damage weapons and beams can do to friendly targets on the current difficulty level.\r\n"
		"Takes 1 to 3 arguments. Arguments left blank will leave the values unmodified.\r\n"
		"\t1:\tMaximum damage beams can do to targets on the same team as the firer. -1 means no limit.\r\n"
		"\t2:\tMaximum damage weapons (and their shockwaves) can do to targets on the same team as the firer. -1 means no limit.\r\n"
		"\t3:\tMaximum damage weapons (and their shockwaves) can do to their firer. -1 means no limit."
	},

	// Karajorma
	{ OP_ALLOW_TREASON, "allow-treason\r\n"
		"\tTurns the Allow Traitor switch on or off in mission. Takes 0 arguments.\r\n"
		"\t1:\tTrue/False."
	},

	// Karajorma
	{ OP_SET_PLAYER_ORDERS, "set-player-orders\r\n"
		"\tChanges the orders friendly AI will accept. Takes 3 or more arguments.\r\n"
		"\t1:\tShip Name (ship must be in-mission)\r\n"
		"\t2:\tTrue/False as to whether this order is allowed or not\r\n"
		"\tRest:\tOrder"
	},

		// MjnMixael
	{ OP_SET_ORDER_ALLOWED_TARGET, "set-order-allowed-for-target\r\n"
		"\tChanges the orders that can be issued against a ship. Takes 3 or more arguments.\r\n"
		"\t1:\tShip Name (ship must be in-mission)\r\n"
		"\t2:\tTrue/False as to whether this order is allowed or not\r\n"
		"\tRest:\tOrder"
	},

		// MjnMixael
	{ OP_ENABLE_GENERAL_ORDERS, "enable-general-orders\r\n"
		"\tEnables or disables general orders defined in sexps.tbl. Takes 2 or more arguments.\r\n"
		"\tDisabled orders are not visible on the comms board at all\r\n"
		"\t1:\tTrue/False as to whether the order is enabled or not\r\n"
		"\tRest:\tThe order to enable or disable"
	},

		// MjnMixael
	{ OP_VALIDATE_GENERAL_ORDERS, "validate-general-orders\r\n"
		"\tValidates or invalidates general orders defined in sexps.tbl. Takes 2 or more arguments.\r\n"
		"\tInvalid orders are greyed out on the comms board and cannot be selected\r\n"
		"\t1:\tTrue/False as to whether the order is enabled or not\r\n"
		"\tRest:\tThe order to validate or invalidate"
	},

	//WMC
	{ OP_HUD_SET_TEXT, "hud-set-text\r\n"
		"\tSets the text value of a given HUD gauge. Works for custom gauges only. Takes 2 arguments...\r\n"
		"\t1:\tHUD gauge to be modified\r\n"
		"\t2:\tText to be set"
	},

	//MjnMixael
	{ OP_HUD_SET_XSTR, "hud-set-xstr\r\n"
		"\tSets the text value of a given HUD gauge to a translated string and replaces variables.\r\n"
		"\tWorks for custom gauges only. Takes 3 arguments...\r\n"
		"\t1:\tHUD gauge to be modified\r\n"
		"\t2:\tText to be set\r\n"
		"\t3:\tXSTR ID to lookup"
	},

	{ OP_HUD_SET_TEXT_NUM, "hud-set-text-num\r\n"
		"\tSets the text value of a given HUD gauge to a number. Works for custom gauges only. Takes 2 arguments...\r\n"
		"\t1:\tHUD gauge to be modified\r\n"
		"\t2:\tNumber to be set"
	},

	{ OP_HUD_SET_MESSAGE, "hud-set-message\r\n"
		"\tSets the text value of a given HUD gauge to a message from the mission's message list. Works for custom gauges only. Takes 2 arguments...\r\n"
		"\t1:\tHUD gauge to be modified\r\n"
		"\t2:\tMessage"
	},

	//WMC
	{ OP_HUD_SET_COORDS, "hud-set-coords\r\n"
		"\tSets the coordinates of a given HUD gauge. Works for custom and retail gauges. The coordinates can be configured to scale according to "
		" screen resolution via the '$HUD-set-coords base resolution' option in game_settings.tbl. Takes 3 arguments...\r\n"
		"\t1:\tHUD gauge to be modified\r\n"
		"\t2:\tCoordinate X component\r\n"
		"\t2:\tCoordinate Y component"
	},

	//WMC
	{ OP_HUD_SET_FRAME, "hud-set-frame\r\n"
		"\tSets the frame of a given HUD gauge's associated image. Works for custom and certain retail gauges. Takes 2 arguments...\r\n"
		"\t1:\tHUD gauge to be modified\r\n"
		"\t2:\tFrame number to be changed to"
	},

	//WMC
	{ OP_HUD_SET_COLOR, "hud-set-color\r\n"
		"\tSets the color of a given HUD gauge. Works for custom and retail gauges. Takes 4 arguments...\r\n"
		"\t1:\tHUD gauge to be modified\r\n"
		"\t2:\tRed component (0-255)\r\n"
		"\t3:\tGreen component (0-255)\r\n"
		"\t4:\tBlue component (0-255)"
	},

	// Goober5000
	{ OP_HUD_RESET_COLOR, "hud-reset-color\r\n"
		"\tResets the color of a given HUD gauge back to its color as defined in the HUD config. Only effective for gauges for which a config exists. Takes 1 or more arguments...\r\n"
		"\tAll:\tHUD gauge to be modified\r\n"
	},

	//WMC
	{ OP_CURRENT_SPEED, "current-speed\r\n"
		"\tReturns the speed of the given object. Takes 1 argument...\r\n"
		"\t1:\tName of the object"
	},

	// Asteroth
	{ OP_TURRET_FIRED_SINCE, "turret-fired-since\r\n"
		"\tReturns true if the turret has been fired within the supplied window of time. Takes 3 arguments...\r\n\r\n"
		"\t1:\tShip name\r\n"
		"\t2:\tTurret name\r\n"
		"\t3:\tTime period to check if the turret was fired (in milliseconds)\r\n"
	},

	// Karajora
	{ OP_PRIMARY_FIRED_SINCE, "primary-fired-since\r\n"
		"\tReturns true if the primary weapon bank specified has been fired within the supplied window of time. Takes 3 arguments...\r\n\r\n"
		"\t1:\tShip name\r\n"
		"\t2:\tWeapon bank number (This is a zero-based index. The first bank is numbered 0.)\r\n"
		"\t3:\tTime period to check if the weapon was fired (in milliseconds)\r\n"
	},

	// Karajora
	{ OP_SECONDARY_FIRED_SINCE, "secondary-fired-since\r\n"
		"\tReturns true if the secondary weapon bank specified has been fired within the supplied window of time. Takes 3 arguments...\r\n\r\n"
		"\t1:\tShip name\r\n"
		"\t2:\tWeapon bank number (This is a zero-based index. The first bank is numbered 0.)\r\n"
		"\t3:\tTime period to check if the weapon was fired (in milliseconds)\r\n"
	},

	// Karajora
	{ OP_HAS_PRIMARY_WEAPON, "has-primary-weapon\r\n"
		"\tReturns true if the primary weapon bank specified has any of the weapons listed. Takes 3 or more arguments...\r\n\r\n"
		"\t1:\tShip name (evaluation returns NAN until the ship is in-mission)\r\n"
		"\t2:\tWeapon bank number (This is a zero-based index. The first bank is numbered 0.)\r\n"
		"\tRest:\tWeapon name\r\n"
	},

	// Karajora
	{ OP_HAS_SECONDARY_WEAPON, "has-secondary-weapon\r\n"
		"\tReturns true if the secondary weapon bank specified has any of the weapons listed. Takes 3 or more arguments...\r\n\r\n"
		"\t1:\tShip name (evaluation returns NAN until the ship is in-mission)\r\n"
		"\t2:\tWeapon bank number (This is a zero-based index. The first bank is numbered 0.)\r\n"
		"\tRest:\tWeapon name\r\n"
	},

	// MjnMixael
	{ OP_TURRET_HAS_PRIMARY_WEAPON, "turret-has-primary-weapon\r\n"
		"\tReturns true if the turret specified has any of the weapons listed. Takes 3 or more arguments...\r\n\r\n"
		"\t1:\tShip name (evaluation returns NAN until the ship is in-mission)\r\n"
		"\t2:\tTurret to check\r\n"
		"\t3:\tTurret bank number (This is a zero-based index. The first bank is numbered 0.)\r\n"
		"\tRest:\tWeapon name\r\n"
	},

	// MjnMixael
	{ OP_TURRET_HAS_SECONDARY_WEAPON, "turret-has-secondary-weapon\r\n"
		"\tReturns true if the turret specified has any of the weapons listed. Takes 3 or more arguments...\r\n\r\n"
		"\t1:\tShip name (evaluation returns NAN until the ship is in-mission)\r\n"
		"\t2:\tTurret to check\r\n"
		"\t3:\tTurret bank number (This is a zero-based index. The first bank is numbered 0.)\r\n"
		"\tRest:\tWeapon name\r\n"
	},

	// Karajora
	{ OP_DIRECTIVE_VALUE, "directive-value\r\n"
		"\tCauses a value to appear in the directive count\r\n"
		"\tAlways returns true. Takes 1 or more arguments...\r\n\r\n"
		"\t1:\tValue\r\n"
		"\t2:\t(Optional) Ignore the directive count set by any earlier SEXPs in the event. If set to false it will add instead\r\n"
	},

	// wookieejedi
	{ OP_GET_HOTKEY, "get-hotkey\r\n"
		"\tReturns the hotkey integer of the ship or wing. Return values range from -1 to 7.\r\n"
		"\tReturn of -1 is no hotkey, return of 0 is F5, return of 1 is F6, ..., return of 7 is F12.\r\n\r\n"
		"\tTakes 1 argument.\r\n\r\n"
		"\t1:\tShip or wing to get hotkey.\r\n"
	},

	//phreak
	{ OP_SCRAMBLE_MESSAGES, "scramble-messages\r\n"
		"\tCauses messages to be sent or received as if the player had sustained communications subsystem or EMP damage.  This effect can be reversed using unscramble-messages.  Takes zero or more arguments.\r\n"
		"\tAll (Optional):\tName of the ship for which to scramble messages.  If no ships are specified, message scrambling will be turned on for the player.\r\n"
	},

	//phreak
	{ OP_UNSCRAMBLE_MESSAGES, "unscramble-messages\r\n"
		"\tUndoes the effects of scramble-messages, causing messages to be sent or received clearly (conditions permitting).  Takes zero or more arguments.\r\n"
		"\tAll (Optional):\tName of the ship for which to scramble messages.  If no ships are specified, message scrambling will be turned off for the player.\r\n"
	},

	{ OP_CUTSCENES_SET_CUTSCENE_BARS, "set-cutscene-bars\r\n"
		"\tShows bars at the top and bottom of screen.  "
		"Takes 0 or 1 arguments...\r\n"
		"\t1:\tMilliseconds for bars to slide in\r\n"
	},

	{ OP_CUTSCENES_UNSET_CUTSCENE_BARS, "unset-cutscene-bars\r\n"
		"\tRemoves cutscene bars.  "
		"Takes 0 or 1 arguments...\r\n"
		"\t1:\tMilliseconds for bars to slide out\r\n"
	},

	{ OP_CUTSCENES_FADE_IN, "fade-in\r\n"
		"\tFades in.  "
		"Takes 0 to 4 arguments...\r\n"
		"\t1:\tTime to fade in (in milliseconds)\r\n"
		"\t2:\tColor to fade from (optional).  If arguments 3 and 4 are specified, this is the R component of an RGB color.  Otherwise it is 1 for white, 2 for red, and any other number for black.\r\n"
		"\t3:\tG component of an RGB color (optional)\r\n"
		"\t4:\tB component of an RGB color (optional)\r\n"
	},

	{ OP_CUTSCENES_FADE_OUT, "fade-out\r\n"
		"\tFades out.  "
		"Takes 0 to 4 arguments...\r\n"
		"\t1:\tTime to fade out (in milliseconds)\r\n"
		"\t2:\tColor to fade to (optional).  If arguments 3 and 4 are specified, this is the R component of an RGB color.  Otherwise it is 1 for white, 2 for red, and any other number for black.\r\n"
		"\t3:\tG component of an RGB color (optional)\r\n"
		"\t4:\tB component of an RGB color (optional)\r\n"
	},

	{ OP_CUTSCENES_SET_CAMERA, "set-camera\r\n"
		"\tSets SEXP camera, or another specified cutscene camera. Automatically disables photo mode while cutscene camera control is active."
		"Takes 0 to 1 arguments...\r\n"
		"\t(optional)\r\n"
		"\t1:\tCamera name (created if nonexistent)\r\n"
	},

	{ OP_CUTSCENES_SET_CAMERA_FACING, "set-camera-facing\r\n"
		"\tMakes the camera face the given point.  "
		"Takes 3 to 6 arguments...\r\n"
		"\t1:\tX position to face\r\n"
		"\t2:\tY position to face\r\n"
		"\t3:\tZ position to face\r\n"
		"\t(optional)\r\n"
		"\t4:\tTotal turn time (milliseconds)\r\n"
		"\t5:\tTime to spend accelerating/decelerating (milliseconds)\r\n"
		"\t6:\tTime to spend decelerating (milliseconds)\r\n"
	},

	{ OP_CUTSCENES_SET_CAMERA_FACING_OBJECT, "set-camera-facing-object\r\n"
		"\tMakes the camera face the given object.  "
		"Takes 1 to 4 arguments...\r\n"
		"\t1:\tObject to face\r\n"
		"\t(optional)\r\n"
		"\t2:\tTotal turn time (milliseconds)\r\n"
		"\t3:\tTime to spend accelerating/decelerating (milliseconds)\r\n"
		"\t4:\tTime to spend decelerating (milliseconds)\r\n"
	},

	{ OP_CUTSCENES_SET_CAMERA_FOV, "set-camera-fov\r\n"
		"\tSets the camera field of view.  "
		"Takes 1 to 4 arguments...\r\n"
		"\t1:\tNew FOV (degrees)\r\n"
		"\t(optional)\r\n"
		"\t2:\tTotal zoom time (milliseconds)\r\n"
		"\t3:\tTime to spend accelerating/decelerating (milliseconds)\r\n"
		"\t4:\tTime to spend decelerating (milliseconds)\r\n"
	},
	
	{ OP_CUTSCENES_SET_CAMERA_HOST, "set-camera-host\r\n"
		"\tSets the object and subystem camera should view from. Camera position is offset from the host. "
		"If the selected subsystem or one of its children has an eyepoint bound to it it will be used for the camera position and orientation. "
		"If the selected subsystem is a turret and has no eyepoint the camera will be at the first firing point and look along the firing direction. "
		"If a valid camera target is set the direction to the target will override any other orientation. "
		"Takes 1 to 2 arguments...\r\n"
		"\t1:\tShip to mount camera on\r\n"
		"\t(optional)\r\n"
		"\t2:\tSubystem to mount camera on\r\n"
	},

	{ OP_CUTSCENES_SET_CAMERA_POSITION, "set-camera-position\r\n"
		"\tSets the camera position to a spot in mission space.  "
		"Takes 3 to 6 arguments...\r\n"
		"\t1:\tX position\r\n"
		"\t2:\tY position\r\n"
		"\t3:\tZ position\r\n"
		"\t(optional)\r\n"
		"\t4:\tTotal turn time (milliseconds)\r\n"
		"\t5:\tTime to spend accelerating/decelerating (milliseconds)\r\n"
		"\t6:\tTime to spend decelerating (milliseconds)\r\n"
	},

	{ OP_CUTSCENES_SET_CAMERA_ROTATION, "set-camera-rotation\r\n"
		"\tSets the camera rotation.  "
		"Takes 3 to 6 arguments...\r\n"
		"\t1:\tPitch (degrees)\r\n"
		"\t2:\tBank (degrees)\r\n"
		"\t3:\tHeading (degrees)\r\n"
		"\t(optional)\r\n"
		"\t4:\tTotal turn time (milliseconds)\r\n"
		"\t5:\tTime to spend accelerating/decelerating (milliseconds)\r\n"
		"\t6:\tTime to spend decelerating (milliseconds)\r\n"
	},

	{ OP_CUTSCENES_SET_CAMERA_TARGET, "set-camera-target\r\n"
		"\tSets the object and subystem camera should track. Camera orientation is offset from the target.  "
		"Takes 1 to 2 arguments...\r\n"
		"\t1:\tShip to track\r\n"
		"\t(optional)\r\n"
		"\t2:\tSubystem to track\r\n"
	},

	{ OP_CUTSCENES_SET_FOV, "set-fov\r\n"
		"\tSets the field of view - overrides all camera settings. Will also override cockpit FOV settings.  "
		"Takes 1 argument...\r\n"
		"\t1:\tNew FOV (degrees)\r\n"
	},

	// Echelon9
	{ OP_CUTSCENES_GET_FOV, "get-fov\r\n"
		"\tReturns the current field of view (in degrees) from the Main camera.\r\n\r\n"
		"Returns a numeric value.  Takes no arguments."
	},

	{ OP_CUTSCENES_RESET_FOV, "reset-fov\r\n"
		"\tResets the field of view.  "
	},

	{ OP_CUTSCENES_RESET_CAMERA, "reset-camera\r\n"
		"\tReleases cutscene camera control. Automatically re-enables photo mode."
		"Takes 1 optional argument...\r\n"
		"\t(optional)\r\n"
		"\t1:\tReset camera data (Position, facing, FOV...) (default: false)"
	},

	{ OP_CUTSCENES_SHOW_SUBTITLE, "show-subtitle (deprecated)\r\n"
		"\tDisplays a subtitle, either an image or a string of text.  Please note that, in images without an alpha channel, black pixels will be treated as transparent.  In images with an alpha channel, black pixels will be drawn as expected.\r\n\r\n"
		"As this operator tries to combine two functions into one and does not adjust coordinates for screen formats, it has been deprecated.\r\n\r\n"
		"Takes 4 to 14 arguments...\r\n"
		"\t1:\tX position (negative value to be from right of screen)\r\n"
		"\t2:\tY position (negative value to be from bottom of screen)\r\n"
		"\t3:\tText to display\r\n"
		"\t4:\tTime to be displayed, not including fadein/out\r\n"
		"\t5:\tImage name (optional)\r\n"
		"\t6:\tFade in time (optional)\r\n"
		"\t7:\tCenter horizontally? (optional)\r\n"
		"\t8:\tCenter vertically? (optional)\r\n"
		"\t9:\tWidth (optional)\r\n"
		"\t10:\tText red component (0-255) (optional)\r\n"
		"\t11:\tText green component (0-255) (optional)\r\n"
		"\t12:\tText blue component (0-255) (optional)\r\n"
		"\t13:\tDrawn after shading? (optional; defaults to false)\r\n"
		"\t14:\tLine vertical spacing, as a percentage, for displaying multi-line subtitles (optional; defaults to 100)\r\n"
	},

	{ OP_CUTSCENES_SHOW_SUBTITLE_TEXT, "show-subtitle-text\r\n"
		"\tDisplays a subtitle in the form of text.  Note that because of the constraints of the subtitle system, textual subtitles are currently limited to 255 characters or fewer.\r\n\r\n"
		"Certain arguments can be specified in a percentage of the screen or an absolute number of pixels.  This is controlled by the '$Show-subtitle uses pixels' game_settings.tbl option.  "
		"Whether percentages or pixels, the subtitle can be configured to scale according to screen resolution via the '$Show-subtitle base resolution' option.\r\n\r\n"
		"Takes 6 to 15 arguments...\r\n"
		"\t1:\tText to display, or the name of a message containing text\r\n"
		"\t2:\tX position (percentage/pixels) - positive measures from the left; negative measures from the right\r\n"
		"\t3:\tY position (percentage/pixels) - positive measures from the top; negative measures from the bottom\r\n"
		"\t4:\tCenter horizontally? (if true, overrides argument #2)\r\n"
		"\t5:\tCenter vertically? (if true, overrides argument #3)\r\n"
		"\t6:\tTime (in milliseconds) to be displayed, not including fade-in/fade-out\r\n"
		"\t7:\tFade time (in milliseconds) to be used for both fade-in and fade-out (optional)\r\n"
		"\t8:\tParagraph width (percentage/pixels) (optional; 0 uses default 200 pixels)\r\n"
		"\t9:\tText red component (0-255) (optional)\r\n"
		"\t10:\tText green component (0-255) (optional)\r\n"
		"\t11:\tText blue component (0-255) (optional)\r\n"
		"\t12:\tText font (optional)\r\n"
		"\t13:\tDrawn after shading? (optional; defaults to false)\r\n"
		"\t14:\tLine vertical spacing (percentage/pixels), for displaying multi-line subtitles (optional; defaults to 100%)\r\n"
		"\t15:\tAdjust subtitle width if screen aspect ratio is different from base resolution? (optional; has no effect if subtitle base resolution is not specified; defaults to true)\r\n"
	},

	{ OP_CUTSCENES_SHOW_SUBTITLE_IMAGE, "show-subtitle-image\r\n"
		"\tDisplays a subtitle in the form of an image.  Please note that, in images without an alpha channel, black pixels will be treated as transparent.  In images with an alpha channel, black pixels will be drawn as expected.\r\n\r\n"
		"Certain arguments can be specified in a percentage of the screen or an absolute number of pixels.  This is controlled by the '$Show-subtitle uses pixels' game_settings.tbl option.  "
		"Whether percentages or pixels, the subtitle can be configured to scale according to screen resolution via the '$Show-subtitle base resolution' option.\r\n\r\n"
		"Takes 8 to 11 arguments...\r\n"
		"\t1:\tImage to display\r\n"
		"\t2:\tX position (percentage/pixels) - positive measures from the left; negative measures from the right\r\n"
		"\t3:\tY position (percentage/pixels) - positive measures from the top; negative measures from the bottom\r\n"
		"\t4:\tCenter horizontally? (if true, overrides argument #2)\r\n"
		"\t5:\tCenter vertically? (if true, overrides argument #3)\r\n"
		"\t6:\tImage width (percentage/pixels) (0 uses original width)\r\n"
		"\t7:\tImage height (percentage/pixels) (0 uses original height)\r\n"
		"\t8:\tTime (in milliseconds) to be displayed, not including fade-in/fade-out\r\n"
		"\t9:\tFade time (in milliseconds) to be used for both fade-in and fade-out (optional)\r\n"
		"\t10:\tDrawn after shading? (optional; defaults to false)\r\n"
		"\t11:\tAdjust subtitle width/height if screen aspect ratio is different from base resolution? (optional; has no effect if subtitle base resolution is not specified; defaults to true)\r\n"
	},

	{ OP_CUTSCENES_SET_TIME_COMPRESSION, "set-time-compression\r\n"
		"\tSets the time compression and prevents it from being changed by the user.  "
		"Takes 1 to 3 arguments...\r\n"
		"\t1:\tNew time compression (% of 1x)\r\n"
		"\t2:\tTime in ms for change to take\r\n"
		"\t3:\tTime compression to start from\r\n"
	},

	{ OP_CUTSCENES_RESET_TIME_COMPRESSION, "reset-time-compression\r\n"
		"\tResets the time compression - that is to say, time compression is set back to 1x, "
		"and the time compression controls are unlocked.  Always call this when done with set-time-compression.  "
	},

	{ OP_CUTSCENES_FORCE_PERSPECTIVE, "lock-perspective\r\n"
		"\tPrevents or allows the player from changing the view mode.  "
		"Takes 1 or 2 arguments...\r\n"
		"\t1:\tTrue to lock the view mode, false to unlock it\r\n"
		"\t2:\tWhat view mode to lock; 0 for first-person, 1 for chase, 2 for external, 3 for top-down, or -1 to not change the current view mode\r\n"
		"\t3:\tIf in first-person, true to lock the hat/slew/free-look/target-track mode, false to unlock it (optional)\r\n"
	},

	{ OP_ALLOW_PHOTO_MODE, "allow-photo-mode\r\n"
		"\tAllows or disallows Photo Mode for this mission.  "
		"Takes 1 argument...\r\n"
		"\t1:\tTrue to allow Photo Mode, false to disallow it\r\n"
	},

	{ OP_SET_CAMERA_SHUDDER, "set-camera-shudder\r\n"
		"\tCauses the camera to shudder.  Normally this will only work if the camera is showing the player's viewpoint (i.e. the HUD), unless the Everywhere flag is set.\r\n\r\n"
		"Takes 2 to 4 arguments...\r\n"
		"\t1: Time (in milliseconds)\r\n"
		"\t2: Intensity.  For comparison, the Maxim has an intensity of 1440.\r\n"
		"\t3: Perpetual (optional).  Whether the effect is perpetual, i.e. does not decay.\r\n"
		"\t4: Everywhere (optional).  Whether the effect is applied to all camera views.\r\n"
	},

	{ OP_JUMP_NODE_SET_JUMPNODE_NAME, "set-jumpnode-name (deprecated in favor of set-jumpnode-display-name)\r\n"
		"\tSets the name of a jump node. Takes 2 arguments...\r\n"
		"\t1: Name of jump node to change name for\r\n"
		"\t2: New name for jump node\r\n\r\n"
		"\tNote: SEXPs referencing the old name will not work after the name change.\r\n"
	},

	{ OP_JUMP_NODE_SET_JUMPNODE_DISPLAY_NAME, "set-jumpnode-display-name\r\n"
		"\tSets the display name of a jump node. Takes 2 arguments...\r\n"
	    "\t1: Name of jump node to change display name for\r\n"
	    "\t2: New display name for jump node\r\n"
	},

	{ OP_JUMP_NODE_SET_JUMPNODE_COLOR, "set-jumpnode-color\r\n"
		"\tSets the color of a jump node.  "
		"Takes 5 arguments...\r\n"
		"\t1:\tJump node to change color for\r\n"
		"\t2:\tRed value\r\n"
		"\t3:\tGreen value\r\n"
		"\t4:\tBlue value\r\n"
		"\t5:\tAlpha value\r\n"
	},

	{ OP_JUMP_NODE_SET_JUMPNODE_MODEL, "set-jumpnode-model\r\n"
		"\tSets the model of a jump node.  "
		"Takes 3 arguments...\r\n"
		"\t1:\tJump node to change model for\r\n"
		"\t2:\tModel filename\r\n"
		"\t3:\tShow as normal model. When this is true, the jumpnode will be rendered like a normal model.\r\n"
	},

	{ OP_JUMP_NODE_SHOW_JUMPNODE, "show-jumpnode\r\n"
		"\tSets a jump node to display on the screen.\r\n"
		"\tAny:\tJump node to show\r\n"
	},

	{ OP_JUMP_NODE_HIDE_JUMPNODE, "hide-jumpnode\r\n"
		"\tSets a jump node to not display on the screen.\r\n"
		"\tAny:\tJump node to hide\r\n"
	},

	// taylor, with modifications by niffiwan and MageKing17
	{ OP_SET_SKYBOX_MODEL, "set-skybox-model\r\n"
		"\tSets the current skybox model.  Takes 1-7 arguments\r\n"
		"\t1:\tModel filename (with .pof extension) to switch to\r\n"
		"\t2:\tRestart animated textures, if they exist (optional, defaults to true)\r\n"
		"\t3-8:\tSet or unset the following skyboxes flags (optional)\r\n"
		"\t\t\tadd-lighting, no-transparency, add-zbuffer\r\n"
		"\t\t\tadd-culling, no-glowmaps, force-clamp\r\n\r\n"
		"Note: If the model filename is set to \"default\" with no extension then it will switch to the mission supplied default skybox.  If it is an empty string or \"none\" then it will remove any current skybox."
	},

	// Goober5000
	{ OP_SET_SKYBOX_ORIENT, "set-skybox-orientation\r\n"
		"\tSets the current skybox orientation.  Takes 3 arguments...\r\n"
		"\t1:\tPitch\r\n"
		"\t2:\tBank\r\n"
		"\t3:\tHeading\r\n"
	},

	// Goober5000
	{ OP_SET_SKYBOX_ALPHA, "set-skybox-alpha\r\n"
		"\tSets the current skybox transparency.  Takes 1 argument...\r\n"
		"\t1:\tAlpha value x1000 (so 1.0 is represented as 1000)\r\n"
	},

	// Goober5000
	{ OP_CHANGE_BACKGROUND, "change-background\r\n"
		"\tSets the displayed suns and bitmaps to one of the stored mission backgrounds.  Takes 1 argument...\r\n"
		"\t1:\tBackground number (starting from 1)\r\n"
	},

	{ OP_ADD_BACKGROUND_BITMAP, "add-background-bitmap (deprecated)\r\n"
		"\tAdds a background bitmap to the sky.  Returns an integer that is stored in a variable so it can be deleted using remove-background-bitmap.  "
		"NOTE: This version of the SEXP uses the old incorrectly-calculated angle math.  Use add-background-bitmap-new instead.\r\n\r\n"
		"Takes 8 to 9 arguments...\r\n"
		"\t1:\tBackground bitmap name\r\n"
		"\t2:\tPitch\r\n"
		"\t3:\tBank\r\n"
		"\t4:\tHeading\r\n"
		"\t5:\tX scale (expressed as a percentage of the original size of the bitmap)\r\n"
		"\t6:\tY scale (expressed as a percentage of the original size of the bitmap)\r\n"
		"\t7:\tX divisions.\r\n"
		"\t8:\tY divisions.\r\n"
		"\t9:\tNumeric variable in which to store the result (optional)\r\n"
	},

	{ OP_ADD_BACKGROUND_BITMAP_NEW, "add-background-bitmap-new\r\n"
		"\tAdds a background bitmap to the sky.  Returns an integer that is stored in a variable so it can be deleted using remove-background-bitmap.  "
		"NOTE: This version of the SEXP treats the angles as correctly calculated.\r\n\r\n"
		"Takes 8 to 9 arguments...\r\n"
		"\t1:\tBackground bitmap name\r\n"
		"\t2:\tPitch\r\n"
		"\t3:\tBank\r\n"
		"\t4:\tHeading\r\n"
		"\t5:\tX scale (expressed as a percentage of the original size of the bitmap)\r\n"
		"\t6:\tY scale (expressed as a percentage of the original size of the bitmap)\r\n"
		"\t7:\tX divisions.\r\n"
		"\t8:\tY divisions.\r\n"
		"\t9:\tNumeric variable in which to store the result (optional)\r\n"
	},

	{ OP_REMOVE_BACKGROUND_BITMAP, "remove-background-bitmap\r\n"
		"\tRemoves the nth background bitmap from the mission\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tZero based bitmap index from the \'Bitmap\' box in the background editor\r\n"
		"\t\t\tYou can also use the result of a previous call to add-background-bitmap to remove that added bitmap\r\n"
	},

	{ OP_ADD_SUN_BITMAP, "add-sun-bitmap (deprecated)\r\n"
		"\tAdds a sun bitmap to the sky.  Returns an integer that is stored in a variable so it can be deleted using remove-sun-bitmap.  "
		"NOTE: This version of the SEXP uses the old incorrectly-calculated angle math.  Use add-sun-bitmap-new instead.\r\n\r\n"
		"Takes 5 to 6 arguments...\r\n"
		"\t1:\tSun bitmap name\r\n"
		"\t2:\tPitch\r\n"
		"\t3:\tBank\r\n"
		"\t4:\tHeading\r\n"
		"\t5:\tScale (expressed as a percentage of the original size of the bitmap)\r\n"
		"\t6:\tNumeric variable in which to store the result (optional)\r\n"
	},

	{ OP_ADD_SUN_BITMAP_NEW, "add-sun-bitmap-new\r\n"
		"\tAdds a sun bitmap to the sky.  Returns an integer that is stored in a variable so it can be deleted using remove-sun-bitmap.  "
		"NOTE: This version of the SEXP treats the angles as correctly calculated.\r\n\r\n"
		"Takes 5 to 6 arguments...\r\n"
		"\t1:\tSun bitmap name\r\n"
		"\t2:\tPitch\r\n"
		"\t3:\tBank\r\n"
		"\t4:\tHeading\r\n"
		"\t5:\tScale (expressed as a percentage of the original size of the bitmap)\r\n"
		"\t6:\tNumeric variable in which to store the result (optional)\r\n"
	},

	{ OP_REMOVE_SUN_BITMAP, "remove-sun-bitmap\r\n"
		"\tRemoves the nth sun from the mission\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tZero based sun index from the \'Suns\' box in the background editor\r\n"
		"\t\t\tYou can also use the result of a previous call to add-sun-bitmap to remove that added sun\r\n"
	},

	{ OP_NEBULA_CHANGE_STORM, "nebula-change-storm\r\n"
		"\tChanges the current nebula storm.  Has no effect if the nebula is not currently active.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tNebula storm to change to\r\n"
	},

	{ OP_NEBULA_TOGGLE_POOF, "nebula-toggle-poof (deprecated in favor of nebula-set-poofs)\r\n"
		"\tToggles the state of a nebula poof\r\n\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tName of the nebula poof to toggle\r\n"
		"\t2:\tA true boolean expression will turn this poof on.  A false one will turn this poof off."
	},

	{ OP_NEBULA_SET_POOFS, "nebula-set-poofs\r\n"
		"\tSets the state of one or more nebula poofs\r\n\r\n"
		"Takes 1 or more arguments...\r\n"
		"\t1:\tA true boolean expression will turn the poofs on.  A false one will turn them off.\r\n"
		"\tRest:\tName of a nebula poof to set.  If no poofs are listed, all tabled poofs will be set.\r\n"
	},

	{ OP_NEBULA_FADE_POOF, "nebula-fade-poof (deprecated in favor of nebula-fade-poofs)\r\n"
		"\tSets a poof pattern to fade in or out over time\r\n"
		"Takes 3 arguments...\r\n"
		"\t1:\tName of the nebula poof to fade\r\n"
		"\t2:\tTime in milliseconds to fade\r\n"
		"\t3:\tWhether to fade in or out.  True to fade in, false to fade out\r\n"
	},

	{ OP_NEBULA_FADE_POOFS, "nebula-fade-poofs\r\n"
		"\tSets one or more poof patterns to fade in or out over time\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tWhether to fade in or out.  True to fade in, false to fade out.\r\n"
		"\t2:\tTime in milliseconds to fade.\r\n"
		"\tRest:\tName of a nebula poof to fade.  If no poofs are listed, all tabled poofs will be faded.\r\n"
	},

	{ OP_NEBULA_CHANGE_PATTERN, "nebula-change-pattern\r\n"
	"\tChanges the current nebula background pattern (as defined in nebula.tbl).  Has no effect if the nebula is not currently active.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tNebula background pattern to change to\r\n"
	},

	{ OP_NEBULA_CHANGE_FOG_COLOR, "nebula-change-fog-color\r\n"
	"\tChanges the current nebula fog color.  Has no effect if the nebula is not currently active.\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1:\tRed (0 - 255)\r\n"
		"\t2:\tGreen (0 - 255)\r\n"
		"\t3:\tBlue (0 - 255)\r\n"
	},

	{ OP_NEBULA_SET_RANGE, "nebula-set-range\r\n"
		"\tSets the basic sensor range of the current nebula, which is multiplied by a species-specific factor to get the \"scan range\".  "
		"Within the scan range, a ship is at least partially targetable (fuzzy blip); within half the scan range, a ship is fully targetable.  "
		"Beyond the scan range, a ship is not targetable.  Note that the range will be reset or overridden if mission-set-nebula is called.\r\n\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tNebula basic sensor range\r\n"
	},

	{ OP_VOLUMETRICS_TOGGLE, "volumetrics-toggle\r\n"
	"\tToggles whether the volumetrics for the current mission are shown or not. "
	"Does nothing if no volumetrics are defined for the mission\r\n"
		"Takes 1 argument...\r\n"
		"\t1:\tWill be shown\r\n"
	},

	{OP_SCRIPT_EVAL_BOOL, "script-eval-bool\r\n"
		"\tEvaluates the concatenation of all arguments as a single script that returns a boolean. "
		"Takes at least 1 argument...\r\n"
		"\tAll:\tScript\r\n"
	},

	{OP_SCRIPT_EVAL_NUM, "script-eval-num\r\n"
		"\tEvaluates the concatenation of all arguments as a single script that returns a number. "
		"Takes at least 1 argument...\r\n"
		"\tAll:\tScript\r\n"
	},

	{ OP_DISABLE_ETS, "disable-ets\r\n"
		"\tSwitches a ships' ETS system off\r\n\r\n"
		"Takes at least 1 argument...\r\n"
		"\tAll:\tList of ships this sexp applies to (ship does not need to be in-mission)\r\n"
	},

	{ OP_ENABLE_ETS, "enable-ets\r\n"
		"\tSwitches a ships' ETS system on\r\n\r\n"
		"Takes at least 1 argument...\r\n"
		"\tAll:\tList of ships this sexp applies to (ship does not need to be in-mission)\r\n"
	},

	{OP_SCRIPT_EVAL_STRING, "script-eval-string\r\n"
		"\tEvaluates script to return a string\r\n\r\n"
		"Takes a multiple of 2 arguments...\r\n"
		"\t1:\tScript (without a leading 'return')\r\n"
		"\t2:\tString variable to hold the result\r\n"
	},

	{OP_SCRIPT_EVAL, "script-eval (deprecated in favor of script-eval-block)\r\n"
		"\tEvaluates the given scripts, one script per argument\r\n"
		"Takes at least 1 argument...\r\n"
		"\tAll:\tScript to evaluate\r\n"
	},

	{OP_SCRIPT_EVAL_BLOCK, "script-eval-block\r\n"
		"\tEvaluates the concatenation of all arguments as a single script\r\n"
		"Takes at least 1 argument...\r\n"
		"\tAll:\tScript to evaluate\r\n"
	},

	{OP_SCRIPT_EVAL_MULTI, "multi-eval\r\n"
		"\tEvaluates script\r\n\r\n"
		"Takes at least 2 arguments...\r\n"
		"\t1:\tScript to evaluate\r\n"
		"\t2:\tTrue/False - Should the script evaluate on the server?\r\n"
		"\t(rest):\tList of players who should evaluate this script. If no player is given, all clients will execute the script\r\n"
	},

	{OP_FORCE_GLIDE, "force-glide\r\n"
		"\tForces a given ship into glide mode, provided it is capable of gliding. Note that the player will not be able to leave glide mode, and that a ship in glide mode cannot warp out or enter autopilot.\r\n"
		"Takes 2 Arguments...\r\n"
		"\t1:\tShip to force (ship must be in-mission)\r\n"
		"\t2:\tTrue to activate glide, False to deactivate\r\n"
	},

	{OP_HUD_SET_DIRECTIVE, "hud-set-directive\r\n"
		"\tSets the text of a given custom hud gauge to the provided text.\r\n"
		"Takes 2 Arguments...\r\n"
		"\t1:\tHUD Gauge name\r\n"
		"\t2:\tText that will be displayed. This text will be treated as directive text, meaning that references to mapped keys will be replaced with the user's preferences.\r\n"
	},

	{OP_HUD_GAUGE_SET_ACTIVE, "hud-gauge-set-active (deprecated)\r\n"
		"\tActivates or deactivates a given hud gauge.  Works for custom and retail gauges.\r\n"
		"Takes 2 Arguments...\r\n"
		"\t1:\tHUD Gauge name\r\n"
		"\t2:\tBoolean, whether or not to display this gauge\r\n"
	},

	{OP_HUD_CLEAR_MESSAGES, "hud-clear-messages\r\n"
		"\tClears active messages displayed on the HUD.\r\n"
		"Takes no arguments\r\n"
	},

	{OP_HUD_ACTIVATE_GAUGE_TYPE, "hud-activate-gauge-type (deprecated)\r\n"
		"\tActivates or deactivates all hud gauges of a given type.\r\n"
		"Takes 2 Arguments...\r\n"
		"\t1:\tGauge Type\r\n"
		"\t2:\tBoolean, whether or not to display this gauge\r\n"
	},

	{OP_HUD_SET_CUSTOM_GAUGE_ACTIVE, "hud-set-custom-gauge-active\r\n"
		"\tActivates or deactivates a custom hud gauge defined in hud_gauges.tbl.\r\n"
		"Takes 2 Arguments...\r\n"
		"\t1:\tBoolean, whether or not to display this gauge\r\n"
		"\tRest:\tHUD Gauge name\r\n"
	},

	{OP_HUD_SET_BUILTIN_GAUGE_ACTIVE, "hud-set-builtin-gauge-active\r\n"
		"\tActivates or deactivates a builtin hud gauge grouping.\r\n"
		"Takes 2 Arguments...\r\n"
		"\t1:\tBoolean, whether or not to display this gauge\r\n"
		"\tRest:\tHUD Gauge Group name\r\n"
	},

	{OP_HUD_FORCE_SENSOR_STATIC, "hud-force-sensor-static\r\n"
		"\tActivates or deactivates hud static as if sensors are damaged.\r\n"
		"Takes 1 Argument...\r\n"
		"\t1:\tBoolean, whether or not to enable sensor static\r\n"
	},

	{OP_HUD_FORCE_EMP_EFFECT, "hud-force-emp-effect\r\n"
		"\tActivates or deactivates emp effect for the player.\r\n"
		"Takes 2 or more Arguments...\r\n"
		"\t1:\tNumber, emp intensity (0 to 500)\r\n"
		"\t2:\tNumber, emp duration in milliseconds\r\n"
		"\t3:\tString or message to display. \"none\" to display nothing. Defaults to \"Emp\"\r\n"
	},

	{OP_SET_SQUADRON_WINGS, "set-squadron-wings\r\n"
		"\tSets the wings displayed on the squadron HUD display.  By default these are Alpha, Beta, Gamma, Delta, and Epsilon.  "
		"The squadron status will be updated to the current status of the wings, but note that if any ships in those wings have "
		"previously been destroyed, the HUD display may look different than expected.  (Specifically, destroyed ships may be "
		"indicated as missing or may cause other ships in the same wing to appear in different positions.)  However, any wings "
		"that are shared between one display and the next will be preserved.\r\n"
		"Takes 1 to " SCP_TOKEN_TO_STR(MAX_SQUADRON_WINGS) " arguments...\r\n"
		"\tAll:\tWing to display\r\n"
	},

	{OP_ADD_TO_COLGROUP, "add-to-collision-group\r\n"
		"\tAdds a ship/prop to the specified collision group(s). Note that there are 32 collision groups, "
		"and that an object may be in several collision groups at the same time\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tShip/prop to add (ships do not need to be in-mission).\r\n"
		"\t2+:\tGroup IDs. Valid IDs are 0 through 31 inclusive.\r\n"
	},

	{OP_REMOVE_FROM_COLGROUP, "remove-from-collision-group\r\n"
		"\tRemoves a ship/prop from the specified collision group(s). Note that there are 32 collision groups, "
		"and that an object may be in several collision groups at the same time\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tShip/prop to add (ships do not need to be in-mission).\r\n"
		"\t2+:\tGroup IDs. Valid IDs are 0 through 31 inclusive.\r\n"
	},

	{OP_ADD_TO_COLGROUP_NEW, "add-to-collision-group-new\r\n"
		"\tAdds one or more ships/props to the specified collision group. There are 32 collision groups, "
		"and an object may be in several collision groups at the same time. This sexp functions identically to "
		"add-to-collision-group, except that the arguments are one group and many ships/props, rather than one ship/prop and many groups.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tGroup ID. Valid IDs are 0 through 31 inclusive.\r\n"
		"\t2+:\tShip/prop to add (ships do not need to be in-mission).\r\n"
	},

	{OP_REMOVE_FROM_COLGROUP_NEW, "remove-from-collision-group-new\r\n"
		"\tRemoves one or more ships/props from the specified collision group. There are 32 collision groups, "
		"and an object may be in several collision groups at the same time. This sexp functions identically to "
		"remove-from-collision-group, except that the arguments are one group and many ships/props, rather than one ship/prop and many groups.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tGroup ID. Valid IDs are 0 through 31 inclusive.\r\n"
		"\t2+:\tShip/prop to remove (ships do not need to be in-mission).\r\n"
	},

	{OP_GET_COLGROUP_ID, "get-collision-group\r\n"
		"\tReturns an objects' collision group ID. Note that this ID is a bitfield.\r\n"
		"Takes 1 Argument...\r\n"
		"\t1:\tObject name\r\n"
	},

	//Valathil
	{OP_SHIP_EFFECT, "ship-effect\r\n"
		"\tPlays an animated shader effect on the ship(s) or wing(s).\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1:\tEffect name (as defined in post_processing.tbl)\r\n"
		"\t2:\tHow long the effect should take in milliseconds\r\n"
		"\tRest:\tShip or wing name (ship must be in-mission)\r\n"
	},

	{OP_CLEAR_SUBTITLES, "clear-subtitles\r\n"
		"\tClears the subtitle queue completely.\r\n"
	},

	{OP_SET_THRUSTERS, "set-thrusters-status\r\n"
		"\tManipulates the thrusters on a ship.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1:\tBoolean, true sets thrusters to visible, false deactivates them.\r\n"
		"\t2:\tRest: List of ships this sexp will work on (ship must be in-mission).\r\n"
	},

	{OP_SET_PLAYER_THROTTLE_SPEED, "set-player-throttle-speed\r\n"
		"\tSets a player's throttle to a percentage of their maximum speed.\r\n"
		"\tThis SEXP has no effect if used on an AI ship, however it will still work on an AI-controlled player ship.\r\n"
		"Takes 2 arguments...\r\n"
		"\t1:\tThe player ship to set the throttle of.\r\n"
		"\t2:\tThe percentage of the player's maximum speed to set their throttle to.\r\n"
		"\t\tThis is capped to either 0 or 100 if outside the valid range."
	},

	{OP_CHANGE_TEAM_COLOR, "change-team-color\r\n"
		"\tChanges the team color setting for one or several ships.\r\n"
		"\tThis sexp has no effect on ships that don't have team colors enabled for them.\r\n"
		"\tTakes 3 or more arguments...\r\n"
		"\t1:\tThe new team color name. Name must be defined in colors.tbl.\r\n"
		"\t2:\tCrossfade time in milliseconds. During this time, colors will be mixed.\r\n"
		"\t3:\tRest: List of ships this sexp will operate on (ships must be in-mission)."
	},

	{OP_CALL_SSM_STRIKE, "call-ssm-strike\r\n"
		"\tCalls a subspace missile strike on the specified ship.\r\n"
		"\tRequires a ssm table (ssm.tbl).\r\n"
		"Takes 3 arguments...\r\n"
		"\t1:\tStrike name.\r\n"
		"\t2:\tCalling team.\r\n"
		"\tRest:\tList of ships the strike will be called on."
	},

	{OP_PLAYER_IS_CHEATING_BASTARD, "player-is-cheating\r\n"
		"\tReturns true if the player is or has been cheating in this mission.\r\n"
	},

	{ OP_IS_LANGUAGE, "is-language\r\n"
		"\tReturns whether the game is running in the specified language.  Takes 1 argument...\r\n"
		"\t1:\tA language.  This can be any string; the SEXP will return true if and only if the string matches the current language.  "
		"Builtin languages are English, German, French, and Polish, and others can be defined in strings.tbl.\r\n"
	},

	{ OP_USED_CHEAT, "used-cheat\r\n"
		"\tReturns true if the given cheat has been used during this mission and there has not been another cheat used since.  Takes 1 argument.\r\n"
		"\t1:\tA cheat string. This can be any string; the SEXP will return true if and only if the string matches the last cheat used.  "
		"All original FS2 cheats are supported, and more can be defined in the cheats.tbl file.\r\n"
	},

	{ OP_OVERRIDE_MOTION_DEBRIS, "set-motion-debris-override\r\n"
		"\tControls whether or not motion debris should be active.  This overrides any choice made by the user through the -nomotiondebris commandline flag.  "
		"Takes 1 argument...\r\n"
		"\t1:\tBoolean: True will disable motion debris, False reenable it.\r\n"
	},

	{ OP_MODIFY_VARIABLE_XSTR, "modify-variable-xstr\r\n"
		"\tSets a variable to a localized string.\r\n\r\n"
		"Takes 3 arguments...\r\n"
		"\t1:\tName of Variable.\r\n"
		"\t2:\tThe default text if no localized version is available.\r\n"
		"\t3:\tThe XSTR index. If set to -1 then the default value will be used\r\n"
	},

	{ OP_REPLACE_TEXTURE, "replace-texture\r\n"
		"\tChanges a texture of a ship to a different texture, similar to the FRED texture replace.\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1: Name of the texture to be replaced.\r\n"
		"\t2: Name of the texture to be changed to.\r\n"
		"\tRest: Name of the ship or wing (ship/wing does not need to be in-mission).\r\n"
	},

	{ OP_REPLACE_TEXTURE_SKYBOX, "replace-skybox-texture\r\n"
		"\tChanges a texture of the skybox to a different texture, similar to the FRED texture replace.\r\n"
		"Takes 2 arguments...\r\n"
		"\t1: Name of the texture to be replaced.\r\n"
		"\t2: Name of the texture to be changed to.\r\n"
	},

	{ OP_SET_ALPHA_MULT, "set-alpha-multiplier\r\n"
		"\tSets the opacity of a ship.\r\n"
		"Takes 2 or more arguments...\r\n"
		"\t1: Opacity (0 = transparent, 100 = opaque)\r\n"
		"\tRest: Name of the ship or wing (ship/wing must be in-mission).\r\n"
	},

	{ OP_TRIGGER_ANIMATION_NEW , "trigger-ship-animation\r\n"
		"\tTriggers an animation on a ship.\r\n"
		"Takes 3 or more arguments...\r\n"
		"\t1: The ship to trigger the animation on (ship must be in-mission).\r\n"
		"\t2: The trigger type of the animation.\r\n"
		"\t3: The triggered-by value of the animation. Must be the same as in the table for the animation. Leave blank if not specified in the table.\r\n"
		"\t\tException: fighterbay-type animations must specify the number of the fighter bay path.\r\n"
		"\t4: If the animation should play forwards (true) or backwards (false). Defaults to true.\r\n"
		"\t5: If the animation should reset before playing. Defaults to false.\r\n"
		"\t6: If the animation should complete instantly. Defaults to false.\r\n"
		"\t7: If the animation should pause, instead of playing. Defaults to false.\r\n"
	},

	{ OP_STOP_LOOPING_ANIMATION, "stop-looping-animation\r\n"
		"\tStops a looping animation once it finishes its current loop.\r\n"
		"Takes 3 arguments...\r\n"
		"\t1: The ship to trigger the animation on (ship must be in-mission).\r\n"
		"\t2: The trigger type of the animation.\r\n"
		"\t3: The triggered-by value of the animation. Must be the same as in the table for the animation. Leave blank if not specified in the table.\r\n"
		"\t\tException: fighterbay-type animations must specify the number of the fighter bay path.\r\n"
	},

	{ OP_UPDATE_MOVEABLE , "update-moveable-animation\r\n"
		"\tUpdates the data for a moveable animation.\r\n"
		"Takes 2 and more arguments...\r\n"
		"\t1: The ship to update the moveable for (ship must be in-mission).\r\n"
		"\t2: The name of the moveable.\r\n"
		"\tRest: The data for the moveable. Depends on moveable type. Refer to the table below:\r\n\r\n"
		"Orientation:\r\n"
		"\tThree numbers, x, y, z rotation respectively, in degrees\r\n"
		"Rotation:\r\n"
		"\tThree numbers, x, y, z rotation respectively, in degrees\r\n"
		"Translation:\r\n"
		"\tThree required numbers: x, y, z position target relative to base, in 1/100th meters\r\n"
		"Axis Rotation:\r\n"
		"\tOne number, rotation angle in degrees\r\n"
		"Inverse Kinematics:\r\n"
		"\tThree required numbers: x, y, z position target relative to base, in 1/100th meters\r\n"
		"\tThree optional numbers: x, y, z rotation target relative to base, in degrees\r\n"
	},

	{ OP_ADVANCE_MOVEABLE, "advance-moveable-animation\r\n"
		"\tAdvances a moveable animation to its final state instantly.\r\n"
		"Takes 2 arguments...\r\n"
		"\t1: The ship (ship must be in-mission).\r\n"
		"\t2: The name of the moveable.\r\n"
	},

	{ OP_TOGGLE_ASTEROID_FIELD, "toggle-asteroid-field\r\n" 
		"\tTurns an existing asteroid or debris field on/off. Will do nothing if no field is defined in the mission file or set with sexps.\r\n"
		"\tNote that this differs from actually removing and adding a field in that it only toggles a field from being rendered or calculated.\r\n"
		"\tTakes 1 argument...\r\n"
		"\t1:\ttrue for field on, false for field off\r\n" 
	},

	{ OP_SET_ASTEROID_FIELD, "set-asteroid-field (deprecated in favor of config-asteroid-field)\r\n" 
		"\tCreates or overwrites the asteroid or debris field with an asteroid field. \r\n"
		"\tTakes 1 or more arguments...\r\n"
		"\t1:\tNumber of asteroids in the field, or 0 to remove an existing field\r\n" 
		"\t2:\t0 for passive field, 1 for active field, 2 for enhanced passive, 3 for enhanced active, defaults to 0\r\n"
		"\t3:\tThe speed of the asteroids, defaults to 0\r\n"
		"\t4:\tTrue to enable brown asteroids, defaults to true\r\n"
		"\t5:\tTrue to enable blue asteroids, defaults to false\r\n"
		"\t6:\tTrue to enable orange asteroids, defaults to false\r\n"
		"\t7:\tOuterbox Min X, defaults to -1000\r\n"
		"\t8:\tOuterbox Max X, defaults to 1000\r\n"
		"\t9:\tOuterbox Min Y, defaults to -1000\r\n"
		"\t10:\tOuterbox Max Y, defaults to 1000\r\n"
		"\t11:\tOuterbox Min Z, defaults to -1000\r\n"
		"\t12:\tOuterbox Max Z, defaults to 1000\r\n"
		"\t13:\tTrue to activate the inner box where no asteroids will be spawned, defaults to false\r\n"
		"\t14:\tInnerbox Min X, defaults to -500\r\n"
		"\t15:\tInnerbox Max X, defaults to 500\r\n"
		"\t16:\tInnerbox Min Y, defaults to -500\r\n"
		"\t17:\tInnerbox Max Y, defaults to 500\r\n"
		"\t18:\tInnerbox Min Z, defaults to -500\r\n"
		"\t19:\tInnerbox Max Z, defaults to 500\r\n"
		"\tRest:\tThe ships the asteroid field will target if it's an active field\r\n"
	},

	{ OP_SET_DEBRIS_FIELD, "set-debris-field (deprecated in favor of config-debris-field)\r\n" 
		"\tCreates or overwrites the asteroid or debris field with a debris field. \r\n"
		"\tTakes 1 or more arguments...\r\n"
		"\t1:\tNumber of debris in the field, or 0 to remove an existing field\r\n" 
		"\t2:\tThe speed of the asteroids, defaults to 0\r\n"
		"\t3:\tThe type of debris, defaults to none\r\n"
		"\t4:\tThe type of debris, defaults to none\r\n"
		"\t5:\tThe type of debris, defaults to none\r\n"
		"\t6:\tOuterbox Min X, defaults to -1000\r\n"
		"\t7:\tOuterbox Max X, defaults to 1000\r\n"
		"\t8:\tOuterbox Min Y, defaults to -1000\r\n"
		"\t9:\tOuterbox Max Y, defaults to 1000\r\n"
		"\t10:\tOuterbox Min Z, defaults to -1000\r\n"
		"\t11:\tOuterbox Max Z, defaults to 1000\r\n"
		"\t12:\tIf the field uses enhanced spawning checks, defaults to false\r\n"
	},

	{ OP_CONFIG_ASTEROID_FIELD, "config-asteroid-field\r\n" 
		"\tCreates or overwrites the asteroid or debris field with an asteroid field. \r\n"
		"\tNote that setting targets for an active field must be done with config-asteroid-targets. \r\n"
		"\tTakes 1 or more arguments...\r\n"
		"\t1:\tNumber of asteroids in the field, or 0 to remove an existing field\r\n" 
		"\t2:\t0 for passive field, 1 for active field, 2 for enhanced passive, 3 for enhanced active, defaults to 0\r\n"
		"\t3:\tThe speed of the asteroids, defaults to 0\r\n"
		"\t4:\tOuterbox Min X, defaults to -1000\r\n"
		"\t5:\tOuterbox Max X, defaults to 1000\r\n"
		"\t6:\tOuterbox Min Y, defaults to -1000\r\n"
		"\t7:\tOuterbox Max Y, defaults to 1000\r\n"
		"\t8:\tOuterbox Min Z, defaults to -1000\r\n"
		"\t9:\tOuterbox Max Z, defaults to 1000\r\n"
		"\t10:\tTrue to activate the inner box where no asteroids will be spawned, defaults to false\r\n"
		"\t11:\tInnerbox Min X, defaults to -500\r\n"
		"\t12:\tInnerbox Max X, defaults to 500\r\n"
		"\t13:\tInnerbox Min Y, defaults to -500\r\n"
		"\t14:\tInnerbox Max Y, defaults to 500\r\n"
		"\t15:\tInnerbox Min Z, defaults to -500\r\n"
		"\t16:\tInnerbox Max Z, defaults to 500\r\n"
		"\tRest:\tThe asteroid types to enable\r\n"
	},

	{ OP_CONFIG_DEBRIS_FIELD, "config-debris-field\r\n" 
		"\tCreates or overwrites the asteroid or debris field with a debris field. \r\n"
		"\tTakes 1 or more arguments...\r\n"
		"\t1:\tNumber of debris in the field, or 0 to remove an existing field\r\n" 
		"\t2:\tThe speed of the asteroids, defaults to 0\r\n"
		"\t3:\tOuterbox Min X, defaults to -1000\r\n"
		"\t4:\tOuterbox Max X, defaults to 1000\r\n"
		"\t5:\tOuterbox Min Y, defaults to -1000\r\n"
		"\t6:\tOuterbox Max Y, defaults to 1000\r\n"
		"\t7:\tOuterbox Min Z, defaults to -1000\r\n"
		"\t8:\tOuterbox Max Z, defaults to 1000\r\n"
		"\t9:\tIf the field uses enhanced spawning checks, defaults to false\r\n"
		"\tRest:\tThe asteroid types to enable\r\n"
	},

	{ OP_CONFIG_FIELD_TARGETS, "config-field-targets\r\n"
		"\tAdds or removes ships from the Asteroid Field targets list. Only works on Active fields. \r\n"
		"\tYou can remove all targets at once by supplying 0 ship arguments. \r\n"
		"\tRemoving all targets will force the field to use default behavior and target the first ship on the escort list. \r\n"
		"\tThe inverse is not true. Ship arguments must be provided to add targets. \r\n"
		"\tTakes 1 or more arguments...\r\n"
		"\t1:\tTrue to add to the list, false to remove\r\n"
		"\tRest:\tThe names of ships to add or remove\r\n"
	},

	{ OP_SET_MOTION_DEBRIS, "set-motion-debris\r\n" 
		"\tSets the bitmap used for motion debris. If set-motion-debris-override is set then this will unset it.\r\n"
		"\tTakes 1 argument...\r\n"
		"\t1:\tthe name of the motion debris\r\n" 
	}
};
// clang-format on


SCP_vector<op_menu_struct> op_menu =
{
	{ "Objectives",     OP_CATEGORY_OBJECTIVE   },
	{ "Time",           OP_CATEGORY_TIME        },
	{ "Logical",        OP_CATEGORY_LOGICAL     },
	{ "Arithmetic",     OP_CATEGORY_ARITHMETIC  },
	{ "Status",         OP_CATEGORY_STATUS      },
	{ "Change",         OP_CATEGORY_CHANGE      },
	{ "Conditionals",   OP_CATEGORY_CONDITIONAL },
	{ "Ai goals",       OP_CATEGORY_AI          },
	{ "Event/Goals",    OP_CATEGORY_GOAL_EVENT  },
	{ "Training",       OP_CATEGORY_TRAINING    },
};

// Goober5000's subcategorization of the Change menu (and possibly other menus in the future,
// if people so choose - see sexp.h)
SCP_vector<op_menu_struct> op_submenu =
{
	{ "Messages and Personas",          CHANGE_SUBCATEGORY_MESSAGING                    },
	{ "AI Control",                     CHANGE_SUBCATEGORY_AI_CONTROL                   },
	{ "Ship Status",                    CHANGE_SUBCATEGORY_SHIP_STATUS                  },
	{ "Weapons, Shields, and Engines",  CHANGE_SUBCATEGORY_SHIELDS_ENGINES_AND_WEAPONS  },
	{ "Subsystems and Health",          CHANGE_SUBCATEGORY_SUBSYSTEMS                   },
	{ "Cargo",                          CHANGE_SUBCATEGORY_CARGO                        },
	{ "Armor and Damage Types",         CHANGE_SUBCATEGORY_ARMOR_AND_DAMAGE_TYPES       },
	{ "Beams and Turrets",              CHANGE_SUBCATEGORY_BEAMS_AND_TURRETS            },
	{ "Models and Textures",            CHANGE_SUBCATEGORY_MODELS_AND_TEXTURES          },
	{ "Coordinate Manipulation",        CHANGE_SUBCATEGORY_COORDINATE_MANIPULATION      },
	{ "Mission and Campaign",           CHANGE_SUBCATEGORY_MISSION_AND_CAMPAIGN         },
	{ "Music and Sound",                CHANGE_SUBCATEGORY_MUSIC_AND_SOUND              },
	{ "HUD",                            CHANGE_SUBCATEGORY_HUD                          },
	{ "Nav Points",                     CHANGE_SUBCATEGORY_NAV                          },
	{ "Cutscenes",                      CHANGE_SUBCATEGORY_CUTSCENES                    },
	{ "Backgrounds and Nebulae",        CHANGE_SUBCATEGORY_BACKGROUND_AND_NEBULA        },
	{ "Jump Nodes",                     CHANGE_SUBCATEGORY_JUMP_NODES                   },
	{ "Special Effects",                CHANGE_SUBCATEGORY_SPECIAL_EFFECTS              },
	{ "Variables",                      CHANGE_SUBCATEGORY_VARIABLES                    },
	{ "Containers",                     CHANGE_SUBCATEGORY_CONTAINERS                   },
	{ "Other",                          CHANGE_SUBCATEGORY_OTHER                        },
	{ "Mission",                        STATUS_SUBCATEGORY_MISSION                      },
	{ "Cutscenes",                      STATUS_SUBCATEGORY_CUTSCENES                    },
	{ "Player",                         STATUS_SUBCATEGORY_PLAYER                       },
	{ "Multiplayer",                    STATUS_SUBCATEGORY_MULTIPLAYER                  },
	{ "Ship Status",                    STATUS_SUBCATEGORY_SHIP_STATUS                  },
	{ "Weapons, Shields, and Engines",  STATUS_SUBCATEGORY_SHIELDS_ENGINES_AND_WEAPONS  },
	{ "Cargo",                          STATUS_SUBCATEGORY_CARGO                        },
	{ "Damage",                         STATUS_SUBCATEGORY_DAMAGE                       },
	{ "Distance and Coordinates",       STATUS_SUBCATEGORY_DISTANCE_AND_COORDINATES     },
	{ "Variables",                      STATUS_SUBCATEGORY_VARIABLES                    },
	{ "Containers",                     STATUS_SUBCATEGORY_CONTAINERS                   },
	{ "Other",                          STATUS_SUBCATEGORY_OTHER                        },
};

/**
 * Internal file used by output_sexps, should not be called from output_sexps
 */
static void output_sexp_html(int sexp_idx, FILE *fp)
{
	if(sexp_idx < 0 || sexp_idx > (int)Operators.size())
		return;

	bool printed=false;

	for(auto& help : Sexp_help)
	{
		if(help.id == Operators[sexp_idx].value)
		{
			char* new_buf = new char[2 * help.help.size()];
			char* dest_ptr = new_buf;
			const char* curr_ptr = help.help.c_str();
			const char* end_ptr = curr_ptr + help.help.size();
			while(curr_ptr < end_ptr)
			{
				if(*curr_ptr == '\n')
				{
					strcpy(dest_ptr, "\n<br>");
					dest_ptr+=5;
				// for html compatibility
				} else if (*curr_ptr == '<') {
					strcpy(dest_ptr, "&lt;");
					dest_ptr += 4;
				// for html compatibility
				} else if (*curr_ptr == '>') {
					strcpy(dest_ptr, "&gt;");
					dest_ptr += 4;
				} else {
					*dest_ptr++ = *curr_ptr;
				}
				curr_ptr++;
			}
			*dest_ptr = '\0';

			fprintf(fp, "<dt><b>%s</b></dt>\n<dd>%s</dd>\n", Operators[sexp_idx].text.c_str(), new_buf);
			delete[] new_buf;

			printed = true;
		}
	}

	if(!printed)
		fprintf(fp, "<dt><b>%s</b></dt>\n<dd>Min arguments: %d, Max arguments: %d</dd>\n", Operators[sexp_idx].text.c_str(), Operators[sexp_idx].min, Operators[sexp_idx].max);
}

/**
 * Output sexp.html file
 */
bool output_sexps(const char *filepath)
{
	FILE *fp = fopen(filepath,"w");

	if(fp == nullptr)
	{
		os::dialogs::Message(os::dialogs::MESSAGEBOX_ERROR, "Error creating SEXP operator list");
		return false; 
	}

	//Header
	fprintf(fp, "<html>\n<head>\n\t<title>SEXP Output - FSO v%s</title>\n</head>\n", FS_VERSION_FULL);
	fputs("<body>", fp);
	fprintf(fp,"\t<h1>SEXP Output - FSO v%s</h1>\n", FS_VERSION_FULL);

	SCP_vector<int> done_sexp_ids;
	int x,y,z;

	//Output an overview
	fputs("<dl>", fp);
	for(x = 0; x < (int)op_menu.size(); x++)
	{
		fprintf(fp, "<dt><a href=\"#%d\">%s</a></dt>", op_menu[x].id, op_menu[x].name.c_str());
		for(y = 0; y < (int)op_submenu.size(); y++)
		{
			if(category_of_subcategory(op_submenu[y].id) == op_menu[x].id)
			{
				fprintf(fp, "<dd><a href=\"#%d\">%s</a></dd>", op_submenu[y].id, op_submenu[y].name.c_str());
			}
		}
	}
	fputs("</dl>", fp);

	//Output the full descriptions
	fputs("<dl>", fp);
	for(x = 0; x < (int)op_menu.size(); x++)
	{
		fprintf(fp, "<dt id=\"%d\"><h2>%s</h2></dt>\n", (int)op_menu[x].id, op_menu[x].name.c_str());
		fputs("<dd>", fp);
		fputs("<dl>", fp);
		for(y = 0; y < (int)op_submenu.size(); y++)
		{
			if(category_of_subcategory(op_submenu[y].id) == op_menu[x].id)
			{
				fprintf(fp, "<dt id=\"%d\"><h3>%s</h3></dt>\n", (int)op_submenu[y].id, op_submenu[y].name.c_str());
				fputs("<dd>", fp);
				fputs("<dl>", fp);
				for(z = 0; z < (int)Operators.size(); z++)
				{
					if((get_category(Operators[z].value) == op_menu[x].id)
						&& (get_subcategory(Operators[z].value) != OP_SUBCATEGORY_NONE)
						&& (get_subcategory(Operators[z].value) == op_submenu[y].id))
					{
						output_sexp_html(z, fp);
					}
				}
				fputs("</dl>", fp);
				fputs("</dd>", fp);
			}
		}
		for(z = 0; z < (int)Operators.size(); z++)
		{
			if((get_category(Operators[z].value) == op_menu[x].id)
				&& (get_subcategory(Operators[z].value) == OP_SUBCATEGORY_NONE))
			{
				output_sexp_html(z, fp);
			}
		}
		fputs("</dl>", fp);
		fputs("</dd>", fp);
	}
	for(z = 0; z < (int)Operators.size(); z++)
	{
		if(get_category(Operators[z].value) == OP_SUBCATEGORY_NONE)
		{
			output_sexp_html(z, fp);
		}
	}
	fputs("</dl>", fp);
	fputs("</body>\n</html>\n", fp);

	fclose(fp);

	return true;
}
