/**
 * Specifies which template should indicate the target location of a player command,
 * given a command type.
 */
var g_TargetMarker = {
	"move": "special/target_marker",
	"map_flare": "special/flare_target_marker"
};

/**
 * Sound we play when displaying a flare.
 */
var g_FlareSound = "audio/interface/alarm/alarmally_1.ogg";

/**
 * Which enemy entity types will be attacked on sight when patroling.
 */
var g_PatrolTargets = ["Unit"];

const g_DisabledTags = { "color": "255 140 0" };

/**
 * List of different actions units can execute,
 * this is mostly used to determine which actions can be executed
 *
 * "execute" is meant to send the command to the engine
 *
 * The next functions will always return false
 * in case you have to continue to seek
 * (i.e. look at the next entity for getActionInfo, the next
 * possible action for the actionCheck ...)
 * They will return an object when the searching is finished
 *
 * "getActionInfo" is used to determine if the action is possible,
 * and also give visual feedback to the user (tooltips, cursors, ...)
 *
 * "preSelectedActionCheck" is used to select actions when the gui buttons
 * were used to set them, but still require a target (like the guard button)
 *
 * "hotkeyActionCheck" is used to check the possibility of actions when
 * a hotkey is pressed
 *
 * "actionCheck" is used to check the possibilty of actions without specific
 * command. For that, the specificness variable is used
 *
 * "specificness" is used to determine how specific an action is,
 * The lower the number, the more specific an action is, and the bigger
 * the chance of selecting that action when multiple actions are possible
 */
