Two new commands actually:
1) Force Land (anywhere)
2) Land at a specific base
Code: Select all
function gadget:GetInfo()
	return {
		name      = "Airbase Repair Command",
		desc      = "Add command to return to airbase for repairs",
		author    = "ashdnazg",
		date      = "12 February 2016",
		license   = "GNU GPL, v2 or later",
		layer     = 1,
		enabled   = true  --  loaded by default?
	}
end
---------------------------------------------------------------------------------
local FORCE_LAND_CMD_ID = 35430
local LAND_CMD_ID = 35431
if (gadgetHandler:IsSyncedCode()) then
--------------------------------------------------------------------------------
-- Synced
--------------------------------------------------------------------------------
local airbaseDefIDs = {}
local planes = {}
local airbases = {} -- airbaseID = { int pieceNum = unitID reservedFor }
local pendingLanders = {}
local landingPlanes = {}
local landedPlanes = {}
local forceLandCmd = {
	id      = FORCE_LAND_CMD_ID,
	name    = "ForceLand",
	action  = "forceland",
	type    = CMDTYPE.ICON,
	tooltip = "Return to base: Force the plane to return to base immediately",
}
local landCmd = {
	id      = LAND_CMD_ID,
	name    = "Land",
	action  = "land",
	cursor  = 'Repair',
	type    = CMDTYPE.ICON_UNIT,
	tooltip = "Land at a specific airbase",
	hidden  = true,
}
-- add logic for deciding which pad to use here
function AddAirBase(unitID)
	local airBasePads = {}
	local pieceMap = Spring.GetUnitPieceMap(unitID)
	for pieceName, pieceNum in pairs(pieceMap) do
		if pieceName:find("pad") then
			airBasePads[pieceNum] = false
		end
	end
	airbases[unitID] = airBasePads
end
-- returns either false or the piece number of the free pad
function CanLandAt(unitID, airbaseID)
	local airbasePads = airbases[airbaseID]
	if not airbasePads then
		return false
	end
	local unitTeamID = Spring.GetUnitTeam(unitID)
	local airbaseTeamID = Spring.GetUnitTeam(airbaseID)
	if not Spring.AreTeamsAllied(unitTeamID, airbaseTeamID) then
		return false
	end
	local padPieceNum = false
	for pieceNum, reservedBy in pairs(airbasePads) do
		-- if somehow no pad expects you, find a vacant one
		if not reservedBy then
			padPieceNum = pieceNum
		end
		if reservedBy == unitID then
			padPieceNum = pieceNum
			break
		end
	end
	return padPieceNum
end
function FindAirBase(unitID)
	local minDist = math.huge
	local closestAirbaseID
	local closestPieceNum
	for airbaseID, _ in pairs(airbases) do
		local pieceNum = CanLandAt(unitID, airbaseID)
		if pieceNum then
			local dist = Spring.GetUnitSeparation(unitID, airbaseID)
			if dist < minDist then
				minDist = dist
				closestAirbaseID = airbaseID
				closestPieceNum = pieceNum
			end
		end
	end
	return closestAirbaseID, closestPieceNum
end
function RemoveLander(unitID)
	if landingPlanes[unitID] then
		local airbaseID, pieceNum = landingPlanes[unitID][1], landingPlanes[unitID][2]
		local airbasePads = airbases[airBaseID]
		if airbasePads then
			airbasePads[pieceNum] = false
		end
		landingPlanes[unitID] = nil
		return
	end
end
function NeedsRepair(unitID)
	local health, maxHealth = Spring.GetUnitHealth(unitID)
	local landAtState = Spring.GetUnitStates(unitID).autorepairlevel
	return health < maxHealth * landAtState;
end
function gadget:UnitCreated(unitID, unitDefID, unitTeam, builderID)
	if UnitDefs[unitDefID].canFly then
		Spring.InsertUnitCmdDesc(unitID, landCmd)
		Spring.InsertUnitCmdDesc(unitID, forceLandCmd)
	end
	local _, _, _, _, buildProgress = Spring.GetUnitHealth(unitID)
	if buildProgress == 1.0 then
		gadget:UnitFinished(unitID, unitDefID, unitTeam)
	end
end
function gadget:UnitDestroyed(unitID, unitDefID, unitTeam)
	RemoveLander(unitID)
	planes[unitID] = nil
	airbases[unitID] = nil
	landingPlanes[unitID] = nil
	landedPlanes[unitID] = nil
	pendingLanders[unitID] = nil
end
function gadget:UnitFinished(unitID, unitDefID, unitTeam)
	if airbaseDefIDs[unitDefID] then
		AddAirBase(unitID)
	end
	if UnitDefs[unitDefID].canFly then
		planes[unitID] = true
	end
end
function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
	if cmdID == LAND_CMD_ID then
		local airbaseID = cmdParams[1]
		return CanLandAt(unitID, airbaseID)
	end
	return true