var g_UnitActions =
{
	"move":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "walk",
				"entities": selection,
				"x": position.x,
				"z": position.z,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getDefault()
			});

			DrawTargetMarker(position);

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_walk",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.unitAI)
				return false;
			return { "possible": true };
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.move") &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("move", target, selection);
			return actionInfo.possible && {
				"type": "move",
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 12,
	},

	"attack-move":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			let targetClasses;
			if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
				targetClasses = { "attack": ["Ranged", "Cavalry", "Infantry"] };
			else
				targetClasses = { "attack": ["Unit"] };

			Engine.PostNetworkCommand({
				"type": "attack-walk",
				"entities": selection,
				"x": position.x,
				"z": position.z,
				"targetClasses": targetClasses,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			DrawTargetMarker(position);

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_attack_move",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.unitAI)
				return false;
			return { "possible": true };
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return isAttackMovePressed() &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("attack-move", target, selection);
			return actionInfo.possible && {
				"type": "attack-move",
				"cursor": "action-attack-move",
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 30,
	},

	"capture":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "attack",
				"entities": selection,
				"target": action.target,
				"allowCapture": true,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_attack",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.attack || !targetState || !targetState.capturePoints)
				return false;

			return {
				"possible": Engine.GuiInterfaceCall("CanAttack", {
					"entity": entState.id,
					"target": targetState.id,
					"types": ["Capture"]
				})
			};
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.capture") &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("capture", target, selection);
			return actionInfo.possible && {
				"type": "capture",
				"cursor": "action-capture",
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 10,
	},

	"attack":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "attack",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront,
				"allowCapture": false,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_attack",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.attack || !targetState || !targetState.hitpoints)
				return false;

			return {
				"possible": Engine.GuiInterfaceCall("CanAttack", {
					"entity": entState.id,
					"target": targetState.id,
					"types": ["!Capture"]
				})
			};
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.attack") &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("attack", target, selection);
			return actionInfo.possible && {
				"type": "attack",
				"cursor": "action-attack",
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 9,
	},

	"call-to-arms": {
		"execute": function(position, action, selection, queued, pushFront)
		{
			let targetClasses;
			if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
				targetClasses = { "attack": ["Unit"] };
			else
				targetClasses = { "attack": ["Unit", "Structure"] };
			Engine.PostNetworkCommand({
				"type": "call-to-arms",
				"entities": selection,
				"position": position,
				"targetClasses": targetClasses,
				"queued": queued,
				"pushFront": pushFront,
				"allowCapture": Engine.HotkeyIsPressed("session.capture"),
				"formation": g_AutoFormation.getNull()
			});
			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			return { "possible": !!entState.unitAI };
		},
		"actionCheck": function(target, selection)
		{
			const actionInfo = getActionInfo("call-to-arms", target, selection);
			return actionInfo.possible && {
				"type": "call-to-arms",
				"cursor": "action-attack",
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.calltoarms") &&
				this.actionCheck(target, selection);
		},
		"preSelectedActionCheck": function(target, selection)
		{
			return preSelectedAction == ACTION_CALLTOARMS &&
				this.actionCheck(target, selection);
		},
		"specificness": 50,
	},

	"patrol":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "patrol",
				"entities": selection,
				"x": position.x,
				"z": position.z,
				"target": action.target,
				"targetClasses": { "attack": g_PatrolTargets },
				"queued": queued,
				"allowCapture": Engine.HotkeyIsPressed("session.capture"),
				"formation": g_AutoFormation.getDefault()
			});

			DrawTargetMarker(position);

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_patrol",
				"entity": action.firstAbleEntity
			});
			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.unitAI || !entState.unitAI.canPatrol)
				return false;

			return { "possible": true };
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.patrol") &&
				this.actionCheck(target, selection);
		},
		"preSelectedActionCheck": function(target, selection)
		{
			return preSelectedAction == ACTION_PATROL &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("patrol", target, selection);
			return actionInfo.possible && {
				"type": "patrol",
				"cursor": "action-patrol",
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 37,
	},

	"heal":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "heal",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_heal",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.heal || !targetState ||
			    !hasClass(targetState, "Unit") || !targetState.needsHeal ||
			    !playerCheck(entState, targetState, ["Player", "Ally"]) ||
			    entState.id == targetState.id) // Healers can't heal themselves.
				return false;

			let unhealableClasses = entState.heal.unhealableClasses;
			if (MatchesClassList(targetState.identity.classes, unhealableClasses))
				return false;

			let healableClasses = entState.heal.healableClasses;
			if (!MatchesClassList(targetState.identity.classes, healableClasses))
				return false;

			return { "possible": true };
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("heal", target, selection);
			return actionInfo.possible && {
				"type": "heal",
				"cursor": "action-heal",
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 7,
	},

	// "Fake" action to check if an entity can be ordered to "construct"
	// which is handled differently from repair as the target does not exist.
	"construct":
	{
		"preSelectedActionCheck": function(target, selection)
		{
			let state = GetEntityState(selection[0]);
			if (state && state.builder &&
			        target && target.constructor && target.constructor.name == "PlacementSupport")
				return { "type": "construct" };
			return false;
		},
		"specificness": 0,
	},

	"repair":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "repair",
				"entities": selection,
				"target": action.target,
				"autocontinue": true,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": action.foundation ? "order_build" : "order_repair",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.builder || !targetState ||
			    !targetState.needsRepair && !targetState.foundation ||
			    !playerCheck(entState, targetState, ["Player", "Ally"]))
				return false;

			return {
				"possible": true,
				"foundation": targetState.foundation
			};
		},
		"preSelectedActionCheck": function(target, selection)
		{
			return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || {
				"type": "none",
				"cursor": "action-repair-disabled",
				"target": null
			});
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.repair") &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("repair", target, selection);
			return actionInfo.possible && {
				"type": "repair",
				"cursor": "action-repair",
				"target": target,
				"foundation": actionInfo.foundation,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 11,
	},

	"gather":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "gather",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_gather",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.resourceGatherRates ||
				!targetState || !targetState.resourceSupply)
				return false;

			let resource;
			if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific])
				resource = targetState.resourceSupply.type.specific;
			else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic])
				resource = targetState.resourceSupply.type.generic;
			if (!resource)
				return false;

			return {
				"possible": true,
				"cursor": "action-gather-" + resource
			};
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("gather", target, selection);
			return actionInfo.possible && {
				"type": "gather",
				"cursor": actionInfo.cursor,
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 1,
	},

	"returnresource":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "returnresource",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_gather",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!targetState || !targetState.resourceDropsite)
				return false;

			let playerState = GetSimState().players[entState.player];
			if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
			{
				if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
					return false;
			}
			else if (!playerCheck(entState, targetState, ["Player"]))
				return false;

			if (!entState.resourceCarrying || !entState.resourceCarrying.length)
				return false;

			let carriedType = entState.resourceCarrying[0].type;
			if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
				return false;

			return {
				"possible": true,
				"cursor": "action-return-" + carriedType
			};
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("returnresource", target, selection);
			return actionInfo.possible && {
				"type": "returnresource",
				"cursor": actionInfo.cursor,
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 2,
	},

	"cancel-setup-trade-route":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "cancel-setup-trade-route",
				"entities": selection,
				"target": action.target,
				"queued": queued
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
			    playerCheck(entState, targetState, ["Enemy"]) ||
			    !(targetState.market.land && hasClass(entState, "Organic") ||
			      targetState.market.naval && hasClass(entState, "Ship")))
				return false;

			let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
				"trader": entState.id,
				"target": targetState.id
			});

			if (!tradingDetails || !tradingDetails.type)
				return false;

			if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets)
				return {
					"possible": true,
					"tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.")
				};
			return false;
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("cancel-setup-trade-route", target, selection);
			return actionInfo.possible && {
				"type": "cancel-setup-trade-route",
				"cursor": "action-cancel-setup-trade-route",
				"tooltip": actionInfo.tooltip,
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 2,
	},

	"setup-trade-route":
	{
		"execute": function(position, action, selection, queued)
		{
			Engine.PostNetworkCommand({
				"type": "setup-trade-route",
				"entities": selection,
				"target": action.target,
				"source": null,
				"route": null,
				"queued": queued,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_trade",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
			    playerCheck(entState, targetState, ["Enemy"]) ||
			    !(targetState.market.land && hasClass(entState, "Organic") ||
			      targetState.market.naval && hasClass(entState, "Ship")))
				return false;

			let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
				"trader": entState.id,
				"target": targetState.id
			});

			if (!tradingDetails)
				return false;

			let tooltip;
			switch (tradingDetails.type)
			{
			case "is first":
				tooltip = translate("Origin trade market.") + "\n";
				if (tradingDetails.hasBothMarkets)
					tooltip += sprintf(translate("Gain: %(gain)s"), {
						"gain": getTradingTooltip(tradingDetails.gain)
					});
				else
					return false;
				break;

			case "is second":
				tooltip = translate("Destination trade market.") + "\n" +
					sprintf(translate("Gain: %(gain)s"), {
						"gain": getTradingTooltip(tradingDetails.gain)
					});
				break;

			case "set first":
				tooltip = translate("Right-click to set as origin trade market");
				break;

			case "set second":
				if (tradingDetails.gain.traderGain == 0)
					return {
						"possible": true,
						"tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags),
						"disabled": true
					};

				tooltip = translate("Right-click to set as destination trade market.") + "\n" +
					sprintf(translate("Gain: %(gain)s"), {
						"gain": getTradingTooltip(tradingDetails.gain)
					});
				break;
			}

			return {
				"possible": true,
				"tooltip": tooltip
			};
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("setup-trade-route", target, selection);
			if (actionInfo.disabled)
				return {
					"type": "none",
					"cursor": "action-setup-trade-route-disabled",
					"target": null,
					"tooltip": actionInfo.tooltip
				};

			return actionInfo.possible && {
				"type": "setup-trade-route",
				"cursor": "action-setup-trade-route",
				"tooltip": actionInfo.tooltip,
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 0,
	},

	"occupy-turret":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "occupy-turret",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_garrison",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.turretable || !targetState || !targetState.turretHolder ||
			    !playerCheck(entState, targetState, ["Player", "MutualAlly"]))
				return false;

			if (!targetState.turretHolder.turretPoints.find(point =>
				!point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
				return false;

			let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
			let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
				"occupied": occupiedTurrets.length,
				"capacity": targetState.turretHolder.turretPoints.length
			});

			if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length)
				tooltip = coloredText(tooltip, "orange");

			return {
				"possible": true,
				"tooltip": tooltip
			};
		},
		"preSelectedActionCheck": function(target, selection)
		{
			return preSelectedAction == ACTION_OCCUPY_TURRET && (this.actionCheck(target, selection) || {
				"type": "none",
				"cursor": "action-occupy-turret-disabled",
				"target": null
			});
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.occupyturret") &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("occupy-turret", target, selection);
			return actionInfo.possible && {
				"type": "occupy-turret",
				"cursor": "action-occupy-turret",
				"tooltip": actionInfo.tooltip,
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 21,
	},

	"garrison":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "garrison",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_garrison",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.garrisonable || !targetState || !targetState.garrisonHolder ||
			    !playerCheck(entState, targetState, ["Player", "MutualAlly"]))
				return false;

			let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
				"garrisoned": targetState.garrisonHolder.occupiedSlots,
				"capacity": targetState.garrisonHolder.capacity
			});

			let extraCount = entState.garrisonable.size;
			if (entState.garrisonHolder)
				extraCount += entState.garrisonHolder.occupiedSlots;

			if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity)
				tooltip = coloredText(tooltip, "orange");

			if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
				return false;

			return {
				"possible": true,
				"tooltip": tooltip
			};
		},
		"preSelectedActionCheck": function(target, selection)
		{
			return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || {
				"type": "none",
				"cursor": "action-garrison-disabled",
				"target": null
			});
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.garrison") &&
				this.actionCheck(target, selection);

		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("garrison", target, selection);
			return actionInfo.possible && {
				"type": "garrison",
				"cursor": "action-garrison",
				"tooltip": actionInfo.tooltip,
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 20,
	},

	"guard":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "guard",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_guard",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!targetState || !targetState.guard || entState.id == targetState.id ||
			    !playerCheck(entState, targetState, ["Player", "Ally"]) ||
			    !entState.unitAI || !entState.unitAI.canGuard)
				return false;

			return { "possible": true };
		},
		"preSelectedActionCheck": function(target, selection)
		{
			return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || {
				"type": "none",
				"cursor": "action-guard-disabled",
				"target": null
			});
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.guard") &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("guard", target, selection);
			return actionInfo.possible && {
				"type": "guard",
				"cursor": "action-guard",
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 40,
	},

	"collect-treasure":
	{
		"execute": function(position, action, selection, queued)
		{
			Engine.PostNetworkCommand({
				"type": "collect-treasure",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"formation": g_AutoFormation.getNull()
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_collect_treasure",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.treasureCollector ||
				!targetState || !targetState.treasure)
				return false;

			return {
				"possible": true,
				"cursor": "action-collect-treasure"
			};
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("collect-treasure", target, selection);
			return actionInfo.possible && {
				"type": "collect-treasure",
				"cursor": actionInfo.cursor,
				"target": target,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 1,
	},

	"remove-guard":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "remove-guard",
				"entities": selection,
				"target": action.target,
				"queued": queued,
				"pushFront": pushFront
			});

			Engine.GuiInterfaceCall("PlaySound", {
				"name": "order_guard",
				"entity": action.firstAbleEntity
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.unitAI || !entState.unitAI.isGuarding)
				return false;
			return { "possible": true };
		},
		"hotkeyActionCheck": function(target, selection)
		{
			return Engine.HotkeyIsPressed("session.guard") &&
				this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("remove-guard", target, selection);
			return actionInfo.possible && {
				"type": "remove-guard",
				"cursor": "action-remove-guard",
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 41,
	},

	"focus-fire":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "focus-fire",
				"entities": selection,
				"x": position.x,
				"z": position.z,
				"target": action.target,
				"data": action.data,
				"queued": queued,
				"pushFront": pushFront
			});

			if (action.data.sound)
				Engine.GuiInterfaceCall("PlaySound", {
					"name": "focus_fire",
					"entity": action.firstAbleEntity
				});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.rallyPoint)
				return false;

			let tooltip;
			let data = { "command": "walk" };
			let cursor = "";
			data.sound = false;

			if (entState.attack && Engine.HotkeyIsPressed("session.focusfire"))
			{
				cursor = "action-target";
				data.command = "attack-only";
				if (targetState && playerCheck(entState, targetState, ["Enemy"]) && !targetState.resourceSupply)
				{
					data.target = targetState.id;
					data.sound = true;
				}
				return {
					"possible": true,
					"data": data,
					"position": targetState && targetState.position,
					"cursor": cursor,
					"tooltip": tooltip
				};
			}
		},
		"hotkeyActionCheck": function(target, selection)
		{
			// Hotkeys are checked in the actionInfo.
			return this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			// We want commands to units take precedence.
			if (selection.some(ent => {
				let entState = GetEntityState(ent);
				return entState && !!entState.unitAI;
			}))
				return false;

			let actionInfo = getActionInfo("focus-fire", target, selection);
			
			return actionInfo.possible && {
				"type": "focus-fire",
				"cursor": actionInfo.cursor,
				"data": actionInfo.data,
				"target": target,
				"tooltip": actionInfo.tooltip,
				"position": actionInfo.position,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 6,
	},

	"set-rallypoint":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			// if there is a position set in the action then use this so that when setting a
			// rally point on an entity it is centered on that entity
			if (action.position)
				position = action.position;

			Engine.PostNetworkCommand({
				"type": "set-rallypoint",
				"entities": selection,
				"x": position.x,
				"z": position.z,
				"data": action.data,
				"queued": queued
			});

			// Display rally point at the new coordinates, to avoid display lag
			Engine.GuiInterfaceCall("DisplayRallyPoint", {
				"entities": selection,
				"x": position.x,
				"z": position.z,
				"queued": queued
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!entState.rallyPoint)
				return false;

			// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
			// except if the autorallypoint hotkey is pressed and the target can produce entities.
			if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") ||
			    !targetState.trainer ||
			    !targetState.trainer.entities.length))
				for (const ent of g_Selection.toList())
					if (targetState.id == ent)
						return false;

			let tooltip;
			let disabled = false;
			// default to walking there (or attack-walking if hotkey pressed)
			let data = { "command": "walk" };
			let cursor = "";

			if (isAttackMovePressed())
			{
				let targetClasses;
				if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
					targetClasses = { "attack": ["Unit"] };
				else
					targetClasses = { "attack": ["Unit", "Structure"] };

				data.command = "attack-walk";
				data.targetClasses = targetClasses;
				cursor = "action-attack-move";
			}

			if (Engine.HotkeyIsPressed("session.repair") && targetState &&
			    (targetState.needsRepair || targetState.foundation) &&
			    playerCheck(entState, targetState, ["Player", "Ally"]))
			{
				data.command = "repair";
				data.target = targetState.id;
				cursor = "action-repair";
			}
			else if (targetState && targetState.garrisonHolder &&
			    playerCheck(entState, targetState, ["Player", "MutualAlly"]))
			{
				data.command = "garrison";
				data.target = targetState.id;
				cursor = "action-garrison";

				tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
					"garrisoned": targetState.garrisonHolder.occupiedSlots,
					"capacity": targetState.garrisonHolder.capacity
				});

				if (targetState.garrisonHolder.occupiedSlots >=
				    targetState.garrisonHolder.capacity)
					tooltip = coloredText(tooltip, "orange");
			}
			else if (targetState && targetState.turretHolder &&
			    playerCheck(entState, targetState, ["Player", "MutualAlly"]))
			{
				data.command = "occupy-turret";
				data.target = targetState.id;
				cursor = "action-garrison";

				let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
				tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
					"occupied": occupiedTurrets.length,
					"capacity": targetState.turretHolder.turretPoints.length
				});

				if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length)
					tooltip = coloredText(tooltip, "orange");
			}
			else if (targetState && targetState.resourceSupply)
			{
				let resourceType = targetState.resourceSupply.type;
				cursor = "action-gather-" + resourceType.specific;

				data.command = "gather-near-position";
				data.resourceType = resourceType;
				data.resourceTemplate = targetState.template;
				if (!targetState.speed)
				{
					data.command = "gather";
					data.target = targetState.id;
				}
			}
			else if (targetState && targetState.treasure)
			{
				cursor = "action-collect-treasure";
				data.command = "collect-treasure-near-position";
				if (!targetState.speed)
				{
					data.command = "collect-treasure";
					data.target = targetState.id;
				}
			}
			else if (entState.market && targetState && targetState.market &&
			         entState.id != targetState.id &&
			         (!entState.market.naval || targetState.market.naval) &&
			         !playerCheck(entState, targetState, ["Enemy"]))
			{
				// Find a trader (if any) that this structure can train.
				let trader;
				if (entState.trainer?.entities?.length)
					for (let i = 0; i < entState.trainer.entities.length; ++i)
						if ((trader = GetTemplateData(entState.trainer.entities[i]).trader))
							break;

				let traderData = {
					"firstMarket": entState.id,
					"secondMarket": targetState.id,
					"template": trader
				};

				let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
				if (gain)
				{
					data.command = "trade";
					data.target = traderData.secondMarket;
					data.source = traderData.firstMarket;
					cursor = "action-setup-trade-route";

					if (gain.traderGain)
						tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
							sprintf(
								trader ?
									translate("Gain: %(gain)s") :
									translate("Expected gain: %(gain)s"),
								{ "gain": getTradingTooltip(gain) });
					else
					{
						disabled = true;
						tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags);
						cursor = "action-setup-trade-route-disabled";
					}
				}
			}
			else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
			{
				data.command = "repair";
				data.target = targetState.id;
				cursor = "action-repair";
			}
			else if (targetState && playerCheck(entState, targetState, ["Enemy"]))
			{
				data.target = targetState.id;
				data.command = "attack";
				if (targetState.hitpoints)
					cursor = "action-attack";
				else if (targetState.capturePoints)
				{
					cursor = "action-capture";
					data.allowCapture = true;
				}
				else
					return false;
			}

			return {
				"possible": true,
				"data": data,
				"position": targetState && targetState.position,
				"cursor": cursor,
				"disabled": disabled,
				"tooltip": tooltip
			};

		},
		"hotkeyActionCheck": function(target, selection)
		{
			// Hotkeys are checked in the actionInfo.
			return this.actionCheck(target, selection);
		},
		"actionCheck": function(target, selection)
		{
			// We want commands to units take precedence.
			if (selection.some(ent => {
				let entState = GetEntityState(ent);
				return entState && !!entState.unitAI;
			}))
				return false;

			let actionInfo = getActionInfo("set-rallypoint", target, selection);
			if (actionInfo.disabled)
				return {
					"type": "none",
					"cursor": actionInfo.cursor,
					"target": null,
					"tooltip": actionInfo.tooltip
				};

			return actionInfo.possible && {
				"type": "set-rallypoint",
				"cursor": actionInfo.cursor,
				"data": actionInfo.data,
				"tooltip": actionInfo.tooltip,
				"position": actionInfo.position,
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 6,
	},

	"unset-rallypoint":
	{
		"execute": function(position, action, selection, queued, pushFront)
		{
			Engine.PostNetworkCommand({
				"type": "unset-rallypoint",
				"entities": selection
			});

			// Remove displayed rally point
			Engine.GuiInterfaceCall("DisplayRallyPoint", {
				"entities": []
			});

			return true;
		},
		"getActionInfo": function(entState, targetState)
		{
			if (!targetState ||
				entState.id != targetState.id || entState.unitAI ||
				!entState.rallyPoint || !entState.rallyPoint.position)
				return false;

			return { "possible": true };
		},
		"actionCheck": function(target, selection)
		{
			let actionInfo = getActionInfo("unset-rallypoint", target, selection);
			return actionInfo.possible && {
				"type": "unset-rallypoint",
				"cursor": "action-unset-rally",
				"firstAbleEntity": actionInfo.entity
			};
		},
		"specificness": 11,
	},

	// This is a "fake" action to show a failure cursor
	// when only uncontrollable entities are selected.
	"uncontrollable":
	{
		"execute": function(position, action, selection, queued)
		{
			return true;
		},
		"actionCheck": function(target, selection)
		{
			// Only show this action if all entities are marked uncontrollable.
			let playerState = g_SimState.players[g_ViewedPlayer];
			if (playerState && playerState.controlsAll || selection.some(ent => {
				let entState = GetEntityState(ent);
				return entState && entState.identity && entState.identity.controllable;
			}))
				return false;

			return {
				"type": "none",
				"cursor": "cursor-no",
				"tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length)
			};
		},
		"specificness": 100,
	},

	"none":
	{
		"execute": function(position, action, selection, queued)
		{
			return true;
		},
		"specificness": 100,
	},
};

var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness);

/**
 * Info and actions for the entity commands
 * Currently displayed in the bottom of the central panel
 */
var g_EntityCommands =
{
	"unload-all": {
		"getInfo": function(entStates)
		{
			let count = 0;
			for (let entState of entStates)
			{
				if (!entState.garrisonHolder)
					continue;

				if (allowedPlayersCheck([entState], ["Player"]))
					count += entState.garrisonHolder.entities.length;
				else
					for (let entity of entState.garrisonHolder.entities)
						if (allowedPlayersCheck([GetEntityState(entity)], ["Player"]))
							++count;
			}

			if (!count)
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
					translate("Unload All") + "\n" +
					bodyFont(translate("Order all units to leave the selected entities.")),
				"icon": "garrison-out.png",
				"count": count,
				"enabled": true
			};
		},
		"execute": function()
		{
			unloadAll();
		},
		"allowedPlayers": ["Player", "Ally"]
	},

	"unload-all-turrets": {
		"getInfo": function(entStates)
		{
			let count = 0;
			for (let entState of entStates)
			{
				if (!entState.turretHolder)
					continue;

				if (allowedPlayersCheck([entState], ["Player"]))
					count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity && turretPoint.ejectable).length;
				else
					for (let turretPoint of entState.turretHolder.turretPoints)
						if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"]))
							++count;
			}

			if (!count)
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") +
					translate("Unload Turrets") + "\n" +
					bodyFont(translate("Order all units to leave the selected turret points.")),
				"icon": "garrison-out.png",
				"count": count,
				"enabled": true
			};
		},
		"execute": function()
		{
			unloadAllTurrets();
		},
		"allowedPlayers": ["Player", "Ally"]
	},

	"delete": {
		"getInfo": function(entStates)
		{
			return entStates.some(entState => !isUndeletable(entState)) ?
				{
					"tooltip":
						colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
						translate("Self-Destruct") + "\n" +
						bodyFont(translate("Destroy the selected entities.") + "\n" +
							colorizeHotkey(
								translate("Use %(hotkey)s to avoid the confirmation dialog."),
								"session.noconfirmation"
							)
						),
					"icon": "kill_small.png",
					"enabled": true
				} :
				{
					// Get all delete reasons and remove duplications
					"tooltip": entStates.map(entState => isUndeletable(entState))
						.filter((reason, pos, self) =>
							self.indexOf(reason) == pos && reason
						).join("\n"),
					"icon": "kill_small_disabled.png",
					"enabled": false
				};
		},
		"execute": function(entStates)
		{
			let entityIDs = entStates.reduce(
				(ids, entState) => {
					if (!isUndeletable(entState))
						ids.push(entState.id);
					return ids;
				},
				[]);

			if (!entityIDs.length)
				return;

			let deleteSelection = () => Engine.PostNetworkCommand({
				"type": "delete-entities",
				"entities": entityIDs
			});

			if (Engine.HotkeyIsPressed("session.noconfirmation"))
				deleteSelection();
			else
				(new DeleteSelectionConfirmation(deleteSelection)).display();
		},
		"allowedPlayers": ["Player"]
	},

	"stop": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.unitAI))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
					translate("Abort") + "\n" +
					bodyFont(translate("Cancel the current orders for the selected units.")),
				"icon": "stop.png",
				"enabled": true
			};
		},
		"execute": function(entStates)
		{
			if (entStates.length)
				stopUnits(entStates.map(entState => entState.id));
		},
		"allowedPlayers": ["Player"]
	},

	"call-to-arms": {
		"getInfo": function(entStates)
		{
			const classes = ["Soldier", "Warship", "Siege", "Healer"];
			if (entStates.every(entState => !MatchesClassList(entState.identity.classes, classes)))
				return false;
			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.calltoarms") +
						translate("Attack") + "\n" +
						bodyFont(translate("Send the selected units on attack move to the specified location after dropping resources.")),
				"icon": "call-to-arms.png",
				"enabled": true
			};
		},
		"execute": function(entStates)
		{
			inputState = INPUT_PRESELECTEDACTION;
			preSelectedAction = ACTION_CALLTOARMS;
		},
		"allowedPlayers": ["Player"]
	},

	"garrison": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.garrisonable ||
				entState.garrisonable.holder != INVALID_ENTITY))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
					translate("Garrison") + "\n" +
					bodyFont(translate("Order the selected units to garrison in a structure or another unit.")),
				"icon": "garrison.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			inputState = INPUT_PRESELECTEDACTION;
			preSelectedAction = ACTION_GARRISON;
		},
		"allowedPlayers": ["Player"]
	},

	"occupy-turret": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.turretable ||
				entState.turretable.holder != INVALID_ENTITY))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.occupyturret") +
					translate("Occupy Turret") + "\n" +
					bodyFont(translate("Order the selected units to occupy a turret point.")),
				"icon": "occupy-turret.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			inputState = INPUT_PRESELECTEDACTION;
			preSelectedAction = ACTION_OCCUPY_TURRET;
		},
		"allowedPlayers": ["Player"]
	},

	"leave-turret": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.turretable ||
				entState.turretable.holder == INVALID_ENTITY ||
				!entState.turretable.ejectable))
				return false;

			return {
				"tooltip": translate("Unload") + "\n" +
					bodyFont(translate("Order the selected units to leave their turret points.")),
				"icon": "leave-turret.png",
				"enabled": true
			};
		},
		"execute": function(entStates)
		{
			leaveTurretPoints();
		},
		"allowedPlayers": ["Player"]
	},

	"repair": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.builder))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
					translate("Repair") + "\n" +
					bodyFont(translate("Order the selected units to repair a structure, ship, or siege engine.")),
				"icon": "repair.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			inputState = INPUT_PRESELECTEDACTION;
			preSelectedAction = ACTION_REPAIR;
		},
		"allowedPlayers": ["Player"]
	},

	"focus-rally": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.rallyPoint))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
					translate("Focus on Rally Point") + "\n" +
					bodyFont(translate("Center the view on the selected rally point.")),
				"icon": "focus-rally.png",
				"enabled": true
			};
		},
		"execute": function(entStates)
		{
			// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
			let focusTarget;
			for (let entState of entStates)
				if (entState.rallyPoint && entState.rallyPoint.position)
				{
					focusTarget = entState.rallyPoint.position;
					break;
				}
			if (!focusTarget)
				for (let entState of entStates)
					if (entState.position)
					{
						focusTarget = entState.position;
						break;
					}

			if (focusTarget)
				Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
		},
		"allowedPlayers": ["Player", "Observer"]
	},

	"back-to-work": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
					translate("Back to Work") + "\n" +
					bodyFont(translate("Order the selected units to resume their work.")),
				"icon": "back-to-work.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			backToWork();
		},
		"allowedPlayers": ["Player"]
	},

	"add-guard": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState =>
				!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
					translate("Guard") + "\n" +
					bodyFont(translate("Order the selected units to guard a structure or unit.")),
				"icon": "add-guard.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			inputState = INPUT_PRESELECTEDACTION;
			preSelectedAction = ACTION_GUARD;
		},
		"allowedPlayers": ["Player"]
	},

	"remove-guard": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
				return false;

			return {
				"tooltip": translate("Abort Guard") + "\n" +
					bodyFont(translate("Order the selected units to stop guarding.")),
				"icon": "remove-guard.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			removeGuard();
		},
		"allowedPlayers": ["Player"]
	},

	"select-trading-goods": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.market))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.gui.barter.toggle") +
					translate("Barter & Trade") + "\n" +
					bodyFont(translate("Open the dialog for managing resource trading and bartering.")),
				"icon": "economics.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			g_TradeDialog.toggle();
		},
		"allowedPlayers": ["Player"]
	},

	"patrol": {
		"getInfo": function(entStates)
		{
			if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
				return false;

			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
					translate("Patrol") + "\n" +
					bodyFont(translate("Order to repeatedly go to a point and come back, attacking all enemies along the way.")),
				"icon": "patrol.png",
				"enabled": true
			};
		},
		"execute": function()
		{
			inputState = INPUT_PRESELECTEDACTION;
			preSelectedAction = ACTION_PATROL;
		},
		"allowedPlayers": ["Player"]
	},

	"share-dropsite": {
		"getInfo": function(entStates)
		{
			let sharableEntities = entStates.filter(
				entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
			if (!sharableEntities.length)
				return false;

			// Returns if none of the entities belong to a player with a mutual ally.
			if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
				(isAlly, playerId) => isAlly && playerId != entState.player)))
				return false;

			return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
				{
					"tooltip": translate("Share Dropsite") + "\n" +
						bodyFont(translate("Allow allies to use this dropsite, now locked.")),
					"icon": "locked_small.png",
					"enabled": true
				} :
				{
					"tooltip": translate("Lock Dropsite") + "\n" +
						bodyFont(translate("Prevent allies from using this dropsite, now shared.")),
					"icon": "unlocked_small.png",
					"enabled": true
				};
		},
		"execute": function(entStates)
		{
			let sharableEntities = entStates.filter(
				entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
			if (sharableEntities)
				Engine.PostNetworkCommand({
					"type": "set-dropsite-sharing",
					"entities": sharableEntities.map(entState => entState.id),
					"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
				});
		},
		"allowedPlayers": ["Player"]
	},

	"is-dropsite-shared": {
		"getInfo": function(entStates)
		{
			let shareableEntities = entStates.filter(
				entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
			if (!shareableEntities.length)
				return false;

			let player = Engine.GetPlayerID();
			let simState = GetSimState();
			if (!g_IsObserver && !simState.players[player].hasSharedDropsites ||
				shareableEntities.every(entState => controlsPlayer(entState.player)))
				return false;

			if (!shareableEntities.every(entState => entState.resourceDropsite.shared))
				return {
					"tooltip": translate("Locked Dropsite") + "\n" +
						bodyFont(translate("The use of this dropsite is prohibited.")),
					"icon": "locked_small.png",
					"enabled": false
				};

			return {
				"tooltip": translate("Shared Dropsite") + "\n" +
					bodyFont(g_IsObserver ? translate("Allies are allowed to use this dropsite.") :
						translate("You are allowed to use this dropsite.")),
				"icon": "unlocked_small.png",
				"enabled": false
			};
		},
		"execute": function(entState)
		{
			// This command button is always disabled.
		},
		"allowedPlayers": ["Ally", "Observer"]
	},

	"autoqueue-on": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.trainer?.entities?.length || !entState.production || entState.production.autoqueue))
				return false;
			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") +
					translate("Activate Auto-Queue") + "\n" +
					bodyFont(translate("Activate the production auto-queue for the selected structures.")),
				"icon": "autoqueue-on.png",
				"enabled": true
			};
		},
		"execute": function(entStates)
		{
			if (entStates.length)
				turnAutoQueueOn();
		},
		"allowedPlayers": ["Player"]
	},

	"autoqueue-off": {
		"getInfo": function(entStates)
		{
			if (entStates.every(entState => !entState.production?.autoqueue))
				return false;
			return {
				"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") +
					translate("Deactivate Auto-Queue") + "\n" +
					bodyFont(translate("Deactivate the production auto-queue for the selected structures.")),
				"icon": "autoqueue-off.png",
				"enabled": true
			};
		},
		"execute": function(entStates)
		{
			if (entStates.length)
				turnAutoQueueOff();
		},
		"allowedPlayers": ["Player"]
	},
};