end
function gadget:CommandFallback(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
	if cmdID == LAND_CMD_ID then
		-- clear old pad
		RemoveLander(unitID)
		-- stop if we've landed
		if landedPlanes[unitID] then
			return true, true
		end
		local airbaseID = cmdParams[1]
		local padPieceNum = CanLandAt(unitID, airbaseID)
		-- failed to land
		if not padPieceNum then
			return true, true
		end
		-- update new pad
		airbases[airbaseID][padPieceNum] = unitID
		landingPlanes[unitID] = {airbaseID, padPieceNum}
		return true, false
	end
	if cmdID == FORCE_LAND_CMD_ID then
		pendingLanders[unitID] = true
		return true, true
	end
	return false
end
function gadget:UnitCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
	if not planes[unitID] then
		return
	end
	landedPlanes[unitID] = nil
	RemoveLander(unitID)
	local airBaseID = Spring.GetUnitTransporter(unitID)
	if not airBaseID then
		return
	end
	local airbasePads = airbases[airBaseID]
	if not airbasePads then
		return
	end
	Spring.UnitDetach(unitID)
	for pieceNum, reservedBy in pairs(airbasePads) do
		if reservedBy == unitID then
			airbasePads[pieceNum] = false
		end
	end
end
function gadget:GameFrame()
	for unitID, _ in pairs(planes) do
		if not landingPlanes[unitID] and not landedPlanes[unitID] and NeedsRepair(unitID) then
			pendingLanders[unitID] = true
		end
	end
	for unitID, t in pairs(landingPlanes) do
		local airbaseID, padPieceNum = t[1], t[2]
		local px, py, pz = Spring.GetUnitPiecePosDir(airbaseID, padPieceNum)
		local ux, uy, uz = Spring.GetUnitPosition(unitID)
		local dx, dy ,dz = ux - px, uy - py, uz - pz
		local r = Spring.GetUnitRadius(unitID)
		local dist = dx * dx + dy * dy + dz * dz
		-- check if we're close enough
		if dist < 4 * r * r then
			Spring.GiveOrderToUnit(unitID, CMD.STOP,{},{})
			Spring.UnitAttach(airbaseID, unitID, padPieceNum)
			landingPlanes[unitID] = nil
			landedPlanes[unitID] = true
		else
			Spring.SetUnitLandGoal(unitID, px, py, pz, r)
		end
	end
	for unitID, _ in pairs(pendingLanders) do
		local closestAirbaseID, closestPieceNum = FindAirBase(unitID)
		if closestAirbaseID then
			Spring.GiveOrderToUnit(unitID, CMD.INSERT,{0, LAND_CMD_ID, 0, closestAirbaseID},{"alt"})
			landingPlanes[unitID] = {closestAirbaseID, closestPieceNum}
			pendingLanders[unitID] = nil
		end
	end
end
function gadget:Initialize()
	for unitDefID, unitDef in pairs(UnitDefs) do
		if unitDef.isAirBase then
			airbaseDefIDs[unitDefID] = true
		end
	end
	-- Fake UnitCreated events for existing units. (for '/luarules reload')
	local allUnits = Spring.GetAllUnits()
	for i=1,#allUnits do
		local unitID = allUnits[i]
		gadget:UnitCreated(unitID, Spring.GetUnitDefID(unitID))
	end
end
function gadget:UnitLoaded(unitID, unitDefID, unitTeam, transportID, transportTeam)
    if (planes[unitID]) then
        Spring.SetUnitNoDraw(unitID, false)
        Spring.SetUnitStealth(unitID, false)
        Spring.SetUnitSonarStealth(unitID, false)
    end
end
--------------------------------------------------------------------------------
-- Unsynced
--------------------------------------------------------------------------------
else
function gadget:Initialize()
	Spring.AssignMouseCursor("Land for repairs", "cursorrepair", false, false)
	Spring.SetCustomCommandDrawData(LAND_CMD_ID, "Land for repairs", {1,0.5,0,.8}, false)
end
function gadget:DefaultCommand()
	local mx, my = Spring.GetMouseState()
	local s, targetID = Spring.TraceScreenRay(mx, my)
	if s ~= "unit" then
		return false
	end
	if not Spring.AreTeamsAllied(Spring.GetMyTeamID(), Spring.GetUnitTeam(targetID)) then
		return false
	end
	if not UnitDefs[Spring.GetUnitDefID(targetID)].isAirBase then
		return false
	end
	for _, unitID in pairs(Spring.GetSelectedUnits()) do
		if UnitDefs[Spring.GetUnitDefID(unitID)].canFly then
			return LAND_CMD_ID
		end
	end
	return false
end
end
It's nowhere near complete, and it probably contains bugs. I publish this mostly to give a reference about the new air+transport stuff and how to use them.
NeedsRepair is where you decide whether a unit needs repair
CanLandAt may need tweaking according to your units and the names of their pieces
and I'm not doing anything currently with the planes on the pad, just iterate on landedPlanes or use UnitLoaded/UnitUnloaded