function playerCheck(entState, targetState, validPlayers)
{
	let playerState = GetSimState().players[entState.player];
	for (let player of validPlayers)
		if (player == "Gaia" && targetState.player == 0 ||
		    player == "Player" && targetState.player == entState.player ||
		    playerState["is" + player] && playerState["is" + player][targetState.player])
			return true;

	return false;
}

/**
 * Checks whether the entities have the right diplomatic status
 * with respect to the currently active player.
 * Also "Observer" can be used.
 *
 * @param {Object[]} entStates - An array containing the entity states to check.
 * @param {string[]} validPlayers - An array containing the diplomatic statuses.
 *
 * @return {boolean} - Whether the currently active player is allowed.
 */
function allowedPlayersCheck(entStates, validPlayers)
{
	// Assume we can only select entities from one player,
	// or it does not matter (e.g. observer).
	let targetState = entStates[0];
	let playerState = GetSimState().players[Engine.GetPlayerID()];

	return validPlayers.some(player =>
		player == "Observer" && g_IsObserver ||
		player == "Player" && controlsPlayer(targetState.player) ||
		playerState && playerState["is" + player] && playerState["is" + player][targetState.player]);
}

function hasClass(entState, className)
{
	// note: use the functions in globalscripts/Templates.js for more versatile matching
	return entState.identity && entState.identity.classes.indexOf(className) != -1;
}

/**
 * Keep in sync with Commands.js.
 */
function isUndeletable(entState)
{
	let playerState = g_SimState.players[entState.player];
	if (playerState && playerState.controlsAll)
		return false;

	if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
		return translate("The entity has to be killed before it can be gathered from");

	if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
		return translate("You cannot destroy this entity as you own less than half the capture points");

	if (!entState.identity.canDelete)
		return translate("This entity is undeletable");

	return false;
}

function DrawTargetMarker(position)
{
	Engine.GuiInterfaceCall("AddTargetMarker", {
		"template": g_TargetMarker.move,
		"x": position.x,
		"z": position.z
	});
}

function renderAndPlayFlare(position, playerGUID)
{
	const playerID = g_PlayerAssignments[playerGUID].player;
	Engine.GuiInterfaceCall("AddTargetMarker", {
		"template": g_TargetMarker.map_flare,
		"x": position.x,
		"z": position.z,
		// Set the owner to gaia if the flare was sent by an observer (to make the target marker white).
		"owner": playerID != -1 ? playerID : 0
	});
	g_MiniMapPanel.flare(position, playerID);
	Engine.PlayUISound(g_FlareSound, false);
	addChatMessage({
		"type": "flare",
		"guid": playerGUID,
		"position": position
	});
}

function getCommandInfo(command, entStates)
{
	return entStates && g_EntityCommands[command] &&
		allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) &&
		g_EntityCommands[command].getInfo(entStates);
}

function getActionInfo(action, target, selection)
{
	if (!selection || !selection.length || !GetEntityState(selection[0]))
		return { "possible": false };

	// Look at the first targeted entity
	// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
	// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
	let targetState = GetEntityState(target);

	let simState = GetSimState();
	let playerState = g_SimState.players[g_ViewedPlayer];

	// Check if any entities in the selection can do some of the available actions.
	for (let entityID of selection)
	{
		let entState = GetEntityState(entityID);
		if (!entState)
			continue;

		if (playerState && !playerState.controlsAll && !entState.identity.controllable)
			continue;

		if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
		{
			let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
			if (r && r.possible)
			{
				r.entity = entityID;
				return r;
			}
		}
	}
	return { "possible": false };
}
